Complete TODO.md: remove event log, retouch Space adventure, add character name translation

- Remove ChatPanel/EventLog entirely (UIFeature, GameState, MetaEngine,
  SpectreRenderer, Program, ChatPanelTests, meta_chat item/box/strings)
- Retouch Space adventure: expand AlienEncounter with 3 proper endings
  (RareBoxStory, AlienTrade, BoxContest with flair secret branch),
  expand SpaceExploration with fuel-gated choices and deeper branching
- Add (NOUVEAU/NEW) prefix to adventure action when no adventure completed
- Translate character names via localization keys (character.* in en/fr.json)
- Replace hardcoded "(unavailable)" with localized hint or fallback text
- Fix snapshot path to use CallerFilePath instead of fragile ../ chain
- Add Workstations summary section to item_utility_report.txt
This commit is contained in:
Samuel Bouchet 2026-03-15 17:08:46 +01:00
parent 006ef1f94a
commit 4d8d5224e1
19 changed files with 413 additions and 241 deletions

27
TODO.md
View file

@ -1,28 +1,3 @@
# TODO
Tous les items ont été traités. Ce fichier est conservé comme historique.
## Noms de personnages
Les noms des personnages dans les histoires ne sont pas traduits à l'affichage. Loreline ne propose a priori pas de système de traduction pour les noms de personnages donc il faudra des clés de traductions dédiées.
## Partir à l'aventure
Lorsqu'on débloque "Partir à l'aventure" mais qu'aucune aventure n'a été lancée il faut ajouter une incentive plus claire à lancer la première aventure. L'entrée devrait avec le préfix (new) ou (nouveau) et être en couleur (si la couleur est débloquée) tant qu'une aventure n'a pas été faite.
## Retouch aventure Space
Dans cette Space aventure, la fin tombe un peu abrute: le concours d'ouverture de boite n'a pas lieu, "Trade items with Zx'thorp" tourne court également.
Attendu: Il faudrait un fin un peu plus sympa ou couper plus tôt (l'aventure peu être plus courte si besoin: la perfection est atteinte non pas lorsqu'il n'y a plus rien à ajouter mais plus rien à enlever !)
Dans cette Space aventure le fuel semble être un sujet sauf qu'en réalité on ne peut pas tomber à court. Ce serait bien de rendre ce state vraiment utile (ex: si on utilise le fuel tôt on ne peut plus l'utiliser plus tard).
## Unavailable
Plutôt que d'avoir "unavailable" pour un choix à condition, ce sera mieux de donner un indice sur ce qu'il faut.
Ex: "(Si j'avais plus de force…)" ou "(Si j'avais des jambes adaptées…)"
## Journal d'événements
pas utile après test: à supprimer complètement.
Tous les items ont été traités.

View file

@ -19,8 +19,8 @@ Examiner la boîte
#opt-ignore // "Ignore it and continue"
L'ignorer et continuer
#opt-scan // "Run a deep scan first"
Lancer un scan approfondi d'abord
#opt-scan // "Run a deep scan first|||Fuel reserves insufficient for deep scan"
Lancer un scan approfondi d'abord|||Réserves de carburant insuffisantes pour un scan approfondi
#deepscan-init // "Deep scan initiated. Fuel reserves reduced."
Scan approfondi lancé. Réserves de carburant réduites.
@ -61,8 +61,8 @@ Ne me dites pas quels mots utiliser pour les boîtes, ARIA.
#opt-open-now // "Open it immediately"
L'ouvrir immédiatement
#opt-scan-first // "Scan it first"
La scanner d'abord
#opt-scan-first // "Scan it first|||Not enough fuel for a scan"
La scanner d'abord|||Pas assez de carburant pour un scan
#opt-poke // "Poke it with a stick"
La pousser avec un bâton
@ -208,7 +208,7 @@ Ah, un autre appréciateur de boîtes ! Prenez ceci comme cadeau de ma collectio
#captain-wasbox // "The alien WAS the box?"
L'extraterrestre ÉTAIT la boîte ?
#alien-intro // I'm Zx'thorp."
#alien-intro // "I'm Zx'thorp."
Je suis Zx'thorp.
#open-normal // "Inside you find a Nebula Shard and what appears to be a map to more boxes."
@ -238,27 +238,117 @@ J'apprécie les boîtes. Surtout celles avec du bon butin.
#opt-rare-box // "Ask about the galaxy's rarest box"
Demander quelle est la boîte la plus rare de la galaxie
#alien-singularity // "The Singularity Box. It contains everything and nothing. Also a coupon."
La Boîte Singulière. Elle contient tout et rien. Et aussi un coupon.
#opt-trade // "Trade items with Zx'thorp"
Échanger des objets avec Zx'thorp
#alien-trade // "I have a Space Helmet for you. In exchange, I want your sense of wonder."
J'ai un Casque Spatial pour vous. En échange, je veux votre sens de l'émerveillement.
#captain-deal // "Deal. Wait--"
Marché. Attendez--
#opt-contest // "Challenge the alien to a box-opening contest"
Défier l'extraterrestre dans un concours d'ouverture de boîtes
#alien-singularity // "The Singularity Box. It contains everything and nothing. Also a coupon."
La Boîte Singulière. Elle contient tout et rien. Et aussi un coupon.
#captain-coupon // "A coupon? For what?"
Un coupon ? Pour quoi ?
#alien-coupon-answer // "For another Singularity Box. It's boxes all the way down."
Pour une autre Boîte Singulière. Ce sont des boîtes jusqu'au bout.
#captain-boxes-inside // "You're saying the universe is just boxes inside boxes?"
Vous dites que l'univers n'est qu'une boîte dans des boîtes ?
#alien-getting-it // "Now you're getting it, Captain."
Vous commencez à comprendre, Capitaine.
#ai-existential // "Commander, I'm detecting a slight existential crisis in your vitals."
Commandant, je détecte une légère crise existentielle dans vos signes vitaux.
#captain-fine // "I'm fine, ARIA. Just rethinking everything I know about reality."
Ça va, ARIA. Je remets juste en question tout ce que je sais sur la réalité.
#alien-trade-offer // "I have a Space Helmet for you. In exchange, I want... a story."
J'ai un Casque Spatial pour vous. En échange, je veux... une histoire.
#captain-story // "A story?"
Une histoire ?
#alien-first-box // "Tell me about the first box you ever opened."
Racontez-moi la première boîte que vous avez ouverte.
#captain-first-box-story // "It was small. Cardboard. My parents gave it to me. Inside was a toy rocket ship."
Elle était petite. En carton. Mes parents me l'avaient offerte. À l'intérieur, il y avait une fusée jouet.
#alien-pause // "..."
...
#alien-beautiful-trade // "That's the most beautiful thing I've ever heard."
C'est la plus belle chose que j'aie jamais entendue.
#alien-gives-helmet // "The alien removes its helmet and places it gently in your hands. Its eyes -- all seven of them -- are glistening."
L'extraterrestre retire son casque et le place délicatement dans vos mains. Ses yeux -- les sept -- brillent.
#captain-crying // "Are you... crying?"
Vous... pleurez ?
#alien-appreciation // "We don't cry. We leak appreciation fluid. It's different."
Nous ne pleurons pas. Nous sécrétons du fluide d'appréciation. C'est différent.
#alien-dare // "You dare? No one out-opens Zx'thorp!"
Vous osez ? Personne ne surpasse Zx'thorp à l'ouverture !
#alien-contest // "...fine. Best of three. You open first."
...très bien. Au meilleur des trois. Vous ouvrez en premier.
#opt-confident // "Open with confidence"
Ouvrir avec assurance
#opt-flair // "Open with theatrical flair"
Ouvrir avec panache
#flair-spin // "You crack your knuckles. You stretch your fingers. You do a little spin."
Vous faites craquer vos doigts. Vous étirez vos mains. Vous faites une petite pirouette.
#captain-watch // "Watch and learn, Zx'thorp."
Regardez et apprenez, Zx'thorp.
#flair-confetti // "You open the box with a flourish, confetti exploding from inside. Wait -- you didn't put confetti in there."
Vous ouvrez la boîte avec un geste théâtral, des confettis explosent de l'intérieur. Attendez -- vous n'avez pas mis de confettis dedans.
#alien-score-flair // "Theatrical. I approve. 8 out of 10."
Théâtral. J'approuve. 8 sur 10.
#ai-confetti // "Commander, where did the confetti come from?"
Commandant, d'où viennent les confettis ?
#captain-heart // "From the heart, ARIA. From the heart."
Du cœur, ARIA. Du cœur.
#contest-open // "You grip the box firmly and open it in one clean motion."
Vous saisissez la boîte fermement et l'ouvrez d'un geste net.
#alien-score // "Clean technique. 7 out of 10."
Technique propre. 7 sur 10.
#alien-produces // "Zx'thorp produces a box from somewhere. You try not to think about where."
Zx'thorp sort une boîte de quelque part. Vous essayez de ne pas penser d'où.
#alien-opens // "The alien opens it using three of its tentacles simultaneously. The lid flies off, does a triple backflip, and lands perfectly back on the box."
L'extraterrestre l'ouvre avec trois de ses tentacules simultanément. Le couvercle s'envole, fait un triple salto arrière et atterrit parfaitement sur la boîte.
#captain-impossible // "That's... that's not even possible."
C'est... c'est même pas possible.
#alien-score-2 // "10 out of 10. I win."
10 sur 10. Je gagne.
#ai-correct // "I'm afraid the alien is correct, Commander. That was objectively superior box-opening."
Je crains que l'extraterrestre ait raison, Commandant. C'était objectivement une ouverture de boîte supérieure.
#captain-rematch // "Rematch. One day. I'll be ready."
Revanche. Un jour. Je serai prêt.
#alien-waiting // "I look forward to it, Captain. Across the stars, I'll be waiting. With boxes."
J'ai hâte, Capitaine. À travers les étoiles, je vous attendrai. Avec des boîtes.
#explore-course // "Setting course for the nearest box. ETA: approximately one dramatic pause."
Cap vers la boîte la plus proche. Arrivée estimée : environ une pause dramatique.
@ -274,20 +364,53 @@ La grande semble contenir les deux autres. Ce sont des boîtes jusqu'au bout.
#opt-big // "Open the biggest box first"
Ouvrir la plus grande boîte en premier
#opt-all // "Open all three at once|||Not enough fuel for maximum box velocity"
Ouvrir les trois en même temps|||Pas assez de carburant pour la vitesse maximale de boîte
#opt-happy // "Leave them orbiting, they seem happy"
Les laisser en orbite, elles ont l'air heureuses
#big-open // "You open the biggest box. Inside: two medium boxes and a note."
Vous ouvrez la plus grande boîte. À l'intérieur : deux boîtes moyennes et un mot.
#big-note // "The note reads: \"Congratulations! You've found the Box Nebula. Population: boxes.\""
Le mot dit : \"Félicitations ! Vous avez trouvé la Nébuleuse des Boîtes. Population : des boîtes.\"
#opt-all // "Open all three at once"
Ouvrir les trois en même temps
#captain-nebula // "There's a whole nebula of boxes?"
Il y a une nébuleuse entière de boîtes ?
#ai-billion // "Scanners confirm it, Commander. Approximately 4.7 billion boxes, all orbiting a supermassive box."
Les scanners confirment, Commandant. Environ 4,7 milliards de boîtes, toutes en orbite autour d'une boîte supermassive.
#opt-deeper // "We must go deeper"
Il faut aller plus loin
#opt-enough // "We've seen enough boxes for one day"
On a vu assez de boîtes pour aujourd'hui
#deeper-open // "You open the two medium boxes. Each contains two smaller boxes."
Vous ouvrez les deux boîtes moyennes. Chacune contient deux boîtes plus petites.
#captain-how-many // "ARIA, how many boxes is this now?"
ARIA, ça fait combien de boîtes maintenant ?
#ai-crashed // "I've lost count, Commander. My box-tracking subroutine has crashed."
J'ai perdu le compte, Commandant. Mon sous-programme de suivi de boîtes a planté.
#captain-count // "Then let the boxes count themselves."
Alors laissons les boîtes se compter elles-mêmes.
#ai-not-how // "...that's not how boxes work, Commander."
...ce n'est pas comme ça que les boîtes fonctionnent, Commandant.
#captain-should-be // "Maybe it should be."
Peut-être que ça devrait l'être.
#captain-velocity // "All three, ARIA. Maximum box velocity."
Les trois, ARIA. Vitesse maximale de boîte.
#ai-enthusiasm // "That's not a real measurement, but I admire your enthusiasm."
Ce n'est pas une vraie mesure, mais j'admire votre enthousiasme.
#ai-enthusiasm // "That's not a real measurement, but I admire your enthusiasm. Fuel reserves depleted for the maneuver."
Ce n'est pas une vraie mesure, mais j'admire votre enthousiasme. Réserves de carburant épuisées pour la manœuvre.
#all-open // "All three boxes burst open simultaneously. The contents mix together in zero gravity."
Les trois boîtes s'ouvrent simultanément. Le contenu se mélange en apesanteur.
@ -298,8 +421,14 @@ Commandant, les objets forment... une plus grande boîte.
#captain-ofcourse // "Of course they are."
Évidemment.
#opt-happy // "Leave them orbiting, they seem happy"
Les laisser en orbite, elles ont l'air heureuses
#ai-proud // "A bigger, better box. It's vibrating at a frequency I've never seen. I think it's... proud of itself."
Une plus grande, meilleure boîte. Elle vibre à une fréquence que je n'ai jamais vue. Je crois qu'elle est... fière d'elle-même.
#captain-everything // "A proud box. Now I've seen everything."
Une boîte fière. Maintenant j'ai tout vu.
#ai-doubt // "Commander, I doubt that very much."
Commandant, j'en doute fortement.
#captain-orbit // "Let them orbit in peace."
Laissons-les orbiter en paix.
@ -307,9 +436,18 @@ Laissons-les orbiter en paix.
#ai-philosophical // "A surprisingly philosophical choice, Commander."
Un choix étonnamment philosophique, Commandant.
#ai-happy // "The boxes seem to orbit faster. I think they're happy."
#ai-happy-boxes // "The boxes seem to orbit faster. I think they're happy."
Les boîtes semblent orbiter plus vite. Je crois qu'elles sont heureuses.
#captain-best-box // "See? Sometimes the best box is the one you don't open."
Vous voyez ? Parfois la meilleure boîte est celle qu'on n'ouvre pas.
#ai-wisest // "I'm going to write that down. That might be the wisest thing you've ever said."
Je vais noter ça. C'est peut-être la chose la plus sage que vous ayez jamais dite.
#captain-used-to-it // "Don't get used to it."
N'y prenez pas goût.
#ending-complete // "Adventure complete. Updating ship's log."
Aventure terminée. Mise à jour du journal de bord.

View file

@ -29,7 +29,7 @@ beat Intro
-> InvestigateBox
Ignore it and continue #opt-ignore
-> IgnoreBox
Run a deep scan first #opt-scan if fuel > 20
Run a deep scan first|||Fuel reserves insufficient for deep scan #opt-scan if fuel > 20
fuel -= 20
-> DeepScan
@ -57,7 +57,7 @@ beat InvestigateBox
choice
Open it immediately #opt-open-now
-> OpenSpaceBox
Scan it first #opt-scan-first if fuel > 10
Scan it first|||Not enough fuel for a scan #opt-scan-first if fuel > 10
fuel -= 10
-> ScanThenOpen
Poke it with a stick #opt-poke
@ -73,23 +73,23 @@ beat BoxWhisperer
captain: I'm not doing anything, ARIA. I'm just listening. #captain-listening
ai: The box has shared a direct route to its home system. No fuel cost. It's folding space around us like... #ai-folding
captain: Like folding a box? #captain-folding
ai: I was going to say \"like origami,\" but yes. Like folding a box. #ai-origami
ai: I was going to say "like origami," but yes. Like folding a box. #ai-origami
trustAlien = true
discovered += 1
-> OpenSpaceBox
beat PokeBox
You extend the ship's robotic arm and gently poke the box. #poke-arm
The box rotates 90 degrees and reveals a label: \"THIS SIDE UP\" #poke-label
ai: Commander, I don't think boxes in zero gravity have an \"up\". #ai-gravity
The box rotates 90 degrees and reveals a label: "THIS SIDE UP" #poke-label
ai: Commander, I don't think boxes in zero gravity have an "up". #ai-gravity
captain: Maybe the box defines its own reality. #captain-reality
ai: That's... philosophically concerning. #ai-philosophy
-> OpenSpaceBox
beat ScanThenOpen
ai: Scan reveals the box contains crystallized starlight and something called a \"Nebula Shard\". #scan-contents
ai: Scan reveals the box contains crystallized starlight and something called a "Nebula Shard". #scan-contents
ai: Also what appears to be a very small, very angry alien. #scan-alien
captain: Define \"very small\". #captain-small
captain: Define "very small". #captain-small
ai: Approximately the size of a box. Commander, I think the alien IS a box. #ai-alienbox
-> OpenSpaceBox
@ -132,7 +132,7 @@ beat LeaveForGood
ai: I hope you're happy, Commander. #ai-happy
captain: I am. No weird space boxes for me today. #captain-happy
ai: Incoming transmission. It's... from the box. #ai-transmission
ai: It says \"you'll be back. they always come back.\" #ai-message
ai: It says "you'll be back. they always come back." #ai-message
captain: That's ominous. I love it. #captain-ominous
-> Ending
@ -158,20 +158,72 @@ beat AlienEncounter
alien: I've been collecting boxes across the galaxy for centuries. #alien-collecting
alien: Most species open them. Very few appreciate them. #alien-appreciate
captain: I appreciate boxes. Especially ones with good loot. #captain-loot
alien: \"Loot.\" What a crude word for cosmic treasures. #alien-crude
alien: "Loot." What a crude word for cosmic treasures. #alien-crude
choice
Ask about the galaxy's rarest box #opt-rare-box
alien: The Singularity Box. It contains everything and nothing. Also a coupon. #alien-singularity
-> Ending
-> RareBoxStory
Trade items with Zx'thorp #opt-trade
alien: I have a Space Helmet for you. In exchange, I want your sense of wonder. #alien-trade
captain: Deal. Wait-- #captain-deal
-> Ending
-> AlienTrade
Challenge the alien to a box-opening contest #opt-contest
alien: You dare? No one out-opens Zx'thorp! #alien-dare
alien: ...fine. Best of three. You open first. #alien-contest
-> Ending
-> BoxContest
beat RareBoxStory
alien: The Singularity Box. It contains everything and nothing. Also a coupon. #alien-singularity
captain: A coupon? For what? #captain-coupon
alien: For another Singularity Box. It's boxes all the way down. #alien-coupon-answer
captain: You're saying the universe is just boxes inside boxes? #captain-boxes-inside
alien: Now you're getting it, Captain. #alien-getting-it
ai: Commander, I'm detecting a slight existential crisis in your vitals. #ai-existential
captain: I'm fine, ARIA. Just rethinking everything I know about reality. #captain-fine
-> Ending
beat AlienTrade
alien: I have a Space Helmet for you. In exchange, I want... a story. #alien-trade-offer
captain: A story? #captain-story
alien: Tell me about the first box you ever opened. #alien-first-box
captain: It was small. Cardboard. My parents gave it to me. Inside was a toy rocket ship. #captain-first-box-story
alien: ... #alien-pause
alien: That's the most beautiful thing I've ever heard. #alien-beautiful-trade
The alien removes its helmet and places it gently in your hands. Its eyes -- all seven of them -- are glistening. #alien-gives-helmet
captain: Are you... crying? #captain-crying
alien: We don't cry. We leak appreciation fluid. It's different. #alien-appreciation
-> Ending
beat BoxContest
alien: You dare? No one out-opens Zx'thorp! #alien-dare
alien: ...fine. Best of three. You open first. #alien-contest
choice
Open with confidence #opt-confident
-> ContestRoundOne
Open with theatrical flair #opt-flair
-> ContestFlair
beat ContestFlair
markSecretBranch("space_box_flair")
You crack your knuckles. You stretch your fingers. You do a little spin. #flair-spin
captain: Watch and learn, Zx'thorp. #captain-watch
You open the box with a flourish, confetti exploding from inside. Wait -- you didn't put confetti in there. #flair-confetti
alien: Theatrical. I approve. 8 out of 10. #alien-score-flair
ai: Commander, where did the confetti come from? #ai-confetti
captain: From the heart, ARIA. From the heart. #captain-heart
-> ContestAlienTurn
beat ContestRoundOne
You grip the box firmly and open it in one clean motion. #contest-open
alien: Clean technique. 7 out of 10. #alien-score
-> ContestAlienTurn
beat ContestAlienTurn
Zx'thorp produces a box from somewhere. You try not to think about where. #alien-produces
The alien opens it using three of its tentacles simultaneously. The lid flies off, does a triple backflip, and lands perfectly back on the box. #alien-opens
captain: That's... that's not even possible. #captain-impossible
alien: 10 out of 10. I win. #alien-score-2
ai: I'm afraid the alien is correct, Commander. That was objectively superior box-opening. #ai-correct
captain: Rematch. One day. I'll be ready. #captain-rematch
alien: I look forward to it, Captain. Across the stars, I'll be waiting. With boxes. #alien-waiting
-> Ending
beat SpaceExploration
ai: Setting course for the nearest box. ETA: approximately one dramatic pause. #explore-course
@ -182,22 +234,54 @@ beat SpaceExploration
choice
Open the biggest box first #opt-big
You open the biggest box. Inside: two medium boxes and a note. #big-open
The note reads: \"Congratulations! You've found the Box Nebula. Population: boxes.\" #big-note
-> Ending
Open all three at once #opt-all
captain: All three, ARIA. Maximum box velocity. #captain-velocity
ai: That's not a real measurement, but I admire your enthusiasm. #ai-enthusiasm
All three boxes burst open simultaneously. The contents mix together in zero gravity. #all-open
ai: Commander, the items are forming... a bigger box. #ai-biggerbox
captain: Of course they are. #captain-ofcourse
-> Ending
-> BigBox
Open all three at once|||Not enough fuel for maximum box velocity #opt-all if fuel >= 30
fuel -= 30
-> AllBoxes
Leave them orbiting, they seem happy #opt-happy
captain: Let them orbit in peace. #captain-orbit
ai: A surprisingly philosophical choice, Commander. #ai-philosophical
ai: The boxes seem to orbit faster. I think they're happy. #ai-happy
-> HappyBoxes
beat BigBox
You open the biggest box. Inside: two medium boxes and a note. #big-open
The note reads: "Congratulations! You've found the Box Nebula. Population: boxes." #big-note
captain: There's a whole nebula of boxes? #captain-nebula
ai: Scanners confirm it, Commander. Approximately 4.7 billion boxes, all orbiting a supermassive box. #ai-billion
choice
We must go deeper #opt-deeper
-> GoDeeper
We've seen enough boxes for one day #opt-enough
-> Ending
beat GoDeeper
You open the two medium boxes. Each contains two smaller boxes. #deeper-open
captain: ARIA, how many boxes is this now? #captain-how-many
ai: I've lost count, Commander. My box-tracking subroutine has crashed. #ai-crashed
captain: Then let the boxes count themselves. #captain-count
ai: ...that's not how boxes work, Commander. #ai-not-how
captain: Maybe it should be. #captain-should-be
-> Ending
beat AllBoxes
captain: All three, ARIA. Maximum box velocity. #captain-velocity
ai: That's not a real measurement, but I admire your enthusiasm. Fuel reserves depleted for the maneuver. #ai-enthusiasm
All three boxes burst open simultaneously. The contents mix together in zero gravity. #all-open
ai: Commander, the items are forming... a bigger box. #ai-biggerbox
captain: Of course they are. #captain-ofcourse
ai: A bigger, better box. It's vibrating at a frequency I've never seen. I think it's... proud of itself. #ai-proud
captain: A proud box. Now I've seen everything. #captain-everything
ai: Commander, I doubt that very much. #ai-doubt
-> Ending
beat HappyBoxes
captain: Let them orbit in peace. #captain-orbit
ai: A surprisingly philosophical choice, Commander. #ai-philosophical
ai: The boxes seem to orbit faster. I think they're happy. #ai-happy-boxes
captain: See? Sometimes the best box is the one you don't open. #captain-best-box
ai: I'm going to write that down. That might be the wisest thing you've ever said. #ai-wisest
captain: Don't get used to it. #captain-used-to-it
-> Ending
beat Ending
ai: Adventure complete. Updating ship's log. #ending-complete
ai: Boxes encountered: $discovered. Box-related existential crises: 1. #ending-log

View file

@ -238,7 +238,6 @@
"rollCount": 1,
"entries": [
{"itemDefinitionId": "meta_completion", "weight": 3},
{"itemDefinitionId": "meta_chat", "weight": 3},
{"itemDefinitionId": "meta_crafting", "weight": 3},
{"itemDefinitionId": "meta_extended_colors", "weight": 3},
{"itemDefinitionId": "box_meta_resources", "weight": 1}

View file

@ -6,7 +6,6 @@
{"id": "meta_resources", "nameKey": "meta.resources", "descriptionKey": "meta.resources.desc", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "ResourcePanel"},
{"id": "meta_stats", "nameKey": "meta.stats", "descriptionKey": "meta.stats.desc", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "StatsPanel"},
{"id": "meta_portrait", "nameKey": "meta.portrait", "descriptionKey": "meta.portrait.desc", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "PortraitPanel"},
{"id": "meta_chat", "nameKey": "meta.chat", "descriptionKey": "meta.chat.desc", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "ChatPanel"},
{"id": "meta_layout", "nameKey": "meta.layout", "descriptionKey": "meta.layout.desc", "category": "Meta", "rarity": "Legendary", "tags": ["Meta"], "metaUnlock": "FullLayout"},
{"id": "meta_shortcuts", "nameKey": "meta.shortcuts", "descriptionKey": "meta.shortcuts.desc", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "KeyboardShortcuts"},
{"id": "meta_animation", "nameKey": "meta.animation", "descriptionKey": "meta.animation.desc", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta"], "metaUnlock": "BoxAnimation"},

View file

@ -19,6 +19,7 @@
"action.appearance": "Change appearance",
"action.save": "Save",
"action.quit": "Return to menu",
"action.new": "NEW",
"prompt.name": "What is your name, brave box-opener?",
"prompt.choose_action": "What would you like to do?",
@ -115,7 +116,6 @@
"meta.resources": "Characteristics Panel",
"meta.stats": "Stats Panel",
"meta.portrait": "Portrait Panel",
"meta.chat": "Chat Panel",
"meta.layout": "Full Layout Mode",
"meta.shortcuts": "Keyboard Shortcuts",
"meta.animation": "Box Opening Animation",
@ -300,8 +300,6 @@
"lore.name_9": "Fragment: The Manual",
"lore.name_10": "Fragment: Schrödinger",
"log.title": "Event Log",
"cookie.1": "A box within a box is still a box.",
"cookie.2": "ERROR: This cookie contains no fortune. Please try again.",
"cookie.3": "You will open many boxes. This prediction has a 100% accuracy rate.",
@ -431,6 +429,7 @@
"adventure.none_available": "No adventures available yet. Keep opening boxes!",
"adventure.coming_soon": "Adventure '{0}' is coming soon! The boxes are still being assembled.",
"adventure.done": "Done",
"adventure.unavailable": "Unavailable",
"adventure.unlocked": "🎉 New adventure unlocked! Discover '{0}' in the adventure menu!",
"adventure.name.Space": "Starbound",
"adventure.name.Medieval": "Castle Cardboard",
@ -442,6 +441,27 @@
"adventure.name.Microscopic": "Cell Division",
"adventure.name.DarkFantasy": "Ashen Wastes",
"adventure.name.Destiny": "Gallery of Echoes",
"character.captain_nova": "Captain Nova",
"character.aria": "ARIA",
"character.zxthorp": "Zx'thorp",
"character.sir_boxalot": "Sir Boxalot",
"character.malkith": "Malkith",
"character.blackbeard_the_unboxable": "Blackbeard the Unboxable",
"character.linu": "Linu",
"character.sandrea": "Sandrea",
"character.samuel": "Samuel",
"character.mordecai_the_grim": "Mordecai the Grim",
"character.pierrick": "Pierrick",
"character.grug": "Grug",
"character.duncan": "Duncan",
"character.chenda": "Chenda",
"character.farah": "Farah",
"character.dr._quantum": "Dr. Quantum",
"character.the_box_entity": "The Box Entity",
"character.dr._cellina": "Dr. Cellina",
"character.mitochondria_mike": "Mitochondria Mike",
"character.the_box": "The Box",
"ui.inventory": "Inventory",
"stats.boxes_opened": "Boxes Opened",
"stats.title": "Stats",
@ -481,7 +501,6 @@
"meta.resources.desc": "Displays your character's characteristics (health, mana, etc.).",
"meta.stats.desc": "Shows your progression stats and character attributes.",
"meta.portrait.desc": "Displays your character's visual appearance.",
"meta.chat.desc": "Shows an event log of recent actions.",
"meta.layout.desc": "Arranges all panels into a full dashboard layout.",
"meta.shortcuts.desc": "Enables keyboard shortcuts for quick actions.",
"meta.animation.desc": "Adds opening animations when unboxing.",

View file

@ -19,6 +19,7 @@
"action.appearance": "Changer d'apparence",
"action.save": "Sauvegarder",
"action.quit": "Retourner au menu",
"action.new": "NOUVEAU",
"prompt.name": "Quel est ton nom, brave ouvreur de boîtes ?",
"prompt.choose_action": "Que veux-tu faire ?",
@ -115,7 +116,6 @@
"meta.resources": "Panneau de caractéristiques",
"meta.stats": "Panneau de statistiques",
"meta.portrait": "Panneau portrait",
"meta.chat": "Panneau de discussion",
"meta.layout": "Mise en page complète",
"meta.shortcuts": "Raccourcis clavier",
"meta.animation": "Animation d'ouverture de boîte",
@ -300,8 +300,6 @@
"lore.name_9": "Fragment : Le Manuel",
"lore.name_10": "Fragment : Schrödinger",
"log.title": "Journal d'événements",
"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%.",
@ -431,6 +429,7 @@
"adventure.none_available": "Aucune aventure disponible. Continue à ouvrir des boîtes !",
"adventure.coming_soon": "L'aventure '{0}' arrive bientôt ! Les boîtes sont encore en cours d'assemblage.",
"adventure.done": "Terminée",
"adventure.unavailable": "Indisponible",
"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",
@ -442,6 +441,28 @@
"adventure.name.Microscopic": "Division cellulaire",
"adventure.name.DarkFantasy": "Terres Cendrées",
"adventure.name.Destiny": "Galerie des Échos",
"character.captain_nova": "Capitaine Nova",
"character.aria": "ARIA",
"character.zxthorp": "Zx'thorp",
"character.sir_boxalot": "Sire Boîtalot",
"character.malkith": "Malkith",
"character.blackbeard_the_unboxable": "Barbe-Noire l'Inouvrable",
"character.linu": "Linu",
"character.sandrea": "Sandrea",
"character.samuel": "Samuel",
"character.mordecai_the_grim": "Mordecai le Sinistre",
"character.pierrick": "Pierrick",
"character.grug": "Grug",
"character.duncan": "Duncan",
"character.chenda": "Chenda",
"character.farah": "Farah",
"character.dr._quantum": "Dr. Quantum",
"character.the_box_entity": "L'Entité Boîte",
"character.dr._cellina": "Dr. Cellina",
"character.mitochondria_mike": "Mike Mitochondrie",
"character.the_box": "La Boîte",
"ui.inventory": "Inventaire",
"stats.boxes_opened": "Boîtes ouvertes",
"stats.title": "Stats",
@ -481,7 +502,6 @@
"meta.resources.desc": "Affiche les caractéristiques de ton personnage (santé, mana, etc.).",
"meta.stats.desc": "Affiche ta progression et les attributs de ton personnage.",
"meta.portrait.desc": "Affiche l'apparence visuelle de ton personnage.",
"meta.chat.desc": "Affiche un journal des événements récents.",
"meta.layout.desc": "Organise tous les panneaux en tableau de bord complet.",
"meta.shortcuts.desc": "Active les raccourcis clavier pour des actions rapides.",
"meta.animation.desc": "Ajoute des animations d'ouverture de boîtes.",

View file

@ -190,7 +190,16 @@ public sealed class AdventureEngine
private void HandleDialogue(Loreline.Interpreter.Dialogue dialogue)
{
_renderer.ShowAdventureDialogue(dialogue.Character, dialogue.Text);
// Translate character name if a localization key exists
string? displayName = dialogue.Character;
if (displayName is not null)
{
string key = $"character.{displayName.ToLowerInvariant().Replace(" ", "_").Replace("'", "")}";
string localized = _loc.Get(key);
if (!localized.StartsWith("[MISSING:"))
displayName = localized;
}
_renderer.ShowAdventureDialogue(displayName, dialogue.Text);
_renderer.WaitForKeyPress();
dialogue.Callback();
}
@ -211,7 +220,8 @@ public sealed class AdventureEngine
}
else
{
options.Add($"(unavailable) {text}");
string prefix = hint ?? _loc.Get("adventure.unavailable");
options.Add($"({prefix}) {text}");
hints.Add(hint);
}
}

View file

@ -28,9 +28,6 @@ public enum UIFeature
/// <summary>Phase 6: ASCII art portrait reflecting equipped cosmetics.</summary>
PortraitPanel,
/// <summary>Phase 6: Chat panel for NPC dialogues and narrative events.</summary>
ChatPanel,
/// <summary>Phase 8: Complete multi-panel layout with all UI elements organized.</summary>
FullLayout,

View file

@ -41,12 +41,6 @@ public sealed class GameState
public HashSet<TextColor> AvailableTextColors { get; set; } = [];
public List<CraftingJob> ActiveCraftingJobs { get; set; } = [];
/// <summary>
/// In-memory event log shown in the ChatPanel (not persisted to save).
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
public List<string> EventLog { get; set; } = [];
/// <summary>
/// Returns the current value of a resource, or 0 if the resource is not tracked.
/// </summary>

View file

@ -391,7 +391,12 @@ public static class Program
actions.Add((_loc.Get("action.inventory"), "inventory"));
if (_state.UnlockedAdventures.Count > 0)
actions.Add((_loc.Get("action.adventure"), "adventure"));
{
string adventureLabel = _loc.Get("action.adventure");
if (_state.CompletedAdventures.Count == 0)
adventureLabel = $"({_loc.Get("action.new")}) {adventureLabel}";
actions.Add((adventureLabel, "adventure"));
}
if (_state.UnlockedCosmetics.Count > 0)
actions.Add((_loc.Get("action.appearance"), "appearance"));
@ -503,7 +508,6 @@ public static class Program
RefreshRenderer();
var featureLabel = _loc.Get(GetUIFeatureLocKey(uiEvt.Feature));
_renderer.ShowUIFeatureUnlocked(featureLabel);
AddEventLog($"* {featureLabel}");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
break;
@ -524,7 +528,6 @@ public static class Program
case ResourceChangedEvent resEvt:
var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
AddEventLog($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
break;
case MessageEvent msgEvt:
@ -542,7 +545,6 @@ public static class Program
case AdventureUnlockedEvent advUnlockedEvt:
var advName = GetAdventureName(advUnlockedEvt.Theme);
_renderer.ShowMessage(_loc.Get("adventure.unlocked", advName));
AddEventLog($">> {advName}");
break;
case AdventureStartedEvent advEvt:
@ -582,10 +584,6 @@ public static class Program
{
_renderer.ShowLootReveal(allLoot);
// Proposal 6A: Feed loot to the event log
foreach (var (name, rarity, _) in allLoot)
AddEventLog($"+ {name} [{_loc.Get($"rarity.{rarity.ToLower()}")}]");
// Resource summary removed — characteristics are shown in the dedicated panel
}
@ -753,7 +751,6 @@ public static class Program
: _loc.Get("inventory.item_used", itemName);
_renderer.ShowMessage(usedMsg);
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
AddEventLog($"{itemName} {UnicodeSupport.Arrow} {resName} {resEvt.OldValue}{UnicodeSupport.Arrow}{resEvt.NewValue}");
break;
case MessageEvent msgEvt:
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
@ -1000,7 +997,6 @@ public static class Program
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",
@ -1018,18 +1014,6 @@ public static class Program
return name.StartsWith("[MISSING:") ? theme.ToString() : name;
}
private const int MaxEventLogEntries = 20;
/// <summary>
/// Adds a message to the in-memory event log displayed in the ChatPanel.
/// </summary>
private static void AddEventLog(string message)
{
_state.EventLog.Add(message);
if (_state.EventLog.Count > MaxEventLogEntries)
_state.EventLog.RemoveAt(0);
}
private static string GetLocalizedName(string definitionId)
{
var itemDef = _registry.GetItem(definitionId);

View file

@ -1,45 +0,0 @@
using OpenTheBox.Localization;
using Spectre.Console;
using Spectre.Console.Rendering;
namespace OpenTheBox.Rendering.Panels;
/// <summary>
/// Renders a compact event log panel showing recent game events.
/// Replaces the original dialogue-only chat panel with a general-purpose log.
/// </summary>
public static class ChatPanel
{
private const int MaxVisibleMessages = 8;
/// <summary>
/// Builds a renderable event log from a list of recent log messages.
/// </summary>
public static IRenderable Render(List<string> logMessages, LocalizationManager? loc = null)
{
var rows = new List<IRenderable>();
// Show only the most recent messages
var visible = logMessages.Count > MaxVisibleMessages
? logMessages.Skip(logMessages.Count - MaxVisibleMessages).ToList()
: logMessages;
if (visible.Count == 0)
{
var emptyText = loc?.Get("craft.panel.empty") ?? "...";
rows.Add(new Markup($"[dim]{Markup.Escape(emptyText)}[/]"));
}
else
{
foreach (var msg in visible)
{
rows.Add(new Markup($"[dim]{Markup.Escape(msg)}[/]"));
}
}
var title = loc?.Get("log.title") ?? "Log";
return new Panel(new Rows(rows))
.Header($"[bold aqua]{Markup.Escape(title)}[/]")
.Border(BoxBorder.Rounded);
}
}

View file

@ -31,7 +31,6 @@ public sealed class RenderContext
public bool HasResourcePanel => Has(UIFeature.ResourcePanel);
public bool HasStatsPanel => Has(UIFeature.StatsPanel);
public bool HasPortraitPanel => Has(UIFeature.PortraitPanel);
public bool HasChatPanel => Has(UIFeature.ChatPanel);
public bool HasFullLayout => Has(UIFeature.FullLayout);
public bool HasKeyboardShortcuts => Has(UIFeature.KeyboardShortcuts);
public bool HasBoxAnimation => Has(UIFeature.BoxAnimation);

View file

@ -24,7 +24,6 @@ public static class RendererFactory
context.HasResourcePanel ||
context.HasStatsPanel ||
context.HasPortraitPanel ||
context.HasChatPanel ||
context.HasFullLayout ||
context.HasKeyboardShortcuts ||
context.HasBoxAnimation ||

View file

@ -496,9 +496,6 @@ public sealed class SpectreRenderer : IRenderer
if (context.HasCraftingPanel)
rightItems.Add(CraftingPanel.Render(state, _registry, _loc));
if (context.HasChatPanel)
rightItems.Add(ChatPanel.Render(state.EventLog, _loc));
if (context.HasCompletionTracker)
rightItems.Add(new Markup($"[bold cyan]{Markup.Escape(_loc.Get("ui.completion", context.CompletionPercent))}[/]"));
@ -547,9 +544,6 @@ public sealed class SpectreRenderer : IRenderer
if (context.HasCraftingPanel)
AnsiConsole.Write(CraftingPanel.Render(state, _registry, _loc));
if (context.HasChatPanel)
AnsiConsole.Write(ChatPanel.Render(state.EventLog, _loc));
if (context.HasCompletionTracker)
AnsiConsole.Write(new Rule($"[bold cyan]{Markup.Escape(_loc.Get("ui.completion", context.CompletionPercent))}[/]").RuleStyle("cyan"));
}

View file

@ -26,7 +26,6 @@ public class MetaEngine
"meta_stats", // StatsPanel
"meta_resources", // ResourcePanel
"meta_portrait", // PortraitPanel
"meta_chat", // ChatPanel
"meta_extended_colors", // ExtendedColors
"meta_animation", // BoxAnimation
"meta_crafting", // CraftingPanel

View file

@ -348,60 +348,6 @@ public class InventoryPanelTests
}
}
public class ChatPanelTests
{
[Fact]
public void Render_Empty_DoesNotThrow()
{
var result = RenderHelper.RenderToString(ChatPanel.Render([]));
Assert.NotEmpty(result);
}
[Fact]
public void Render_SingleMessage_DoesNotThrow()
{
var messages = new List<string> { "The door creaks open." };
var result = RenderHelper.RenderToString(ChatPanel.Render(messages));
Assert.NotEmpty(result);
}
[Fact]
public void Render_MultipleMessages_DoesNotThrow()
{
var messages = new List<string>
{
"+ Small Health Potion [Common]",
"★ Text Colors",
"Health: 0 → 10",
"🗺 Starbound"
};
var result = RenderHelper.RenderToString(ChatPanel.Render(messages));
Assert.NotEmpty(result);
}
[Fact]
public void Render_OverflowMessages_ShowsOnlyRecent()
{
var messages = Enumerable.Range(0, 15)
.Select(i => $"Event {i}")
.ToList();
var result = RenderHelper.RenderToString(ChatPanel.Render(messages));
Assert.NotEmpty(result);
}
[Fact]
public void Render_SpecialCharacters_DoesNotThrow()
{
var messages = new List<string>
{
"+ [Boss] item <rare>",
"A [mysterious] voice echoes."
};
var result = RenderHelper.RenderToString(ChatPanel.Render(messages));
Assert.NotEmpty(result);
}
}
// ── RenderContext + RendererFactory Tests ────────────────────────────────
public class RenderContextTests
@ -457,7 +403,6 @@ public class RenderContextTests
Assert.False(ctx.HasResourcePanel);
Assert.False(ctx.HasStatsPanel);
Assert.False(ctx.HasPortraitPanel);
Assert.False(ctx.HasChatPanel);
Assert.False(ctx.HasFullLayout);
Assert.False(ctx.HasKeyboardShortcuts);
Assert.False(ctx.HasBoxAnimation);
@ -488,7 +433,6 @@ public class RendererFactoryTests
[InlineData(UIFeature.ResourcePanel)]
[InlineData(UIFeature.StatsPanel)]
[InlineData(UIFeature.PortraitPanel)]
[InlineData(UIFeature.ChatPanel)]
[InlineData(UIFeature.FullLayout)]
[InlineData(UIFeature.KeyboardShortcuts)]
[InlineData(UIFeature.BoxAnimation)]
@ -749,7 +693,6 @@ public class SpectreRendererOutputTests : IDisposable
ctx.Unlock(UIFeature.StatsPanel);
ctx.Unlock(UIFeature.ResourcePanel);
ctx.Unlock(UIFeature.InventoryPanel);
ctx.Unlock(UIFeature.ChatPanel);
ctx.Unlock(UIFeature.CompletionTracker);
ctx.CompletionPercent = 42;
var r = new SpectreRenderer(ctx, _loc);

View file

@ -420,7 +420,6 @@ public class ContentValidationTests
[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",
@ -1889,6 +1888,29 @@ public class ContentValidationTests
}
}
// Workstation summary: what each bench can craft
report.AppendLine("## Workstations");
report.AppendLine(new string('─', 80));
var recipesByStation = registry.Recipes.Values
.GroupBy(r => r.Workstation)
.OrderBy(g => g.Key.ToString());
foreach (var stationGroup in recipesByStation)
{
report.AppendLine($" 🔨 {stationGroup.Key}");
foreach (var recipe in stationGroup.OrderBy(r => r.Id))
{
var ingredientNames = recipe.Ingredients
.Select(i => registry.Items.TryGetValue(i.ItemDefinitionId, out var iDef)
? $"{loc.Get(iDef.NameKey)} x{i.Quantity}"
: $"{i.ItemDefinitionId} x{i.Quantity}");
var resultName = registry.Items.TryGetValue(recipe.Result.ItemDefinitionId, out var rDef)
? loc.Get(rDef.NameKey)
: recipe.Result.ItemDefinitionId;
report.AppendLine($" {recipe.Id}: {string.Join(" + ", ingredientNames)} → {resultName} x{recipe.Result.Quantity}");
}
report.AppendLine();
}
// Orphan check: items with no usage at all
report.AppendLine("## Orphan Items (no usage context)");
report.AppendLine(new string('─', 80));
@ -1903,9 +1925,9 @@ public class ContentValidationTests
string reportText = report.ToString();
// Write snapshot to repo root (tests/snapshots/) so it can be committed
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
var snapshotDir = Path.Combine(repoRoot, "tests", "snapshots");
// Write snapshot to tests/snapshots/ relative to the test project file
var testProjectDir = Path.GetDirectoryName(GetThisFilePath())!;
var snapshotDir = Path.Combine(testProjectDir, "..", "snapshots");
Directory.CreateDirectory(snapshotDir);
var snapshotPath = Path.Combine(snapshotDir, "item_utility_report.txt");
@ -1950,4 +1972,6 @@ public class ContentValidationTests
if (item.Tags.Contains("Cookie")) count++;
return count;
}
private static string GetThisFilePath([System.Runtime.CompilerServices.CallerFilePath] string path = "") => path;
}

View file

@ -1,5 +1,5 @@
# Item Utility Report
# Total items: 146
# Total items: 145
# Total boxes: 31
# Total recipes: 18
@ -481,7 +481,7 @@
[NO USE] material_wood_nail (Common) — Bois
## Meta (34 items)
## Meta (33 items)
────────────────────────────────────────────────────────────────────────────────
[**] meta_colors (Rare) — Couleurs de texte
Loot: box_meta_basics
@ -511,10 +511,6 @@
Loot: box_meta_basics
Unlock: PortraitPanel
[**] meta_chat (Epic) — Panneau de discussion
Loot: box_meta_deep
Unlock: ChatPanel
[**] meta_layout (Legendary) — Mise en page complète
Loot: box_meta_interface
Unlock: FullLayout
@ -610,6 +606,50 @@
[*] meta_stat_wisdom (Rare) — Sagesse
Loot: box_meta_mastery
## Workstations
────────────────────────────────────────────────────────────────────────────────
🔨 DrawingTable
chart_star_navigation: Coordonnées mystérieuses x1 + Carte stellaire x1 → Clé d'accès au sas x1
🔨 EngineerDesk
engineer_rocket_boots: Short x1 + Titane x1 + Éclat de nébuleuse x2 → Bottes à réaction x1
🔨 EngravingBench
engrave_royal_seal: Blason de chevalier x1 + Parchemin ancien x1 → Sceau royal x1
🔨 Forge
craft_box_cool: Acier x2 + Néon x1 → box_cool x1
forge_armored_plate: Acier x3 + Fibre de carbone x2 → Armure x1
forge_carbonfiber_sheet: Fibre de carbone x4 → Fibre de carbone x1
🔨 Foundry
refine_wood: Bois x2 → Bois x1
🔨 Furnace
smelt_bronze_ingot: Bronze x3 → Bronze x1
smelt_iron_ingot: Fer x3 → Fer x1
smelt_steel_ingot: Acier x4 → Acier x1
smelt_titanium_ingot: Titane x5 → Titane x1
🔨 GeneticModStation
splice_glowing_dna: Échantillon de bactérie sentiente x1 + Prion amical (probablement) x1 → Brin d'ADN luminescent x1
🔨 MatterSynthesizer
fuse_cosmic_crystal: Éclat de nébuleuse x2 → Cristal de quasar x1
🔨 Printer3D
craft_box_epic: Titane x2 + Fibre de carbone x1 + Or x1 → box_epic x1
🔨 StasisChamber
preserve_amber: Dent de dinosaure x2 → Pierre d'ambre x1
🔨 TransformationPentacle
enchant_dark_grimoire: Anneau maudit x2 → Grimoire du nécromancien x1
🔨 Workbench
craft_box_ok_tier: Bois x2 + Bronze x1 → box_ok_tier x1
craft_pilot_glasses: Lunettes de soleil x1 + Bronze x1 + Argent x1 → Lunettes d'aviateur x1
## Orphan Items (no usage context)
────────────────────────────────────────────────────────────────────────────────
material_wood_nail (Material, Common)