Starter-Kit-City-Builder/scripts/mission/learning_panel.gd

681 lines
25 KiB
GDScript

extends Control
signal completed(mission)
signal panel_opened
signal panel_closed
# Store variables for signal connections
var user_input # For single input (backward compatibility)
var user_inputs = [] # Array for multiple inputs
var input_labels = [] # Array for input labels
var submit_button
var mission: MissionData
var correct_answer: String = "A"
var is_answer_correct: bool = false
func _ready():
# Hide panel initially
visible = false
# Ensure we're set to process regardless of pause state
process_mode = Node.PROCESS_MODE_ALWAYS
# Make sure we grab input focus
mouse_filter = Control.MOUSE_FILTER_STOP
# Wait for the scene to be ready
await get_tree().process_frame
# Make sure we're on the right layer
z_index = 100
# Only get references needed for signal connections
user_input = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer/UserInput")
submit_button = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/SubmitButtonContainer/SubmitButton")
# Clear the user inputs array
user_inputs = []
input_labels = []
# Connect button signals if the button exists
if submit_button != null:
if not submit_button.is_connected("pressed", Callable(self, "_on_submit_button_pressed")):
submit_button.pressed.connect(_on_submit_button_pressed)
else:
push_error("Submit button not found in learning panel")
func show_learning_panel(mission_data: MissionData):
# Check if the mission data is valid
if mission_data == null:
push_error("Invalid mission data provided to learning panel")
return
mission = mission_data
# Make panel fully visible and ensure process mode is set to handle input while paused
visible = true
process_mode = Node.PROCESS_MODE_ALWAYS
# First, reset the panel to a clean state
_reset_panel()
# Use traditional text and graph mode
_setup_traditional_mode()
# Set up the correct answer from mission data
if not mission.correct_answer.is_empty():
correct_answer = mission.correct_answer
else:
# Default answer based on mission type
correct_answer = "1" if not mission.power_math_content.is_empty() else "A"
# Set up user input fields based on mission data
if mission.num_of_user_inputs > 1:
_setup_multiple_user_inputs()
# Set focus to the first input field after a short delay
get_tree().create_timer(0.2).timeout.connect(func():
if user_inputs.size() > 0 and user_inputs[0]:
user_inputs[0].grab_focus()
print("Set focus to the first input field in multi-input")
)
else:
# Traditional single input
if user_input:
user_input.placeholder_text = mission.question_text if not mission.question_text.is_empty() else "Enter your answer"
# Set focus to the single input field after a short delay
get_tree().create_timer(0.2).timeout.connect(func():
user_input.grab_focus()
print("Set focus to the single input field")
)
# Hide the HUD when learning panel is shown
var hud = get_node_or_null("/root/Main/CanvasLayer/HUD")
if hud:
hud.visible = false
# Make sure we're on top
if get_parent():
get_parent().move_child(self, get_parent().get_child_count() - 1)
# Make sure we're at the proper z-index
z_index = 100
# Disable background interaction by creating a fullscreen invisible barrier
_disable_background_interaction()
# Emit signal to lock building controls
panel_opened.emit()
print("Panel is now visible = ", visible)
# Creates an invisible fullscreen barrier to block clicks on the background
func _disable_background_interaction():
# Remove any existing barrier
var existing_barrier = get_node_or_null("BackgroundBarrier")
if existing_barrier:
existing_barrier.queue_free()
# Create a new barrier
var barrier = ColorRect.new()
barrier.name = "BackgroundBarrier"
barrier.color = Color(0, 0, 0, 0.01) # Almost transparent
barrier.anchor_right = 1.0
barrier.anchor_bottom = 1.0
barrier.mouse_filter = Control.MOUSE_FILTER_STOP # Block mouse events
barrier.z_index = -1 # Behind the panel UI
# Add it as the first child of the panel
add_child(barrier)
move_child(barrier, 0)
print("Background interaction disabled")
# Function to create multiple user input fields
func _setup_multiple_user_inputs():
# Clear any existing user inputs
user_inputs = []
input_labels = []
# Get the container where inputs should be added
var user_input_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer")
if not user_input_container:
push_error("User input container not found")
return
# If there's an existing single input, hide it
if user_input and user_input.get_parent() == user_input_container:
user_input.visible = false
# Create a centering container for better alignment
var center_container = CenterContainer.new()
center_container.name = "InputCenterContainer"
center_container.size_flags_horizontal = Control.SIZE_FILL
user_input_container.add_child(center_container)
# Add margin around the grid
var margin_container = MarginContainer.new()
margin_container.name = "InputMarginContainer"
margin_container.add_theme_constant_override("margin_top", 10)
margin_container.add_theme_constant_override("margin_bottom", 10)
center_container.add_child(margin_container)
# Create a grid container for inputs
var grid = GridContainer.new()
grid.name = "MultiInputGrid"
grid.columns = 2 # Label and input in each row
grid.size_flags_horizontal = Control.SIZE_FILL
grid.add_theme_constant_override("h_separation", 15) # Add horizontal spacing between columns
grid.add_theme_constant_override("v_separation", 10) # Add vertical spacing between rows
margin_container.add_child(grid)
# Create each input field
for i in range(mission.num_of_user_inputs):
# Create label
var label = Label.new()
label.name = "InputLabel" + str(i)
label.text = mission.input_labels[i] if i < mission.input_labels.size() else "Input " + str(i+1) + ":"
label.size_flags_horizontal = Control.SIZE_EXPAND
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT # Right-align text
# Set larger font size
var font_size = 26
label.add_theme_font_size_override("font_size", font_size)
# Add right margin for better spacing
var style = StyleBoxEmpty.new()
style.content_margin_right = 10 # Add 10 pixels of right margin
label.add_theme_stylebox_override("normal", style)
grid.add_child(label)
input_labels.append(label)
# Create input field
var input_field = LineEdit.new()
input_field.name = "UserInput" + str(i)
input_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
input_field.placeholder_text = "Enter value"
input_field.custom_minimum_size.x = 150 # Increase minimum width
input_field.custom_minimum_size.y = 40 # Set height for the input field
# Style the input field
input_field.alignment = HORIZONTAL_ALIGNMENT_LEFT # Left-align text inside the field
input_field.add_theme_font_size_override("font_size", 26) # Match label font size
# Connect text submitted signal
input_field.text_submitted.connect(_on_user_input_text_submitted)
grid.add_child(input_field)
user_inputs.append(input_field)
# Add spacing after the grid
var spacer = Control.new()
spacer.name = "InputSpacer"
spacer.custom_minimum_size.y = 20
user_input_container.add_child(spacer)
# Add a hint button below the inputs
var hint_button_container = HBoxContainer.new()
hint_button_container.name = "HintButtonContainer"
hint_button_container.size_flags_horizontal = Control.SIZE_FILL
hint_button_container.alignment = BoxContainer.ALIGNMENT_CENTER
user_input_container.add_child(hint_button_container)
var hint_button = Button.new()
hint_button.name = "HintButton"
hint_button.text = "Need a Hint?"
hint_button.custom_minimum_size = Vector2(200, 40)
# Style the hint button
var button_style = StyleBoxFlat.new()
button_style.bg_color = Color(0.2, 0.2, 0.3, 0.8)
button_style.border_width_left = 2
button_style.border_width_top = 2
button_style.border_width_right = 2
button_style.border_width_bottom = 2
button_style.border_color = Color(0.376, 0.760, 0.658, 0.5) # Teal border
button_style.corner_radius_top_left = 5
button_style.corner_radius_top_right = 5
button_style.corner_radius_bottom_right = 5
button_style.corner_radius_bottom_left = 5
hint_button.add_theme_stylebox_override("normal", button_style)
hint_button.add_theme_font_size_override("font_size", 20)
hint_button.pressed.connect(_on_hint_button_pressed)
hint_button_container.add_child(hint_button)
# Reset the panel to a clean state
func _reset_panel():
# Reset answer state
is_answer_correct = false
# Clear single input if it exists
if user_input:
user_input.text = ""
# Clear multiple inputs if they exist
for input in user_inputs:
if input:
input.text = ""
# Hide feedback label
var feedback_label = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer/FeedbackLabel")
if feedback_label:
feedback_label.visible = false
# Clean up any added UI elements
var user_input_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer")
if user_input_container:
# Clean up the input center container and all its children
var input_center_container = user_input_container.get_node_or_null("InputCenterContainer")
if input_center_container:
input_center_container.queue_free()
# Clean up input spacer if it exists
var input_spacer = user_input_container.get_node_or_null("InputSpacer")
if input_spacer:
input_spacer.queue_free()
# Clean up any TopMargin that might have been added
var top_margin = user_input_container.get_node_or_null("TopMargin")
if top_margin:
top_margin.queue_free()
# Reset custom sizing
user_input_container.custom_minimum_size.y = 0
user_input_container.size_flags_vertical = Control.SIZE_FILL
# Show the default input field if it exists
if user_input and user_input.get_parent() == user_input_container:
user_input.visible = true
# Clear the user inputs arrays
user_inputs = []
input_labels = []
# Reset submit button
if submit_button:
submit_button.text = "SUBMIT"
# Disconnect complete mission signal if connected
if submit_button.is_connected("pressed", Callable(self, "_on_complete_mission")):
submit_button.pressed.disconnect(_on_complete_mission)
# Connect submit button signal
if not submit_button.is_connected("pressed", Callable(self, "_on_submit_button_pressed")):
submit_button.pressed.connect(_on_submit_button_pressed)
# Sets up the traditional mode with separate title, text, and graph elements
func _setup_traditional_mode():
# Set the mission title
var mission_title_label = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/TitleContainer/MissionTitleLabel")
if mission_title_label:
mission_title_label.text = mission.title.to_upper()
else:
push_error("MissionTitleLabel node not found")
# Set the intro text
var intro_text = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/IntroText")
if intro_text:
intro_text.text = mission.intro_text if not mission.intro_text.is_empty() else "Welcome to this mission!"
else:
push_error("IntroText node not found")
# Set the description text
var description_text = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/DescriptionText")
if description_text:
description_text.text = mission.description
else:
push_error("DescriptionText node not found")
# Set up mission-specific content for construction or power mission
_setup_mission_specific_content()
# Send question_shown dialog to learning companion if available
if mission.companion_dialog.has("question_shown"):
var dialog_data = mission.companion_dialog["question_shown"]
const JSBridge = preload("res://scripts/javascript_bridge.gd")
if JSBridge.has_interface():
JSBridge.get_interface().sendCompanionDialog("question_shown", dialog_data)
print("Setup traditional mode complete")
# Set up mission-specific content based on the mission type
func _setup_mission_specific_content():
# Clear existing content first
_clear_existing_content()
# Decide which content to show
if mission.power_math_content.is_empty():
# This is a construction company mission
_setup_construction_mission()
else:
# This is a power math mission
_setup_power_math_mission()
# Clear existing content before setting up new content
func _clear_existing_content():
# Find the main containers
var graph_center_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/GraphCenterContainer")
var question_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/CompanyDataContainer")
# Clear power math content from the graph container
if graph_center_container:
var power_math_label = graph_center_container.get_node_or_null("PowerMathLabel")
if power_math_label:
power_math_label.queue_free()
# Reset the container that will hold our question text (previously used for company data)
if question_container:
question_container.visible = false
var text_label = question_container.get_node_or_null("CompanyDataLabel")
if text_label:
text_label.text = ""
# Set up construction company mission content
func _setup_construction_mission():
print("Setting up construction company mission")
# 1. Show the graph image
var graph_image = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/GraphCenterContainer/GraphImage")
if graph_image:
if mission.graph_path.is_empty():
graph_image.visible = false
else:
# Load and show the graph
var graph_texture = load(mission.graph_path)
if graph_texture:
# Set the texture
graph_image.texture = graph_texture
# Configure proper scaling based on the image:
# - Get the image size
var image_size = graph_texture.get_size()
print("Image dimensions: " + str(image_size.x) + "x" + str(image_size.y))
# - Determine if we need to adjust scaling based on image dimensions
var target_width = 1000 # Match the custom_minimum_size from the scene
var target_height = 500
# - Adjust the expansion mode based on image size relative to target size
if image_size.x < target_width * 0.5 or image_size.y < target_height * 0.5:
# Small image - use SCALE expansion mode to make it larger
graph_image.expand_mode = 1 # SCALE
graph_image.stretch_mode = 5 # KEEP_ASPECT_CENTERED
print("Using SCALE expansion for small image")
else:
# Larger image - use KEEP_SIZE or KEEP_WIDTH expansion mode
graph_image.expand_mode = 2 # KEEP_WIDTH
graph_image.stretch_mode = 5 # KEEP_ASPECT_CENTERED
print("Using KEEP_WIDTH expansion for larger image")
# Set custom minimum size if needed
if image_size.x > 800:
# For larger images, use reasonable dimensions
graph_image.custom_minimum_size = Vector2(min(1000, max(800, image_size.x)), min(500, max(400, image_size.y)))
else:
# For smaller images, scale them up
graph_image.custom_minimum_size = Vector2(1000, 500)
graph_image.visible = true
print("Successfully loaded graph image for construction mission: " + mission.graph_path)
else:
graph_image.visible = false
print("Failed to load graph image")
# 2. Set question text instead of company data
var company_data_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/CompanyDataContainer")
if company_data_container:
company_data_container.visible = true
# Get the label where we'll display the question text
var company_data_label = company_data_container.get_node_or_null("CompanyDataLabel")
if company_data_label:
# Create a formatted question text
var formatted_text = "[center]\n"
# Add the question text in a centered, clear format
if not mission.question_text.is_empty():
formatted_text += "[color=#dddddd][font_size=26]" + mission.question_text + "[/font_size][/color]"
formatted_text += "\n[/center]"
# Set the formatted text
company_data_label.text = formatted_text
company_data_label.custom_minimum_size.y = 80 # Reduce height for just question text
# Set up power math mission content
func _setup_power_math_mission():
print("Setting up power math mission")
# 1. Check if we have a graph image to display
var graph_image = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/GraphCenterContainer/GraphImage")
if graph_image:
# Only show the graph image if a path is specified in the mission
if not mission.graph_path.is_empty():
# Try loading the graph image
var graph_texture = load(mission.graph_path)
if graph_texture:
# Set the texture
graph_image.texture = graph_texture
# Configure proper scaling based on the image:
# - Get the image size
var image_size = graph_texture.get_size()
print("Image dimensions: " + str(image_size.x) + "x" + str(image_size.y))
# - Determine if we need to adjust scaling based on image dimensions
var target_width = 1000 # Match the custom_minimum_size from the scene
var target_height = 500
# - Adjust the expansion mode based on image size relative to target size
if image_size.x < target_width * 0.5 or image_size.y < target_height * 0.5:
# Small image - use SCALE expansion mode to make it larger
graph_image.expand_mode = 1 # SCALE
graph_image.stretch_mode = 5 # KEEP_ASPECT_CENTERED
print("Using SCALE expansion for small image")
else:
# Larger image - use KEEP_SIZE or KEEP_WIDTH expansion mode
graph_image.expand_mode = 2 # KEEP_WIDTH
graph_image.stretch_mode = 5 # KEEP_ASPECT_CENTERED
print("Using KEEP_WIDTH expansion for larger image")
# Set custom minimum size if needed
if image_size.x > 800:
# For larger images, use reasonable dimensions
graph_image.custom_minimum_size = Vector2(min(1000, max(800, image_size.x)), min(500, max(400, image_size.y)))
else:
# For smaller images, scale them up
graph_image.custom_minimum_size = Vector2(1000, 500)
graph_image.visible = true
print("Successfully loaded graph image for power mission: " + mission.graph_path)
else:
graph_image.visible = false
print("Failed to load graph image for power mission: " + mission.graph_path)
else:
graph_image.visible = false
print("No graph path specified for power mission")
# 2. Hide company data container
var company_data_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/CompanyDataContainer")
if company_data_container:
company_data_container.visible = false
# 3. Add power math content if we're not showing a graph
var graph_center_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/GraphCenterContainer")
if graph_center_container:
# Only show power math content if we don't have a graph image or if it's not visible
if mission.graph_path.is_empty() or not graph_image or not graph_image.visible:
# Create power math label
var power_math_label = graph_center_container.get_node_or_null("PowerMathLabel")
if power_math_label:
power_math_label.queue_free()
# Create new label for the power math content
power_math_label = RichTextLabel.new()
power_math_label.name = "PowerMathLabel"
power_math_label.custom_minimum_size = Vector2(1000, 500) # Smaller size to match new dimensions
power_math_label.bbcode_enabled = true
power_math_label.fit_content = true
graph_center_container.add_child(power_math_label)
# Set the power math content
if mission.power_math_content.is_empty():
power_math_label.text = "No power math content available."
else:
power_math_label.text = mission.power_math_content
power_math_label.visible = true
print("Added power math content as text")
func hide_learning_panel():
visible = false
# Show the HUD again when learning panel is hidden
var hud = get_node_or_null("/root/Main/CanvasLayer/HUD")
if hud:
hud.visible = true
# Remove the barrier and re-enable background interaction
var barrier = get_node_or_null("BackgroundBarrier")
if barrier:
barrier.queue_free()
# Unpause the game tree if it was paused
if get_tree().paused:
get_tree().paused = false
# Emit signal to unlock building controls
panel_closed.emit()
func _on_user_input_text_submitted(submitted_text):
_check_answer()
func _on_submit_button_pressed():
_check_answer()
func _check_answer():
# Make sure mission is valid
if mission == null:
push_error("Mission is null in _check_answer")
return
var user_answer = ""
# Handle multiple inputs if present
if mission.num_of_user_inputs > 1 and not user_inputs.is_empty():
var answers = []
for input in user_inputs:
if input:
answers.append(input.text.strip_edges())
user_answer = ",".join(answers)
# Fall back to single input
elif user_input:
user_answer = user_input.text.strip_edges()
else:
push_error("Cannot check answer: no user input fields available")
return
# Convert to uppercase for case-insensitive comparison when appropriate
if not "," in correct_answer: # Don't uppercase comma-separated values
user_answer = user_answer.to_upper()
# Get the feedback label
var feedback_label = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer/FeedbackLabel")
if not feedback_label:
push_error("Feedback label not found")
return
# Make feedback visible
feedback_label.visible = true
if user_answer == correct_answer:
is_answer_correct = true
# Show feedback text
if not mission.feedback_text.is_empty():
feedback_label.text = mission.feedback_text
else:
feedback_label.text = "Correct! You've solved this problem successfully."
feedback_label.add_theme_color_override("font_color", Color(0, 0.7, 0.2))
# Send correct answer dialog to learning companion if available
if mission.companion_dialog.has("correct_answer"):
var dialog_data = mission.companion_dialog["correct_answer"]
const JSBridge = preload("res://scripts/javascript_bridge.gd")
if JSBridge.has_interface():
JSBridge.get_interface().sendCompanionDialog("correct_answer", dialog_data)
# Change submit button to "Complete" button
if submit_button:
submit_button.text = "COMPLETE"
# Disconnect submit and connect complete signals
if submit_button.is_connected("pressed", Callable(self, "_on_submit_button_pressed")):
submit_button.pressed.disconnect(_on_submit_button_pressed)
if not submit_button.is_connected("pressed", Callable(self, "_on_complete_mission")):
submit_button.pressed.connect(_on_complete_mission)
else:
# Show incorrect feedback
if not mission.incorrect_feedback.is_empty():
feedback_label.text = mission.incorrect_feedback
else:
feedback_label.text = "Not quite right. Please try again."
feedback_label.add_theme_color_override("font_color", Color(0.9, 0.2, 0.2))
# Send incorrect answer dialog to learning companion if available
if mission.companion_dialog.has("incorrect_answer"):
var dialog_data = mission.companion_dialog["incorrect_answer"]
const JSBridge = preload("res://scripts/javascript_bridge.gd")
if JSBridge.has_interface():
JSBridge.get_interface().sendCompanionDialog("incorrect_answer", dialog_data)
func _on_complete_mission():
if is_answer_correct:
# Complete the learning objective
for objective in mission.objectives:
if objective.type == MissionObjective.ObjectiveType.LEARNING:
objective.progress(objective.target_count)
# Hide the panel
hide_learning_panel()
# Emit signal with mission as argument
completed.emit(mission)
func _on_hint_button_pressed():
print("Hint button pressed")
# First hint request
if mission.companion_dialog.has("hint_request"):
var dialog_data = mission.companion_dialog["hint_request"]
const JSBridge = preload("res://scripts/javascript_bridge.gd")
if JSBridge.has_interface():
JSBridge.get_interface().sendCompanionDialog("hint_request", dialog_data)
# Additional hint if available and first hint was already shown
# We'll use a timer to ensure there's a delay between hints
var second_hint_timer = Timer.new()
second_hint_timer.wait_time = 6.0 # Wait 6 seconds before showing second hint
second_hint_timer.one_shot = true
second_hint_timer.autostart = true
add_child(second_hint_timer)
# Connect the timeout signal
second_hint_timer.timeout.connect(func():
if mission.companion_dialog.has("hint_second"):
var dialog_data = mission.companion_dialog["hint_second"]
const JSBridge = preload("res://scripts/javascript_bridge.gd")
if JSBridge.has_interface():
JSBridge.get_interface().sendCompanionDialog("hint_second", dialog_data)
# Clean up the timer
second_hint_timer.queue_free()
)