Accents and bug tracking

This commit is contained in:
Samuel Bouchet 2026-03-11 18:33:10 +01:00
parent c9f8a9566a
commit 2c96cba174
10 changed files with 537 additions and 240 deletions

73
bugs.md Normal file
View file

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

View file

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

View file

@ -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"},

View file

@ -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."
}

View file

@ -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."
}

View file

@ -44,5 +44,8 @@ public enum UIFeature
CraftingPanel,
/// <summary>Phase 5: Shows completion percentage of all unique content.</summary>
CompletionTracker
CompletionTracker,
/// <summary>Phase 2: Automatic save when returning to the hub. Removes the manual save action.</summary>
AutoSave
}

View file

@ -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<string> { "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<string>
// Rebuild saves list (may have changed after new game / save)
existingSaves = _saveManager.ListSlots();
var options = new List<string>();
var actions = new List<string>();
// 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<ItemConsumedEvent>().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()}"
};

View file

@ -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}]");
}
}

View file

@ -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}]");
}
}
}

View file

@ -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<Dictionary<string, string>>(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<Dictionary<string, string>>(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<Dictionary<string, string>>(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, string>
{
[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<UIFeature>();
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<Dictionary<string, string>>(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]