implemented statistics logic and stats screen in main menu

main
Meik Schürmann 2026-06-27 12:32:46 +02:00
parent 856aaefb38
commit 8dde32b9d2
15 changed files with 346 additions and 0 deletions

View File

@ -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]

View File

@ -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

View File

@ -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"]

View File

@ -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"]

View File

@ -2,6 +2,7 @@ extends EnemyBase
func _ready() -> void:
super()
enemy_type = "blue_slime"
speed = 15.0
max_hp = 20
hp = max_hp

View File

@ -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()

View File

@ -2,6 +2,7 @@ extends EnemyBase
func _ready() -> void:
super()
enemy_type = "fire_slime"
speed = 15.0
max_hp = 25
hp = max_hp

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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]

View File

@ -0,0 +1 @@
uid://vrgjw34wuhnj

View File

@ -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")

View File

@ -0,0 +1 @@
uid://ddgqwj6630ocf

View File

@ -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)