diff --git a/project.godot b/project.godot index b95920e..bf07853 100644 --- a/project.godot +++ b/project.godot @@ -23,6 +23,7 @@ buses/default_bus_layout="uid://5oswo22yvmtg" MusicManager="*uid://dmu7041wi1upt" SpellLibrary="*res://scripts/SpellLibrary.gd" +StatsManager="*res://scripts/stats_manager.gd" [display] diff --git a/scenes/game.gd b/scenes/game.gd index 19dc616..8a75d6f 100644 --- a/scenes/game.gd +++ b/scenes/game.gd @@ -10,12 +10,14 @@ func _ready(): MusicManager.play(preload("res://assets/music&sfx/music/ashveil-355633.mp3")) add_child(_debug_menu_script.new()) $World/Witch.died.connect(_on_witch_died) + StatsManager.start_run() func _process(_delta: float) -> void: var t = int(_spawn_control.elapsed_time) _timer_label.text = "%02d:%02d" % [t / 60, t % 60] func _on_witch_died(killer) -> void: + StatsManager.end_run(true) var witch = $World/Witch var player = $World/Player $PauseMenu.process_mode = Node.PROCESS_MODE_DISABLED diff --git a/scenes/mainmenu.tscn b/scenes/mainmenu.tscn index f377d02..391b265 100644 --- a/scenes/mainmenu.tscn +++ b/scenes/mainmenu.tscn @@ -150,6 +150,23 @@ theme_override_styles/disabled = SubResource("StyleBoxEmpty_normal") theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_pressed") text = "Credits" +[node name="StatsButton" type="Button" parent="VBoxContainer" unique_id=333333333] +custom_minimum_size = Vector2(250, 50) +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.95, 0.8, 1) +theme_override_colors/font_hover_color = Color(1, 1, 0.55, 1) +theme_override_colors/font_pressed_color = Color(1, 1, 0.75, 1) +theme_override_colors/font_focus_color = Color(1, 0.95, 0.8, 1) +theme_override_fonts/font = ExtResource("4_3vvtq") +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxEmpty_normal") +theme_override_styles/hover = SubResource("StyleBoxFlat_hover") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/focus = SubResource("StyleBoxEmpty_focus") +theme_override_styles/disabled = SubResource("StyleBoxEmpty_normal") +theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_pressed") +text = "Stats" + [node name="QuitButton" type="Button" parent="VBoxContainer" unique_id=1704957427] custom_minimum_size = Vector2(250, 50) layout_mode = 2 @@ -170,4 +187,5 @@ text = "Quit" [connection signal="pressed" from="VBoxContainer/PlayButton" to="." method="_on_play_button_pressed"] [connection signal="pressed" from="VBoxContainer/OptionsButton" to="." method="_on_options_button_pressed"] [connection signal="pressed" from="VBoxContainer/CreditsButton" to="." method="_on_credits_button_pressed"] +[connection signal="pressed" from="VBoxContainer/StatsButton" to="." method="_on_stats_button_pressed"] [connection signal="pressed" from="VBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"] diff --git a/scenes/stats_screen.tscn b/scenes/stats_screen.tscn new file mode 100644 index 0000000..ecbc9a2 --- /dev/null +++ b/scenes/stats_screen.tscn @@ -0,0 +1,128 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://scripts/stats_screen.gd" id="1_stats"] +[ext_resource type="Texture2D" uid="uid://2ddggeetjj1t" path="res://assets/images/screenshot_game.png" id="2_stats"] +[ext_resource type="FontFile" uid="uid://8v71dcws4q6o" path="res://assets/fonts/slkscre.ttf" id="3_stats"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_normal"] +content_margin_left = 10.0 +content_margin_top = 4.0 +content_margin_right = 10.0 +content_margin_bottom = 4.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hover"] +bg_color = Color(0.45, 0.1, 0.7, 0.35) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.75, 0.45, 1.0, 0.75) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +content_margin_left = 10.0 +content_margin_top = 4.0 +content_margin_right = 10.0 +content_margin_bottom = 4.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pressed"] +bg_color = Color(0.3, 0.05, 0.5, 0.55) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.9, 0.6, 1.0, 1.0) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +content_margin_left = 10.0 +content_margin_top = 4.0 +content_margin_right = 10.0 +content_margin_bottom = 4.0 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_focus"] +content_margin_left = 10.0 +content_margin_top = 4.0 +content_margin_right = 10.0 +content_margin_bottom = 4.0 + +[node name="StatsScreen" type="CanvasLayer"] +script = ExtResource("1_stats") + +[node name="BackgroundSprite" type="TextureRect" parent="."] +z_index = -2 +offset_left = 4.0 +offset_top = 2.0 +offset_right = 1277.0 +offset_bottom = 718.0 +texture = ExtResource("2_stats") +expand_mode = 2 + +[node name="BackgroundRect" type="ColorRect" parent="."] +z_index = -1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0.6) + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +z_index = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -308.5 +offset_top = -330.0 +offset_right = 308.5 +offset_bottom = 330.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/separation = 10 +alignment = 1 + +[node name="TitleLabel" type="Label" parent="VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.9, 0.6, 1) +theme_override_colors/font_outline_color = Color(0.4, 0.05, 0.65, 1) +theme_override_constants/outline_size = 3 +theme_override_fonts/font = ExtResource("3_stats") +theme_override_font_sizes/font_size = 56 +text = "Statistics" +horizontal_alignment = 1 + +[node name="Spacer" type="Control" parent="VBoxContainer"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 8) + +[node name="StatsContainer" type="VBoxContainer" parent="VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 4 +size_flags_horizontal = 3 + +[node name="Spacer2" type="Control" parent="VBoxContainer"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 12) + +[node name="BackButton" type="Button" parent="VBoxContainer"] +custom_minimum_size = Vector2(250, 50) +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.95, 0.8, 1) +theme_override_colors/font_hover_color = Color(1, 1, 0.55, 1) +theme_override_colors/font_pressed_color = Color(1, 1, 0.75, 1) +theme_override_colors/font_focus_color = Color(1, 0.95, 0.8, 1) +theme_override_fonts/font = ExtResource("3_stats") +theme_override_font_sizes/font_size = 32 +theme_override_styles/normal = SubResource("StyleBoxEmpty_normal") +theme_override_styles/hover = SubResource("StyleBoxFlat_hover") +theme_override_styles/pressed = SubResource("StyleBoxFlat_pressed") +theme_override_styles/focus = SubResource("StyleBoxEmpty_focus") +theme_override_styles/disabled = SubResource("StyleBoxEmpty_normal") +theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_pressed") +text = "Back" + +[connection signal="pressed" from="VBoxContainer/BackButton" to="." method="_on_back_button_pressed"] diff --git a/scripts/blue_slime.gd b/scripts/blue_slime.gd index 4ee0882..8bd12c3 100644 --- a/scripts/blue_slime.gd +++ b/scripts/blue_slime.gd @@ -2,6 +2,7 @@ extends EnemyBase func _ready() -> void: super() + enemy_type = "blue_slime" speed = 15.0 max_hp = 20 hp = max_hp diff --git a/scripts/enemy_base.gd b/scripts/enemy_base.gd index 1a75485..61082c2 100644 --- a/scripts/enemy_base.gd +++ b/scripts/enemy_base.gd @@ -8,6 +8,7 @@ extends CharacterBody2D @export var max_hp: int = 10 +var enemy_type: String = "slime" var is_dying = false var is_hurt = false var hp: int @@ -49,6 +50,7 @@ func die(): animated_sprite_2d.sprite_frames.set_animation_loop(death_anim, false) animated_sprite_2d.play(death_anim) _play_hit_sound() + StatsManager.on_kill(enemy_type) died.emit(self) await animated_sprite_2d.animation_finished queue_free() diff --git a/scripts/fire_slime.gd b/scripts/fire_slime.gd index a4a62d4..9a86899 100644 --- a/scripts/fire_slime.gd +++ b/scripts/fire_slime.gd @@ -2,6 +2,7 @@ extends EnemyBase func _ready() -> void: super() + enemy_type = "fire_slime" speed = 15.0 max_hp = 25 hp = max_hp diff --git a/scripts/mainmenu.gd b/scripts/mainmenu.gd index dc95f9d..449c933 100644 --- a/scripts/mainmenu.gd +++ b/scripts/mainmenu.gd @@ -12,5 +12,8 @@ func _on_options_button_pressed() -> void: func _on_credits_button_pressed() -> void: get_tree().change_scene_to_file("res://scenes/credits.tscn") +func _on_stats_button_pressed() -> void: + get_tree().change_scene_to_file("res://scenes/stats_screen.tscn") + func _on_quit_button_pressed() -> void: get_tree().quit() diff --git a/scripts/pause_menu.gd b/scripts/pause_menu.gd index 4cf3e42..5c6ccc5 100644 --- a/scripts/pause_menu.gd +++ b/scripts/pause_menu.gd @@ -18,6 +18,7 @@ func _on_continue_button_pressed() -> void: toggle_pause() func _on_quit_button_pressed() -> void: + StatsManager.end_run(false) get_tree().quit() diff --git a/scripts/perk_effects.gd b/scripts/perk_effects.gd index f6f221f..a42cc5b 100644 --- a/scripts/perk_effects.gd +++ b/scripts/perk_effects.gd @@ -316,6 +316,7 @@ func knife_cooldown_upgrade(): available_perks.append(upg) func _shoot_throwing_knife() -> void: + StatsManager.on_spell_cast("throwing_knife") for i in range(throwing_knife_count): var knife = throwing_knife.instantiate() knife.global_position = witch.global_position diff --git a/scripts/stats_manager.gd b/scripts/stats_manager.gd new file mode 100644 index 0000000..ade85ac --- /dev/null +++ b/scripts/stats_manager.gd @@ -0,0 +1,87 @@ +extends Node + +const SAVE_PATH = "user://stats.json" + +var _session_start_ms: int = 0 +var _run_active: bool = false + +var data: Dictionary = { + "runs_played": 0, + "total_deaths": 0, + "longest_run_seconds": 0.0, + "total_survive_seconds": 0.0, + "kills": { + "slime": 0, + "blue_slime": 0, + "fire_slime": 0 + }, + "spells_cast": { + "fireball": 0, + "shuriken": 0, + "laser": 0, + "fire_swirl": 0, + "tornado": 0, + "purge": 0, + "throwing_knife": 0 + } +} + +func _ready() -> void: + _load() + +func start_run() -> void: + _session_start_ms = Time.get_ticks_msec() + _run_active = true + data["runs_played"] += 1 + _save() + +func end_run(died: bool) -> void: + if not _run_active: + return + _run_active = false + var survived_sec = (Time.get_ticks_msec() - _session_start_ms) / 1000.0 + if died: + data["total_deaths"] += 1 + data["total_survive_seconds"] += survived_sec + if survived_sec > data["longest_run_seconds"]: + data["longest_run_seconds"] = survived_sec + _save() + +func on_kill(enemy_type: String) -> void: + if data["kills"].has(enemy_type): + data["kills"][enemy_type] += 1 + +func on_spell_cast(spell_type: String) -> void: + if data["spells_cast"].has(spell_type): + data["spells_cast"][spell_type] += 1 + +func _save() -> void: + var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE) + if file == null: + return + file.store_string(JSON.stringify(data, "\t")) + file.close() + +func _load() -> void: + if not FileAccess.file_exists(SAVE_PATH): + return + var file = FileAccess.open(SAVE_PATH, FileAccess.READ) + if file == null: + return + var json = JSON.new() + var err = json.parse(file.get_as_text()) + file.close() + if err != OK: + return + var loaded = json.get_data() + if typeof(loaded) != TYPE_DICTIONARY: + return + for key in loaded: + if not data.has(key): + continue + if typeof(data[key]) == TYPE_DICTIONARY and typeof(loaded[key]) == TYPE_DICTIONARY: + for sub in loaded[key]: + if data[key].has(sub): + data[key][sub] = loaded[key][sub] + else: + data[key] = loaded[key] diff --git a/scripts/stats_manager.gd.uid b/scripts/stats_manager.gd.uid new file mode 100644 index 0000000..234091c --- /dev/null +++ b/scripts/stats_manager.gd.uid @@ -0,0 +1 @@ +uid://vrgjw34wuhnj diff --git a/scripts/stats_screen.gd b/scripts/stats_screen.gd new file mode 100644 index 0000000..13bd4e0 --- /dev/null +++ b/scripts/stats_screen.gd @@ -0,0 +1,93 @@ +extends CanvasLayer + +@onready var _container: VBoxContainer = $VBoxContainer/StatsContainer +var _font = preload("res://assets/fonts/slkscre.ttf") + +func _ready() -> void: + _build_stats() + +func _build_stats() -> void: + var d = StatsManager.data + + _add_section("General") + _add_row("Runs Played", str(d["runs_played"])) + _add_row("Deaths", str(d["total_deaths"])) + _add_row("Total Time Survived", _fmt_time(d["total_survive_seconds"])) + _add_row("Longest Run", _fmt_time(d["longest_run_seconds"])) + + _add_gap() + _add_section("Enemies Killed") + var kill_names = {"slime": "Slime", "blue_slime": "Blue Slime", "fire_slime": "Fire Slime"} + var total_kills = 0 + for key in kill_names: + var n: int = d["kills"][key] + _add_row(kill_names[key], str(n)) + total_kills += n + _add_row("Total", str(total_kills)) + + _add_gap() + _add_section("Spells Cast") + var spell_names = { + "fireball": "Fireball", + "shuriken": "Shuriken", + "laser": "Laser", + "fire_swirl": "Fire Swirl", + "tornado": "Tornado", + "purge": "Purge", + "throwing_knife": "Throwing Knife" + } + var total_spells = 0 + for key in spell_names: + var n: int = d["spells_cast"][key] + _add_row(spell_names[key], str(n)) + total_spells += n + _add_row("Total", str(total_spells)) + +func _add_section(title: String) -> void: + var lbl := Label.new() + lbl.text = "- " + title + " -" + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + lbl.add_theme_font_override("font", _font) + lbl.add_theme_font_size_override("font_size", 20) + lbl.add_theme_color_override("font_color", Color(0.75, 0.45, 1.0, 1.0)) + _container.add_child(lbl) + +func _add_row(label: String, value: String) -> void: + var hbox := HBoxContainer.new() + hbox.add_theme_constant_override("separation", 8) + + var lbl := Label.new() + lbl.text = label + lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL + lbl.add_theme_font_override("font", _font) + lbl.add_theme_font_size_override("font_size", 18) + lbl.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8, 1.0)) + hbox.add_child(lbl) + + var val := Label.new() + val.text = value + val.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + val.custom_minimum_size = Vector2(80, 0) + val.add_theme_font_override("font", _font) + val.add_theme_font_size_override("font_size", 18) + val.add_theme_color_override("font_color", Color(1.0, 1.0, 0.55, 1.0)) + hbox.add_child(val) + + _container.add_child(hbox) + +func _add_gap() -> void: + var spacer := Control.new() + spacer.custom_minimum_size = Vector2(0, 8) + _container.add_child(spacer) + +func _fmt_time(seconds: float) -> String: + var s := int(seconds) + var h := s / 3600 + var m := (s % 3600) / 60 + var sec := s % 60 + if h > 0: + return "%dh %02dm %02ds" % [h, m, sec] + return "%dm %02ds" % [m, sec] + +func _on_back_button_pressed() -> void: + get_tree().change_scene_to_file("res://scenes/mainmenu.tscn") diff --git a/scripts/stats_screen.gd.uid b/scripts/stats_screen.gd.uid new file mode 100644 index 0000000..16bb0a1 --- /dev/null +++ b/scripts/stats_screen.gd.uid @@ -0,0 +1 @@ +uid://ddgqwj6630ocf diff --git a/scripts/witch.gd b/scripts/witch.gd index 37e49eb..c71df1b 100644 --- a/scripts/witch.gd +++ b/scripts/witch.gd @@ -59,6 +59,7 @@ func _on_collect(DropsBase): func shoot_fireballs(): + StatsManager.on_spell_cast("fireball") match perk_effects.get_mode("fireball.cast"): "shotgun": await _cast_fireball_shotgun() @@ -133,12 +134,14 @@ func _play_fire_sfx() -> void: asp.finished.connect(asp.queue_free) func shoot_fire_swirl(): + StatsManager.on_spell_cast("fire_swirl") var fs = fire_swirl.instantiate() fs.global_position = global_position get_parent().add_child(fs) camera.shake(0.3, 0.8) func shoot_shuriken(): + StatsManager.on_spell_cast("shuriken") for i in range(shuriken_count): var sh = shuriken.instantiate() sh.global_position = global_position @@ -146,6 +149,7 @@ func shoot_shuriken(): await get_tree().create_timer(0.2).timeout func shoot_tornado(): + StatsManager.on_spell_cast("tornado") var target = get_nearest_enemy(global_position) var tw = tornado.instantiate() tw.global_position = target.global_position if target != null else global_position @@ -153,6 +157,7 @@ func shoot_tornado(): camera.shake(0.3, 0.8) func shoot_laser(): + StatsManager.on_spell_cast("laser") var ls = laser.instantiate() ls.global_position = global_position get_parent().add_child(ls) @@ -166,6 +171,7 @@ func shoot_laser(): asp.finished.connect(asp.queue_free) func shoot_purge(): + StatsManager.on_spell_cast("purge") var pg = purge.instantiate() pg.global_position = global_position get_parent().add_child(pg)