From 2c96cba1747e180a82ddfca25dccc7c0e96b653c Mon Sep 17 00:00:00 2001 From: Samuel Bouchet Date: Wed, 11 Mar 2026 18:33:10 +0100 Subject: [PATCH] Accents and bug tracking --- bugs.md | 73 ++++ content/data/boxes.json | 1 + content/data/items.json | 1 + content/strings/en.json | 4 +- content/strings/fr.json | 410 ++++++++++---------- src/OpenTheBox/Core/Enums/UIFeature.cs | 5 +- src/OpenTheBox/Program.cs | 144 +++++-- src/OpenTheBox/Rendering/BasicRenderer.cs | 4 +- src/OpenTheBox/Rendering/SpectreRenderer.cs | 12 +- tests/OpenTheBox.Tests/UnitTest1.cs | 123 ++++++ 10 files changed, 537 insertions(+), 240 deletions(-) create mode 100644 bugs.md diff --git a/bugs.md b/bugs.md new file mode 100644 index 0000000..58e9dde --- /dev/null +++ b/bugs.md @@ -0,0 +1,73 @@ +# Bug tracker + +Les sujets dans FIXME doivent être corrigé, puis déplacé dans "DONE", puis commit de ce fichier avec le fix. + +# FIXME + +## Adventure error +``` +Que veux-tu faire ? + 1. Ouvrir une boite (1) + 2. Voir l'inventaire + 3. Partir a l'aventure + 4. Sauvegarder + 5. Retourner au menu +> +Entre un nombre entre 1 et 5. +> 3 +Partir a l'aventure + 1. DarkFantasy + 2. Retour +> 1 +ERROR: Adventure error: Unexpected character: ? at (124:81:6496:0) +Appuie sur une touche pour continuer... +``` +=> Les aventures devraient toutes fonctionner +=> écrire des tests pour valider a priori que ce genre de bug ne survient plus + + +## Arms translation +``` +Que veux-tu faire ? + 1. Ouvrir une boite (1) + 2. Voir l'inventaire + 3. Partir a l'aventure + 4. Changer d'apparence + 5. Sauvegarder + 6. Retourner au menu +> 4 +Changer d'apparence + 1. [Arms] Bras normaux + 2. Retour +> 1 +Equipped Arms: Regular +``` +=> "Arms" devrait être traduit en français +=> "Equipped Arms: Regular" devrait être traduit en français +=> Vérifie dans le code qu'il n'y a pas d'autres textes en dur + +## Duplicated cosmetics + +``` +Que veux-tu faire ? + 1. Ouvrir une boite (2) + 2. Voir l'inventaire + 3. Partir a l'aventure + 4. Changer d'apparence + 5. Sauvegarder + 6. Retourner au menu +> 4 +Changer d'apparence + 1. [Arms] Bras normaux + 2. [Hair] Cheveux longs + 3. [Arms] Bras mecaniques + 4. [Hair] Cheveux courts + 5. [Body] T-shirt basique + 6. [Eyes] Yeux verts + 7. Retour +``` +Bien que l'inventaire ne m'indique qu'un seul exemplaire possédé, +observé: le choix multiple m'indique plusieurs fois les même cosmétiques. +Attendu: le choix multiple n'indique qu'une fois chaque cosmétique. + +# DONE \ No newline at end of file diff --git a/content/data/boxes.json b/content/data/boxes.json index 8f4e9da..081c584 100644 --- a/content/data/boxes.json +++ b/content/data/boxes.json @@ -208,6 +208,7 @@ "rollCount": 1, "entries": [ {"itemDefinitionId": "meta_colors", "weight": 5}, + {"itemDefinitionId": "meta_autosave", "weight": 6}, {"itemDefinitionId": "meta_arrows", "weight": 4}, {"itemDefinitionId": "meta_animation", "weight": 4}, {"itemDefinitionId": "box_meta_interface", "weight": 1} diff --git a/content/data/items.json b/content/data/items.json index b52d3b8..8fa79a1 100644 --- a/content/data/items.json +++ b/content/data/items.json @@ -11,6 +11,7 @@ {"id": "meta_shortcuts", "nameKey": "meta.shortcuts", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "KeyboardShortcuts"}, {"id": "meta_animation", "nameKey": "meta.animation", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta"], "metaUnlock": "BoxAnimation"}, {"id": "meta_crafting", "nameKey": "meta.crafting", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "CraftingPanel"}, + {"id": "meta_autosave", "nameKey": "meta.autosave", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta"], "metaUnlock": "AutoSave"}, {"id": "meta_resource_health", "nameKey": "resource.health", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Health"}, {"id": "meta_resource_mana", "nameKey": "resource.mana", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Mana"}, {"id": "meta_resource_food", "nameKey": "resource.food", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Food"}, diff --git a/content/strings/en.json b/content/strings/en.json index 6df0463..b68c2b5 100644 --- a/content/strings/en.json +++ b/content/strings/en.json @@ -425,5 +425,7 @@ "box.endgame.desc": "You found all the resources. This is it. The last box. Are you ready?", "item.endgame_crown": "Crown of Completion", "item.destiny_token": "Token of Destiny", - "adventure.secret_branch_found": "You feel a hidden path revealing itself..." + "adventure.secret_branch_found": "You feel a hidden path revealing itself...", + "meta.autosave": "Auto-Save", + "save.autosaved": "Game auto-saved." } diff --git a/content/strings/fr.json b/content/strings/fr.json index 9aafe1c..6c5f3e5 100644 --- a/content/strings/fr.json +++ b/content/strings/fr.json @@ -1,5 +1,5 @@ { - "game.title": "OUVRE LA BOITE", + "game.title": "OUVRE LA BOÎTE", "game.subtitle": "Qu'est-ce qu'il y a dedans ? Un seul moyen de le savoir.", "game.version": "v0.1.0", @@ -10,139 +10,139 @@ "menu.back": "Retour", "menu.continue": "Continuer", "menu.save": "Sauvegarder", - "menu.settings": "Parametres", + "menu.settings": "Paramètres", - "action.open_box": "Ouvrir une boite", + "action.open_box": "Ouvrir une boîte", "action.inventory": "Voir l'inventaire", "action.craft": "Fabriquer", - "action.adventure": "Partir a l'aventure", + "action.adventure": "Partir à l'aventure", "action.appearance": "Changer d'apparence", "action.save": "Sauvegarder", "action.quit": "Retourner au menu", - "prompt.name": "Quel est ton nom, brave ouvreur de boites ?", + "prompt.name": "Quel est ton nom, brave ouvreur de boîtes ?", "prompt.choose_action": "Que veux-tu faire ?", - "prompt.choose_box": "Quelle boite veux-tu ouvrir ?", + "prompt.choose_box": "Quelle boîte veux-tu ouvrir ?", "prompt.choose_interaction": "Plusieurs interactions possibles ! Choisis-en une :", "prompt.press_key": "Appuie sur une touche pour continuer...", "box.opening": "Ouverture de {0}...", - "box.opened": "{0} ouverte ! (Rarete : {1})", + "box.opened": "{0} ouverte ! (Rareté : {1})", "box.opened_short": "{0} ouverte !", "box.shimmer": "Quelque chose scintille...", - "box.found": "Tu as trouve : {0} !", - "box.found_box": "A l'interieur il y avait... une autre boite ! {0} !", - "box.empty": "La boite est vide ! Philosophique.", - "box.no_boxes": "Tu n'as aucune boite. Comment t'as fait ?", + "box.found": "Tu as trouvé : {0} !", + "box.found_box": "À l'intérieur il y avait... une autre boîte ! {0} !", + "box.empty": "La boîte est vide ! Philosophique.", + "box.no_boxes": "Tu n'as aucune boîte. Comment t'as fait ?", "box.auto_open": "{0} s'ouvre automatiquement !", - "loot.received": "Tu as recu :", + "loot.received": "Tu as reçu :", "loot.title": "Butin !", "loot.name": "Nom", - "loot.rarity": "Rarete", - "loot.category": "Categorie", - "ui.feature_unlocked": "NOUVELLE FONCTIONNALITE : {0}", - "ui.completion": "Completion : {0}%", + "loot.rarity": "Rareté", + "loot.category": "Catégorie", + "ui.feature_unlocked": "NOUVELLE FONCTIONNALITÉ : {0}", + "ui.completion": "Complétion : {0}%", "prompt.what_do": "Que fais-tu ?", "prompt.invalid_choice": "Entre un nombre entre 1 et {0}.", - "box.starter": "Boite de depart", - "box.starter.desc": "Ta premiere boite. Le debut de tout. Ou de rien. Probablement de quelque chose quand meme.", - "box.box_of_boxes": "Boite a boite", - "box.box_of_boxes.desc": "Une boite qui contient... des boites. C'est des boites jusqu'en bas.", - "box.not_great": "Boite pas ouf", - "box.not_great.desc": "Elle est pas geniale. Elle est pas terrible. Elle... est.", - "box.ok_tier": "Boite ok tiers", - "box.ok_tier.desc": "La mediocrite n'a jamais ete aussi carree.", - "box.cool": "Boite coolos", - "box.cool.desc": "La on commence a causer. A causer cool.", - "box.epic": "Boite epique", - "box.epic.desc": "L'orchestre s'intensifie. La foule retient son souffle. C'est... une boite.", - "box.legendhair": "Boite legend'hair", - "box.legendhair.desc": "La coiffure la plus legendaire que tu deballeras jamais. Cheveu-jour-d'hui, legende demain.", - "box.legendary": "Boite legendaire", - "box.legendary.desc": "Les legendes parlent de cette boite. Doucement quand meme, c'est une boite.", - "box.adventure": "Boite aventure", - "box.adventure.desc": "Contient la cle de l'aventure ! Litteralement, parfois.", - "box.style": "Boite stylee", - "box.style.desc": "La mode est ephemere. Le style sorti d'une boite est eternel.", - "box.improvement": "Boite d'amelioration", - "box.improvement.desc": "On peut toujours s'ameliorer. Surtout avec des boites.", - "box.supply": "Boite de fourniture", - "box.supply.desc": "Des fournitures ! Le sang vital de tout passione d'ouverture de boites.", - "box.meta_basics": "Boite Meta - Les Bases", - "box.meta_basics.desc": "Couleurs, fleches, animations. Le fondement de la vision.", - "box.meta_interface": "Boite Meta - L'Interface", - "box.meta_interface.desc": "Panneaux, ressources, stats. Les outils de la comprehension.", - "box.meta_deep": "Boite Meta - Personnalisation", - "box.meta_deep.desc": "Couleurs etendues, artisanat, chat, portrait. Exprime-toi.", - "box.meta_resources": "Boite Meta - Ressources", - "box.meta_resources.desc": "Deverouille la capacite de voir ce que tu as. Et ce qui te manque.", - "box.meta_mastery": "Boite Meta - La Maitrise", - "box.meta_mastery.desc": "Mise en page, stats, polices. Les touches finales d'un vrai maitre des boites.", - "box.black": "Boite noire", - "box.black.desc": "Personne ne sait ce qu'il y a dedans. Meme pas la boite.", - "box.story": "Boite a histoire", - "box.story.desc": "Chaque boite a une histoire. Celle-ci plus que les autres.", - "box.music": "Boite a musique", - "box.music.desc": "Do do do do. La musique de boite c'est la meilleure musique.", - "box.cookie": "Boite a Cookies", - "box.cookie.desc": "La fortune sourit aux audacieux. Et a ceux qui ouvrent des boites.", + "box.starter": "Boîte de départ", + "box.starter.desc": "Ta première boîte. Le début de tout. Ou de rien. Probablement de quelque chose quand même.", + "box.box_of_boxes": "Boîte à boîte", + "box.box_of_boxes.desc": "Une boîte qui contient... des boîtes. C'est des boîtes jusqu'en bas.", + "box.not_great": "Boîte pas ouf", + "box.not_great.desc": "Elle est pas géniale. Elle est pas terrible. Elle... est.", + "box.ok_tier": "Boîte ok tiers", + "box.ok_tier.desc": "La médiocrité n'a jamais été aussi carrée.", + "box.cool": "Boîte coolos", + "box.cool.desc": "Là on commence à causer. À causer cool.", + "box.epic": "Boîte épique", + "box.epic.desc": "L'orchestre s'intensifie. La foule retient son souffle. C'est... une boîte.", + "box.legendhair": "Boîte legend'hair", + "box.legendhair.desc": "La coiffure la plus légendaire que tu déballeras jamais. Cheveu-jour-d'hui, légende demain.", + "box.legendary": "Boîte légendaire", + "box.legendary.desc": "Les légendes parlent de cette boîte. Doucement quand même, c'est une boîte.", + "box.adventure": "Boîte aventure", + "box.adventure.desc": "Contient la clé de l'aventure ! Littéralement, parfois.", + "box.style": "Boîte stylée", + "box.style.desc": "La mode est éphémère. Le style sorti d'une boîte est éternel.", + "box.improvement": "Boîte d'amélioration", + "box.improvement.desc": "On peut toujours s'améliorer. Surtout avec des boîtes.", + "box.supply": "Boîte de fourniture", + "box.supply.desc": "Des fournitures ! Le sang vital de tout passionné d'ouverture de boîtes.", + "box.meta_basics": "Boîte Méta - Les Bases", + "box.meta_basics.desc": "Couleurs, flèches, animations. Le fondement de la vision.", + "box.meta_interface": "Boîte Méta - L'Interface", + "box.meta_interface.desc": "Panneaux, ressources, stats. Les outils de la compréhension.", + "box.meta_deep": "Boîte Méta - Personnalisation", + "box.meta_deep.desc": "Couleurs étendues, artisanat, chat, portrait. Exprime-toi.", + "box.meta_resources": "Boîte Méta - Ressources", + "box.meta_resources.desc": "Déverrouille la capacité de voir ce que tu as. Et ce qui te manque.", + "box.meta_mastery": "Boîte Méta - La Maîtrise", + "box.meta_mastery.desc": "Mise en page, stats, polices. Les touches finales d'un vrai maître des boîtes.", + "box.black": "Boîte noire", + "box.black.desc": "Personne ne sait ce qu'il y a dedans. Même pas la boîte.", + "box.story": "Boîte à histoire", + "box.story.desc": "Chaque boîte a une histoire. Celle-ci plus que les autres.", + "box.music": "Boîte à musique", + "box.music.desc": "Do do do do. La musique de boîte c'est la meilleure musique.", + "box.cookie": "Boîte à Cookies", + "box.cookie.desc": "La fortune sourit aux audacieux. Et à ceux qui ouvrent des boîtes.", - "box.adventure.space": "Boite d'aventure spatiale", - "box.adventure.space.desc": "Vers l'infini et au-dela ! (Boite non incluse dans l'infini)", - "box.adventure.medieval": "Boite d'aventure medievale", - "box.adventure.medieval.desc": "Oyez, oyez ! Une boite d'aventure d'antan !", - "box.adventure.pirate": "Boite d'aventure pirate", - "box.adventure.pirate.desc": "Arr ! X marque la boite !", - "box.adventure.contemporary": "Boite d'aventure contemporaine", - "box.adventure.contemporary.desc": "Une boite pour les temps modernes. Livree avec l'anxiete du WiFi.", - "box.adventure.sentimental": "Boite d'aventure sentimentale", - "box.adventure.sentimental.desc": "Cette boite te fait ressentir des choses. Surtout de la curiosite.", - "box.adventure.prehistoric": "Boite d'aventure prehistorique", - "box.adventure.prehistoric.desc": "Ouga bouga boite. Tres vieille. Beaucoup mystere.", - "box.adventure.cosmic": "Boite d'aventure cosmique", - "box.adventure.cosmic.desc": "L'univers est une boite. Cette boite est un univers.", - "box.adventure.microscopic": "Boite d'aventure microscopique", + "box.adventure.space": "Boîte d'aventure spatiale", + "box.adventure.space.desc": "Vers l'infini et au-delà ! (Boîte non incluse dans l'infini)", + "box.adventure.medieval": "Boîte d'aventure médiévale", + "box.adventure.medieval.desc": "Oyez, oyez ! Une boîte d'aventure d'antan !", + "box.adventure.pirate": "Boîte d'aventure pirate", + "box.adventure.pirate.desc": "Arr ! X marque la boîte !", + "box.adventure.contemporary": "Boîte d'aventure contemporaine", + "box.adventure.contemporary.desc": "Une boîte pour les temps modernes. Livrée avec l'anxiété du WiFi.", + "box.adventure.sentimental": "Boîte d'aventure sentimentale", + "box.adventure.sentimental.desc": "Cette boîte te fait ressentir des choses. Surtout de la curiosité.", + "box.adventure.prehistoric": "Boîte d'aventure préhistorique", + "box.adventure.prehistoric.desc": "Ouga bouga boîte. Très vieille. Beaucoup mystère.", + "box.adventure.cosmic": "Boîte d'aventure cosmique", + "box.adventure.cosmic.desc": "L'univers est une boîte. Cette boîte est un univers.", + "box.adventure.microscopic": "Boîte d'aventure microscopique", "box.adventure.microscopic.desc": "La taille ne compte pas. Sauf quand si. Zoom !", - "box.adventure.darkfantasy": "Boite d'aventure dark fantasy", - "box.adventure.darkfantasy.desc": "Les tenebres t'attendent. Et aussi une boite. Une boite sombre.", + "box.adventure.darkfantasy": "Boîte d'aventure dark fantasy", + "box.adventure.darkfantasy.desc": "Les ténèbres t'attendent. Et aussi une boîte. Une boîte sombre.", - "meta.unlocked": "NOUVELLE FONCTIONNALITE : {0} !", + "meta.unlocked": "NOUVELLE FONCTIONNALITÉ : {0} !", "meta.colors": "Couleurs de texte", - "meta.extended_colors": "Palette de couleurs etendue", - "meta.arrows": "Navigation avec les fleches", + "meta.extended_colors": "Palette de couleurs étendue", + "meta.arrows": "Navigation avec les flèches", "meta.inventory": "Panneau d'inventaire", "meta.resources": "Panneau de ressources", "meta.stats": "Panneau de statistiques", "meta.portrait": "Panneau portrait", "meta.chat": "Panneau de discussion", - "meta.layout": "Mise en page complete", + "meta.layout": "Mise en page complète", "meta.shortcuts": "Raccourcis clavier", - "meta.animation": "Animation d'ouverture de boite", + "meta.animation": "Animation d'ouverture de boîte", "meta.crafting": "Panneau de fabrication", - "meta.completion": "Suivi de completion", + "meta.completion": "Suivi de complétion", "item.rarity.common": "Commun", "item.rarity.uncommon": "Peu commun", "item.rarity.rare": "Rare", - "item.rarity.epic": "Epique", - "item.rarity.legendary": "Legendaire", + "item.rarity.epic": "Épique", + "item.rarity.legendary": "Légendaire", "item.rarity.mythic": "Mythique", - "resource.health": "Sante", + "resource.health": "Santé", "resource.mana": "Mana", "resource.food": "Nourriture", "resource.stamina": "Endurance", "resource.blood": "Sang", "resource.gold": "Or", - "resource.oxygen": "Oxygene", - "resource.energy": "Energie", + "resource.oxygen": "Oxygène", + "resource.energy": "Énergie", "stat.strength": "Force", "stat.intelligence": "Intelligence", "stat.luck": "Chance", "stat.charisma": "Charisme", - "stat.dexterity": "Dexterite", + "stat.dexterity": "Dextérité", "stat.wisdom": "Sagesse", "item.meta_font_consolas": "Police : Consolas", @@ -154,10 +154,10 @@ "cosmetic.hair.long": "Cheveux longs", "cosmetic.hair.ponytail": "Queue de cheval", "cosmetic.hair.braided": "Tresses", - "cosmetic.hair.cyberpunk": "Cheveux neon cyberpunk", + "cosmetic.hair.cyberpunk": "Cheveux néon cyberpunk", "cosmetic.hair.fire": "Cheveux en feu", - "cosmetic.hair.stardust": "Coiffure Poussiere d'Etoile legendaire", - "cosmetic.eyes.none": "Pas d'yeux (mysterieux !)", + "cosmetic.hair.stardust": "Coiffure Poussière d'Étoile légendaire", + "cosmetic.eyes.none": "Pas d'yeux (mystérieux !)", "cosmetic.eyes.blue": "Yeux bleus", "cosmetic.eyes.green": "Yeux verts", "cosmetic.eyes.redorange": "Yeux rouge-orange", @@ -166,31 +166,31 @@ "cosmetic.eyes.sunglasses": "Lunettes de soleil", "cosmetic.eyes.pilotglasses": "Lunettes d'aviateur", "cosmetic.eyes.aircraftglasses": "Lunettes de pilote de chasse", - "cosmetic.eyes.cybernetic": "Yeux cybernetiques", + "cosmetic.eyes.cybernetic": "Yeux cybernétiques", "cosmetic.eyes.magician": "Lunettes de magicien", "cosmetic.body.naked": "Torse nu", "cosmetic.body.regulartshirt": "T-shirt basique", "cosmetic.body.sexytshirt": "T-shirt sexy", "cosmetic.body.suit": "Costume", "cosmetic.body.armored": "Armure", - "cosmetic.body.robotic": "Chassis robotique", + "cosmetic.body.robotic": "Châssis robotique", "cosmetic.legs.none": "Flottant (pas de jambes !)", "cosmetic.legs.naked": "Jambes nues", "cosmetic.legs.slip": "Slip", "cosmetic.legs.short": "Short", "cosmetic.legs.panty": "Culotte", - "cosmetic.legs.rocketboots": "Bottes a reaction", + "cosmetic.legs.rocketboots": "Bottes à réaction", "cosmetic.legs.pegleg": "Jambe de bois", "cosmetic.legs.tentacles": "Tentacules", "cosmetic.arms.none": "Pas de bras (mode T-Rex)", "cosmetic.arms.short": "Bras courts", "cosmetic.arms.regular": "Bras normaux", "cosmetic.arms.long": "Bras longs extensibles", - "cosmetic.arms.mechanical": "Bras mecaniques", + "cosmetic.arms.mechanical": "Bras mécaniques", "cosmetic.arms.wings": "Ailes", "cosmetic.arms.extrapair": "Quatre bras", - "cosmetic.gender_error": "Nouveau genre (ERREUR : les boites n'ont pas de genre. La boite s'excuse pour la confusion.)", + "cosmetic.gender_error": "Nouveau genre (ERREUR : les boîtes n'ont pas de genre. La boîte s'excuse pour la confusion.)", "tint.none": "Naturel", "tint.cyan": "Cyan", @@ -200,10 +200,10 @@ "tint.light": "Clair", "tint.dark": "Sombre", "tint.rainbow": "Arc-en-ciel", - "tint.neon": "Neon", + "tint.neon": "Néon", "tint.silver": "Argent", "tint.gold": "Or", - "tint.void": "Neant", + "tint.void": "Néant", "material.wood": "Bois", "material.bronze": "Bronze", @@ -213,7 +213,7 @@ "material.diamond": "Diamant", "material.carbonfiber": "Fibre de carbone", "material.form.raw": "Brut", - "material.form.refined": "Raffine", + "material.form.refined": "Raffiné", "material.form.nail": "Clou", "material.form.plank": "Planche", "material.form.ingot": "Lingot", @@ -222,118 +222,118 @@ "material.form.dust": "Poudre", "material.form.gem": "Gemme", - "item.health_potion_small": "Petite Potion de Sante", - "item.health_potion_medium": "Potion de Sante Moyenne", - "item.health_potion_large": "Grande Potion de Sante", + "item.health_potion_small": "Petite Potion de Santé", + "item.health_potion_medium": "Potion de Santé Moyenne", + "item.health_potion_large": "Grande Potion de Santé", "item.mana_crystal_small": "Petit Cristal de Mana", "item.mana_crystal_medium": "Cristal de Mana Moyen", "item.food_ration": "Ration alimentaire", "item.stamina_drink": "Boisson d'endurance", "item.blood_vial": "Fiole de sang", "item.gold_pouch": "Bourse d'or", - "item.oxygen_tank": "Reservoir d'oxygene", - "item.energy_cell": "Cellule d'energie", + "item.oxygen_tank": "Réservoir d'oxygène", + "item.energy_cell": "Cellule d'énergie", "item.space.badge": "Badge d'astronaute", - "item.space.phone": "Numero de telephone alien", - "item.space.key": "Cle d'acces au sas", + "item.space.phone": "Numéro de téléphone alien", + "item.space.key": "Clé d'accès au sas", "item.space.map": "Carte stellaire", - "item.space.coordinates": "Coordonnees mysterieuses", + "item.space.coordinates": "Coordonnées mystérieuses", "item.space.helmet": "Casque spatial", "item.medieval.crest": "Blason de chevalier", - "item.medieval.sword": "Replique d'Excalibur", + "item.medieval.sword": "Réplique d'Excalibur", "item.medieval.scroll": "Parchemin ancien", "item.medieval.seal": "Sceau royal", - "item.medieval.key": "Cle du donjon", - "item.pirate.map": "Carte au tresor", - "item.pirate.compass": "Boussole enchantee", + "item.medieval.key": "Clé du donjon", + "item.pirate.map": "Carte au trésor", + "item.pirate.compass": "Boussole enchantée", "item.pirate.feather": "Plume de perroquet", "item.pirate.rum": "Bouteille de rhum", "item.pirate.flag": "Jolly Roger", - "item.pirate.key": "Cle du coffre", + "item.pirate.key": "Clé du coffre", "item.contemporary.phone": "Smartphone", - "item.contemporary.card": "Carte de credit", - "item.contemporary.ticket": "Ticket de metro", - "item.contemporary.usb": "Cle USB suspecte", - "item.contemporary.key": "Cle d'appartement", + "item.contemporary.card": "Carte de crédit", + "item.contemporary.ticket": "Ticket de métro", + "item.contemporary.usb": "Clé USB suspecte", + "item.contemporary.key": "Clé d'appartement", "item.contemporary.badge": "Badge d'entreprise", "item.sentimental.letter": "Lettre d'amour", - "item.sentimental.flower": "Fleur sechee", + "item.sentimental.flower": "Fleur séchée", "item.sentimental.album": "Album photo", - "item.sentimental.melody": "Melodie de boite a musique", + "item.sentimental.melody": "Mélodie de boîte à musique", "item.sentimental.teddy": "Vieil ours en peluche", - "item.sentimental.phone": "Numero de l'ex", + "item.sentimental.phone": "Numéro de l'ex", "item.prehistoric.tooth": "Dent de dinosaure", "item.prehistoric.painting": "Fragment de peinture rupestre", "item.prehistoric.amber": "Pierre d'ambre", "item.prehistoric.club": "Massue en os", "item.prehistoric.fossil": "Fossile de trilobite", - "item.cosmic.shard": "Eclat de nebuleuse", + "item.cosmic.shard": "Éclat de nébuleuse", "item.cosmic.fragment": "Fragment de trou noir", "item.cosmic.crystal": "Cristal de quasar", - "item.cosmic.dust": "Poussiere cosmique", - "item.cosmic.core": "Coeur d'etoile", - "item.microscopic.bacteria": "Echantillon de bacterie sentiente", + "item.cosmic.dust": "Poussière cosmique", + "item.cosmic.core": "Cœur d'étoile", + "item.microscopic.bacteria": "Échantillon de bactérie sentiente", "item.microscopic.dna": "Brin d'ADN luminescent", - "item.microscopic.membrane": "Membrane cellulaire renforcee", + "item.microscopic.membrane": "Membrane cellulaire renforcée", "item.microscopic.mitochondria": "Mitochondrie hyperactive", "item.microscopic.prion": "Prion amical (probablement)", "item.darkfantasy.ring": "Anneau maudit", "item.darkfantasy.rune": "Rune de sang", "item.darkfantasy.cloak": "Cape d'ombre", - "item.darkfantasy.grimoire": "Grimoire du necromancien", - "item.darkfantasy.gem": "Gemme d'ame", - "item.darkfantasy.key": "Cle en os", + "item.darkfantasy.grimoire": "Grimoire du nécromancien", + "item.darkfantasy.gem": "Gemme d'âme", + "item.darkfantasy.key": "Clé en os", "item.resource_max_up": "{0} Max +1", "item.resource_up": "{0} +1", "item.stat_boost": "{0} +1", - "item.resource_max_health": "Amelioration Capacite Sante", - "item.resource_max_mana": "Amelioration Capacite Mana", - "item.resource_max_food": "Amelioration Capacite Nourriture", - "item.resource_max_stamina": "Amelioration Capacite Endurance", - "item.resource_max_gold": "Amelioration Capacite Or", - "item.resource_max_blood": "Amelioration Capacite Sang", - "item.resource_max_oxygen": "Amelioration Capacite Oxygene", - "item.resource_max_energy": "Amelioration Capacite Energie", - "item.music_melody": "Melodie de boite", + "item.resource_max_health": "Amélioration Capacité Santé", + "item.resource_max_mana": "Amélioration Capacité Mana", + "item.resource_max_food": "Amélioration Capacité Nourriture", + "item.resource_max_stamina": "Amélioration Capacité Endurance", + "item.resource_max_gold": "Amélioration Capacité Or", + "item.resource_max_blood": "Amélioration Capacité Sang", + "item.resource_max_oxygen": "Amélioration Capacité Oxygène", + "item.resource_max_energy": "Amélioration Capacité Énergie", + "item.music_melody": "Mélodie de boîte", "item.cookie_fortune": "Fortune Cookie", - "item.mysterious_key": "Cle mysterieuse", - "item.mysterious_key.desc": "Une cle pour... quelque chose. La boite sait, mais la boite ne parle pas.", + "item.mysterious_key": "Clé mystérieuse", + "item.mysterious_key.desc": "Une clé pour... quelque chose. La boîte sait, mais la boîte ne parle pas.", - "lore.fragment_1": "Au commencement, il y avait une boite. La boite contenait une autre boite. Et c'est ainsi que ca a ete, et que ca sera.", - "lore.fragment_2": "L'Ancien Ordre des Ouvreurs de Boites n'a qu'un seul commandement : Tu ouvriras tes boites.", - "lore.fragment_3": "Certains disent que l'univers lui-meme est une boite, attendant d'etre ouverte par quelqu'un d'assez curieux.", - "lore.fragment_4": "La premiere boite a ete ouverte par Farah, qui a trouve a l'interieur le concept d''interieur'.", - "lore.fragment_5": "Malkith a un jour ouvert une boite contenant le son d'une seule main qui applaudit. Personne ne sait ce que ca veut dire.", - "lore.fragment_6": "La legende dit qu'il existe une boite qui contient toutes les autres boites. L'ouvrir causerait un paradoxe. Ou un remboursement.", - "lore.fragment_7": "Duncan a essaye de fermer une boite un jour. Le syndicat des boites s'est mis en greve pendant trois semaines.", - "lore.fragment_8": "Pierrick a construit une machine a ouvrir des boites. Elle s'est ouverte elle-meme. Puis elle a ouvert la machine. Puis elle a ouvert le concept d'ouverture.", - "lore.fragment_9": "Samuel a ecrit le premier manuel d'ouverture de boites. Chapitre 1 : Ouvre la boite. Chapitre 2 : Voir Chapitre 1.", - "lore.fragment_10": "La Boite Noire contient un chat. Ou pas. Jusqu'a ce que tu l'ouvres, elle en contient et n'en contient pas. Le chat est aussi une boite.", + "lore.fragment_1": "Au commencement, il y avait une boîte. La boîte contenait une autre boîte. Et c'est ainsi que ça a été, et que ça sera.", + "lore.fragment_2": "L'Ancien Ordre des Ouvreurs de Boîtes n'a qu'un seul commandement : Tu ouvriras tes boîtes.", + "lore.fragment_3": "Certains disent que l'univers lui-même est une boîte, attendant d'être ouverte par quelqu'un d'assez curieux.", + "lore.fragment_4": "La première boîte a été ouverte par Farah, qui a trouvé à l'intérieur le concept d'« intérieur ».", + "lore.fragment_5": "Malkith a un jour ouvert une boîte contenant le son d'une seule main qui applaudit. Personne ne sait ce que ça veut dire.", + "lore.fragment_6": "La légende dit qu'il existe une boîte qui contient toutes les autres boîtes. L'ouvrir causerait un paradoxe. Ou un remboursement.", + "lore.fragment_7": "Duncan a essayé de fermer une boîte un jour. Le syndicat des boîtes s'est mis en grève pendant trois semaines.", + "lore.fragment_8": "Pierrick a construit une machine à ouvrir des boîtes. Elle s'est ouverte elle-même. Puis elle a ouvert la machine. Puis elle a ouvert le concept d'ouverture.", + "lore.fragment_9": "Samuel a écrit le premier manuel d'ouverture de boîtes. Chapitre 1 : Ouvre la boîte. Chapitre 2 : Voir Chapitre 1.", + "lore.fragment_10": "La Boîte Noire contient un chat. Ou pas. Jusqu'à ce que tu l'ouvres, elle en contient et n'en contient pas. Le chat est aussi une boîte.", - "cookie.1": "Une boite dans une boite reste une boite.", - "cookie.2": "ERREUR : Ce cookie ne contient aucune fortune. Reessayez.", - "cookie.3": "Vous ouvrirez beaucoup de boites. Cette prediction a un taux de precision de 100%.", - "cookie.4": "Le vrai tresor, c'etait les boites qu'on a ouvertes en chemin.", - "cookie.5": "ATTENTION : Les effets secondaires de l'ouverture de boites incluent la joie, la confusion et des bras-tentacules.", - "cookie.6": "Demain tu trouveras une boite. Puis une autre. Puis une autre. Envoyez de l'aide.", - "cookie.7": "Ton nombre porte-bonheur est le nombre de boites que tu as ouvertes. Donc... beaucoup.", - "cookie.8": "Confucius dit : celui qui ouvre boite trouve boite. Celui qui n'ouvre pas boite trouve aussi boite. Boite est inevitable.", - "cookie.9": "Un voyage de mille boites commence par une seule ouverture.", - "cookie.10": "Si tu lis ceci, tu as passe trop de temps a ouvrir des boites. Je rigole, ca n'existe pas.", - "cookie.11": "La boite donne, et la boite redonne des boites.", - "cookie.12": "En Russie sovietique, c'est la boite qui t'ouvre.", - "cookie.13": "Au secours je suis piege dans une usine a fortune cookies a l'interieur d'une boite.", - "cookie.14": "Cette fortune a ete intentionnellement laissee vide. Je rigole. Ou pas ?", - "cookie.15": "Tu es l'elu. Celui qui ouvre les boites. Vraiment une noble vocation.", - "cookie.16": "Plot twist : la boite c'etait les amis qu'on s'est faits en chemin.", - "cookie.17": "Schrodinger a appele. Il veut recuperer son concept de boite.", - "cookie.18": "Si tu ouvres une boite et que personne n'est la pour l'entendre, est-ce que ca fait un loot ?", - "cookie.19": "Aujourd'hui est un bon jour pour ouvrir des boites. Demain aussi. Tous les jours, en fait.", - "cookie.20": "Ton animal totem est une boite. Ton pouvoir special c'est l'ouverture.", + "cookie.1": "Une boîte dans une boîte reste une boîte.", + "cookie.2": "ERREUR : Ce cookie ne contient aucune fortune. Réessayez.", + "cookie.3": "Vous ouvrirez beaucoup de boîtes. Cette prédiction a un taux de précision de 100%.", + "cookie.4": "Le vrai trésor, c'était les boîtes qu'on a ouvertes en chemin.", + "cookie.5": "ATTENTION : Les effets secondaires de l'ouverture de boîtes incluent la joie, la confusion et des bras-tentacules.", + "cookie.6": "Demain tu trouveras une boîte. Puis une autre. Puis une autre. Envoyez de l'aide.", + "cookie.7": "Ton nombre porte-bonheur est le nombre de boîtes que tu as ouvertes. Donc... beaucoup.", + "cookie.8": "Confucius dit : celui qui ouvre boîte trouve boîte. Celui qui n'ouvre pas boîte trouve aussi boîte. Boîte est inévitable.", + "cookie.9": "Un voyage de mille boîtes commence par une seule ouverture.", + "cookie.10": "Si tu lis ceci, tu as passé trop de temps à ouvrir des boîtes. Je rigole, ça n'existe pas.", + "cookie.11": "La boîte donne, et la boîte redonne des boîtes.", + "cookie.12": "En Russie soviétique, c'est la boîte qui t'ouvre.", + "cookie.13": "Au secours je suis piégé dans une usine à fortune cookies à l'intérieur d'une boîte.", + "cookie.14": "Cette fortune a été intentionnellement laissée vide. Je rigole. Ou pas ?", + "cookie.15": "Tu es l'élu. Celui qui ouvre les boîtes. Vraiment une noble vocation.", + "cookie.16": "Plot twist : la boîte c'était les amis qu'on s'est faits en chemin.", + "cookie.17": "Schrödinger a appelé. Il veut récupérer son concept de boîte.", + "cookie.18": "Si tu ouvres une boîte et que personne n'est là pour l'entendre, est-ce que ça fait un loot ?", + "cookie.19": "Aujourd'hui est un bon jour pour ouvrir des boîtes. Demain aussi. Tous les jours, en fait.", + "cookie.20": "Ton animal totem est une boîte. Ton pouvoir spécial c'est l'ouverture.", "character.farah": "Farah", "character.malkith": "Malkith", @@ -345,36 +345,36 @@ "character.pierrick": "Pierrick", "character.nova": "Capitaine Nova", "character.aria": "ARIA", - "character.blackbeard": "Barbe-Noire l'Indeboitable", - "character.mordecai": "Mordecai le Sinistre", - "character.zephyr": "Zephyr", + "character.blackbeard": "Barbe-Noire l'Indéboîtable", + "character.mordecai": "Mordecaï le Sinistre", + "character.zephyr": "Zéphyr", "character.quantum": "Dr. Quantum", "adventure.start": "Commencer l'aventure {0}", "adventure.resume": "Reprendre l'aventure {0}", - "adventure.completed": "Aventure terminee ! Tu es maintenant un aventurier de boites certifie.", - "adventure.item_granted": "Recu : {0} x{1}", + "adventure.completed": "Aventure terminée ! Tu es maintenant un aventurier de boîtes certifié.", + "adventure.item_granted": "Reçu : {0} x{1}", "adventure.item_removed": "Perdu : {0}", "adventure.resource_added": "{0} +{1}", - "interaction.key_chest": "La cle rentre ! Le coffre s'ouvre automatiquement !", - "interaction.key_no_match": "Cette cle semble ouvrir quelque chose... mais tu ne l'as pas encore. Peut-etre qu'une future boite le fournira.", - "interaction.craft_available": "Nouvelle recette disponible a {0} !", + "interaction.key_chest": "La clé rentre ! Le coffre s'ouvre automatiquement !", + "interaction.key_no_match": "Cette clé semble ouvrir quelque chose... mais tu ne l'as pas encore. Peut-être qu'une future boîte le fournira.", + "interaction.craft_available": "Nouvelle recette disponible à {0} !", "save.saving": "Sauvegarde en cours...", - "save.saved": "Partie sauvegardee dans l'emplacement '{0}'.", + "save.saved": "Partie sauvegardée dans l'emplacement '{0}'.", "save.loading": "Chargement...", - "save.loaded": "Partie chargee depuis l'emplacement '{0}'.", - "save.no_saves": "Aucune sauvegarde trouvee.", + "save.loaded": "Partie chargée depuis l'emplacement '{0}'.", + "save.no_saves": "Aucune sauvegarde trouvée.", "save.choose_slot": "Choisis un emplacement de sauvegarde :", - "error.invalid_input": "Entree invalide. Reessaie, brave ouvreur de boites.", - "error.no_boxes": "Tu n'as aucune boite a ouvrir. Comment t'as fait ? Ouvre plus de boites pour avoir des boites.", + "error.invalid_input": "Entrée invalide. Réessaie, brave ouvreur de boîtes.", + "error.no_boxes": "Tu n'as aucune boîte à ouvrir. Comment t'as fait ? Ouvre plus de boîtes pour avoir des boîtes.", "error.not_enough_resources": "Pas assez de {0}. Il t'en manque {1}.", - "misc.boxes_opened": "Total de boites ouvertes : {0}", + "misc.boxes_opened": "Total de boîtes ouvertes : {0}", "misc.play_time": "Temps de jeu : {0}", - "misc.welcome_back": "Bon retour, {0} ! Tes boites se sont ennuyees.", + "misc.welcome_back": "Bon retour, {0} ! Tes boîtes se sont ennuyées.", "recipe.refine_wood": "Raffiner le bois", "recipe.smelt_bronze_ingot": "Fondre un lingot de bronze", @@ -382,48 +382,50 @@ "recipe.smelt_steel_ingot": "Fondre un lingot d'acier", "recipe.smelt_titanium_ingot": "Fondre un lingot de titane", "recipe.forge_carbonfiber_sheet": "Presser une feuille de fibre de carbone", - "recipe.brew_health_potion_medium": "Brasser une potion de sante moyenne", + "recipe.brew_health_potion_medium": "Brasser une potion de santé moyenne", "recipe.brew_mana_crystal_medium": "Raffiner un cristal de mana moyen", - "recipe.synthesize_energy_cell": "Synthetiser une cellule d'energie", - "recipe.pressurize_oxygen_tank": "Pressuriser un reservoir d'oxygene", + "recipe.synthesize_energy_cell": "Synthétiser une cellule d'énergie", + "recipe.pressurize_oxygen_tank": "Pressuriser un réservoir d'oxygène", "recipe.craft_pilot_glasses": "Fabriquer des lunettes d'aviateur", "recipe.forge_armored_plate": "Forger une armure", - "recipe.engineer_rocket_boots": "Concevoir des bottes a reaction", + "recipe.engineer_rocket_boots": "Concevoir des bottes à réaction", "recipe.chart_star_navigation": "Cartographier la navigation stellaire", "recipe.engrave_royal_seal": "Graver un sceau royal", "recipe.enchant_dark_grimoire": "Enchanter le grimoire sombre", "recipe.fuse_cosmic_crystal": "Fusionner un cristal cosmique", - "recipe.splice_glowing_dna": "Episser de l'ADN luminescent", + "recipe.splice_glowing_dna": "Épisser de l'ADN luminescent", "recipe.preserve_amber": "Conserver une pierre d'ambre", - "recipe.craft_box_ok_tier": "Fabriquer une boite ok tiers", - "recipe.craft_box_cool": "Fabriquer une boite coolos", - "recipe.craft_box_supply": "Fabriquer une boite de fourniture", - "recipe.craft_box_epic": "Fabriquer une boite epique", + "recipe.craft_box_ok_tier": "Fabriquer une boîte ok tiers", + "recipe.craft_box_cool": "Fabriquer une boîte coolos", + "recipe.craft_box_supply": "Fabriquer une boîte de fourniture", + "recipe.craft_box_epic": "Fabriquer une boîte épique", - "action.collect_crafting": "Recuperer les fabrications", - "craft.started": "Fabrication auto : {0} a l'atelier {1}", - "craft.completed": "{0} a termine la fabrication !", - "craft.done": "Termine", + "action.collect_crafting": "Récupérer les fabrications", + "craft.started": "Fabrication auto : {0} à l'atelier {1}", + "craft.completed": "{0} a terminé la fabrication !", + "craft.done": "Terminé", "craft.panel.title": "Ateliers", - "craft.panel.empty": "Aucun atelier en activite.", + "craft.panel.empty": "Aucun atelier en activité.", "item.blueprint.foundry": "Plan de Fonderie", - "item.blueprint.workbench": "Plan d'Etabli", + "item.blueprint.workbench": "Plan d'Établi", "item.blueprint.furnace": "Plan de Fourneau", "item.blueprint.forge": "Plan de Forge", "item.blueprint.alchemy": "Plan de Table d'Alchimie", - "item.blueprint.engineer": "Plan de Bureau d'Ingenieur", - "item.blueprint.drawing": "Plan de Table a Dessin", + "item.blueprint.engineer": "Plan de Bureau d'Ingénieur", + "item.blueprint.drawing": "Plan de Table à Dessin", "item.blueprint.engraving": "Plan de Banc de Gravure", "item.blueprint.pentacle": "Plan de Pentacle de Transformation", "item.blueprint.printer": "Plan d'Imprimante 3D", - "item.blueprint.synthesizer": "Plan de Synthetiseur de Matiere", - "item.blueprint.genetic": "Plan de Station de Modification Genetique", + "item.blueprint.synthesizer": "Plan de Synthétiseur de Matière", + "item.blueprint.genetic": "Plan de Station de Modification Génétique", "item.blueprint.stasis": "Plan de Chambre de Stase", - "box.endgame": "La Boite Finale", - "box.endgame.desc": "Tu as trouve toutes les ressources. C'est la derniere boite. Es-tu pret ?", + "box.endgame": "La Boîte Finale", + "box.endgame.desc": "Tu as trouvé toutes les ressources. C'est la dernière boîte. Es-tu prêt ?", "item.endgame_crown": "Couronne d'Accomplissement", "item.destiny_token": "Jeton du Destin", - "adventure.secret_branch_found": "Tu sens un chemin secret se reveler..." + "adventure.secret_branch_found": "Tu sens un chemin secret se révéler...", + "meta.autosave": "Sauvegarde automatique", + "save.autosaved": "Partie sauvegardée automatiquement." } diff --git a/src/OpenTheBox/Core/Enums/UIFeature.cs b/src/OpenTheBox/Core/Enums/UIFeature.cs index c495d02..97d5931 100644 --- a/src/OpenTheBox/Core/Enums/UIFeature.cs +++ b/src/OpenTheBox/Core/Enums/UIFeature.cs @@ -44,5 +44,8 @@ public enum UIFeature CraftingPanel, /// Phase 5: Shows completion percentage of all unique content. - CompletionTracker + CompletionTracker, + + /// Phase 2: Automatic save when returning to the hub. Removes the manual save action. + AutoSave } diff --git a/src/OpenTheBox/Program.cs b/src/OpenTheBox/Program.cs index b33792a..e9ee12b 100644 --- a/src/OpenTheBox/Program.cs +++ b/src/OpenTheBox/Program.cs @@ -66,6 +66,31 @@ public static class Program private static async Task MainMenuLoop() { + // Check for existing saves to determine startup flow + var existingSaves = _saveManager.ListSlots(); + + if (existingSaves.Count > 0) + { + // Saves exist: load locale from the most recent save, skip language prompt + var mostRecent = existingSaves[0]; // Already sorted by SavedAt descending + var recentState = _saveManager.Load(mostRecent.Name); + if (recentState != null) + { + _loc.Change(recentState.CurrentLocale); + _renderer = RendererFactory.Create(_renderContext, _loc, _registry); + } + } + else + { + // No saves: prompt language first + _renderer.Clear(); + var langOptions = new List { "English", "Français" }; + int langChoice = _renderer.ShowSelection("Language / Langue", langOptions); + var selectedLocale = langChoice == 0 ? Locale.EN : Locale.FR; + _loc.Change(selectedLocale); + _renderer = RendererFactory.Create(_renderContext, _loc, _registry); + } + while (_running) { _renderer.Clear(); @@ -76,26 +101,79 @@ public static class Program _renderer.ShowMessage(_loc.Get("game.subtitle")); _renderer.ShowMessage(""); - var options = new List + // Rebuild saves list (may have changed after new game / save) + existingSaves = _saveManager.ListSlots(); + + var options = new List(); + var actions = new List(); + + // If saves exist, show "Continuer" as first option with most recent save info + if (existingSaves.Count > 0) { - _loc.Get("menu.new_game"), - _loc.Get("menu.load_game"), - _loc.Get("menu.language"), - _loc.Get("menu.quit") - }; + var recent = existingSaves[0]; + var savedAt = recent.SavedAt.ToLocalTime(); + options.Add($"{_loc.Get("menu.continue")} ({recent.Name} {savedAt:dd/MM/yyyy HH:mm})"); + actions.Add("continue"); + } + + options.Add(_loc.Get("menu.new_game")); + actions.Add("new_game"); + + if (existingSaves.Count > 1) + { + options.Add(_loc.Get("menu.load_game")); + actions.Add("load_game"); + } + + options.Add(_loc.Get("menu.language")); + actions.Add("language"); + + options.Add(_loc.Get("menu.quit")); + actions.Add("quit"); int choice = _renderer.ShowSelection("", options); - switch (choice) + switch (actions[choice]) { - case 0: await NewGame(); break; - case 1: await LoadGame(); break; - case 2: ChangeLanguage(); break; - case 3: _running = false; break; + case "continue": + await ContinueGame(existingSaves[0].Name); + break; + case "new_game": + await NewGame(); + break; + case "load_game": + await LoadGame(); + break; + case "language": + ChangeLanguage(); + break; + case "quit": + _running = false; + break; } } } + private static async Task ContinueGame(string slotName) + { + var loaded = _saveManager.Load(slotName); + if (loaded == null) + { + _renderer.ShowError("Failed to load save."); + _renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); + return; + } + + _state = loaded; + _loc.Change(_state.CurrentLocale); + InitializeGame(); + + _renderer.ShowMessage(_loc.Get("misc.welcome_back", _state.PlayerName)); + _renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); + + await GameLoop(); + } + private static async Task NewGame() { string name = _renderer.ShowTextInput(_loc.Get("prompt.name")); @@ -181,6 +259,12 @@ public static class Program { while (_running) { + // Auto-save when returning to the hub (if the feature is unlocked) + if (_state.HasUIFeature(UIFeature.AutoSave)) + { + _saveManager.Save(_state, _state.PlayerName); + } + // Tick crafting jobs (InProgress → Completed) TickCraftingJobs(); @@ -225,7 +309,9 @@ public static class Program if (completedJobs.Count > 0) actions.Add((_loc.Get("action.collect_crafting") + $" ({completedJobs.Count})", "collect_crafting")); - actions.Add((_loc.Get("action.save"), "save")); + if (!_state.HasUIFeature(UIFeature.AutoSave)) + actions.Add((_loc.Get("action.save"), "save")); + actions.Add((_loc.Get("action.quit"), "quit")); return actions; @@ -283,6 +369,12 @@ public static class Program // so we don't show "You received" for items the player never actually gets var autoConsumedIds = events.OfType().Select(e => e.InstanceId).ToHashSet(); + // Collect all received items to show as a single grouped loot reveal + var allLoot = new List<(string name, string rarity, string category)>(); + + // Show only the primary box opening (not auto-opened intermediaries) + bool primaryBoxShown = false; + foreach (var evt in events) { switch (evt) @@ -290,14 +382,12 @@ public static class Program case BoxOpenedEvent boxEvt: var boxDef = _registry.GetBox(boxEvt.BoxId); var boxName = _loc.Get(boxDef?.NameKey ?? boxEvt.BoxId); - if (boxEvt.IsAutoOpen) - { - _renderer.ShowMessage(_loc.Get("box.auto_open", boxName)); - } - else + if (!boxEvt.IsAutoOpen && !primaryBoxShown) { _renderer.ShowBoxOpening(boxName, boxDef?.Rarity.ToString() ?? "Common"); + primaryBoxShown = true; } + // Auto-opened boxes are silent — their loot appears in the grouped reveal break; case ItemReceivedEvent itemEvt: @@ -306,14 +396,11 @@ public static class Program break; var itemDef = _registry.GetItem(itemEvt.Item.DefinitionId); var itemBoxDef = itemDef is null ? _registry.GetBox(itemEvt.Item.DefinitionId) : null; - _renderer.ShowLootReveal( - [ - ( - GetLocalizedName(itemEvt.Item.DefinitionId), - (itemDef?.Rarity ?? itemBoxDef?.Rarity ?? ItemRarity.Common).ToString(), - (itemDef?.Category ?? ItemCategory.Box).ToString() - ) - ]); + allLoot.Add(( + GetLocalizedName(itemEvt.Item.DefinitionId), + (itemDef?.Rarity ?? itemBoxDef?.Rarity ?? ItemRarity.Common).ToString(), + (itemDef?.Category ?? ItemCategory.Box).ToString() + )); break; case UIFeatureUnlockedEvent uiEvt: @@ -377,6 +464,12 @@ public static class Program } } + // Show all received loot as a single grouped reveal + if (allLoot.Count > 0) + { + _renderer.ShowLootReveal(allLoot); + } + _renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); } @@ -571,6 +664,7 @@ public static class Program UIFeature.BoxAnimation => "meta.animation", UIFeature.CraftingPanel => "meta.crafting", UIFeature.CompletionTracker => "meta.completion", + UIFeature.AutoSave => "meta.autosave", _ => $"meta.{feature.ToString().ToLower()}" }; diff --git a/src/OpenTheBox/Rendering/BasicRenderer.cs b/src/OpenTheBox/Rendering/BasicRenderer.cs index 68b012a..ba595b6 100644 --- a/src/OpenTheBox/Rendering/BasicRenderer.cs +++ b/src/OpenTheBox/Rendering/BasicRenderer.cs @@ -25,7 +25,7 @@ public sealed class BasicRenderer(LocalizationManager loc) : IRenderer Console.WriteLine(loc.Get("box.opening", boxName)); Console.WriteLine("..."); Console.WriteLine("......"); - Console.WriteLine(loc.Get("box.opened", boxName, rarity)); + Console.WriteLine(loc.Get("box.opened_short", boxName)); } public void ShowLootReveal(List<(string name, string rarity, string category)> items) @@ -34,7 +34,7 @@ public sealed class BasicRenderer(LocalizationManager loc) : IRenderer for (int i = 0; i < items.Count; i++) { var (name, rarity, category) = items[i]; - Console.WriteLine($" - {name} [{rarity}] ({category})"); + Console.WriteLine($" - {name} [{rarity}]"); } } diff --git a/src/OpenTheBox/Rendering/SpectreRenderer.cs b/src/OpenTheBox/Rendering/SpectreRenderer.cs index 52a1326..2adc389 100644 --- a/src/OpenTheBox/Rendering/SpectreRenderer.cs +++ b/src/OpenTheBox/Rendering/SpectreRenderer.cs @@ -69,7 +69,7 @@ public sealed class SpectreRenderer : IRenderer { Console.WriteLine(_loc.Get("box.opening", boxName)); Thread.Sleep(500); - Console.WriteLine(_loc.Get("box.opened", boxName, rarity)); + Console.WriteLine(_loc.Get("box.opened_short", boxName)); } } @@ -83,16 +83,14 @@ public sealed class SpectreRenderer : IRenderer .Border(TableBorder.Rounded) .Title($"[bold yellow]{Markup.Escape(_loc.Get("loot.title"))}[/]") .AddColumn(new TableColumn($"[bold]{Markup.Escape(_loc.Get("loot.name"))}[/]").Centered()) - .AddColumn(new TableColumn($"[bold]{Markup.Escape(_loc.Get("loot.rarity"))}[/]").Centered()) - .AddColumn(new TableColumn($"[bold]{Markup.Escape(_loc.Get("loot.category"))}[/]").Centered()); + .AddColumn(new TableColumn($"[bold]{Markup.Escape(_loc.Get("loot.rarity"))}[/]").Centered()); foreach (var (name, rarity, category) in items) { string color = RarityColor(rarity); table.AddRow( $"[{color}]{Markup.Escape(name)}[/]", - $"[{color}]{Markup.Escape(rarity)}[/]", - Markup.Escape(category)); + $"[{color}]{Markup.Escape(rarity)}[/]"); } AnsiConsole.Write(table); @@ -103,7 +101,7 @@ public sealed class SpectreRenderer : IRenderer foreach (var (name, rarity, category) in items) { string color = RarityColor(rarity); - AnsiConsole.MarkupLine($" - [{color}]{Markup.Escape(name)}[/] [{color}][[{Markup.Escape(rarity)}]][/] ({Markup.Escape(category)})"); + AnsiConsole.MarkupLine($" - [{color}]{Markup.Escape(name)}[/] [{color}][[{Markup.Escape(rarity)}]][/]"); } } else @@ -111,7 +109,7 @@ public sealed class SpectreRenderer : IRenderer Console.WriteLine(_loc.Get("loot.received")); foreach (var (name, rarity, category) in items) { - Console.WriteLine($" - {name} [{rarity}] ({category})"); + Console.WriteLine($" - {name} [{rarity}]"); } } } diff --git a/tests/OpenTheBox.Tests/UnitTest1.cs b/tests/OpenTheBox.Tests/UnitTest1.cs index 8ecbafb..a19649a 100644 --- a/tests/OpenTheBox.Tests/UnitTest1.cs +++ b/tests/OpenTheBox.Tests/UnitTest1.cs @@ -340,6 +340,129 @@ public class ContentValidationTests Assert.Empty(missing); } + [Fact] + public void BoxDescriptionKeys_ExistInLocalization() + { + var boxes = LoadBoxes(); + var en = LoadEnStrings(); + + var missing = boxes + .Where(b => !string.IsNullOrEmpty(b.DescriptionKey) && !en.ContainsKey(b.DescriptionKey)) + .Select(b => $"{b.Id} -> descriptionKey '{b.DescriptionKey}'") + .ToList(); + + Assert.Empty(missing); + } + + [Fact] + public void ItemNameKeys_ExistInFrLocalization() + { + var items = LoadItems(); + var frJson = File.ReadAllText(FrStringsPath); + var fr = JsonSerializer.Deserialize>(frJson)!; + + var missing = items + .Where(i => !fr.ContainsKey(i.NameKey)) + .Select(i => $"{i.Id} -> nameKey '{i.NameKey}'") + .ToList(); + + Assert.Empty(missing); + } + + [Fact] + public void BoxNameKeys_ExistInFrLocalization() + { + var boxes = LoadBoxes(); + var frJson = File.ReadAllText(FrStringsPath); + var fr = JsonSerializer.Deserialize>(frJson)!; + + var missing = boxes + .Where(b => !fr.ContainsKey(b.NameKey)) + .Select(b => $"{b.Id} -> nameKey '{b.NameKey}'") + .ToList(); + + Assert.Empty(missing); + } + + [Fact] + public void BoxDescriptionKeys_ExistInFrLocalization() + { + var boxes = LoadBoxes(); + var frJson = File.ReadAllText(FrStringsPath); + var fr = JsonSerializer.Deserialize>(frJson)!; + + var missing = boxes + .Where(b => !string.IsNullOrEmpty(b.DescriptionKey) && !fr.ContainsKey(b.DescriptionKey)) + .Select(b => $"{b.Id} -> descriptionKey '{b.DescriptionKey}'") + .ToList(); + + Assert.Empty(missing); + } + + [Fact] + public void AllUIFeatures_HaveLocalizationKeys() + { + var en = LoadEnStrings(); + + // Map of UIFeature -> expected localization key + var featureKeys = new Dictionary + { + [UIFeature.TextColors] = "meta.colors", + [UIFeature.ExtendedColors] = "meta.extended_colors", + [UIFeature.ArrowKeySelection] = "meta.arrows", + [UIFeature.InventoryPanel] = "meta.inventory", + [UIFeature.ResourcePanel] = "meta.resources", + [UIFeature.StatsPanel] = "meta.stats", + [UIFeature.PortraitPanel] = "meta.portrait", + [UIFeature.ChatPanel] = "meta.chat", + [UIFeature.FullLayout] = "meta.layout", + [UIFeature.KeyboardShortcuts] = "meta.shortcuts", + [UIFeature.BoxAnimation] = "meta.animation", + [UIFeature.CraftingPanel] = "meta.crafting", + [UIFeature.CompletionTracker] = "meta.completion", + [UIFeature.AutoSave] = "meta.autosave", + }; + + var missing = featureKeys + .Where(kv => !en.ContainsKey(kv.Value)) + .Select(kv => $"{kv.Key} -> '{kv.Value}'") + .ToList(); + + Assert.Empty(missing); + + // Also verify every UIFeature enum value has a mapping + var allFeatures = Enum.GetValues(); + var unmapped = allFeatures.Where(f => !featureKeys.ContainsKey(f)).ToList(); + Assert.Empty(unmapped); + } + + [Fact] + public void AllMetaUnlockItems_ReferenceValidUIFeatures() + { + var items = LoadItems(); + var metaItems = items.Where(i => i.MetaUnlock.HasValue).ToList(); + + // Every meta item's MetaUnlock value should be a valid UIFeature + // (this is guaranteed by deserialization, but let's verify the round-trip) + Assert.All(metaItems, item => + Assert.True(Enum.IsDefined(item.MetaUnlock!.Value), + $"Item {item.Id} has invalid metaUnlock value")); + } + + [Fact] + public void EnAndFr_HaveIdenticalKeysets() + { + var en = LoadEnStrings(); + var frJson = File.ReadAllText(FrStringsPath); + var fr = JsonSerializer.Deserialize>(frJson)!; + + var onlyInEn = en.Keys.Where(k => !fr.ContainsKey(k)).ToList(); + var onlyInFr = fr.Keys.Where(k => !en.ContainsKey(k)).ToList(); + + Assert.Empty(onlyInEn); + Assert.Empty(onlyInFr); + } + // ── ContentRegistry integration ────────────────────────────────────── [Fact]