diff --git a/data/localization/fr.json b/data/localization/fr.json index 08652d1..2403f0b 100644 --- a/data/localization/fr.json +++ b/data/localization/fr.json @@ -1,11 +1,6 @@ { "profile_select.title": "Choisis ton profil", "profile_select.new_profile_button": "Nouveau profil", - "profile_select.delete_button": "Supprimer", - "profile_select.delete_confirm_title": "Supprimer ce profil ?", - "profile_select.delete_confirm_message": "Cette action efface pour toujours la progression de ce profil.", - "profile_select.delete_confirm_yes": "Oui, supprimer", - "profile_select.delete_confirm_no": "Non, annuler", "profile_select.empty_state": "Pas encore de profil : crée le premier !", "profile_create.title": "Crée ton profil", "profile_create.name_label": "Comment t'appelles-tu ?", @@ -116,5 +111,11 @@ "music.marsh": "Marais", "music.meadow": "Prairie", "music.river": "Rivière", - "music.night": "Nuit" + "music.night": "Nuit", + "adult.delete_button": "Supprimer ce profil", + "adult.delete_confirm_title": "Supprimer ce profil ?", + "adult.delete_confirm_message": "Effacer pour toujours le profil « %s » et toute sa progression ? Pense à l'exporter avant si tu veux pouvoir le restaurer.", + "adult.delete_confirm_yes": "Oui, supprimer", + "adult.delete_confirm_no": "Non, annuler", + "adult.delete_done": "Profil supprimé." } diff --git a/docs/specs/decisions.md b/docs/specs/decisions.md index 7e692e3..742f1d9 100644 --- a/docs/specs/decisions.md +++ b/docs/specs/decisions.md @@ -354,3 +354,7 @@ Ajouts à l'arborescence proposée : **Phrases quasi identiques** : 14 énoncés (QCM et corrections) trop proches d'une autre phrase (ex. « Les petits chiens courent. » / « Les petits chiens jouent. » / « Les petits chiens courent vite. ») ont été remplacés par des phrases originales distinctes, indices et explications ajustés. Garde-fou permanent `tests/data/test_no_near_duplicate_sentences.gd` : deux phrases d'au moins 4 mots ne peuvent pas partager 75 % ou plus de leurs mots. Les dictées (audio déjà généré) n'ont pas été modifiées. **Voix** : l'utilisateur confirme que la voix de synthèse (« La fleur est belle. ») fonctionne avec le build Linux. + +## 37. Suppression d'un profil réservée à l'adulte (version 0.5.6) + +Demande utilisateur. Le bouton « Supprimer » est retiré de la sélection de profil (côté enfant) ; la suppression se fait dans l'espace adulte, derrière la barrière (point 32), via « Supprimer ce profil » avec une confirmation explicite qui nomme le profil et rappelle de l'exporter avant. Une suppression n'affecte jamais un autre profil. Le cahier des charges §9 (confirmation explicite) est respecté. diff --git a/docs/testing/manual-checklist.md b/docs/testing/manual-checklist.md index d83fed6..3b1683d 100644 --- a/docs/testing/manual-checklist.md +++ b/docs/testing/manual-checklist.md @@ -20,7 +20,7 @@ Tout ce qui suit ne peut pas être vérifié sans écran (rendu, mise en page, a 12quater. Score et récompenses (point 30 de `decisions.md`) : terminer une mission sans aucune erreur donne 3 étoiles, avec un indice 2, avec une réponse montrée 1 (message encourageant, jamais de blâme) ; la carte affiche « Total d'étoiles » et « Étoiles : n/3 » sur chaque lieu sans texte coupé ; rejouer avec un meilleur score affiche « Nouveau record » et n'ajoute que la différence ; « Ma collection » montre les objets trouvés et les objets mystère avec la façon de les obtenir (liste défilante). Quitter puis relancer : étoiles et objets conservés. 12bis. Répéter rapidement les points 8-12 sur une mission de chacune des deux nouvelles règles (« La ruche qui se multiplie » pour le pluriel du nom, « Le sentier des bons compagnons » pour l'accord du déterminant) : jamais vu à l'écran, seulement validé en headless (point 25 de `decisions.md`). 13. Quitter le jeu puis relancer `make run` : le profil et sa progression doivent être conservés (sélectionner le même profil, vérifier que l'état affiché sur la carte a changé par rapport à l'état initial). -14. Depuis la sélection de profil, supprimer un profil : une confirmation explicite doit apparaître avant la suppression effective ; annuler doit ne rien supprimer. +14. La suppression d'un profil n'est plus sur la sélection de profil (point 37 de `decisions.md`) : Réglages → Espace adulte (barrière) → choisir le profil → « Supprimer ce profil » : une confirmation explicite nomme le profil ; annuler ne supprime rien ; les autres profils restent intacts. ## Accessibilité et interface diff --git a/project.godot b/project.godot index 7ae2814..56f8108 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Les Aventuriers des mots" -config/version="0.5.5" +config/version="0.5.6" run/main_scene="res://scenes/boot/boot.tscn" config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" diff --git a/scenes/profiles/profile_select.gd b/scenes/profiles/profile_select.gd index 5e6fa50..27029ca 100644 --- a/scenes/profiles/profile_select.gd +++ b/scenes/profiles/profile_select.gd @@ -1,32 +1,21 @@ extends Control -## Ecran de selection de profil : un profil par enfant, suppression avec -## confirmation explicite (cahier des charges §9). - -var _pending_delete_profile_id: String = "" +## Ecran de selection de profil : un profil par enfant. La suppression d'un +## profil appartient a l'adulte (espace adulte, derriere sa barriere). @onready var _title_label: Label = %TitleLabel @onready var _profile_list: VBoxContainer = %ProfileList @onready var _empty_state_label: Label = %EmptyStateLabel @onready var _new_profile_button: Button = %NewProfileButton @onready var _settings_button: Button = %SettingsButton -@onready var _delete_confirm_dialog: ConfirmationDialog = %DeleteConfirmDialog func _ready() -> void: _title_label.text = UIText.get_text("profile_select.title") _new_profile_button.text = UIText.get_text("profile_select.new_profile_button") _empty_state_label.text = UIText.get_text("profile_select.empty_state") - _delete_confirm_dialog.title = UIText.get_text("profile_select.delete_confirm_title") - _delete_confirm_dialog.dialog_text = UIText.get_text("profile_select.delete_confirm_message") - _delete_confirm_dialog.ok_button_text = UIText.get_text("profile_select.delete_confirm_yes") - _delete_confirm_dialog.cancel_button_text = UIText.get_text( - "profile_select.delete_confirm_no" - ) - _settings_button.text = UIText.get_text("profile_select.settings_button") _settings_button.pressed.connect(_on_settings_pressed) _new_profile_button.pressed.connect(_on_new_profile_pressed) - _delete_confirm_dialog.confirmed.connect(_on_delete_confirmed) AudioManager.play_region_music("CE1") _refresh_profile_list() @@ -54,12 +43,6 @@ func _build_profile_row(summary: Dictionary) -> Control: select_button.pressed.connect(_on_profile_selected.bind(summary["profile_id"])) row.add_child(select_button) - var delete_button := Button.new() - delete_button.theme_type_variation = &"SecondaryButton" - delete_button.text = UIText.get_text("profile_select.delete_button") - delete_button.pressed.connect(_on_delete_requested.bind(summary["profile_id"])) - row.add_child(delete_button) - return row @@ -68,19 +51,6 @@ func _on_profile_selected(profile_id: String) -> void: GameManager.goto(GameManager.SCENE_WORLD_MAP) -func _on_delete_requested(profile_id: String) -> void: - _pending_delete_profile_id = profile_id - _delete_confirm_dialog.popup_centered() - - -func _on_delete_confirmed() -> void: - if _pending_delete_profile_id.is_empty(): - return - ProfileManager.delete_profile(_pending_delete_profile_id) - _pending_delete_profile_id = "" - _refresh_profile_list() - - func _on_new_profile_pressed() -> void: GameManager.goto(GameManager.SCENE_PROFILE_CREATE) diff --git a/scenes/profiles/profile_select.tscn b/scenes/profiles/profile_select.tscn index 71d0381..679e8f5 100644 --- a/scenes/profiles/profile_select.tscn +++ b/scenes/profiles/profile_select.tscn @@ -75,6 +75,3 @@ unique_name_in_owner = true layout_mode = 2 theme_type_variation = &"SecondaryButton" custom_minimum_size = Vector2(0, 52) - -[node name="DeleteConfirmDialog" type="ConfirmationDialog" parent="."] -unique_name_in_owner = true diff --git a/scenes/ui/adult_space.gd b/scenes/ui/adult_space.gd index ab807d4..466c0df 100644 --- a/scenes/ui/adult_space.gd +++ b/scenes/ui/adult_space.gd @@ -21,6 +21,8 @@ var _schema: Dictionary = {} @onready var _summary_label: Label = %SummaryLabel @onready var _skills_box: VBoxContainer = %SkillsBox @onready var _export_button: Button = %ExportButton +@onready var _delete_button: Button = %DeleteButton +@onready var _delete_dialog: ConfirmationDialog = %DeleteDialog @onready var _transfer_status_label: Label = %TransferStatusLabel @onready var _restore_title_label: Label = %RestoreTitleLabel @onready var _restore_list: VBoxContainer = %RestoreList @@ -33,6 +35,12 @@ func _ready() -> void: _title_label.text = UIText.get_text("adult.title") _set_gate_texts() _export_button.text = UIText.get_text("adult.export_button") + _delete_button.text = UIText.get_text("adult.delete_button") + _delete_button.pressed.connect(_on_delete_pressed) + _delete_dialog.title = UIText.get_text("adult.delete_confirm_title") + _delete_dialog.ok_button_text = UIText.get_text("adult.delete_confirm_yes") + _delete_dialog.cancel_button_text = UIText.get_text("adult.delete_confirm_no") + _delete_dialog.confirmed.connect(_on_delete_confirmed) _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") @@ -80,6 +88,7 @@ func _refresh_profiles() -> void: _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 + _delete_button.disabled = not has_profiles if has_profiles: _profile_option.select(0) _show_profile(_profile_option.get_item_metadata(0)) @@ -194,6 +203,24 @@ func _refresh_restore_list() -> void: _restore_list.add_child(button) +func _on_delete_pressed() -> void: + if _selected_profile_id.is_empty(): + return + var index := _profile_option.selected + _delete_dialog.dialog_text = ( + UIText.get_text("adult.delete_confirm_message") % _profile_option.get_item_text(index) + ) + _delete_dialog.popup_centered() + + +func _on_delete_confirmed() -> void: + if _selected_profile_id.is_empty(): + return + ProfileManager.delete_profile(_selected_profile_id) + _transfer_status_label.text = UIText.get_text("adult.delete_done") + _refresh_profiles() + + func _on_restore_pressed(path: String) -> void: var new_id := ProfileManager.new_profile_id() if SaveTransfer.import_as_new_profile(path, _schema, new_id): diff --git a/scenes/ui/adult_space.tscn b/scenes/ui/adult_space.tscn index 914a117..a4790d6 100644 --- a/scenes/ui/adult_space.tscn +++ b/scenes/ui/adult_space.tscn @@ -116,6 +116,12 @@ unique_name_in_owner = true layout_mode = 2 autowrap_mode = 2 +[node name="DeleteButton" type="Button" parent="Margin/Card/VBox/ContentBox/Scroll/Inner"] +unique_name_in_owner = true +layout_mode = 2 +theme_type_variation = &"SecondaryButton" +custom_minimum_size = Vector2(0, 52) + [node name="RestoreTitleLabel" type="Label" parent="Margin/Card/VBox/ContentBox/Scroll/Inner"] unique_name_in_owner = true layout_mode = 2 @@ -137,3 +143,6 @@ unique_name_in_owner = true layout_mode = 2 theme_type_variation = &"SecondaryButton" custom_minimum_size = Vector2(0, 52) + +[node name="DeleteDialog" type="ConfirmationDialog" parent="."] +unique_name_in_owner = true diff --git a/tests/world/test_settings_scenes.gd b/tests/world/test_settings_scenes.gd index 4f1d287..0915a7f 100644 --- a/tests/world/test_settings_scenes.gd +++ b/tests/world/test_settings_scenes.gd @@ -29,3 +29,44 @@ func test_adult_space_hides_content_until_gate_is_solved() -> void: scene._on_gate_submitted() assert_false(gate.visible) assert_true(content.visible) + + +func test_profile_deletion_belongs_to_the_adult_space() -> void: + var profile_id := ProfileManager.create_profile("ASupprimer", "CE1") + var other_id := ProfileManager.create_profile("AGarder", "CE1") + + # Choix de profil : aucun bouton « Supprimer » pour l'enfant. + var select_scene: Control = add_child_autofree( + load("res://scenes/profiles/profile_select.tscn").instantiate() + ) + await wait_process_frames(1) + for button in select_scene.find_children("*", "Button", true, false): + assert_ne((button as Button).text, "Supprimer", "pas de suppression cote enfant") + assert_null(select_scene.find_child("DeleteConfirmDialog", true, false)) + + # Espace adulte : apres la barriere, suppression avec confirmation. + var scene: Control = add_child_autofree(ADULT_SCENE.instantiate()) + scene.get_node("%GateInput").text = str(scene._challenge["answer"]) + scene._on_gate_submitted() + await wait_process_frames(1) + + var option: OptionButton = scene.get_node("%ProfileOption") + var target_index := -1 + for i in option.item_count: + if option.get_item_metadata(i) == profile_id: + target_index = i + assert_ne(target_index, -1) + option.select(target_index) + scene._on_profile_option_selected(target_index) + + scene._on_delete_pressed() + var dialog: ConfirmationDialog = scene.get_node("%DeleteDialog") + assert_true(dialog.visible, "une confirmation doit etre demandee") + assert_true(ProfileManager.list_profiles().has(profile_id), "rien n'est supprime avant") + dialog.hide() + + scene._on_delete_confirmed() + assert_false(ProfileManager.list_profiles().has(profile_id)) + assert_true(ProfileManager.list_profiles().has(other_id), "les autres profils sont intacts") + + ProfileManager.delete_profile(other_id)