implemented achievement system with screen and unlockables

main
Meik Schürmann 2026-06-27 16:08:00 +02:00
parent 8dde32b9d2
commit 7c3d95aa9f
10 changed files with 507 additions and 17 deletions

View File

@ -24,6 +24,7 @@ buses/default_bus_layout="uid://5oswo22yvmtg"
MusicManager="*uid://dmu7041wi1upt" MusicManager="*uid://dmu7041wi1upt"
SpellLibrary="*res://scripts/SpellLibrary.gd" SpellLibrary="*res://scripts/SpellLibrary.gd"
StatsManager="*res://scripts/stats_manager.gd" StatsManager="*res://scripts/stats_manager.gd"
AchievementManager="*res://scripts/achievement_manager.gd"
[display] [display]

View File

@ -0,0 +1,182 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/achievements_screen.gd" id="1_ach"]
[ext_resource type="Texture2D" uid="uid://2ddggeetjj1t" path="res://assets/images/screenshot_game.png" id="2_ach"]
[ext_resource type="FontFile" uid="uid://8v71dcws4q6o" path="res://assets/fonts/slkscre.ttf" id="3_ach"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_info"]
bg_color = Color(0.04, 0.04, 0.1, 0.9)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.4, 0.3, 0.1, 0.8)
content_margin_left = 10.0
content_margin_top = 8.0
content_margin_right = 10.0
content_margin_bottom = 8.0
[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="AchievementsScreen" type="CanvasLayer"]
script = ExtResource("1_ach")
[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_ach")
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 = -300.0
offset_right = 308.5
offset_bottom = 300.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_ach")
theme_override_font_sizes/font_size = 56
text = "Achievements"
horizontal_alignment = 1
[node name="CounterLabel" type="Label" parent="VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.9, 0.85, 0.75, 1)
theme_override_fonts/font = ExtResource("3_ach")
theme_override_font_sizes/font_size = 20
text = "Unlocked: 0 of 10"
horizontal_alignment = 1
[node name="Spacer1" type="Control" parent="VBoxContainer"]
layout_mode = 2
custom_minimum_size = Vector2(0, 8)
[node name="AchievementGrid" type="GridContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 4
theme_override_constants/h_separation = 6
theme_override_constants/v_separation = 6
columns = 5
[node name="Spacer2" type="Control" parent="VBoxContainer"]
layout_mode = 2
custom_minimum_size = Vector2(0, 8)
[node name="InfoPanel" type="PanelContainer" parent="VBoxContainer"]
custom_minimum_size = Vector2(0, 72)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_info")
[node name="InfoVBox" type="VBoxContainer" parent="VBoxContainer/InfoPanel"]
layout_mode = 2
theme_override_constants/separation = 2
[node name="InfoTitle" type="Label" parent="VBoxContainer/InfoPanel/InfoVBox"]
layout_mode = 2
theme_override_fonts/font = ExtResource("3_ach")
theme_override_font_sizes/font_size = 20
theme_override_colors/font_color = Color(1, 1, 0.55, 1)
[node name="InfoDesc" type="Label" parent="VBoxContainer/InfoPanel/InfoVBox"]
layout_mode = 2
theme_override_fonts/font = ExtResource("3_ach")
theme_override_font_sizes/font_size = 16
theme_override_colors/font_color = Color(0.9, 0.85, 0.75, 1)
text = "Hover over an achievement to see details."
[node name="InfoReward" type="Label" parent="VBoxContainer/InfoPanel/InfoVBox"]
layout_mode = 2
theme_override_fonts/font = ExtResource("3_ach")
theme_override_font_sizes/font_size = 16
theme_override_colors/font_color = Color(0.75, 0.45, 1.0, 1)
[node name="Spacer3" type="Control" parent="VBoxContainer"]
layout_mode = 2
custom_minimum_size = Vector2(0, 8)
[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_ach")
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

@ -18,6 +18,7 @@ func _process(_delta: float) -> void:
func _on_witch_died(killer) -> void: func _on_witch_died(killer) -> void:
StatsManager.end_run(true) StatsManager.end_run(true)
AchievementManager.check_all()
var witch = $World/Witch var witch = $World/Witch
var player = $World/Player var player = $World/Player
$PauseMenu.process_mode = Node.PROCESS_MODE_DISABLED $PauseMenu.process_mode = Node.PROCESS_MODE_DISABLED

View File

@ -116,6 +116,40 @@ theme_override_styles/disabled = SubResource("StyleBoxEmpty_normal")
theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_pressed") theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_pressed")
text = "Play" text = "Play"
[node name="AchievementsButton" type="Button" parent="VBoxContainer" unique_id=444444444]
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 = "Achievements"
[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="OptionsButton" type="Button" parent="VBoxContainer" unique_id=1460263213] [node name="OptionsButton" type="Button" parent="VBoxContainer" unique_id=1460263213]
custom_minimum_size = Vector2(250, 50) custom_minimum_size = Vector2(250, 50)
layout_mode = 2 layout_mode = 2
@ -150,23 +184,6 @@ theme_override_styles/disabled = SubResource("StyleBoxEmpty_normal")
theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_pressed") theme_override_styles/hover_pressed = SubResource("StyleBoxFlat_pressed")
text = "Credits" 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] [node name="QuitButton" type="Button" parent="VBoxContainer" unique_id=1704957427]
custom_minimum_size = Vector2(250, 50) custom_minimum_size = Vector2(250, 50)
layout_mode = 2 layout_mode = 2
@ -187,5 +204,6 @@ text = "Quit"
[connection signal="pressed" from="VBoxContainer/PlayButton" to="." method="_on_play_button_pressed"] [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/OptionsButton" to="." method="_on_options_button_pressed"]
[connection signal="pressed" from="VBoxContainer/CreditsButton" to="." method="_on_credits_button_pressed"] [connection signal="pressed" from="VBoxContainer/CreditsButton" to="." method="_on_credits_button_pressed"]
[connection signal="pressed" from="VBoxContainer/AchievementsButton" to="." method="_on_achievements_button_pressed"]
[connection signal="pressed" from="VBoxContainer/StatsButton" to="." method="_on_stats_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"] [connection signal="pressed" from="VBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"]

View File

@ -0,0 +1,193 @@
extends Node
const SAVE_PATH = "user://achievements.json"
var achievements: Array = []
func _ready() -> void:
_define()
_load()
check_all()
func _define() -> void:
achievements = [
{
"id": "first_blood",
"title": "First Blood",
"description": "Kill your very first enemy.",
"condition_type": "kills_total",
"threshold": 1,
"reward": "+10 Max HP at the start of every run",
"color": Color(0.55, 0.08, 0.08),
"icon_char": "I",
"unlocked": false
},
{
"id": "slime_slayer",
"title": "Slime Slayer",
"description": "Kill 25 Blue Slimes.",
"condition_type": "kills_blue_slime",
"threshold": 25,
"reward": "Shurikens pierce 1 additional enemy",
"color": Color(0.15, 0.2, 0.65),
"icon_char": "B",
"unlocked": false
},
{
"id": "fire_bane",
"title": "Fire Bane",
"description": "Kill 50 Fire Slimes.",
"condition_type": "kills_fire_slime",
"threshold": 50,
"reward": "Fireball permanently deals +20% more damage",
"color": Color(0.75, 0.28, 0.05),
"icon_char": "F",
"unlocked": false
},
{
"id": "apprentice",
"title": "Apprentice",
"description": "Cast 50 spells in total.",
"condition_type": "spells_total",
"threshold": 50,
"reward": "Cauldron brews with only 2 fruits instead of 3",
"color": Color(0.45, 0.1, 0.7),
"icon_char": "A",
"unlocked": false
},
{
"id": "slime_hunter",
"title": "Slime Hunter",
"description": "Kill 100 enemies in total.",
"condition_type": "kills_total",
"threshold": 100,
"reward": "Each run starts with 1 random fruit already in the cauldron",
"color": Color(0.15, 0.5, 0.15),
"icon_char": "H",
"unlocked": false
},
{
"id": "survivor",
"title": "Survivor",
"description": "Survive 3 minutes in a single run.",
"condition_type": "survive_best",
"threshold": 180,
"reward": "Regenerate 5 HP every 10 seconds",
"color": Color(0.1, 0.4, 0.65),
"icon_char": "S",
"unlocked": false
},
{
"id": "veteran",
"title": "Veteran",
"description": "Play 15 runs.",
"condition_type": "runs_played",
"threshold": 15,
"reward": "Start each run with 1 random perk already applied",
"color": Color(0.6, 0.45, 0.05),
"icon_char": "V",
"unlocked": false
},
{
"id": "archmage",
"title": "Archmage",
"description": "Cast 300 spells in total.",
"condition_type": "spells_total",
"threshold": 300,
"reward": "All spells permanently deal +15% more damage",
"color": Color(0.25, 0.05, 0.5),
"icon_char": "M",
"unlocked": false
},
{
"id": "iron_witch",
"title": "Iron Witch",
"description": "Survive 7 minutes in a single run.",
"condition_type": "survive_best",
"threshold": 420,
"reward": "+25 Max HP permanently",
"color": Color(0.25, 0.35, 0.5),
"icon_char": "W",
"unlocked": false
},
{
"id": "exterminator",
"title": "Exterminator",
"description": "Kill 500 enemies in total.",
"condition_type": "kills_total",
"threshold": 500,
"reward": "Level-up screen shows 4 perk cards instead of 3",
"color": Color(0.65, 0.05, 0.05),
"icon_char": "X",
"unlocked": false
},
]
func check_all() -> void:
var changed = false
for ach in achievements:
if ach.unlocked:
continue
if _meets(ach):
ach.unlocked = true
changed = true
if changed:
_save()
func _meets(ach: Dictionary) -> bool:
var d = StatsManager.data
var t: int = ach.threshold
match ach.condition_type:
"kills_total":
var total = 0
for v in d["kills"].values():
total += v
return total >= t
"kills_blue_slime":
return d["kills"]["blue_slime"] >= t
"kills_fire_slime":
return d["kills"]["fire_slime"] >= t
"spells_total":
var total = 0
for v in d["spells_cast"].values():
total += v
return total >= t
"survive_best":
return d["longest_run_seconds"] >= t
"runs_played":
return d["runs_played"] >= t
return false
func get_unlocked_count() -> int:
var n = 0
for ach in achievements:
if ach.unlocked:
n += 1
return n
func _save() -> void:
var ids: Array = []
for ach in achievements:
if ach.unlocked:
ids.append(ach.id)
var f = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if f == null:
return
f.store_string(JSON.stringify(ids))
f.close()
func _load() -> void:
if not FileAccess.file_exists(SAVE_PATH):
return
var f = FileAccess.open(SAVE_PATH, FileAccess.READ)
if f == null:
return
var j = JSON.new()
var text = f.get_as_text()
f.close()
if j.parse(text) != OK:
return
var ids = j.get_data()
if typeof(ids) != TYPE_ARRAY:
return
for ach in achievements:
ach.unlocked = ach.id in ids

View File

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

View File

@ -0,0 +1,89 @@
extends CanvasLayer
@onready var _grid: GridContainer = $VBoxContainer/AchievementGrid
@onready var _counter: Label = $VBoxContainer/CounterLabel
@onready var _info_title: Label = $VBoxContainer/InfoPanel/InfoVBox/InfoTitle
@onready var _info_desc: Label = $VBoxContainer/InfoPanel/InfoVBox/InfoDesc
@onready var _info_reward: Label = $VBoxContainer/InfoPanel/InfoVBox/InfoReward
var _font = preload("res://assets/fonts/slkscre.ttf")
var _frame_tex = preload("res://assets/Tiny RPG Mana Soul GUI v1.0/20250420manaSoul9SlicesA-Sheet.png")
func _ready() -> void:
AchievementManager.check_all()
_update_counter()
_build_grid()
_clear_info()
func _build_grid() -> void:
for ach in AchievementManager.achievements:
_grid.add_child(_make_slot(ach))
func _make_slot(ach: Dictionary) -> Control:
var slot := Control.new()
slot.custom_minimum_size = Vector2(80, 80)
slot.mouse_filter = Control.MOUSE_FILTER_STOP
var bg := ColorRect.new()
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
bg.color = ach.color if ach.unlocked else Color(0.06, 0.06, 0.1, 1.0)
slot.add_child(bg)
var frame := NinePatchRect.new()
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
frame.texture = _frame_tex
frame.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
frame.patch_margin_left = 8
frame.patch_margin_right = 8
frame.patch_margin_top = 8
frame.patch_margin_bottom = 8
if not ach.unlocked:
frame.modulate = Color(0.4, 0.4, 0.4, 1.0)
slot.add_child(frame)
var lbl := Label.new()
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
lbl.text = ach.icon_char
lbl.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
lbl.add_theme_font_override("font", _font)
lbl.add_theme_font_size_override("font_size", 26)
if ach.unlocked:
lbl.add_theme_color_override("font_color", Color(1.0, 1.0, 0.9, 1.0))
lbl.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.9))
lbl.add_theme_constant_override("outline_size", 2)
else:
lbl.add_theme_color_override("font_color", Color(0.22, 0.22, 0.22, 1.0))
slot.add_child(lbl)
slot.mouse_entered.connect(_on_slot_enter.bind(ach))
slot.mouse_exited.connect(_on_slot_exit)
return slot
func _on_slot_enter(ach: Dictionary) -> void:
if ach.unlocked:
_info_title.text = ach.title + " [UNLOCKED]"
_info_title.add_theme_color_override("font_color", Color(1.0, 1.0, 0.55, 1.0))
else:
_info_title.text = ach.title + " [LOCKED]"
_info_title.add_theme_color_override("font_color", Color(0.75, 0.45, 1.0, 1.0))
_info_desc.text = ach.description
_info_reward.text = "Reward: " + ach.reward
func _on_slot_exit() -> void:
_clear_info()
func _clear_info() -> void:
_info_title.text = ""
_info_desc.text = "Hover over an achievement to see details."
_info_reward.text = ""
func _update_counter() -> void:
var n := AchievementManager.get_unlocked_count()
var total := AchievementManager.achievements.size()
_counter.text = "Unlocked: %d of %d" % [n, total]
func _on_back_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/mainmenu.tscn")

View File

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

View File

@ -12,6 +12,9 @@ func _on_options_button_pressed() -> void:
func _on_credits_button_pressed() -> void: func _on_credits_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/credits.tscn") get_tree().change_scene_to_file("res://scenes/credits.tscn")
func _on_achievements_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/achievements_screen.tscn")
func _on_stats_button_pressed() -> void: func _on_stats_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/stats_screen.tscn") get_tree().change_scene_to_file("res://scenes/stats_screen.tscn")

View File

@ -19,6 +19,7 @@ func _on_continue_button_pressed() -> void:
func _on_quit_button_pressed() -> void: func _on_quit_button_pressed() -> void:
StatsManager.end_run(false) StatsManager.end_run(false)
AchievementManager.check_all()
get_tree().quit() get_tree().quit()