Add settings screen and adult space with progress report and save export/restore (0.4.0)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Denis Defolie
2026-10-11 22:49:01 +02:00
parent 97a065879c
commit dbf513e686
45 changed files with 1390 additions and 14 deletions
+207
View File
@@ -0,0 +1,207 @@
extends Control
## Espace adulte (cahier des charges §9 et §10) : tableau de progres par
## profil, notions a retravailler, export et restauration des sauvegardes.
## Derriere une petite barriere (AdultGate). Aucune action ici n'est
## destructive : la restauration cree un nouveau profil, l'export ecrit un
## nouveau fichier.
var _challenge: Dictionary = {}
var _rng := RandomNumberGenerator.new()
var _selected_profile_id := ""
var _schema: Dictionary = {}
@onready var _title_label: Label = %TitleLabel
@onready var _gate_box: VBoxContainer = %GateBox
@onready var _gate_question_label: Label = %GateQuestionLabel
@onready var _gate_input: LineEdit = %GateInput
@onready var _gate_error_label: Label = %GateErrorLabel
@onready var _gate_ok_button: Button = %GateOkButton
@onready var _content_box: VBoxContainer = %ContentBox
@onready var _profile_option: OptionButton = %ProfileOption
@onready var _summary_label: Label = %SummaryLabel
@onready var _skills_box: VBoxContainer = %SkillsBox
@onready var _export_button: Button = %ExportButton
@onready var _transfer_status_label: Label = %TransferStatusLabel
@onready var _restore_title_label: Label = %RestoreTitleLabel
@onready var _restore_list: VBoxContainer = %RestoreList
@onready var _folder_label: Label = %FolderLabel
@onready var _back_button: Button = %BackButton
func _ready() -> void:
_schema = DataLoader.load_schema("save")
_title_label.text = UIText.get_text("adult.title")
_set_gate_texts()
_export_button.text = UIText.get_text("adult.export_button")
_restore_title_label.text = UIText.get_text("adult.restore_title")
_folder_label.text = UIText.get_text("adult.folder_label") % SaveTransfer.default_export_dir()
_back_button.text = UIText.get_text("adult.back_button")
_gate_ok_button.pressed.connect(_on_gate_submitted)
_gate_input.text_submitted.connect(_on_gate_text_submitted)
_export_button.pressed.connect(_on_export_pressed)
_profile_option.item_selected.connect(_on_profile_option_selected)
_back_button.pressed.connect(_on_back_pressed)
_rng.randomize()
_challenge = AdultGate.make_challenge(_rng)
_gate_question_label.text = (
UIText.get_text("adult.gate_question") % [_challenge["a"], _challenge["b"]]
)
_gate_box.visible = true
_content_box.visible = false
func _set_gate_texts() -> void:
_gate_ok_button.text = UIText.get_text("adult.gate_button")
_gate_error_label.text = UIText.get_text("adult.gate_error")
_gate_error_label.visible = false
func _on_gate_text_submitted(_text: String) -> void:
_on_gate_submitted()
func _on_gate_submitted() -> void:
if not AdultGate.is_correct(_challenge, _gate_input.text):
_gate_error_label.visible = true
_gate_input.clear()
return
_gate_box.visible = false
_content_box.visible = true
_refresh_profiles()
_refresh_restore_list()
func _refresh_profiles() -> void:
_profile_option.clear()
var summaries := ProfileManager.list_profile_summaries()
for summary: Dictionary in summaries:
_profile_option.add_item("%s (%s)" % [summary["display_name"], summary["school_level"]])
_profile_option.set_item_metadata(_profile_option.item_count - 1, summary["profile_id"])
var has_profiles := not summaries.is_empty()
_export_button.disabled = not has_profiles
if has_profiles:
_profile_option.select(0)
_show_profile(_profile_option.get_item_metadata(0))
else:
_selected_profile_id = ""
_summary_label.text = UIText.get_text("adult.no_profile")
_clear_skills()
func _on_profile_option_selected(index: int) -> void:
_show_profile(_profile_option.get_item_metadata(index))
func _show_profile(profile_id: String) -> void:
_selected_profile_id = profile_id
var save: Variant = SaveManager.load(profile_id)
_clear_skills()
if save == null:
_summary_label.text = UIText.get_text("adult.profile_unreadable")
return
var level: String = save["school_level"]
var report := ProgressReport.build(
save, ExerciseEngine.get_missions_for_level(level).size(), ExerciseEngine.get_reward_items().size()
)
_summary_label.text = UIText.get_text("adult.summary") % [
report["stars"],
report["missions_done"],
report["missions_total"],
report["items_owned"],
report["items_total"],
report["session_count"],
]
var skills: Array = report["skills"]
if skills.is_empty():
_skills_box.add_child(_skill_label(UIText.get_text("adult.no_skills_yet"), false))
return
for row: Dictionary in skills:
_skills_box.add_child(_skill_label(_skill_text(row), row["needs_work"]))
func _skill_text(row: Dictionary) -> String:
var line := (
UIText.get_text("adult.skill_line")
% [
UIText.get_text("skill.%s" % row["skill_id"]),
UIText.get_text("world_map.status_%s" % row["status"]),
row["successes"],
row["attempts"],
row["hints_used"],
]
)
if row["needs_work"]:
line += "\n" + UIText.get_text("adult.needs_work")
return line
func _skill_label(text: String, highlight: bool) -> Control:
var card := PanelContainer.new()
card.theme_type_variation = &"SoftCard"
var label := Label.new()
label.text = text
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
if highlight:
label.theme_type_variation = &"Emphasis"
card.add_child(label)
return card
func _clear_skills() -> void:
for child in _skills_box.get_children():
_skills_box.remove_child(child)
child.queue_free()
func _on_export_pressed() -> void:
if _selected_profile_id.is_empty():
return
var path := SaveTransfer.export_profile(
_selected_profile_id, _schema, SaveTransfer.default_export_dir()
)
if path.is_empty():
_transfer_status_label.text = UIText.get_text("adult.export_failed")
else:
_transfer_status_label.text = UIText.get_text("adult.export_done") % path
_refresh_restore_list()
func _refresh_restore_list() -> void:
for child in _restore_list.get_children():
_restore_list.remove_child(child)
child.queue_free()
var exports := SaveTransfer.list_exports(SaveTransfer.default_export_dir(), _schema)
if exports.is_empty():
var empty := Label.new()
empty.text = UIText.get_text("adult.restore_empty")
empty.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_restore_list.add_child(empty)
return
for entry: Dictionary in exports:
var button := Button.new()
button.theme_type_variation = &"SecondaryButton"
button.custom_minimum_size = Vector2(0, 48)
if entry["valid"]:
button.text = (
UIText.get_text("adult.restore_entry")
% [entry["display_name"], entry["school_level"], entry["path"].get_file()]
)
button.pressed.connect(_on_restore_pressed.bind(entry["path"]))
else:
button.text = UIText.get_text("adult.restore_invalid") % entry["path"].get_file()
button.disabled = true
_restore_list.add_child(button)
func _on_restore_pressed(path: String) -> void:
var new_id := ProfileManager.new_profile_id()
if SaveTransfer.import_as_new_profile(path, _schema, new_id):
_transfer_status_label.text = UIText.get_text("adult.restore_done")
_refresh_profiles()
else:
_transfer_status_label.text = UIText.get_text("adult.restore_failed")
func _on_back_pressed() -> void:
GameManager.goto(GameManager.SCENE_SETTINGS)
+1
View File
@@ -0,0 +1 @@
uid://xselbc7st0o8
+139
View File
@@ -0,0 +1,139 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://scenes/ui/adult_space.gd" id="1"]
[ext_resource type="PackedScene" path="res://scenes/ui/backdrop.tscn" id="backdrop"]
[node name="AdultSpace" 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")
[node name="Backdrop" parent="." instance=ExtResource("backdrop")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Margin" 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 = 24
theme_override_constants/margin_right = 40
theme_override_constants/margin_bottom = 24
[node name="Card" type="PanelContainer" parent="Margin"]
layout_mode = 2
[node name="VBox" type="VBoxContainer" parent="Margin/Card"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="TitleLabel" type="Label" parent="Margin/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"CardTitle"
horizontal_alignment = 1
[node name="GateBox" type="VBoxContainer" parent="Margin/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 12
alignment = 1
[node name="GateQuestionLabel" type="Label" parent="Margin/Card/VBox/GateBox"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Emphasis"
horizontal_alignment = 1
[node name="GateInput" type="LineEdit" parent="Margin/Card/VBox/GateBox"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(0, 56)
alignment = 1
[node name="GateErrorLabel" type="Label" parent="Margin/Card/VBox/GateBox"]
unique_name_in_owner = true
layout_mode = 2
horizontal_alignment = 1
[node name="GateOkButton" type="Button" parent="Margin/Card/VBox/GateBox"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(0, 56)
[node name="ContentBox" type="VBoxContainer" parent="Margin/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 10
[node name="ProfileOption" type="OptionButton" parent="Margin/Card/VBox/ContentBox"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(0, 48)
[node name="Scroll" type="ScrollContainer" parent="Margin/Card/VBox/ContentBox"]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="Inner" type="VBoxContainer" parent="Margin/Card/VBox/ContentBox/Scroll"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 10
[node name="SummaryLabel" type="Label" parent="Margin/Card/VBox/ContentBox/Scroll/Inner"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Emphasis"
autowrap_mode = 2
[node name="SkillsBox" type="VBoxContainer" parent="Margin/Card/VBox/ContentBox/Scroll/Inner"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
[node name="ExportButton" type="Button" parent="Margin/Card/VBox/ContentBox/Scroll/Inner"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(0, 52)
[node name="TransferStatusLabel" type="Label" parent="Margin/Card/VBox/ContentBox/Scroll/Inner"]
unique_name_in_owner = true
layout_mode = 2
autowrap_mode = 2
[node name="RestoreTitleLabel" type="Label" parent="Margin/Card/VBox/ContentBox/Scroll/Inner"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Emphasis"
[node name="RestoreList" type="VBoxContainer" parent="Margin/Card/VBox/ContentBox/Scroll/Inner"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
[node name="FolderLabel" type="Label" parent="Margin/Card/VBox/ContentBox/Scroll/Inner"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Small"
autowrap_mode = 2
[node name="BackButton" type="Button" parent="Margin/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"SecondaryButton"
custom_minimum_size = Vector2(0, 52)
+85
View File
@@ -0,0 +1,85 @@
extends Control
## Reglages de l'appareil : volume, taille du texte, animations. Accessible
## depuis la carte et la selection de profil sans barriere (l'enfant peut
## baisser le son lui-meme). L'espace adulte est un bouton separe, derriere
## une petite barriere.
@onready var _title_label: Label = %TitleLabel
@onready var _volume_label: Label = %VolumeLabel
@onready var _volume_slider: HSlider = %VolumeSlider
@onready var _text_size_label: Label = %TextSizeLabel
@onready var _text_size_box: HBoxContainer = %TextSizeBox
@onready var _animations_label: Label = %AnimationsLabel
@onready var _animations_box: HBoxContainer = %AnimationsBox
@onready var _adult_button: Button = %AdultButton
@onready var _back_button: Button = %BackButton
func _ready() -> void:
_title_label.text = UIText.get_text("settings.title")
_volume_label.text = UIText.get_text("settings.volume_label")
_text_size_label.text = UIText.get_text("settings.text_size_label")
_animations_label.text = UIText.get_text("settings.animations_label")
_adult_button.text = UIText.get_text("settings.adult_button")
_back_button.text = UIText.get_text("settings.back_button")
_volume_slider.min_value = 0.0
_volume_slider.max_value = 1.0
_volume_slider.step = 1.0 / AppSettings.VOLUME_STEPS
_volume_slider.value = SettingsManager.volume
_volume_slider.value_changed.connect(_on_volume_changed)
_build_text_size_buttons()
_build_animation_buttons()
_adult_button.pressed.connect(_on_adult_pressed)
_back_button.pressed.connect(_on_back_pressed)
func _build_text_size_buttons() -> void:
var group := ButtonGroup.new()
for index in range(AppSettings.TEXT_SCALES.size()):
var button := Button.new()
button.text = UIText.get_text("settings.text_size_%d" % index)
button.toggle_mode = true
button.button_group = group
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.custom_minimum_size = Vector2(0, 52)
button.button_pressed = index == SettingsManager.text_scale_index
button.pressed.connect(_on_text_size_pressed.bind(index))
_text_size_box.add_child(button)
func _build_animation_buttons() -> void:
var group := ButtonGroup.new()
for enabled: bool in [true, false]:
var button := Button.new()
button.text = UIText.get_text("settings.animations_on" if enabled else "settings.animations_off")
button.toggle_mode = true
button.button_group = group
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.custom_minimum_size = Vector2(0, 52)
button.button_pressed = enabled == SettingsManager.animations_enabled
button.pressed.connect(_on_animations_chosen.bind(enabled))
_animations_box.add_child(button)
func _on_volume_changed(value: float) -> void:
SettingsManager.set_volume(value)
func _on_text_size_pressed(index: int) -> void:
SettingsManager.set_text_scale_index(index)
func _on_animations_chosen(enabled: bool) -> void:
SettingsManager.set_animations_enabled(enabled)
func _on_adult_pressed() -> void:
GameManager.goto(GameManager.SCENE_ADULT_SPACE)
func _on_back_pressed() -> void:
GameManager.goto(GameManager.settings_return_scene)
+1
View File
@@ -0,0 +1 @@
uid://dyg8223otmtcm
+91
View File
@@ -0,0 +1,91 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://scenes/ui/settings.gd" id="1"]
[ext_resource type="PackedScene" path="res://scenes/ui/backdrop.tscn" id="backdrop"]
[node name="Settings" 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")
[node name="Backdrop" parent="." instance=ExtResource("backdrop")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Margin" 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 = 32
theme_override_constants/margin_top = 24
theme_override_constants/margin_right = 32
theme_override_constants/margin_bottom = 24
[node name="Center" type="CenterContainer" parent="Margin"]
layout_mode = 2
[node name="Card" type="PanelContainer" parent="Margin/Center"]
layout_mode = 2
custom_minimum_size = Vector2(640, 0)
[node name="VBox" type="VBoxContainer" parent="Margin/Center/Card"]
layout_mode = 2
theme_override_constants/separation = 14
[node name="TitleLabel" type="Label" parent="Margin/Center/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"CardTitle"
horizontal_alignment = 1
[node name="VolumeLabel" type="Label" parent="Margin/Center/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Emphasis"
[node name="VolumeSlider" type="HSlider" parent="Margin/Center/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(0, 40)
[node name="TextSizeLabel" type="Label" parent="Margin/Center/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Emphasis"
[node name="TextSizeBox" type="HBoxContainer" parent="Margin/Center/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 10
[node name="AnimationsLabel" type="Label" parent="Margin/Center/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"Emphasis"
[node name="AnimationsBox" type="HBoxContainer" parent="Margin/Center/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 10
[node name="AdultButton" type="Button" parent="Margin/Center/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"SecondaryButton"
custom_minimum_size = Vector2(0, 52)
[node name="BackButton" type="Button" parent="Margin/Center/Card/VBox"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(0, 56)