Fixes
This commit is contained in:
parent
9e1e0112c9
commit
d69aa5b4a4
27 changed files with 564 additions and 71 deletions
137
bugs.md
137
bugs.md
|
|
@ -4,29 +4,116 @@ Les sujets dans FIXME doivent être corrigé, puis déplacé dans "DONE", puis c
|
|||
|
||||
# FIXME
|
||||
|
||||
## direct number
|
||||
|
||||
La fonctionnalité "navigation avec les flèche" devrait laisser la possibilité de continuer d'utiliser les numéros du claviers (pavé alpha ou alphanumérique) pour sélectionner les choix correspondant avec un seul bouton
|
||||
|
||||
## panneau d'inventaire impraticable
|
||||
|
||||
```
|
||||
┌─Resources─────────────────┐
|
||||
│ No resources visible yet. │
|
||||
└───────────────────────────┘
|
||||
┌─Inventory───────────────────────────────────────────────┐
|
||||
│ Inventory │
|
||||
│ ┌───────────────────────────┬──────────┬────────┬─────┐ │
|
||||
│ │ Name │ Category │ Rarity │ Qty │ │
|
||||
│ ├───────────────────────────┼──────────┼────────┼─────┤ │
|
||||
│ │ blood_vial │ - │ - │ 1 │ │
|
||||
│ │ box_epic │ - │ - │ 1 │ │
|
||||
│ │ box_music │ - │ - │ 2 │ │
|
||||
│ │ box_not_great │ - │ - │ 1 │ │
|
||||
│ │ box_ok_tier │ - │ - │ 3 │ │
|
||||
│ │ contemporary_phone │ - │ - │ 1 │ │
|
||||
│ │ contemporary_usb │ - │ - │ 1 │ │
|
||||
│ │ cosmetic_arms_regular │ - │ - │ 1 │ │
|
||||
│ │ cosmetic_body_robotic │ - │ - │ 1 │ │
|
||||
│ │ cosmetic_eyes_blue │ - │ - │ 1 │ │
|
||||
│ │ cosmetic_eyes_brown │ - │ - │ 1 │ │
|
||||
│ │ cosmetic_hair_cyberpunk │ - │ - │ 1 │ │
|
||||
│ │ cosmetic_hair_short │ - │ - │ 1 │ │
|
||||
│ │ cosmetic_legs_rocketboots │ - │ - │ 1 │ │
|
||||
│ │ food_ration │ - │ - │ 2 │ │
|
||||
│ │ gold_pouch │ - │ - │ 1 │ │
|
||||
│ │ health_potion_medium │ - │ - │ 1 │ │
|
||||
│ │ health_potion_small │ - │ - │ 4 │ │
|
||||
│ │ lore_10 │ - │ - │ 1 │ │
|
||||
│ │ lore_2 │ - │ - │ 2 │ │
|
||||
│ │ lore_3 │ - │ - │ 1 │ │
|
||||
│ │ lore_5 │ - │ - │ 2 │ │
|
||||
│ │ lore_6 │ - │ - │ 1 │ │
|
||||
│ │ lore_9 │ - │ - │ 1 │ │
|
||||
│ │ mana_crystal_small │ - │ - │ 3 │ │
|
||||
│ │ material_bronze_ingot │ - │ - │ 1 │ │
|
||||
│ │ material_bronze_raw │ - │ - │ 2 │ │
|
||||
│ │ material_carbonfiber_raw │ - │ - │ 1 │ │
|
||||
│ │ material_iron_raw │ - │ - │ 3 │ │
|
||||
│ │ medieval_scroll │ - │ - │ 1 │ │
|
||||
│ │ meta_animation │ - │ - │ 1 │ │
|
||||
│ │ meta_arrows │ - │ - │ 1 │ │
|
||||
│ │ meta_autosave │ - │ - │ 1 │ │
|
||||
│ │ meta_colors │ - │ - │ 1 │ │
|
||||
│ │ meta_inventory │ - │ - │ 1 │ │
|
||||
│ │ meta_resources │ - │ - │ 1 │ │
|
||||
│ │ music_melody │ - │ - │ 1 │ │
|
||||
│ │ resource_max_food │ - │ - │ 1 │ │
|
||||
│ │ resource_max_health │ - │ - │ 1 │ │
|
||||
│ │ stamina_drink │ - │ - │ 1 │ │
|
||||
│ │ tint_cyan │ - │ - │ 4 │ │
|
||||
│ │ tint_orange │ - │ - │ 3 │ │
|
||||
│ └───────────────────────────┴──────────┴────────┴─────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
Que veux-tu faire ?
|
||||
|
||||
> Ouvrir une boîte (7)
|
||||
Voir l'inventaire
|
||||
Partir à l'aventure
|
||||
Changer d'apparence
|
||||
Retourner au menu
|
||||
```
|
||||
|
||||
la liste d'action se retrouve collée tout en bas, on ne voit plus les stats qui dépassent en haut, les objets dans le tableau ne sont pas traduits, n'ont pas de catégorie, ni de rareté.
|
||||
Dans "voir l'inventaire" ça fonctionne mais le tableau est trop grand, la casse "name" est gigantesque car certains noms sont trop longs et on perds l'association avec la rareté.
|
||||
|
||||
Attendu: homogénéiser les rendus
|
||||
Attendu: la colonne name a une taille raisonnable, les noms trops longs défilent (comme des <marquee> en allez-retours) si pas trop complexe
|
||||
Attendu: la hauteur est limitée et les flèches Pg up et Pg down permettent de scroll. Un indicateur indique que ces touches sont dispos.
|
||||
Attendu: le panneau inventaire est positionné à côté des stats (pour éviter de le cacher) en + d'être limité en hauteur
|
||||
Attendu: le rendu complet à tout moment doit passer dans 50 lignes de hauteur.
|
||||
|
||||
## raccourcis claviers
|
||||
|
||||
j'ai débloqué la meta interface raccourcis clavier mais ça ne change rien…
|
||||
Par ailleurs les raccourcis claviers (direct numbers évoqués plus haut) devraient être de base pour des raisons d'accessibilité.
|
||||
|
||||
## meta - interface
|
||||
|
||||
les meta interface sont toujours obtenues dans des meta - les bases. Ce sera mieux de ne plus looter de meta base mais directement des meta interface.
|
||||
idem base => interface => personnalisation. Donner directement la bonne boite.
|
||||
|
||||
## bug cosmétique
|
||||
|
||||
erreur survenue lorsque j'ai essayé d'ouvrir l'interface "change d'apparence" après avoir débloqué une box cosmétique
|
||||
|
||||
[2026-03-11 22:14:10] InvalidOperationException: Could not find color or style 'Cheveux'.
|
||||
at Spectre.Console.StyleParser.Parse(String text) in /_/src/Spectre.Console/StyleParser.cs:line 10
|
||||
at Spectre.Console.MarkupParser.Parse(String text, Style style) in /_/src/Spectre.Console/Internal/Text/Markup/MarkupParser.cs:line 29
|
||||
at Spectre.Console.SelectionPrompt`1.Spectre.Console.IListPromptStrategy<T>.Render(IAnsiConsole console, Boolean scrollable, Int32 cursorIndex, IEnumerable`1 items, Boolean skipUnselectableItems, String searchText) in /_/src/Spectre.Console/Prompts/SelectionPrompt.cs:line 167
|
||||
at Spectre.Console.ListPrompt`1.BuildRenderable(ListPromptState`1 state) in /_/src/Spectre.Console/Prompts/List/ListPrompt.cs:line 89
|
||||
at Spectre.Console.ListPromptRenderHook`1.Process(RenderOptions options, IEnumerable`1 renderables)+MoveNext()
|
||||
at Spectre.Console.AnsiBuilder.Build(IAnsiConsole console, IRenderable renderable) in /_/src/Spectre.Console/Internal/Backends/Ansi/AnsiBuilder.cs:line 17
|
||||
at Spectre.Console.AnsiConsoleBackend.Write(IRenderable renderable) in /_/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs:line 30
|
||||
at Spectre.Console.CursorExtensions.Hide(IAnsiConsoleCursor cursor) in /_/src/Spectre.Console/Extensions/CursorExtensions.cs:line 33
|
||||
at Spectre.Console.ListPrompt`1.Show(ListPromptTree`1 tree, Func`2 converter, SelectionMode selectionMode, Boolean skipUnselectableItems, Boolean searchEnabled, Int32 requestedPageSize, Boolean wrapAround, CancellationToken cancellationToken) in /_/src/Spectre.Console/Prompts/List/ListPrompt.cs:line 55
|
||||
at Spectre.Console.SelectionPrompt`1.ShowAsync(IAnsiConsole console, CancellationToken cancellationToken)
|
||||
at OpenTheBox.Rendering.SpectreRenderer.ShowSelection(String prompt, List`1 options) in D:\projets\openthebox\src\OpenTheBox\Rendering\SpectreRenderer.cs:line 123
|
||||
at OpenTheBox.Program.ChangeAppearance() in D:\projets\openthebox\src\OpenTheBox\Program.cs:line 590
|
||||
at OpenTheBox.Program.ExecuteAction(String action) in D:\projets\openthebox\src\OpenTheBox\Program.cs:line 327
|
||||
at OpenTheBox.Program.GameLoop() in D:\projets\openthebox\src\OpenTheBox\Program.cs:line 287
|
||||
at OpenTheBox.Program.NewGame() in D:\projets\openthebox\src\OpenTheBox\Program.cs:line 193
|
||||
at OpenTheBox.Program.MainMenuLoop() in D:\projets\openthebox\src\OpenTheBox\Program.cs:line 142
|
||||
at OpenTheBox.Program.Main(String[] args) in D:\projets\openthebox\src\OpenTheBox\Program.cs:line 40
|
||||
|
||||
=> Ajouter un test après le correctif pour attraper les cas similaires.
|
||||
|
||||
# DONE
|
||||
|
||||
## farah question
|
||||
|
||||
```
|
||||
[farah]
|
||||
C'est la boite de l'ete ou Chenda a emmenage a cote.
|
||||
[farah]
|
||||
Vous vous passiez des mots dedans. Dans un sens et dans l'autre, par-dessus la cloture.
|
||||
```
|
||||
=> Pas un bug. L'idée de Chenda et du joueur échangeant des messages dans une boîte par-dessus la clôture est une métaphore originale de l'aventure Sentimental. Elle n'est pas directement inspirée d'un jeu spécifique — c'est une variation sur le thème des "boîtes à messages" entre voisins/amis d'enfance, un motif classique dans les récits nostalgiques. Si ça ressemble à un autre jeu, c'est probablement une convergence thématique !
|
||||
|
||||
## missing translation
|
||||
```
|
||||
Cheveux équipé : [MISSING:cosmetic.hair.stardustlegendary]
|
||||
```
|
||||
=> Fix : le code utilisait `cosmeticValue.ToLower()` pour construire la clé au lieu du `nameKey` de l'item. Corrigé avec lookup par `(CosmeticSlot, CosmeticValue)` dans le registre. Tests ajoutés.
|
||||
|
||||
## Loot d'aventure
|
||||
=> Fix : Ajouté un `AdventureUnlockedEvent` émis par `MetaEngine` quand une aventure est débloquée. Affiché dans `RenderEvents()` avec le message localisé "🎉 Nouvelle aventure débloquée ! Découvre '{0}' dans « Partir à l'aventure » !"
|
||||
|
||||
## Pacing meta et boites pas ouf
|
||||
=> Fix balancing dans boxes.json :
|
||||
- `box_of_boxes` : réduit poids de `box_not_great` de 10→7, augmenté `box_ok_tier` de 5→7 (ratio basic passe de 67%/33% à 50%/50%)
|
||||
- `box_not_great` : ajouté `box_ok_tier` (weight 2) et `box_meta_basics` (weight 1) dans le loot — les boîtes pas ouf peuvent maintenant dropper directement du mieux
|
||||
- `box_ok_tier` : augmenté poids de `box_meta_basics` de 1→2 (doublement des chances de meta)
|
||||
|
|
|
|||
|
|
@ -154,6 +154,27 @@ Transférer l'email aux RH
|
|||
#opt-reply-all // "Reply all with \"Please remove me from this thread\""
|
||||
Répondre à tous avec \"Merci de me retirer de ce fil\"
|
||||
|
||||
#opt-vip // "Offer to sponsor the box's corporate integration personally|||Your financial resources might open some doors here..."
|
||||
Proposer de sponsoriser personnellement l'intégration de la boîte|||Vos ressources financières pourraient ouvrir des portes ici...
|
||||
|
||||
#vip-offer // "You reply to the box's email with a generous corporate sponsorship offer. The box hums approvingly."
|
||||
Vous répondez à l'email de la boîte avec une généreuse offre de sponsoring. La boîte ronronne d'approbation.
|
||||
|
||||
#sandrea-corner // "You're... buying the box a corner office? With a window?"
|
||||
Tu... tu achètes un bureau d'angle à la boîte ? Avec vue ?
|
||||
|
||||
#samuel-printer // "I've been here three years and I sit next to the printer. The LOUD printer."
|
||||
Ça fait trois ans que je suis là et je suis assis à côté de l'imprimante. L'imprimante BRUYANTE.
|
||||
|
||||
#vip-counter // "Counter-offer accepted. I will also require a company card and a parking spot."
|
||||
\"Contre-offre acceptée. J'exigerai également une carte entreprise et une place de parking.\"
|
||||
|
||||
#sandrea-negotiated // "The box negotiated better than our entire sales department. In one email."
|
||||
La boîte a mieux négocié que tout notre service commercial. En un seul email.
|
||||
|
||||
#samuel-salary // "Can the box negotiate MY salary next?"
|
||||
La boîte peut négocier MON salaire après ?
|
||||
|
||||
#reply-chaos // "You hit Reply All. The office erupts."
|
||||
Vous cliquez sur Répondre à tous. Le bureau explose.
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,27 @@ Regarder plus loin -- dézoomer à la place
|
|||
#opt-refuse // "Refuse to look -- some ideas are traps"
|
||||
Refuser de regarder -- certaines idées sont des pièges
|
||||
|
||||
#opt-enlightened // "Derive the box's mathematical structure from its signal|||Your mind races with equations that might decode the pattern..."
|
||||
Déduire la structure mathématique de la boîte à partir de son signal|||Votre esprit bouillonne d'équations qui pourraient décoder le schéma...
|
||||
|
||||
#enlightened-write // "You grab a whiteboard and start writing. Equations pour out of you like water from a cosmic faucet."
|
||||
Vous attrapez un tableau blanc et commencez à écrire. Les équations jaillissent de vous comme l'eau d'un robinet cosmique.
|
||||
|
||||
#quantum-proof // "The signal isn't random. It's a proof. A mathematical proof that boxes are the fundamental topology of reality."
|
||||
Le signal n'est pas aléatoire. C'est une preuve. Une preuve mathématique que les boîtes sont la topologie fondamentale de la réalité.
|
||||
|
||||
#quantum-euler // "Euler's formula, but for boxes. Every vertex, every edge, every face -- they all resolve to cubes."
|
||||
La formule d'Euler, mais pour les boîtes. Chaque sommet, chaque arête, chaque face -- tout se résout en cubes.
|
||||
|
||||
#quantum-unified // "I've just unified quantum mechanics and general relativity. The answer was boxes. It was ALWAYS boxes."
|
||||
Je viens d'unifier la mécanique quantique et la relativité générale. La réponse, c'était les boîtes. Ça a TOUJOURS été les boîtes.
|
||||
|
||||
#quantum-grand // "Twenty years of research, and the Grand Unified Theory is: put it in a box."
|
||||
Vingt ans de recherche, et la Théorie du Grand Tout Unifié est : mettez-le dans une boîte.
|
||||
|
||||
#enlightened-recalibrate // "The instruments recalibrate themselves. They didn't need your help -- they just needed someone to understand."
|
||||
Les instruments se recalibrent d'eux-mêmes. Ils n'avaient pas besoin de votre aide -- ils avaient juste besoin que quelqu'un comprenne.
|
||||
|
||||
#refuse-puppet // "You refuse. You're a scientist, not a box's puppet."
|
||||
Vous refusez. Vous êtes un scientifique, pas la marionnette d'une boîte.
|
||||
|
||||
|
|
|
|||
|
|
@ -271,6 +271,30 @@ Ouvrir une boîte et voir ce qui se passe
|
|||
#opt-convince // "Convince Mordecai to open them together"
|
||||
Convaincre Mordecai de les ouvrir ensemble
|
||||
|
||||
#opt-blood // "Press your hand to the boxes and offer your blood as a bridge|||You feel a strange warmth in your veins, as though something ancient recognizes you..."
|
||||
Poser votre main sur les boîtes et offrir votre sang comme pont|||Vous ressentez une étrange chaleur dans vos veines, comme si quelque chose d'ancien vous reconnaissait...
|
||||
|
||||
#blood-press // "You press your palm against the nearest box. Your blood sings. The box answers."
|
||||
Vous pressez votre paume contre la boîte la plus proche. Votre sang chante. La boîte répond.
|
||||
|
||||
#blood-voices // "Every box in the vault flickers -- not with light, but with voices. Twelve thousand souls, speaking at once."
|
||||
Chaque boîte du caveau scintille -- pas de lumière, mais de voix. Douze mille âmes, parlant en même temps.
|
||||
|
||||
#pierrick-introduce // "They're not screaming. They're... introducing themselves."
|
||||
Elles ne crient pas. Elles... se présentent.
|
||||
|
||||
#mordecai-impossible // "Impossible. I've tried to speak with them for decades. They never answered ME."
|
||||
Impossible. J'ai essayé de leur parler pendant des décennies. Elles ne m'ont jamais répondu.
|
||||
|
||||
#pierrick-jailer // "Maybe they didn't trust you. You're their jailer. I'm just a baker with strange blood."
|
||||
Peut-être qu'elles ne vous faisaient pas confiance. Vous êtes leur geôlier. Moi je suis juste un boulanger au sang étrange.
|
||||
|
||||
#blood-consensus // "The souls whisper a consensus: half wish to stay, half wish to leave. They've voted. Through cardboard. Democracy at its most absurd."
|
||||
Les âmes chuchotent un consensus : la moitié souhaite rester, l'autre moitié souhaite partir. Elles ont voté. À travers du carton. La démocratie dans ce qu'elle a de plus absurde.
|
||||
|
||||
#mordecai-voted // "They VOTED? Without a suggestion box? Wait -- they ARE the suggestion boxes."
|
||||
Elles ont VOTÉ ? Sans boîte à suggestions ? Attendez -- elles SONT les boîtes à suggestions.
|
||||
|
||||
#one-pick // "You pick a box at random. It's small, blue, and warm. The label reads: \"Elara. Farmer. Liked cats.\""
|
||||
Vous choisissez une boîte au hasard. Elle est petite, bleue et chaude. L'étiquette dit : \"Elara. Fermière. Aimait les chats.\"
|
||||
|
||||
|
|
|
|||
|
|
@ -166,6 +166,24 @@ Combattre le dragon
|
|||
#opt-sneak // "Try to sneak past while the dragon is distracted"
|
||||
Essayer de se faufiler pendant que le dragon est distrait
|
||||
|
||||
#opt-charm // "Flash your most dazzling smile at the dragon|||Something about your magnetic personality might work here..."
|
||||
Adresser votre plus beau sourire au dragon|||Quelque chose dans votre personnalité magnétique pourrait fonctionner ici...
|
||||
|
||||
#charm-smile // "You step forward, lock eyes with Scorchtangle, and deliver the most radiant smile in the kingdom's history."
|
||||
Vous faites un pas en avant, croisez le regard de Scorchtangle, et offrez le sourire le plus radieux de l'histoire du royaume.
|
||||
|
||||
#charm-blush // "The dragon blinks. Then blushes. Can dragons blush? This one can. His scales turn a gentle pink."
|
||||
Le dragon cligne des yeux. Puis rougit. Les dragons peuvent-ils rougir ? Celui-ci, oui. Ses écailles virent au rose tendre.
|
||||
|
||||
#knight-swoon // "By the Square Gods... the dragon is SWOONING. I've never seen a box-dragon swoon before."
|
||||
Par les Dieux Carrés... le dragon est en PÂMOISON. Je n'ai jamais vu un dragon-boîte se pâmer auparavant.
|
||||
|
||||
#charm-belly // "Scorchtangle rolls over like a puppy and presents his belly, which is covered in tiny box-shaped scales."
|
||||
Scorchtangle se retourne comme un chiot et présente son ventre, couvert de minuscules écailles en forme de boîte.
|
||||
|
||||
#knight-brave // "You've charmed a dragon with nothing but raw charisma. That's either very brave or very weird."
|
||||
Vous avez charmé un dragon avec rien d'autre que du charisme brut. C'est soit très courageux, soit très bizarre.
|
||||
|
||||
#fight-charge // "You charge at Scorchtangle with your definitely-not-adequate weapon."
|
||||
Vous chargez Scorchtangle avec votre arme clairement inadaptée.
|
||||
|
||||
|
|
|
|||
|
|
@ -238,6 +238,27 @@ Observer la réponse immunitaire scientifiquement
|
|||
#opt-ask-mike // "Ask Mike to do something"
|
||||
Demander à Mike de faire quelque chose
|
||||
|
||||
#opt-surgery // "Carefully extract the virus with surgical precision|||Your hands are remarkably steady -- perhaps steady enough for microscopic work..."
|
||||
Extraire soigneusement le virus avec une précision chirurgicale|||Vos mains sont remarquablement stables -- peut-être assez stables pour un travail microscopique...
|
||||
|
||||
#surgeon-hands // "Your hands move with impossible precision. At this scale, a nanometer of error means disaster. You don't make errors."
|
||||
Vos mains bougent avec une précision impossible. À cette échelle, un nanomètre d'erreur signifie la catastrophe. Vous ne faites pas d'erreurs.
|
||||
|
||||
#surgeon-peel // "You pinch the virus between two fingers and gently peel it from the membrane receptor, like removing a sticker without tearing it."
|
||||
Vous pincez le virus entre deux doigts et le décollez délicatement du récepteur membranaire, comme retirer un autocollant sans le déchirer.
|
||||
|
||||
#cellina-fingers // "That's... that's not possible. You just performed manual phagocytosis. With your FINGERS."
|
||||
C'est... c'est pas possible. Vous venez de réaliser une phagocytose manuelle. Avec vos DOIGTS.
|
||||
|
||||
#mike-splinter // "I've seen a lot of things inside this cell. Someone hand-removing a virus like a splinter is a new one."
|
||||
J'ai vu beaucoup de choses dans cette cellule. Quelqu'un qui retire un virus à la main comme une écharde, c'est une première.
|
||||
|
||||
#cellina-lock // "The receptor isn't even damaged. That's like picking a lock without scratching it. At the MOLECULAR level."
|
||||
Le récepteur n'est même pas endommagé. C'est comme crocheter une serrure sans la rayer. Au niveau MOLÉCULAIRE.
|
||||
|
||||
#mike-friend // "Can you do that to the weird protein that's been stuck in the Golgi for three days? Asking for a friend. The friend is me."
|
||||
Vous pourriez faire ça avec la protéine bizarre coincée dans le Golgi depuis trois jours ? C'est pour un ami. L'ami c'est moi.
|
||||
|
||||
#fight-hurl // "You grab a nearby antibody -- it's shaped like a Y, which is just a box with arms -- and hurl it at the virus."
|
||||
Vous attrapez un anticorps à proximité -- il a la forme d'un Y, qui n'est qu'une boîte avec des bras -- et le lancez sur le virus.
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,30 @@ Suggérer que la carte est peut-être à l'envers
|
|||
#opt-question // "Question whether a box drawing constitutes a map"
|
||||
Questionner si un dessin de boîte constitue une carte
|
||||
|
||||
#opt-pegleg // "Stomp your leg on the deck with authority|||Your sea legs might earn some respect around here..."
|
||||
Frapper du pied sur le pont avec autorité|||Vos jambes de marin pourraient vous valoir un peu de respect par ici...
|
||||
|
||||
#pegleg-stomp // "You stomp your peg leg on the cardboard deck. It makes a deeply satisfying THUNK."
|
||||
Vous frappez votre jambe de bois sur le pont en carton. Ça fait un BOUM profondément satisfaisant.
|
||||
|
||||
#pegleg-eyes // "The entire crew goes silent. Then Blackbeard's eyes widen."
|
||||
L'équipage entier se tait. Puis les yeux de Barbenoire s'écarquillent.
|
||||
|
||||
#captain-pegleg // "That sound... that THUNK... ye have a PEG LEG!"
|
||||
Ce son... ce BOUM... vous avez une JAMBE DE BOIS !
|
||||
|
||||
#captain-one // "Linu! This one's got the leg! THE LEG! They're one of US!"
|
||||
Linu ! Celui-là a la jambe ! LA JAMBE ! C'est l'un des NÔTRES !
|
||||
|
||||
#linu-excited // "The crew is going wild, Captain. They haven't been this excited since we found that box of rum."
|
||||
L'équipage est en délire, Capitaine. Ils n'ont pas été aussi excités depuis qu'on a trouvé cette boîte de rhum.
|
||||
|
||||
#captain-destiny // "Forget the map! This changes everything! A peg-legged sailor on a cardboard ship -- that's DESTINY!"
|
||||
Oubliez la carte ! Ça change tout ! Un marin à jambe de bois sur un navire en carton -- c'est le DESTIN !
|
||||
|
||||
#captain-rations // "Ye get double rations! And by rations I mean cardboard, but DOUBLE cardboard!"
|
||||
Vous avez droit à double ration ! Et par rations je veux dire du carton, mais du DOUBLE carton !
|
||||
|
||||
#captain-x // "Of course it's a map! It has an X!"
|
||||
Bien sûr que c'est une carte ! Il y a un X !
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,30 @@ Suggérer d'utiliser la boîte comme abri
|
|||
#opt-dance // "Try to understand the box through interpretive dance"
|
||||
Essayer de comprendre la boîte par la danse interprétative
|
||||
|
||||
#opt-wrestle // "Challenge the box to a test of raw strength|||Something about your physique suggests a more direct approach..."
|
||||
Défier la boîte dans une épreuve de force brute|||Quelque chose dans votre physique suggère une approche plus directe...
|
||||
|
||||
#champion-lift // "You grab the box and lift it above your head. The tribe gasps."
|
||||
Vous attrapez la boîte et la soulevez au-dessus de votre tête. La tribu en a le souffle coupé.
|
||||
|
||||
#grug-lift // "STRONG ONE LIFT THE THING! No one ever lift thing before! Everyone too busy poking and eating!"
|
||||
FORT SOULEVER LA CHOSE ! Personne jamais soulever chose avant ! Tout le monde trop occupé à pousser et manger !
|
||||
|
||||
#duncan-trophy // "Strong One hold thing above head like trophy! Like mammoth tusk but SQUARE!"
|
||||
Fort tenir chose au-dessus de la tête comme trophée ! Comme défense de mammouth mais CARRÉ !
|
||||
|
||||
#champion-slam // "You slam the box down on a rock. The rock breaks. The box is fine."
|
||||
Vous fracassez la boîte sur un caillou. Le caillou se brise. La boîte va bien.
|
||||
|
||||
#grug-rock // "ROCK LOST! BOX WON! Box is STRONGER than rock! This change everything!"
|
||||
CAILLOU A PERDU ! BOÎTE A GAGNÉ ! Boîte est PLUS FORTE que caillou ! Ça changer tout !
|
||||
|
||||
#duncan-rewritten // "For thousands of years, rock was strongest thing. Now box is strongest thing. History is rewritten."
|
||||
Pendant des milliers d'années, caillou était chose la plus forte. Maintenant boîte est chose la plus forte. L'histoire est réécrite.
|
||||
|
||||
#grug-champion // "Strong One is champion of box! All cave people agree! Even Carl!"
|
||||
Fort est champion de boîte ! Tous les hommes des cavernes d'accord ! Même Carl !
|
||||
|
||||
#worship-chant // "The tribe gathers around the box and begins chanting. The chant is \"BOX. BOX. BOX.\""
|
||||
La tribu se rassemble autour de la boîte et commence à chanter. Le chant est \"BOÎTE. BOÎTE. BOÎTE.\"
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,30 @@ Lire le mot plié
|
|||
#opt-figurine // "Look at the small wooden figurine"
|
||||
Regarder la petite figurine en bois
|
||||
|
||||
#opt-truesight // "Look deeper -- past the objects, into what they meant|||You feel like you could see the invisible threads connecting everything in this box..."
|
||||
Regarder plus profondément -- au-delà des objets, dans ce qu'ils signifiaient|||Vous avez l'impression de pouvoir voir les fils invisibles qui relient tout dans cette boîte...
|
||||
|
||||
#true-look // "You look at the box, but not at the things inside. You look at the spaces between them."
|
||||
Vous regardez la boîte, mais pas les choses à l'intérieur. Vous regardez les espaces entre elles.
|
||||
|
||||
#true-threads // "And you see it -- faint, golden threads. Connecting the photo to the note, the note to the figurine, the figurine back to the photo."
|
||||
Et vous le voyez -- des fils dorés, ténus. Reliant la photo au mot, le mot à la figurine, la figurine à la photo.
|
||||
|
||||
#farah-staring // "You're doing that thing again. That staring-at-nothing-but-seeing-everything thing."
|
||||
Tu refais ce truc. Ce truc où tu fixes le vide mais tu vois tout.
|
||||
|
||||
#true-moments // "Every thread is a moment. The photo connects to the day you laughed so hard you couldn't breathe. The note connects to the night you stayed up talking until sunrise."
|
||||
Chaque fil est un instant. La photo est liée au jour où vous avez ri si fort que vous ne pouviez plus respirer. Le mot est lié à la nuit où vous êtes restés à parler jusqu'au lever du soleil.
|
||||
|
||||
#true-figurine // "The figurine connects to the afternoon you realized this wasn't just friendship anymore."
|
||||
La figurine est liée à l'après-midi où vous avez réalisé que ce n'était plus juste de l'amitié.
|
||||
|
||||
#farah-see // "What do you see?"
|
||||
Qu'est-ce que tu vois ?
|
||||
|
||||
#farah-felt // "You know what, don't tell me. Some things are better felt than explained."
|
||||
Tu sais quoi, ne me dis rien. Certaines choses sont mieux ressenties qu'expliquées.
|
||||
|
||||
#photo-polaroid // "It's a polaroid. Slightly faded. Two teenagers standing in front of a moving truck."
|
||||
C'est un polaroid. Légèrement fané. Deux adolescents debout devant un camion de déménagement.
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,30 @@ La scanner d'abord
|
|||
#opt-poke // "Poke it with a stick"
|
||||
La pousser avec un bâton
|
||||
|
||||
#opt-whisper // "Close your eyes and listen to what the box is really saying|||You sense there might be more to this box than meets the eye..."
|
||||
Fermer les yeux et écouter ce que la boîte dit vraiment|||Vous sentez qu'il y a peut-être plus dans cette boîte qu'il n'y paraît...
|
||||
|
||||
#whisper-listen // "You close your eyes. The hum of the ship fades. Something else rises -- a frequency beneath frequencies."
|
||||
Vous fermez les yeux. Le bourdonnement du vaisseau s'estompe. Autre chose émerge -- une fréquence sous les fréquences.
|
||||
|
||||
#whisper-coordinates // "The box is transmitting. Not data. Not sound. Coordinates woven into the fabric of space-time itself."
|
||||
La boîte transmet. Pas des données. Pas du son. Des coordonnées tissées dans le tissu même de l'espace-temps.
|
||||
|
||||
#ai-brainwaves // "Commander, how are you doing that? The box just... aligned with your brainwaves."
|
||||
Commandant, comment faites-vous ça ? La boîte vient de... s'aligner sur vos ondes cérébrales.
|
||||
|
||||
#captain-listening // "I'm not doing anything, ARIA. I'm just listening."
|
||||
Je ne fais rien, ARIA. J'écoute, c'est tout.
|
||||
|
||||
#ai-folding // "The box has shared a direct route to its home system. No fuel cost. It's folding space around us like..."
|
||||
La boîte a partagé un itinéraire direct vers son système d'origine. Sans coût en carburant. Elle plie l'espace autour de nous comme...
|
||||
|
||||
#captain-folding // "Like folding a box?"
|
||||
Comme plier une boîte ?
|
||||
|
||||
#ai-origami // "I was going to say \"like origami,\" but yes. Like folding a box."
|
||||
J'allais dire \"comme de l'origami\", mais oui. Comme plier une boîte.
|
||||
|
||||
#poke-arm // "You extend the ship's robotic arm and gently poke the box."
|
||||
Vous étendez le bras robotique du vaisseau et poussez délicatement la boîte.
|
||||
|
||||
|
|
|
|||
|
|
@ -359,6 +359,8 @@
|
|||
|
||||
"interaction.key_chest": "The key fits! The chest opens automatically!",
|
||||
"interaction.key_no_match": "This key seems to fit something... but you don't have it yet. Perhaps a future box will provide.",
|
||||
"interaction.treasure_located": "The map and compass align! Treasure located!",
|
||||
"interaction.map_coordinates": "The map reveals mysterious coordinates...",
|
||||
"interaction.craft_available": "New recipe available at {0}!",
|
||||
|
||||
"save.saving": "Saving...",
|
||||
|
|
@ -441,5 +443,17 @@
|
|||
"adventure.coming_soon": "Adventure '{0}' is coming soon! The boxes are still being assembled.",
|
||||
"adventure.done": "Done",
|
||||
"adventure.unlocked": "🎉 New adventure unlocked! Discover '{0}' in the adventure menu!",
|
||||
"adventure.name.Space": "Starbound",
|
||||
"adventure.name.Medieval": "Castle Cardboard",
|
||||
"adventure.name.Pirate": "Corsair's Cove",
|
||||
"adventure.name.Contemporary": "Office Unboxing",
|
||||
"adventure.name.Sentimental": "The Box of Us",
|
||||
"adventure.name.Prehistoric": "First Box",
|
||||
"adventure.name.Cosmic": "Infinite Collapse",
|
||||
"adventure.name.Microscopic": "Cell Division",
|
||||
"adventure.name.DarkFantasy": "Ashen Wastes",
|
||||
"adventure.name.Destiny": "Gallery of Echoes",
|
||||
"stats.boxes_opened": "Boxes Opened",
|
||||
"stats.title": "Stats",
|
||||
"misc.welcome": "Welcome, {0}!"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,6 +359,8 @@
|
|||
|
||||
"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.treasure_located": "La carte et la boussole s'alignent ! Trésor localisé !",
|
||||
"interaction.map_coordinates": "La carte révèle des coordonnées mystérieuses...",
|
||||
"interaction.craft_available": "Nouvelle recette disponible à {0} !",
|
||||
|
||||
"save.saving": "Sauvegarde en cours...",
|
||||
|
|
@ -441,5 +443,17 @@
|
|||
"adventure.coming_soon": "L'aventure '{0}' arrive bientôt ! Les boîtes sont encore en cours d'assemblage.",
|
||||
"adventure.done": "Terminée",
|
||||
"adventure.unlocked": "🎉 Nouvelle aventure débloquée ! Découvre '{0}' dans « Partir à l'aventure » !",
|
||||
"adventure.name.Space": "Odyssée stellaire",
|
||||
"adventure.name.Medieval": "Château Carton",
|
||||
"adventure.name.Pirate": "Crique du Corsaire",
|
||||
"adventure.name.Contemporary": "Boîte au bureau",
|
||||
"adventure.name.Sentimental": "La Boîte à nous",
|
||||
"adventure.name.Prehistoric": "Première Boîte",
|
||||
"adventure.name.Cosmic": "Effondrement infini",
|
||||
"adventure.name.Microscopic": "Division cellulaire",
|
||||
"adventure.name.DarkFantasy": "Terres Cendrées",
|
||||
"adventure.name.Destiny": "Galerie des Échos",
|
||||
"stats.boxes_opened": "Boîtes ouvertes",
|
||||
"stats.title": "Stats",
|
||||
"misc.welcome": "Bienvenue, {0} !"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ public sealed class GameState
|
|||
public required Locale CurrentLocale { get; set; }
|
||||
public required DateTime CreatedAt { get; set; }
|
||||
public required TimeSpan TotalPlayTime { get; set; }
|
||||
public required HashSet<FontStyle> AvailableFonts { get; set; }
|
||||
public required HashSet<TextColor> AvailableTextColors { get; set; }
|
||||
public required List<CraftingJob> ActiveCraftingJobs { get; set; }
|
||||
public HashSet<FontStyle> AvailableFonts { get; set; } = [];
|
||||
public HashSet<TextColor> AvailableTextColors { get; set; } = [];
|
||||
public List<CraftingJob> ActiveCraftingJobs { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current value of a resource, or 0 if the resource is not tracked.
|
||||
|
|
|
|||
20
src/OpenTheBox/Data/ContentJsonContext.cs
Normal file
20
src/OpenTheBox/Data/ContentJsonContext.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using OpenTheBox.Core.Boxes;
|
||||
using OpenTheBox.Core.Crafting;
|
||||
using OpenTheBox.Core.Interactions;
|
||||
using OpenTheBox.Core.Items;
|
||||
|
||||
namespace OpenTheBox.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated JSON context for content data files (items, boxes, interactions, recipes).
|
||||
/// Enables trim-safe and reflection-free deserialization.
|
||||
/// </summary>
|
||||
[JsonSerializable(typeof(List<ItemDefinition>))]
|
||||
[JsonSerializable(typeof(List<BoxDefinition>))]
|
||||
[JsonSerializable(typeof(List<InteractionRule>))]
|
||||
[JsonSerializable(typeof(List<Recipe>))]
|
||||
[JsonSourceGenerationOptions(
|
||||
PropertyNameCaseInsensitive = true,
|
||||
UseStringEnumConverter = true)]
|
||||
internal partial class ContentJsonContext : JsonSerializerContext;
|
||||
|
|
@ -12,12 +12,6 @@ namespace OpenTheBox.Data;
|
|||
/// </summary>
|
||||
public class ContentRegistry
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
private readonly Dictionary<string, ItemDefinition> _items = [];
|
||||
private readonly Dictionary<string, BoxDefinition> _boxes = [];
|
||||
private readonly List<InteractionRule> _interactionRules = [];
|
||||
|
|
@ -53,7 +47,7 @@ public class ContentRegistry
|
|||
if (File.Exists(itemsPath))
|
||||
{
|
||||
var json = File.ReadAllText(itemsPath);
|
||||
var items = JsonSerializer.Deserialize<List<ItemDefinition>>(json, JsonOptions);
|
||||
var items = JsonSerializer.Deserialize(json, ContentJsonContext.Default.ListItemDefinition);
|
||||
if (items is not null)
|
||||
{
|
||||
foreach (var item in items)
|
||||
|
|
@ -64,7 +58,7 @@ public class ContentRegistry
|
|||
if (File.Exists(boxesPath))
|
||||
{
|
||||
var json = File.ReadAllText(boxesPath);
|
||||
var boxes = JsonSerializer.Deserialize<List<BoxDefinition>>(json, JsonOptions);
|
||||
var boxes = JsonSerializer.Deserialize(json, ContentJsonContext.Default.ListBoxDefinition);
|
||||
if (boxes is not null)
|
||||
{
|
||||
foreach (var box in boxes)
|
||||
|
|
@ -75,7 +69,7 @@ public class ContentRegistry
|
|||
if (File.Exists(interactionsPath))
|
||||
{
|
||||
var json = File.ReadAllText(interactionsPath);
|
||||
var rules = JsonSerializer.Deserialize<List<InteractionRule>>(json, JsonOptions);
|
||||
var rules = JsonSerializer.Deserialize(json, ContentJsonContext.Default.ListInteractionRule);
|
||||
if (rules is not null)
|
||||
{
|
||||
foreach (var rule in rules)
|
||||
|
|
@ -86,7 +80,7 @@ public class ContentRegistry
|
|||
if (recipesPath is not null && File.Exists(recipesPath))
|
||||
{
|
||||
var json = File.ReadAllText(recipesPath);
|
||||
var recipes = JsonSerializer.Deserialize<List<Recipe>>(json, JsonOptions);
|
||||
var recipes = JsonSerializer.Deserialize(json, ContentJsonContext.Default.ListRecipe);
|
||||
if (recipes is not null)
|
||||
{
|
||||
foreach (var recipe in recipes)
|
||||
|
|
|
|||
9
src/OpenTheBox/Localization/LocalizationJsonContext.cs
Normal file
9
src/OpenTheBox/Localization/LocalizationJsonContext.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace OpenTheBox.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated JSON context for localization string files.
|
||||
/// </summary>
|
||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||
internal partial class LocalizationJsonContext : JsonSerializerContext;
|
||||
|
|
@ -42,7 +42,7 @@ public sealed class LocalizationManager
|
|||
return;
|
||||
|
||||
string json = File.ReadAllText(path);
|
||||
var parsed = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
var parsed = JsonSerializer.Deserialize(json, LocalizationJsonContext.Default.DictionaryStringString);
|
||||
|
||||
if (parsed is not null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>full</TrimMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public sealed class SaveData
|
|||
/// Schema version for forward/backward compatibility checks.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public int Version { get; init; } = 1;
|
||||
public int Version { get; init; } = SaveMigrator.CurrentVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of when the save was created.
|
||||
|
|
|
|||
14
src/OpenTheBox/Persistence/SaveJsonContext.cs
Normal file
14
src/OpenTheBox/Persistence/SaveJsonContext.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace OpenTheBox.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated JSON context for save/load operations.
|
||||
/// Enables trim-safe and reflection-free serialization of game state.
|
||||
/// </summary>
|
||||
[JsonSerializable(typeof(SaveData))]
|
||||
[JsonSourceGenerationOptions(
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
UseStringEnumConverter = true)]
|
||||
internal partial class SaveJsonContext : JsonSerializerContext;
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using OpenTheBox.Core;
|
||||
|
||||
namespace OpenTheBox.Persistence;
|
||||
|
|
@ -14,13 +13,6 @@ public sealed class SaveManager
|
|||
private const string SaveDirectory = "saves";
|
||||
private const string Extension = ".otb";
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the save directory exists.
|
||||
/// </summary>
|
||||
|
|
@ -51,12 +43,13 @@ public sealed class SaveManager
|
|||
State = state
|
||||
};
|
||||
|
||||
string json = JsonSerializer.Serialize(data, SerializerOptions);
|
||||
string json = JsonSerializer.Serialize(data, SaveJsonContext.Default.SaveData);
|
||||
File.WriteAllText(SlotPath(slotName), json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a game state from the specified slot.
|
||||
/// Automatically migrates old save formats to the current version.
|
||||
/// Returns null if the slot does not exist or cannot be deserialized.
|
||||
/// </summary>
|
||||
public GameState? Load(string slotName = "autosave")
|
||||
|
|
@ -67,8 +60,11 @@ public sealed class SaveManager
|
|||
return null;
|
||||
|
||||
string json = File.ReadAllText(path);
|
||||
var data = JsonSerializer.Deserialize<SaveData>(json, SerializerOptions);
|
||||
|
||||
// Migrate old saves if needed
|
||||
json = SaveMigrator.MigrateJson(json);
|
||||
|
||||
var data = JsonSerializer.Deserialize(json, SaveJsonContext.Default.SaveData);
|
||||
return data?.State;
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +84,7 @@ public sealed class SaveManager
|
|||
try
|
||||
{
|
||||
string json = File.ReadAllText(file);
|
||||
var data = JsonSerializer.Deserialize<SaveData>(json, SerializerOptions);
|
||||
var data = JsonSerializer.Deserialize(json, SaveJsonContext.Default.SaveData);
|
||||
if (data is not null)
|
||||
{
|
||||
slots.Add((slotName, data.SavedAt));
|
||||
|
|
|
|||
64
src/OpenTheBox/Persistence/SaveMigrator.cs
Normal file
64
src/OpenTheBox/Persistence/SaveMigrator.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace OpenTheBox.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Migrates save files from older versions to the current version.
|
||||
/// Each migration step transforms the raw JSON DOM from version N to N+1.
|
||||
/// This allows saves to be upgraded incrementally across multiple versions.
|
||||
/// </summary>
|
||||
public static class SaveMigrator
|
||||
{
|
||||
/// <summary>
|
||||
/// The current save format version. Increment this when adding a new migration.
|
||||
/// </summary>
|
||||
public const int CurrentVersion = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Takes raw save JSON, detects its version, applies all necessary migrations,
|
||||
/// and returns the updated JSON string ready for deserialization.
|
||||
/// </summary>
|
||||
public static string MigrateJson(string json)
|
||||
{
|
||||
var root = JsonNode.Parse(json);
|
||||
if (root is null) return json;
|
||||
|
||||
int version = root["version"]?.GetValue<int>() ?? 1;
|
||||
|
||||
if (version >= CurrentVersion)
|
||||
return json;
|
||||
|
||||
while (version < CurrentVersion)
|
||||
{
|
||||
root = version switch
|
||||
{
|
||||
1 => MigrateV1ToV2(root),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Unknown save version {version}. Cannot migrate.")
|
||||
};
|
||||
version++;
|
||||
}
|
||||
|
||||
root["version"] = CurrentVersion;
|
||||
return root.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V1 → V2: Add missing fields introduced after initial release.
|
||||
/// - state.availableFonts (empty array)
|
||||
/// - state.availableTextColors (empty array)
|
||||
/// - state.activeCraftingJobs (empty array)
|
||||
/// </summary>
|
||||
private static JsonNode MigrateV1ToV2(JsonNode root)
|
||||
{
|
||||
var state = root["state"];
|
||||
if (state is null) return root;
|
||||
|
||||
state["availableFonts"] ??= new JsonArray();
|
||||
state["availableTextColors"] ??= new JsonArray();
|
||||
state["activeCraftingJobs"] ??= new JsonArray();
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
|
@ -433,7 +433,7 @@ public static class Program
|
|||
break;
|
||||
|
||||
case AdventureUnlockedEvent advUnlockedEvt:
|
||||
_renderer.ShowMessage(_loc.Get("adventure.unlocked", advUnlockedEvt.Theme.ToString()));
|
||||
_renderer.ShowMessage(_loc.Get("adventure.unlocked", GetAdventureName(advUnlockedEvt.Theme)));
|
||||
break;
|
||||
|
||||
case AdventureStartedEvent advEvt:
|
||||
|
|
@ -521,8 +521,8 @@ public static class Program
|
|||
var options = available.Select(a =>
|
||||
{
|
||||
bool completed = _state.CompletedAdventures.Contains(a.ToString());
|
||||
string prefix = completed ? $"[{_loc.Get("adventure.done")}] " : "";
|
||||
return prefix + a.ToString();
|
||||
string prefix = completed ? $"[[{_loc.Get("adventure.done")}]] " : "";
|
||||
return prefix + GetAdventureName(a);
|
||||
}).ToList();
|
||||
options.Add(_loc.Get("menu.back"));
|
||||
|
||||
|
|
@ -549,7 +549,7 @@ public static class Program
|
|||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
_renderer.ShowMessage(_loc.Get("adventure.coming_soon", theme.ToString()));
|
||||
_renderer.ShowMessage(_loc.Get("adventure.coming_soon", GetAdventureName(theme)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -689,6 +689,14 @@ public static class Program
|
|||
_ => $"meta.{feature.ToString().ToLower()}"
|
||||
};
|
||||
|
||||
private static string GetAdventureName(AdventureTheme theme)
|
||||
{
|
||||
string key = $"adventure.name.{theme}";
|
||||
var name = _loc.Get(key);
|
||||
// Fallback to enum name if no localization key exists
|
||||
return name.StartsWith("[MISSING:") ? theme.ToString() : name;
|
||||
}
|
||||
|
||||
private static string GetLocalizedName(string definitionId)
|
||||
{
|
||||
var itemDef = _registry.GetItem(definitionId);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using OpenTheBox.Core;
|
||||
using OpenTheBox.Core.Enums;
|
||||
using OpenTheBox.Localization;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ public static class StatsPanel
|
|||
/// Builds a renderable stats display from the current game state.
|
||||
/// Only stats present in <see cref="GameState.VisibleStats"/> are shown.
|
||||
/// </summary>
|
||||
public static IRenderable Render(GameState state)
|
||||
public static IRenderable Render(GameState state, LocalizationManager? loc = null)
|
||||
{
|
||||
var rows = new List<IRenderable>();
|
||||
|
||||
|
|
@ -29,16 +30,14 @@ public static class StatsPanel
|
|||
rows.Add(new Markup($" [{color}]{Markup.Escape(label)}:[/] [bold]{value}[/]"));
|
||||
}
|
||||
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
rows.Add(new Markup("[dim]No stats visible yet.[/]"));
|
||||
}
|
||||
|
||||
// Add total boxes opened as a bonus stat
|
||||
rows.Add(new Markup($" [silver]Boxes Opened:[/] [bold]{state.TotalBoxesOpened}[/]"));
|
||||
string boxesLabel = loc?.Get("stats.boxes_opened") ?? "Boxes Opened";
|
||||
rows.Add(new Markup($" [silver]{Markup.Escape(boxesLabel)}:[/] [bold]{state.TotalBoxesOpened}[/]"));
|
||||
|
||||
string title = loc?.Get("stats.title") ?? "Stats";
|
||||
|
||||
return new Panel(new Rows(rows))
|
||||
.Header("[bold magenta]Stats[/]")
|
||||
.Header($"[bold magenta]{Markup.Escape(title)}[/]")
|
||||
.Border(BoxBorder.Rounded);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -378,7 +378,7 @@ public sealed class SpectreRenderer : IRenderer
|
|||
layout["Portrait"].Update(new Panel("[dim]???[/]").Header("Portrait"));
|
||||
|
||||
if (context.HasStatsPanel)
|
||||
layout["Stats"].Update(StatsPanel.Render(state));
|
||||
layout["Stats"].Update(StatsPanel.Render(state, _loc));
|
||||
else
|
||||
layout["Stats"].Update(new Panel("[dim]???[/]").Header("Stats"));
|
||||
|
||||
|
|
@ -422,7 +422,7 @@ public sealed class SpectreRenderer : IRenderer
|
|||
|
||||
if (context.HasStatsPanel)
|
||||
{
|
||||
AnsiConsole.Write(StatsPanel.Render(state));
|
||||
AnsiConsole.Write(StatsPanel.Render(state, _loc));
|
||||
}
|
||||
|
||||
if (context.HasResourcePanel)
|
||||
|
|
|
|||
|
|
@ -120,15 +120,31 @@ public class InteractionEngine(ContentRegistry registry)
|
|||
state.RemoveItem(triggerItem.Id);
|
||||
events.Add(new ItemConsumedEvent(triggerItem.Id));
|
||||
|
||||
// Produce result items if ResultData specifies item definition ids (comma-separated)
|
||||
// Handle result based on ResultType
|
||||
if (rule.ResultData is not null)
|
||||
{
|
||||
var resultItemIds = rule.ResultData.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
foreach (var resultItemId in resultItemIds)
|
||||
if (rule.ResultData.StartsWith("adventure:") || rule.ResultData.StartsWith("adventure_unlock:"))
|
||||
{
|
||||
var resultInstance = ItemInstance.Create(resultItemId);
|
||||
state.AddItem(resultInstance);
|
||||
events.Add(new ItemReceivedEvent(resultInstance));
|
||||
// Unlock an adventure theme (e.g., "adventure:Pirate" or "adventure_unlock:Space")
|
||||
var themeName = rule.ResultData.Contains("adventure_unlock:")
|
||||
? rule.ResultData["adventure_unlock:".Length..]
|
||||
: rule.ResultData["adventure:".Length..];
|
||||
if (Enum.TryParse<AdventureTheme>(themeName, out var theme)
|
||||
&& state.UnlockedAdventures.Add(theme))
|
||||
{
|
||||
events.Add(new AdventureUnlockedEvent(theme));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Produce result items (item definition ids, comma-separated)
|
||||
var resultItemIds = rule.ResultData.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
foreach (var resultItemId in resultItemIds)
|
||||
{
|
||||
var resultInstance = ItemInstance.Create(resultItemId);
|
||||
state.AddItem(resultInstance);
|
||||
events.Add(new ItemReceivedEvent(resultInstance));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -702,6 +702,61 @@ public class ContentValidationTests
|
|||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
// ── French translation tag coverage ────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("space")]
|
||||
[InlineData("medieval")]
|
||||
[InlineData("pirate")]
|
||||
[InlineData("contemporary")]
|
||||
[InlineData("sentimental")]
|
||||
[InlineData("prehistoric")]
|
||||
[InlineData("cosmic")]
|
||||
[InlineData("microscopic")]
|
||||
[InlineData("darkfantasy")]
|
||||
public void Adventure_FrenchTranslationCoversAllTags(string theme)
|
||||
{
|
||||
var enPath = Path.Combine(ContentRoot, "adventures", theme, "intro.lor");
|
||||
var frPath = Path.Combine(ContentRoot, "adventures", theme, "intro.fr.lor");
|
||||
Assert.True(File.Exists(enPath), $"Missing EN script: {enPath}");
|
||||
Assert.True(File.Exists(frPath), $"Missing FR translation: {frPath}");
|
||||
|
||||
// Extract tags from EN file: tags appear at end of lines as #tag-name
|
||||
var enContent = File.ReadAllLines(enPath);
|
||||
var tagRegex = new System.Text.RegularExpressions.Regex(@"#([a-z][a-z0-9_-]*)\b");
|
||||
var enTags = new HashSet<string>();
|
||||
foreach (var line in enContent)
|
||||
{
|
||||
var trimmed = line.TrimStart();
|
||||
if (trimmed.StartsWith("//")) continue;
|
||||
|
||||
foreach (System.Text.RegularExpressions.Match m in tagRegex.Matches(line))
|
||||
{
|
||||
enTags.Add(m.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tags from FR file: tags appear at start of lines as #tag-name
|
||||
var frContent = File.ReadAllLines(frPath);
|
||||
var frTags = new HashSet<string>();
|
||||
foreach (var line in frContent)
|
||||
{
|
||||
var trimmed = line.TrimStart();
|
||||
if (trimmed.StartsWith("#"))
|
||||
{
|
||||
var match = tagRegex.Match(trimmed);
|
||||
if (match.Success)
|
||||
frTags.Add(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
var missingInFr = enTags.Except(frTags).ToList();
|
||||
Assert.True(
|
||||
missingInFr.Count == 0,
|
||||
$"[{theme}] {missingInFr.Count} EN tag(s) missing in FR translation:\n " +
|
||||
string.Join("\n ", missingInFr.OrderBy(t => t)));
|
||||
}
|
||||
|
||||
// ── Full run integration tests ─────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue