diff --git a/scenes/ui/Credits.gd b/scenes/ui/Credits.gd new file mode 100644 index 0000000..67929b8 --- /dev/null +++ b/scenes/ui/Credits.gd @@ -0,0 +1,242 @@ +extends Control + +signal back_pressed + +const YAML_SOURCES: Array[String] = [ + "res://assets/sources.yaml", + # Future sources: + # "res://assets/audio/audio-sources.yaml", + # "res://assets/sprites/sprite-sources.yaml", +] + +@onready var scroll_container: ScrollContainer = $MarginContainer/VBoxContainer/ScrollContainer +@onready var credits_text: RichTextLabel = $MarginContainer/VBoxContainer/ScrollContainer/CreditsText +@onready var back_button: Button = $MarginContainer/VBoxContainer/BackButton + + +func _ready() -> void: + DebugManager.log_info("Credits scene ready", "Credits") + _load_and_display_credits() + back_button.grab_focus() + DebugManager.log_info("Back button focused for gamepad support", "Credits") + + +func _load_and_display_credits() -> void: + """Load credits from multiple YAML files and display formatted output""" + var all_credits_data: Dictionary = {} + + # Load all YAML source files + for yaml_path in YAML_SOURCES: + var yaml_data: Dictionary = _load_yaml_file(yaml_path) + if not yaml_data.is_empty(): + _merge_credits_data(all_credits_data, yaml_data) + + # Generate and display formatted credits + _display_formatted_credits(all_credits_data) + + +func _load_yaml_file(yaml_path: String) -> Dictionary: + """Load and parse a YAML file into a dictionary structure""" + var file := FileAccess.open(yaml_path, FileAccess.READ) + + if not file: + DebugManager.log_warn("Could not open YAML file: %s" % yaml_path, "Credits") + return {} + + var content: String = file.get_as_text() + file.close() + + DebugManager.log_info("Loaded YAML file: %s" % yaml_path, "Credits") + return _parse_yaml_content(content) + + +func _parse_yaml_content(yaml_content: String) -> Dictionary: + """Parse YAML content into structured dictionary""" + var result: Dictionary = {} + var lines: Array = yaml_content.split("\n") + var current_section: String = "" + var current_subsection: String = "" + var current_asset: String = "" + var current_asset_data: Dictionary = {} + + for line in lines: + var trimmed: String = line.strip_edges() + + # Skip comments and empty lines + if trimmed.is_empty() or trimmed.begins_with("#"): + continue + + # Top-level section (audio, sprites, textures, etc.) + if not line.begins_with(" ") and not line.begins_with("\t") and trimmed.ends_with(":"): + if current_asset and not current_asset_data.is_empty(): + _store_asset_data( + result, current_section, current_subsection, current_asset, current_asset_data + ) + current_section = trimmed.trim_suffix(":") + current_subsection = "" + current_asset = "" + current_asset_data = {} + if not result.has(current_section): + result[current_section] = {} + + # Subsection (music, sfx, characters, etc.) + elif line.begins_with(" ") and not line.begins_with(" ") and trimmed.ends_with(":"): + if current_asset and not current_asset_data.is_empty(): + _store_asset_data( + result, current_section, current_subsection, current_asset, current_asset_data + ) + current_subsection = trimmed.trim_suffix(":") + current_asset = "" + current_asset_data = {} + if current_section and not result[current_section].has(current_subsection): + result[current_section][current_subsection] = {} + + # Asset name + elif trimmed.begins_with('"') and trimmed.contains('":'): + if current_asset and not current_asset_data.is_empty(): + _store_asset_data( + result, current_section, current_subsection, current_asset, current_asset_data + ) + var parts: Array = trimmed.split('"') + current_asset = parts[1] if parts.size() > 1 else "" + current_asset_data = {} + + # Asset properties + elif ":" in trimmed: + var parts: Array = trimmed.split(":", false, 1) + if parts.size() == 2: + var key: String = parts[0].strip_edges() + var value: String = parts[1].strip_edges().trim_prefix('"').trim_suffix('"') + if value and value != '""': + current_asset_data[key] = value + + # Store last asset + if current_asset and not current_asset_data.is_empty(): + _store_asset_data( + result, current_section, current_subsection, current_asset, current_asset_data + ) + + return result + + +func _store_asset_data( + result: Dictionary, section: String, subsection: String, asset: String, data: Dictionary +) -> void: + """Store parsed asset data into result dictionary""" + if not section or not asset: + return + + if subsection: + if not result[section].has(subsection): + result[section][subsection] = {} + result[section][subsection][asset] = data + else: + result[section][asset] = data + + +func _merge_credits_data(target: Dictionary, source: Dictionary) -> void: + """Merge source credits data into target dictionary""" + for section in source: + if not target.has(section): + target[section] = {} + for subsection in source[section]: + if source[section][subsection] is Dictionary: + if not target[section].has(subsection): + target[section][subsection] = {} + for asset in source[section][subsection]: + target[section][subsection][asset] = source[section][subsection][asset] + + +func _display_formatted_credits(credits_data: Dictionary) -> void: + """Generate BBCode formatted credits from parsed data""" + if not credits_text: + DebugManager.log_error("Credits text node is null, cannot display credits", "Credits") + return + + var credits_bbcode: String = "[center][b][font_size=32]CREDITS[/font_size][/b][/center]\n\n" + + # Audio section + if credits_data.has("audio"): + credits_bbcode += "[b][font_size=24]AUDIO[/font_size][/b]\n\n" + credits_bbcode += _format_section(credits_data["audio"]) + + # Sprites section + if credits_data.has("sprites"): + credits_bbcode += "[b][font_size=24]GRAPHICS[/font_size][/b]\n\n" + credits_bbcode += _format_section(credits_data["sprites"]) + + # Textures section + if credits_data.has("textures"): + if not credits_data.has("sprites"): + credits_bbcode += "[b][font_size=24]GRAPHICS[/font_size][/b]\n\n" + credits_bbcode += _format_section(credits_data["textures"]) + + # Game development credits + credits_bbcode += "[b][font_size=24]GAME DEVELOPMENT[/font_size][/b]\n\n" + credits_bbcode += "[i]Developed with Godot Engine 4.4[/i]\n" + credits_bbcode += "[url=https://godotengine.org]https://godotengine.org[/url]\n\n" + + credits_text.bbcode_enabled = true + credits_text.text = credits_bbcode + DebugManager.log_info("Credits displayed successfully", "Credits") + + +func _format_section(section_data: Dictionary) -> String: + """Format a credits section with subsections and assets""" + var result: String = "" + + for subsection in section_data: + if section_data[subsection] is Dictionary: + # Add subsection header + var subsection_title: String = subsection.capitalize() + result += "[i]" + subsection_title + "[/i]\n" + + # Add assets in this subsection + for asset in section_data[subsection]: + var asset_data: Dictionary = section_data[subsection][asset] + result += _format_asset(asset_data) + + result += "\n" + + return result + + +func _format_asset(asset_data: Dictionary) -> String: + """Format a single asset's credit information""" + var result: String = "" + + if asset_data.has("attribution") and asset_data["attribution"]: + result += "[b]" + asset_data["attribution"] + "[/b]\n" + + if asset_data.has("license") and asset_data["license"]: + result += "License: " + asset_data["license"] + "\n" + + if asset_data.has("source") and asset_data["source"]: + result += "[url=" + asset_data["source"] + "]Source[/url]\n" + + if result: + result += "\n" + + return result + + +func _on_back_button_pressed() -> void: + AudioManager.play_ui_click() + DebugManager.log_info("Back button pressed", "Credits") + GameManager.exit_to_main_menu() + + +func _input(event: InputEvent) -> void: + if event.is_action_pressed("ui_back") or event.is_action_pressed("action_east"): + _on_back_button_pressed() + elif event.is_action_pressed("move_up") or event.is_action_pressed("ui_up"): + _scroll_credits(-50.0) + elif event.is_action_pressed("move_down") or event.is_action_pressed("ui_down"): + _scroll_credits(50.0) + + +func _scroll_credits(amount: float) -> void: + """Scroll the credits by the specified amount""" + var current_scroll: float = scroll_container.scroll_vertical + scroll_container.scroll_vertical = int(current_scroll + amount) + DebugManager.log_debug("Scrolled credits to: %d" % scroll_container.scroll_vertical, "Credits") diff --git a/scenes/ui/Credits.gd.uid b/scenes/ui/Credits.gd.uid new file mode 100644 index 0000000..1905d28 --- /dev/null +++ b/scenes/ui/Credits.gd.uid @@ -0,0 +1 @@ +uid://cwygtx0r6gdt1 diff --git a/scenes/ui/Credits.tscn b/scenes/ui/Credits.tscn new file mode 100644 index 0000000..3ced2c4 --- /dev/null +++ b/scenes/ui/Credits.tscn @@ -0,0 +1,52 @@ +[gd_scene load_steps=2 format=3 uid="uid://cspq2y7mvjxn5"] + +[ext_resource type="Script" path="res://scenes/ui/Credits.gd" id="1_credits"] + +[node name="Credits" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_credits") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 40 +theme_override_constants/margin_top = 40 +theme_override_constants/margin_right = 40 +theme_override_constants/margin_bottom = 40 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 +follow_focus = true + +[node name="CreditsText" type="RichTextLabel" parent="MarginContainer/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +bbcode_enabled = true +text = "[center][b]CREDITS[/b][/center] + +Loading credits..." +fit_content = true +scroll_active = false + +[node name="BackButton" type="Button" parent="MarginContainer/VBoxContainer"] +custom_minimum_size = Vector2(150, 50) +layout_mode = 2 +size_flags_horizontal = 4 +text = "Back" + +[connection signal="pressed" from="MarginContainer/VBoxContainer/BackButton" to="." method="_on_back_button_pressed"] diff --git a/scenes/ui/MainMenu.gd b/scenes/ui/MainMenu.gd index fdec744..4b3a643 100644 --- a/scenes/ui/MainMenu.gd +++ b/scenes/ui/MainMenu.gd @@ -31,6 +31,12 @@ func _on_settings_button_pressed() -> void: open_settings.emit() +func _on_credits_button_pressed() -> void: + AudioManager.play_ui_click() + DebugManager.log_info("Credits pressed", "MainMenu") + GameManager.show_credits() + + func _on_exit_button_pressed() -> void: AudioManager.play_ui_click() DebugManager.log_info("Exit pressed", "MainMenu") @@ -43,6 +49,7 @@ func _setup_menu_navigation() -> void: menu_buttons.append($MenuContainer/NewGameButton) menu_buttons.append($MenuContainer/SettingsButton) + menu_buttons.append($MenuContainer/CreditsButton) menu_buttons.append($MenuContainer/ExitButton) for button in menu_buttons: diff --git a/scenes/ui/MainMenu.tscn b/scenes/ui/MainMenu.tscn index 6e08e22..828f31e 100644 --- a/scenes/ui/MainMenu.tscn +++ b/scenes/ui/MainMenu.tscn @@ -111,6 +111,10 @@ text = "New Game" layout_mode = 2 text = "Settings" +[node name="CreditsButton" type="Button" parent="MenuContainer"] +layout_mode = 2 +text = "Credits" + [node name="ExitButton" type="Button" parent="MenuContainer"] layout_mode = 2 text = "Exit" @@ -120,4 +124,5 @@ layout_mode = 1 [connection signal="pressed" from="MenuContainer/NewGameButton" to="." method="_on_new_game_button_pressed"] [connection signal="pressed" from="MenuContainer/SettingsButton" to="." method="_on_settings_button_pressed"] +[connection signal="pressed" from="MenuContainer/CreditsButton" to="." method="_on_credits_button_pressed"] [connection signal="pressed" from="MenuContainer/ExitButton" to="." method="_on_exit_button_pressed"]