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 # TODO
Tous les items ont été traités. Ce fichier est conservé comme historique. Tous les items ont été traités.
## 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.

View file

@ -19,8 +19,8 @@ Examiner la boîte
#opt-ignore // "Ignore it and continue" #opt-ignore // "Ignore it and continue"
L'ignorer et continuer L'ignorer et continuer
#opt-scan // "Run a deep scan first" #opt-scan // "Run a deep scan first|||Fuel reserves insufficient for deep scan"
Lancer un scan approfondi d'abord Lancer un scan approfondi d'abord|||Réserves de carburant insuffisantes pour un scan approfondi
#deepscan-init // "Deep scan initiated. Fuel reserves reduced." #deepscan-init // "Deep scan initiated. Fuel reserves reduced."
Scan approfondi lancé. Réserves de carburant réduites. 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" #opt-open-now // "Open it immediately"
L'ouvrir immédiatement L'ouvrir immédiatement
#opt-scan-first // "Scan it first" #opt-scan-first // "Scan it first|||Not enough fuel for a scan"
La scanner d'abord La scanner d'abord|||Pas assez de carburant pour un scan
#opt-poke // "Poke it with a stick" #opt-poke // "Poke it with a stick"
La pousser avec un bâton 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?" #captain-wasbox // "The alien WAS the box?"
L'extraterrestre ÉTAIT la boîte ? L'extraterrestre ÉTAIT la boîte ?
#alien-intro // I'm Zx'thorp." #alien-intro // "I'm Zx'thorp."
Je suis Zx'thorp. Je suis Zx'thorp.
#open-normal // "Inside you find a Nebula Shard and what appears to be a map to more boxes." #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" #opt-rare-box // "Ask about the galaxy's rarest box"
Demander quelle est la boîte la plus rare de la galaxie 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" #opt-trade // "Trade items with Zx'thorp"
Échanger des objets avec 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" #opt-contest // "Challenge the alien to a box-opening contest"
Défier l'extraterrestre dans un concours d'ouverture de boîtes 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!" #alien-dare // "You dare? No one out-opens Zx'thorp!"
Vous osez ? Personne ne surpasse Zx'thorp à l'ouverture ! Vous osez ? Personne ne surpasse Zx'thorp à l'ouverture !
#alien-contest // "...fine. Best of three. You open first." #alien-contest // "...fine. Best of three. You open first."
...très bien. Au meilleur des trois. Vous ouvrez en premier. ...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." #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. 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" #opt-big // "Open the biggest box first"
Ouvrir la plus grande boîte en premier 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." #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. 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.\"" #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.\" 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" #captain-nebula // "There's a whole nebula of boxes?"
Ouvrir les trois en même temps 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." #captain-velocity // "All three, ARIA. Maximum box velocity."
Les trois, ARIA. Vitesse maximale de boîte. Les trois, ARIA. Vitesse maximale de boîte.
#ai-enthusiasm // "That's not a real measurement, but I admire your enthusiasm." #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. 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." #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. 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." #captain-ofcourse // "Of course they are."
Évidemment. Évidemment.
#opt-happy // "Leave them orbiting, they seem happy" #ai-proud // "A bigger, better box. It's vibrating at a frequency I've never seen. I think it's... proud of itself."
Les laisser en orbite, elles ont l'air heureuses 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." #captain-orbit // "Let them orbit in peace."
Laissons-les orbiter en paix. Laissons-les orbiter en paix.
@ -307,9 +436,18 @@ Laissons-les orbiter en paix.
#ai-philosophical // "A surprisingly philosophical choice, Commander." #ai-philosophical // "A surprisingly philosophical choice, Commander."
Un choix étonnamment philosophique, Commandant. 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. 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." #ending-complete // "Adventure complete. Updating ship's log."
Aventure terminée. Mise à jour du journal de bord. Aventure terminée. Mise à jour du journal de bord.

View file

@ -29,7 +29,7 @@ beat Intro
-> InvestigateBox -> InvestigateBox
Ignore it and continue #opt-ignore Ignore it and continue #opt-ignore
-> IgnoreBox -> 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 fuel -= 20
-> DeepScan -> DeepScan
@ -57,7 +57,7 @@ beat InvestigateBox
choice choice
Open it immediately #opt-open-now Open it immediately #opt-open-now
-> OpenSpaceBox -> 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 fuel -= 10
-> ScanThenOpen -> ScanThenOpen
Poke it with a stick #opt-poke 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 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 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 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 trustAlien = true
discovered += 1 discovered += 1
-> OpenSpaceBox -> OpenSpaceBox
beat PokeBox beat PokeBox
You extend the ship's robotic arm and gently poke the box. #poke-arm 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 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 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 captain: Maybe the box defines its own reality. #captain-reality
ai: That's... philosophically concerning. #ai-philosophy ai: That's... philosophically concerning. #ai-philosophy
-> OpenSpaceBox -> OpenSpaceBox
beat ScanThenOpen 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 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 ai: Approximately the size of a box. Commander, I think the alien IS a box. #ai-alienbox
-> OpenSpaceBox -> OpenSpaceBox
@ -132,7 +132,7 @@ beat LeaveForGood
ai: I hope you're happy, Commander. #ai-happy ai: I hope you're happy, Commander. #ai-happy
captain: I am. No weird space boxes for me today. #captain-happy captain: I am. No weird space boxes for me today. #captain-happy
ai: Incoming transmission. It's... from the box. #ai-transmission 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 captain: That's ominous. I love it. #captain-ominous
-> Ending -> Ending
@ -158,19 +158,71 @@ beat AlienEncounter
alien: I've been collecting boxes across the galaxy for centuries. #alien-collecting alien: I've been collecting boxes across the galaxy for centuries. #alien-collecting
alien: Most species open them. Very few appreciate them. #alien-appreciate alien: Most species open them. Very few appreciate them. #alien-appreciate
captain: I appreciate boxes. Especially ones with good loot. #captain-loot 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 choice
Ask about the galaxy's rarest box #opt-rare-box Ask about the galaxy's rarest box #opt-rare-box
alien: The Singularity Box. It contains everything and nothing. Also a coupon. #alien-singularity -> RareBoxStory
-> Ending
Trade items with Zx'thorp #opt-trade 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 -> AlienTrade
captain: Deal. Wait-- #captain-deal
-> Ending
Challenge the alien to a box-opening contest #opt-contest Challenge the alien to a box-opening contest #opt-contest
-> 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: You dare? No one out-opens Zx'thorp! #alien-dare
alien: ...fine. Best of three. You open first. #alien-contest 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 -> Ending
beat SpaceExploration beat SpaceExploration
@ -182,20 +234,52 @@ beat SpaceExploration
choice choice
Open the biggest box first #opt-big Open the biggest box first #opt-big
-> 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
-> HappyBoxes
beat BigBox
You open the biggest box. Inside: two medium boxes and a note. #big-open 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 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 -> Ending
Open all three at once #opt-all
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 captain: All three, ARIA. Maximum box velocity. #captain-velocity
ai: That's not a real measurement, but I admire your enthusiasm. #ai-enthusiasm 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 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 ai: Commander, the items are forming... a bigger box. #ai-biggerbox
captain: Of course they are. #captain-ofcourse 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 -> Ending
Leave them orbiting, they seem happy #opt-happy
beat HappyBoxes
captain: Let them orbit in peace. #captain-orbit captain: Let them orbit in peace. #captain-orbit
ai: A surprisingly philosophical choice, Commander. #ai-philosophical ai: A surprisingly philosophical choice, Commander. #ai-philosophical
ai: The boxes seem to orbit faster. I think they're happy. #ai-happy 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 -> Ending
beat Ending beat Ending

View file

@ -238,7 +238,6 @@
"rollCount": 1, "rollCount": 1,
"entries": [ "entries": [
{"itemDefinitionId": "meta_completion", "weight": 3}, {"itemDefinitionId": "meta_completion", "weight": 3},
{"itemDefinitionId": "meta_chat", "weight": 3},
{"itemDefinitionId": "meta_crafting", "weight": 3}, {"itemDefinitionId": "meta_crafting", "weight": 3},
{"itemDefinitionId": "meta_extended_colors", "weight": 3}, {"itemDefinitionId": "meta_extended_colors", "weight": 3},
{"itemDefinitionId": "box_meta_resources", "weight": 1} {"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_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_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_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_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_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"}, {"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.appearance": "Change appearance",
"action.save": "Save", "action.save": "Save",
"action.quit": "Return to menu", "action.quit": "Return to menu",
"action.new": "NEW",
"prompt.name": "What is your name, brave box-opener?", "prompt.name": "What is your name, brave box-opener?",
"prompt.choose_action": "What would you like to do?", "prompt.choose_action": "What would you like to do?",
@ -115,7 +116,6 @@
"meta.resources": "Characteristics Panel", "meta.resources": "Characteristics Panel",
"meta.stats": "Stats Panel", "meta.stats": "Stats Panel",
"meta.portrait": "Portrait Panel", "meta.portrait": "Portrait Panel",
"meta.chat": "Chat Panel",
"meta.layout": "Full Layout Mode", "meta.layout": "Full Layout Mode",
"meta.shortcuts": "Keyboard Shortcuts", "meta.shortcuts": "Keyboard Shortcuts",
"meta.animation": "Box Opening Animation", "meta.animation": "Box Opening Animation",
@ -300,8 +300,6 @@
"lore.name_9": "Fragment: The Manual", "lore.name_9": "Fragment: The Manual",
"lore.name_10": "Fragment: Schrödinger", "lore.name_10": "Fragment: Schrödinger",
"log.title": "Event Log",
"cookie.1": "A box within a box is still a box.", "cookie.1": "A box within a box is still a box.",
"cookie.2": "ERROR: This cookie contains no fortune. Please try again.", "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.", "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.none_available": "No adventures available yet. Keep opening boxes!",
"adventure.coming_soon": "Adventure '{0}' is coming soon! The boxes are still being assembled.", "adventure.coming_soon": "Adventure '{0}' is coming soon! The boxes are still being assembled.",
"adventure.done": "Done", "adventure.done": "Done",
"adventure.unavailable": "Unavailable",
"adventure.unlocked": "🎉 New adventure unlocked! Discover '{0}' in the adventure menu!", "adventure.unlocked": "🎉 New adventure unlocked! Discover '{0}' in the adventure menu!",
"adventure.name.Space": "Starbound", "adventure.name.Space": "Starbound",
"adventure.name.Medieval": "Castle Cardboard", "adventure.name.Medieval": "Castle Cardboard",
@ -442,6 +441,27 @@
"adventure.name.Microscopic": "Cell Division", "adventure.name.Microscopic": "Cell Division",
"adventure.name.DarkFantasy": "Ashen Wastes", "adventure.name.DarkFantasy": "Ashen Wastes",
"adventure.name.Destiny": "Gallery of Echoes", "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", "ui.inventory": "Inventory",
"stats.boxes_opened": "Boxes Opened", "stats.boxes_opened": "Boxes Opened",
"stats.title": "Stats", "stats.title": "Stats",
@ -481,7 +501,6 @@
"meta.resources.desc": "Displays your character's characteristics (health, mana, etc.).", "meta.resources.desc": "Displays your character's characteristics (health, mana, etc.).",
"meta.stats.desc": "Shows your progression stats and character attributes.", "meta.stats.desc": "Shows your progression stats and character attributes.",
"meta.portrait.desc": "Displays your character's visual appearance.", "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.layout.desc": "Arranges all panels into a full dashboard layout.",
"meta.shortcuts.desc": "Enables keyboard shortcuts for quick actions.", "meta.shortcuts.desc": "Enables keyboard shortcuts for quick actions.",
"meta.animation.desc": "Adds opening animations when unboxing.", "meta.animation.desc": "Adds opening animations when unboxing.",

View file

@ -19,6 +19,7 @@
"action.appearance": "Changer d'apparence", "action.appearance": "Changer d'apparence",
"action.save": "Sauvegarder", "action.save": "Sauvegarder",
"action.quit": "Retourner au menu", "action.quit": "Retourner au menu",
"action.new": "NOUVEAU",
"prompt.name": "Quel est ton nom, brave ouvreur de boîtes ?", "prompt.name": "Quel est ton nom, brave ouvreur de boîtes ?",
"prompt.choose_action": "Que veux-tu faire ?", "prompt.choose_action": "Que veux-tu faire ?",
@ -115,7 +116,6 @@
"meta.resources": "Panneau de caractéristiques", "meta.resources": "Panneau de caractéristiques",
"meta.stats": "Panneau de statistiques", "meta.stats": "Panneau de statistiques",
"meta.portrait": "Panneau portrait", "meta.portrait": "Panneau portrait",
"meta.chat": "Panneau de discussion",
"meta.layout": "Mise en page complète", "meta.layout": "Mise en page complète",
"meta.shortcuts": "Raccourcis clavier", "meta.shortcuts": "Raccourcis clavier",
"meta.animation": "Animation d'ouverture de boîte", "meta.animation": "Animation d'ouverture de boîte",
@ -300,8 +300,6 @@
"lore.name_9": "Fragment : Le Manuel", "lore.name_9": "Fragment : Le Manuel",
"lore.name_10": "Fragment : Schrödinger", "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.1": "Une boîte dans une boîte reste une boîte.",
"cookie.2": "ERREUR : Ce cookie ne contient aucune fortune. Réessayez.", "cookie.2": "ERREUR : Ce cookie ne contient aucune fortune. Réessayez.",
"cookie.3": "Vous ouvrirez beaucoup de boîtes. Cette prédiction a un taux de précision de 100%.", "cookie.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.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.coming_soon": "L'aventure '{0}' arrive bientôt ! Les boîtes sont encore en cours d'assemblage.",
"adventure.done": "Terminée", "adventure.done": "Terminée",
"adventure.unavailable": "Indisponible",
"adventure.unlocked": "🎉 Nouvelle aventure débloquée ! Découvre '{0}' dans « Partir à l'aventure » !", "adventure.unlocked": "🎉 Nouvelle aventure débloquée ! Découvre '{0}' dans « Partir à l'aventure » !",
"adventure.name.Space": "Odyssée stellaire", "adventure.name.Space": "Odyssée stellaire",
"adventure.name.Medieval": "Château Carton", "adventure.name.Medieval": "Château Carton",
@ -442,6 +441,28 @@
"adventure.name.Microscopic": "Division cellulaire", "adventure.name.Microscopic": "Division cellulaire",
"adventure.name.DarkFantasy": "Terres Cendrées", "adventure.name.DarkFantasy": "Terres Cendrées",
"adventure.name.Destiny": "Galerie des Échos", "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", "ui.inventory": "Inventaire",
"stats.boxes_opened": "Boîtes ouvertes", "stats.boxes_opened": "Boîtes ouvertes",
"stats.title": "Stats", "stats.title": "Stats",
@ -481,7 +502,6 @@
"meta.resources.desc": "Affiche les caractéristiques de ton personnage (santé, mana, etc.).", "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.stats.desc": "Affiche ta progression et les attributs de ton personnage.",
"meta.portrait.desc": "Affiche l'apparence visuelle 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.layout.desc": "Organise tous les panneaux en tableau de bord complet.",
"meta.shortcuts.desc": "Active les raccourcis clavier pour des actions rapides.", "meta.shortcuts.desc": "Active les raccourcis clavier pour des actions rapides.",
"meta.animation.desc": "Ajoute des animations d'ouverture de boîtes.", "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) 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(); _renderer.WaitForKeyPress();
dialogue.Callback(); dialogue.Callback();
} }
@ -211,7 +220,8 @@ public sealed class AdventureEngine
} }
else else
{ {
options.Add($"(unavailable) {text}"); string prefix = hint ?? _loc.Get("adventure.unavailable");
options.Add($"({prefix}) {text}");
hints.Add(hint); hints.Add(hint);
} }
} }

View file

@ -28,9 +28,6 @@ public enum UIFeature
/// <summary>Phase 6: ASCII art portrait reflecting equipped cosmetics.</summary> /// <summary>Phase 6: ASCII art portrait reflecting equipped cosmetics.</summary>
PortraitPanel, 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> /// <summary>Phase 8: Complete multi-panel layout with all UI elements organized.</summary>
FullLayout, FullLayout,

View file

@ -41,12 +41,6 @@ public sealed class GameState
public HashSet<TextColor> AvailableTextColors { get; set; } = []; public HashSet<TextColor> AvailableTextColors { get; set; } = [];
public List<CraftingJob> ActiveCraftingJobs { 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> /// <summary>
/// Returns the current value of a resource, or 0 if the resource is not tracked. /// Returns the current value of a resource, or 0 if the resource is not tracked.
/// </summary> /// </summary>

View file

@ -391,7 +391,12 @@ public static class Program
actions.Add((_loc.Get("action.inventory"), "inventory")); actions.Add((_loc.Get("action.inventory"), "inventory"));
if (_state.UnlockedAdventures.Count > 0) 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) if (_state.UnlockedCosmetics.Count > 0)
actions.Add((_loc.Get("action.appearance"), "appearance")); actions.Add((_loc.Get("action.appearance"), "appearance"));
@ -503,7 +508,6 @@ public static class Program
RefreshRenderer(); RefreshRenderer();
var featureLabel = _loc.Get(GetUIFeatureLocKey(uiEvt.Feature)); var featureLabel = _loc.Get(GetUIFeatureLocKey(uiEvt.Feature));
_renderer.ShowUIFeatureUnlocked(featureLabel); _renderer.ShowUIFeatureUnlocked(featureLabel);
AddEventLog($"* {featureLabel}");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); _renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
break; break;
@ -524,7 +528,6 @@ public static class Program
case ResourceChangedEvent resEvt: case ResourceChangedEvent resEvt:
var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}"); var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}"); _renderer.ShowMessage($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
AddEventLog($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
break; break;
case MessageEvent msgEvt: case MessageEvent msgEvt:
@ -542,7 +545,6 @@ public static class Program
case AdventureUnlockedEvent advUnlockedEvt: case AdventureUnlockedEvent advUnlockedEvt:
var advName = GetAdventureName(advUnlockedEvt.Theme); var advName = GetAdventureName(advUnlockedEvt.Theme);
_renderer.ShowMessage(_loc.Get("adventure.unlocked", advName)); _renderer.ShowMessage(_loc.Get("adventure.unlocked", advName));
AddEventLog($">> {advName}");
break; break;
case AdventureStartedEvent advEvt: case AdventureStartedEvent advEvt:
@ -582,10 +584,6 @@ public static class Program
{ {
_renderer.ShowLootReveal(allLoot); _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 // Resource summary removed — characteristics are shown in the dedicated panel
} }
@ -753,7 +751,6 @@ public static class Program
: _loc.Get("inventory.item_used", itemName); : _loc.Get("inventory.item_used", itemName);
_renderer.ShowMessage(usedMsg); _renderer.ShowMessage(usedMsg);
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}"); _renderer.ShowMessage($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
AddEventLog($"{itemName} {UnicodeSupport.Arrow} {resName} {resEvt.OldValue}{UnicodeSupport.Arrow}{resEvt.NewValue}");
break; break;
case MessageEvent msgEvt: case MessageEvent msgEvt:
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? [])); _renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
@ -1000,7 +997,6 @@ public static class Program
UIFeature.ResourcePanel => "meta.resources", UIFeature.ResourcePanel => "meta.resources",
UIFeature.StatsPanel => "meta.stats", UIFeature.StatsPanel => "meta.stats",
UIFeature.PortraitPanel => "meta.portrait", UIFeature.PortraitPanel => "meta.portrait",
UIFeature.ChatPanel => "meta.chat",
UIFeature.FullLayout => "meta.layout", UIFeature.FullLayout => "meta.layout",
UIFeature.KeyboardShortcuts => "meta.shortcuts", UIFeature.KeyboardShortcuts => "meta.shortcuts",
UIFeature.BoxAnimation => "meta.animation", UIFeature.BoxAnimation => "meta.animation",
@ -1018,18 +1014,6 @@ public static class Program
return name.StartsWith("[MISSING:") ? theme.ToString() : name; 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) private static string GetLocalizedName(string definitionId)
{ {
var itemDef = _registry.GetItem(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 HasResourcePanel => Has(UIFeature.ResourcePanel);
public bool HasStatsPanel => Has(UIFeature.StatsPanel); public bool HasStatsPanel => Has(UIFeature.StatsPanel);
public bool HasPortraitPanel => Has(UIFeature.PortraitPanel); public bool HasPortraitPanel => Has(UIFeature.PortraitPanel);
public bool HasChatPanel => Has(UIFeature.ChatPanel);
public bool HasFullLayout => Has(UIFeature.FullLayout); public bool HasFullLayout => Has(UIFeature.FullLayout);
public bool HasKeyboardShortcuts => Has(UIFeature.KeyboardShortcuts); public bool HasKeyboardShortcuts => Has(UIFeature.KeyboardShortcuts);
public bool HasBoxAnimation => Has(UIFeature.BoxAnimation); public bool HasBoxAnimation => Has(UIFeature.BoxAnimation);

View file

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

View file

@ -496,9 +496,6 @@ public sealed class SpectreRenderer : IRenderer
if (context.HasCraftingPanel) if (context.HasCraftingPanel)
rightItems.Add(CraftingPanel.Render(state, _registry, _loc)); rightItems.Add(CraftingPanel.Render(state, _registry, _loc));
if (context.HasChatPanel)
rightItems.Add(ChatPanel.Render(state.EventLog, _loc));
if (context.HasCompletionTracker) if (context.HasCompletionTracker)
rightItems.Add(new Markup($"[bold cyan]{Markup.Escape(_loc.Get("ui.completion", context.CompletionPercent))}[/]")); 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) if (context.HasCraftingPanel)
AnsiConsole.Write(CraftingPanel.Render(state, _registry, _loc)); AnsiConsole.Write(CraftingPanel.Render(state, _registry, _loc));
if (context.HasChatPanel)
AnsiConsole.Write(ChatPanel.Render(state.EventLog, _loc));
if (context.HasCompletionTracker) if (context.HasCompletionTracker)
AnsiConsole.Write(new Rule($"[bold cyan]{Markup.Escape(_loc.Get("ui.completion", context.CompletionPercent))}[/]").RuleStyle("cyan")); 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_stats", // StatsPanel
"meta_resources", // ResourcePanel "meta_resources", // ResourcePanel
"meta_portrait", // PortraitPanel "meta_portrait", // PortraitPanel
"meta_chat", // ChatPanel
"meta_extended_colors", // ExtendedColors "meta_extended_colors", // ExtendedColors
"meta_animation", // BoxAnimation "meta_animation", // BoxAnimation
"meta_crafting", // CraftingPanel "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 ──────────────────────────────── // ── RenderContext + RendererFactory Tests ────────────────────────────────
public class RenderContextTests public class RenderContextTests
@ -457,7 +403,6 @@ public class RenderContextTests
Assert.False(ctx.HasResourcePanel); Assert.False(ctx.HasResourcePanel);
Assert.False(ctx.HasStatsPanel); Assert.False(ctx.HasStatsPanel);
Assert.False(ctx.HasPortraitPanel); Assert.False(ctx.HasPortraitPanel);
Assert.False(ctx.HasChatPanel);
Assert.False(ctx.HasFullLayout); Assert.False(ctx.HasFullLayout);
Assert.False(ctx.HasKeyboardShortcuts); Assert.False(ctx.HasKeyboardShortcuts);
Assert.False(ctx.HasBoxAnimation); Assert.False(ctx.HasBoxAnimation);
@ -488,7 +433,6 @@ public class RendererFactoryTests
[InlineData(UIFeature.ResourcePanel)] [InlineData(UIFeature.ResourcePanel)]
[InlineData(UIFeature.StatsPanel)] [InlineData(UIFeature.StatsPanel)]
[InlineData(UIFeature.PortraitPanel)] [InlineData(UIFeature.PortraitPanel)]
[InlineData(UIFeature.ChatPanel)]
[InlineData(UIFeature.FullLayout)] [InlineData(UIFeature.FullLayout)]
[InlineData(UIFeature.KeyboardShortcuts)] [InlineData(UIFeature.KeyboardShortcuts)]
[InlineData(UIFeature.BoxAnimation)] [InlineData(UIFeature.BoxAnimation)]
@ -749,7 +693,6 @@ public class SpectreRendererOutputTests : IDisposable
ctx.Unlock(UIFeature.StatsPanel); ctx.Unlock(UIFeature.StatsPanel);
ctx.Unlock(UIFeature.ResourcePanel); ctx.Unlock(UIFeature.ResourcePanel);
ctx.Unlock(UIFeature.InventoryPanel); ctx.Unlock(UIFeature.InventoryPanel);
ctx.Unlock(UIFeature.ChatPanel);
ctx.Unlock(UIFeature.CompletionTracker); ctx.Unlock(UIFeature.CompletionTracker);
ctx.CompletionPercent = 42; ctx.CompletionPercent = 42;
var r = new SpectreRenderer(ctx, _loc); var r = new SpectreRenderer(ctx, _loc);

View file

@ -420,7 +420,6 @@ public class ContentValidationTests
[UIFeature.ResourcePanel] = "meta.resources", [UIFeature.ResourcePanel] = "meta.resources",
[UIFeature.StatsPanel] = "meta.stats", [UIFeature.StatsPanel] = "meta.stats",
[UIFeature.PortraitPanel] = "meta.portrait", [UIFeature.PortraitPanel] = "meta.portrait",
[UIFeature.ChatPanel] = "meta.chat",
[UIFeature.FullLayout] = "meta.layout", [UIFeature.FullLayout] = "meta.layout",
[UIFeature.KeyboardShortcuts] = "meta.shortcuts", [UIFeature.KeyboardShortcuts] = "meta.shortcuts",
[UIFeature.BoxAnimation] = "meta.animation", [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 // Orphan check: items with no usage at all
report.AppendLine("## Orphan Items (no usage context)"); report.AppendLine("## Orphan Items (no usage context)");
report.AppendLine(new string('─', 80)); report.AppendLine(new string('─', 80));
@ -1903,9 +1925,9 @@ public class ContentValidationTests
string reportText = report.ToString(); string reportText = report.ToString();
// Write snapshot to repo root (tests/snapshots/) so it can be committed // Write snapshot to tests/snapshots/ relative to the test project file
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); var testProjectDir = Path.GetDirectoryName(GetThisFilePath())!;
var snapshotDir = Path.Combine(repoRoot, "tests", "snapshots"); var snapshotDir = Path.Combine(testProjectDir, "..", "snapshots");
Directory.CreateDirectory(snapshotDir); Directory.CreateDirectory(snapshotDir);
var snapshotPath = Path.Combine(snapshotDir, "item_utility_report.txt"); var snapshotPath = Path.Combine(snapshotDir, "item_utility_report.txt");
@ -1950,4 +1972,6 @@ public class ContentValidationTests
if (item.Tags.Contains("Cookie")) count++; if (item.Tags.Contains("Cookie")) count++;
return count; return count;
} }
private static string GetThisFilePath([System.Runtime.CompilerServices.CallerFilePath] string path = "") => path;
} }

View file

@ -1,5 +1,5 @@
# Item Utility Report # Item Utility Report
# Total items: 146 # Total items: 145
# Total boxes: 31 # Total boxes: 31
# Total recipes: 18 # Total recipes: 18
@ -481,7 +481,7 @@
[NO USE] material_wood_nail (Common) — Bois [NO USE] material_wood_nail (Common) — Bois
## Meta (34 items) ## Meta (33 items)
──────────────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────────────────
[**] meta_colors (Rare) — Couleurs de texte [**] meta_colors (Rare) — Couleurs de texte
Loot: box_meta_basics Loot: box_meta_basics
@ -511,10 +511,6 @@
Loot: box_meta_basics Loot: box_meta_basics
Unlock: PortraitPanel Unlock: PortraitPanel
[**] meta_chat (Epic) — Panneau de discussion
Loot: box_meta_deep
Unlock: ChatPanel
[**] meta_layout (Legendary) — Mise en page complète [**] meta_layout (Legendary) — Mise en page complète
Loot: box_meta_interface Loot: box_meta_interface
Unlock: FullLayout Unlock: FullLayout
@ -610,6 +606,50 @@
[*] meta_stat_wisdom (Rare) — Sagesse [*] meta_stat_wisdom (Rare) — Sagesse
Loot: box_meta_mastery 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) ## Orphan Items (no usage context)
──────────────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────────────────
material_wood_nail (Material, Common) material_wood_nail (Material, Common)