From 7a854ccc157a0ac6cc37f622348db2d8567db676 Mon Sep 17 00:00:00 2001 From: Samuel Bouchet Date: Sun, 15 Mar 2026 15:05:45 +0100 Subject: [PATCH] Remove 6 unused resource types and add item utility snapshot test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip Health, Mana, Food, Stamina, Oxygen, Energy — only Gold and Blood remain as they serve as adventure gates (Contemporary ≥30, DarkFantasy ≥20). Remove 22 orphaned items, 5 recipes, and the AlchemyTable workstation. Replace energy_cell in rocket_boots recipe with cosmic_shard. Change box_endgame condition from AllResourcesVisible to BoxesOpenedAbove:500. Add ItemUtilitySnapshot test that maps every item to its usage contexts (loot sources, crafting, interactions, adventures) and generates a report. DEBUG overwrites the snapshot; RELEASE asserts no changes. Update specifications.md and CLAUDE.md to reflect resource cleanup. Remove obsolete bugs.md and refactoring_plan.md. --- CLAUDE.md | 26 +- bugs.md | 41 ---- content/data/boxes.json | 50 +--- content/data/items.json | 24 +- content/data/recipes.json | 51 +--- content/strings/en.json | 30 +-- content/strings/fr.json | 30 +-- refactoring_plan.md | 165 ------------- specifications.md | 44 ++-- src/OpenTheBox/Core/Enums/ResourceType.cs | 29 +-- .../Rendering/Panels/ResourcePanel.cs | 8 +- tests/OpenTheBox.Tests/CraftingTests.cs | 4 +- tests/OpenTheBox.Tests/RendererTests.cs | 16 +- tests/OpenTheBox.Tests/UnitTest1.cs | 225 ++++++++++++++++++ 14 files changed, 309 insertions(+), 434 deletions(-) delete mode 100644 refactoring_plan.md diff --git a/CLAUDE.md b/CLAUDE.md index c501017..d642a54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ See [specifications.md](specifications.md) for detailed content organization. - `Core/Enums/` — All enum types (StatType, ResourceType, AdventureTheme, CosmeticSlot, etc.) - `Adventures/` — AdventureEngine (Loreline bridge + custom functions) - `Simulation/` — Game engines (BoxEngine, MetaEngine, ResourceEngine, CraftingEngine, GameSimulation) -- `Rendering/` — IRenderer interface, SpectreRenderer, RenderContext, panel components +- `Rendering/` — IRenderer interface, SpectreRenderer, RenderContext, UnicodeSupport, panel components - `Rendering/Panels/` — Individual UI panels (PortraitPanel, ResourcePanel, StatsPanel, etc.) - `Data/` — ContentRegistry, ItemDefinition, BoxDefinition data loading - `Persistence/` — SaveManager, SaveData (JSON serialization) @@ -30,6 +30,11 @@ dotnet run --project src/OpenTheBox # Spectre.Console sequent dotnet run --project src/OpenTheBox -- --snapshot 5 # Load snapshot save #5 ``` +On Windows, use the launcher for best UTF-8 support: +``` +OpenTheBox.cmd +``` + ## Test ``` dotnet test @@ -47,6 +52,17 @@ Hints for disabled choices use `|||` separator: `Option text|||Hint text #label full documentation: https://loreline.app/fr/docs/ +## Resources (Characteristics) +Only 2 resource types exist: **Gold** and **Blood**. Both have adventure secret branch gates: +- Gold ≥ 30 → Contemporary VIP branch +- Blood ≥ 20 → DarkFantasy Blood Communion branch + +Consumables: `gold_pouch` (+50 Gold), `blood_vial` (+5 Blood), `resource_max_gold`, `resource_max_blood`. +Other resources (Health, Mana, Food, etc.) were removed as they had no gameplay impact. + +## Ephemeral Items +Fortune cookies (`cookie_fortune`) and music melodies (`music_melody`) are consumed immediately on receipt. They trigger their effects (fortune message, melody playback) but never appear in the player's inventory. + ## Pacing Test To check game progression balance after modifying loot tables (`content/data/boxes.json`): ``` @@ -62,6 +78,13 @@ Key things to look for: Weights are in `content/data/boxes.json`. The main generator is `box_of_boxes` (auto-opens, produces the next box). Adjust weights there and in tier boxes (`box_not_great`, `box_ok_tier`, etc.) to tune pacing. +## Item Utility Audit +To generate a full report of all items and their usage contexts: +``` +dotnet test --filter "ItemUtilitySnapshot" --logger "console;verbosity=detailed" +``` +The report is written to `tests/snapshots/item_utility_report.txt`. It shows every item with all its usage contexts (loot, crafting, interactions, etc.) sorted by utility score. Items with no usage are flagged as orphans. In DEBUG mode the snapshot is overwritten; in RELEASE mode the test fails if the content changed. + ## Save Snapshots (Visual Testing) Generate save files at 9 progression stages for quick visual testing: ``` @@ -126,3 +149,4 @@ To capture rendering for a new panel or UI element: - GameState is mutable, passed by reference to engines - All user-facing strings go through LocalizationManager - Enum names match JSON string values (PascalCase) +- Unicode characters (★, →, ✓) auto-detected via UnicodeSupport; ASCII fallback for cmd.exe diff --git a/bugs.md b/bugs.md index 57ed279..22df47d 100644 --- a/bugs.md +++ b/bugs.md @@ -4,44 +4,3 @@ Les sujets dans FIXME doivent être corrigé, puis déplacé dans "DONE", puis c # FIXME - - -# DONE - -## Terminal size 120×30 — DONE -SpectreRenderer cap désormais `AnsiConsole.Profile.Width` à 120 colonnes (RefWidth). Le layout est conçu pour tenir dans 30 lignes. Les constantes `RefWidth=120` et `RefHeight=30` servent de référence. - -## Layout compact tmux — DONE -RenderFullLayout utilise des `Table.NoBorder()` pour placer les panels côte à côte sans gaps. Row 1: Portrait(20) | Stats(30) | Resources(fill). Row 2: Inventory(60) | Crafting+Chat+Completion(fill). RenderSequentialPanels groupe aussi les panels top côte à côte quand plusieurs sont débloqués. - -## Couleurs sur le portrait — DONE -Chaque type de cosmétique a maintenant sa propre couleur intrinsèque (yeux bleus=dodgerblue, cheveux feu=red, cyberpunk=aqua, etc.). Les tints explicites (HairTint/BodyTint) restent prioritaires. Les yeux, jambes et bras ont aussi leurs propres couleurs au lieu d'être tous blancs. - - - -## panneau d'inventaire — DONE -Scroll interactif implémenté : ↑↓ ligne par ligne, PgUp/PgDn page par page, Esc/Q pour sortir. Panneau limité à 15 lignes avec indicateur de position (ex: 1-15/42). Noms traduits, catégories, raretés colorées, colonne Name fixée à 24 chars. - -## Polices de caractère — supprimées -Impossible de changer la police du terminal programmatiquement. Les items font ont été supprimés du jeu (items.json, boxes.json, enum, code). - -## Portrait représente une boîte — DONE -Le portrait ASCII art représente maintenant une boîte (+------+) avec des cosmétiques dessus : cheveux sur le dessus, yeux sur la face, corps comme décoration, jambes en dessous, bras sur les côtés. - -## Police de caractère — DONE -Les fonts sont des collectibles purs (comptent pour la complétion). Le terminal gère sa propre police. Un message explicite est maintenant affiché au loot : « Police 'X' collectionnée ! (Collectible — la police de votre terminal reste inchangée) ». - -## Double crochets aventures — DONE -Le préfixe [Terminée] utilisait [[...]] (échappement Spectre) mais ShowSelection échappe déjà les options. Corrigé en utilisant des crochets simples. - -## Boîtes meta auto-upgrade — DONE -BoxEngine détecte automatiquement quand tous les items d'un tier meta sont obtenus et upgrade le box_meta vers le tier suivant : basics → interface → deep → resources → mastery. Revert de la fusion incorrecte. - -## Double boîte d'aventure pirate (interactions) — DONE -Le prompt ChoiceRequiredEvent utilisait un texte anglais hardcodé comme clé de localisation → [MISSING:...]. Corrigé : utilise la clé "prompt.choose_interaction" et affiche les DescriptionKey des règles (traduits) au lieu des IDs bruts. - -## Aventure destiny FR — DONE -Créé intro.fr.lor pour l'aventure destiny (68 tags traduits). Tests ajoutés (existence, parsing, couverture tags). - -## Fin de partie après destiny — DONE -Après l'aventure destiny, un choix épilogue est proposé : continuer en jeu libre ou refermer la boîte (quitter). Le ton reste poétique et cohérent avec le récit. diff --git a/content/data/boxes.json b/content/data/boxes.json index 045d24f..6fe6aa6 100644 --- a/content/data/boxes.json +++ b/content/data/boxes.json @@ -36,7 +36,7 @@ {"itemDefinitionId": "box_story", "weight": 1, "condition": {"type": "BoxesOpenedAbove", "value": 5}}, {"itemDefinitionId": "box_cookie", "weight": 1, "condition": {"type": "BoxesOpenedAbove", "value": 3}}, {"itemDefinitionId": "box_music", "weight": 1, "condition": {"type": "BoxesOpenedAbove", "value": 10}}, - {"itemDefinitionId": "box_endgame", "weight": 1, "condition": {"type": "AllResourcesVisible"}} + {"itemDefinitionId": "box_endgame", "weight": 1, "condition": {"type": "BoxesOpenedAbove", "value": 500}} ] } }, @@ -52,7 +52,6 @@ "entries": [ {"itemDefinitionId": "material_wood_raw", "weight": 5}, {"itemDefinitionId": "material_bronze_raw", "weight": 3}, - {"itemDefinitionId": "food_ration", "weight": 4}, {"itemDefinitionId": "cosmetic_hair_short", "weight": 2}, {"itemDefinitionId": "cosmetic_eyes_brown", "weight": 2}, {"itemDefinitionId": "cosmetic_body_tshirt", "weight": 2}, @@ -77,8 +76,6 @@ "entries": [ {"itemDefinitionId": "material_iron_raw", "weight": 4}, {"itemDefinitionId": "material_bronze_ingot", "weight": 3}, - {"itemDefinitionId": "health_potion_small", "weight": 4}, - {"itemDefinitionId": "mana_crystal_small", "weight": 3}, {"itemDefinitionId": "gold_pouch", "weight": 3}, {"itemDefinitionId": "cosmetic_hair_long", "weight": 2}, {"itemDefinitionId": "cosmetic_eyes_blue", "weight": 2}, @@ -101,9 +98,6 @@ "entries": [ {"itemDefinitionId": "material_steel_raw", "weight": 3}, {"itemDefinitionId": "material_iron_ingot", "weight": 3}, - {"itemDefinitionId": "health_potion_medium", "weight": 3}, - {"itemDefinitionId": "mana_crystal_medium", "weight": 3}, - {"itemDefinitionId": "stamina_drink", "weight": 3}, {"itemDefinitionId": "cosmetic_hair_ponytail", "weight": 2}, {"itemDefinitionId": "cosmetic_eyes_sunglasses", "weight": 2}, {"itemDefinitionId": "cosmetic_eyes_magician", "weight": 2}, @@ -128,10 +122,7 @@ "entries": [ {"itemDefinitionId": "material_titanium_raw", "weight": 2}, {"itemDefinitionId": "material_steel_ingot", "weight": 3}, - {"itemDefinitionId": "health_potion_large", "weight": 2}, {"itemDefinitionId": "blood_vial", "weight": 2}, - {"itemDefinitionId": "energy_cell", "weight": 2}, - {"itemDefinitionId": "oxygen_tank", "weight": 2}, {"itemDefinitionId": "cosmetic_hair_cyberpunk", "weight": 2}, {"itemDefinitionId": "cosmetic_eyes_pilotglasses", "weight": 2}, {"itemDefinitionId": "cosmetic_body_suit", "weight": 2}, @@ -264,14 +255,8 @@ "guaranteedRolls": ["box_of_boxes"], "rollCount": 1, "entries": [ - {"itemDefinitionId": "meta_resource_health", "weight": 2}, - {"itemDefinitionId": "meta_resource_mana", "weight": 2}, - {"itemDefinitionId": "meta_resource_food", "weight": 2}, {"itemDefinitionId": "meta_resource_gold", "weight": 2}, - {"itemDefinitionId": "meta_resource_stamina", "weight": 1}, {"itemDefinitionId": "meta_resource_blood", "weight": 1}, - {"itemDefinitionId": "meta_resource_oxygen", "weight": 1}, - {"itemDefinitionId": "meta_resource_energy", "weight": 1}, {"itemDefinitionId": "box_meta_mastery", "weight": 1} ] } @@ -365,9 +350,7 @@ {"itemDefinitionId": "space_phone", "weight": 3}, {"itemDefinitionId": "space_coordinates", "weight": 3}, {"itemDefinitionId": "space_key", "weight": 2}, - {"itemDefinitionId": "space_map", "weight": 2}, - {"itemDefinitionId": "oxygen_tank", "weight": 3}, - {"itemDefinitionId": "energy_cell", "weight": 2} + {"itemDefinitionId": "space_map", "weight": 2} ] } }, @@ -386,8 +369,7 @@ {"itemDefinitionId": "medieval_scroll", "weight": 3}, {"itemDefinitionId": "medieval_seal", "weight": 2}, {"itemDefinitionId": "medieval_key", "weight": 3}, - {"itemDefinitionId": "mysterious_key", "weight": 2}, - {"itemDefinitionId": "health_potion_medium", "weight": 3} + {"itemDefinitionId": "mysterious_key", "weight": 2} ] } }, @@ -445,8 +427,7 @@ {"itemDefinitionId": "sentimental_letter", "weight": 3}, {"itemDefinitionId": "sentimental_flower", "weight": 4}, {"itemDefinitionId": "sentimental_teddy", "weight": 3}, - {"itemDefinitionId": "sentimental_phone", "weight": 3}, - {"itemDefinitionId": "health_potion_small", "weight": 2} + {"itemDefinitionId": "sentimental_phone", "weight": 3} ] } }, @@ -464,8 +445,7 @@ {"itemDefinitionId": "prehistoric_tooth", "weight": 4}, {"itemDefinitionId": "prehistoric_amber", "weight": 3}, {"itemDefinitionId": "prehistoric_fossil", "weight": 2}, - {"itemDefinitionId": "material_wood_raw", "weight": 4}, - {"itemDefinitionId": "food_ration", "weight": 4} + {"itemDefinitionId": "material_wood_raw", "weight": 4} ] } }, @@ -483,7 +463,6 @@ {"itemDefinitionId": "cosmic_shard", "weight": 4}, {"itemDefinitionId": "cosmic_crystal", "weight": 3}, {"itemDefinitionId": "cosmic_core", "weight": 1}, - {"itemDefinitionId": "energy_cell", "weight": 3}, {"itemDefinitionId": "tint_void", "weight": 1} ] } @@ -501,9 +480,7 @@ "entries": [ {"itemDefinitionId": "microscopic_bacteria", "weight": 4}, {"itemDefinitionId": "microscopic_dna", "weight": 3}, - {"itemDefinitionId": "microscopic_prion", "weight": 2}, - {"itemDefinitionId": "mana_crystal_small", "weight": 3}, - {"itemDefinitionId": "health_potion_small", "weight": 3} + {"itemDefinitionId": "microscopic_prion", "weight": 2} ] } }, @@ -537,19 +514,12 @@ "guaranteedRolls": ["box_of_boxes"], "rollCount": 1, "entries": [ - {"itemDefinitionId": "resource_max_health", "weight": 3}, - {"itemDefinitionId": "resource_max_mana", "weight": 3}, - {"itemDefinitionId": "resource_max_food", "weight": 3}, - {"itemDefinitionId": "resource_max_stamina", "weight": 3}, {"itemDefinitionId": "resource_max_gold", "weight": 2}, {"itemDefinitionId": "resource_max_blood", "weight": 1}, - {"itemDefinitionId": "resource_max_oxygen", "weight": 1}, - {"itemDefinitionId": "resource_max_energy", "weight": 1}, {"itemDefinitionId": "blueprint_foundry", "weight": 4, "condition": {"type": "HasUIFeature", "targetId": "CraftingPanel"}}, {"itemDefinitionId": "blueprint_workbench", "weight": 4, "condition": {"type": "HasUIFeature", "targetId": "CraftingPanel"}}, {"itemDefinitionId": "blueprint_furnace", "weight": 4, "condition": {"type": "HasUIFeature", "targetId": "CraftingPanel"}}, {"itemDefinitionId": "blueprint_drawing", "weight": 3, "condition": {"type": "HasUIFeature", "targetId": "CraftingPanel"}}, - {"itemDefinitionId": "blueprint_alchemy", "weight": 3, "condition": {"type": "HasUIFeature", "targetId": "CraftingPanel"}}, {"itemDefinitionId": "blueprint_engineer", "weight": 3, "condition": {"type": "HasUIFeature", "targetId": "CraftingPanel"}} ] } @@ -564,14 +534,8 @@ "guaranteedRolls": ["box_of_boxes"], "rollCount": 2, "entries": [ - {"itemDefinitionId": "health_potion_small", "weight": 5}, - {"itemDefinitionId": "mana_crystal_small", "weight": 4}, - {"itemDefinitionId": "food_ration", "weight": 5}, - {"itemDefinitionId": "stamina_drink", "weight": 4}, {"itemDefinitionId": "gold_pouch", "weight": 4}, - {"itemDefinitionId": "blood_vial", "weight": 1}, - {"itemDefinitionId": "oxygen_tank", "weight": 1}, - {"itemDefinitionId": "energy_cell", "weight": 1} + {"itemDefinitionId": "blood_vial", "weight": 1} ] } }, diff --git a/content/data/items.json b/content/data/items.json index 6a18a9d..0a9df4c 100644 --- a/content/data/items.json +++ b/content/data/items.json @@ -12,14 +12,8 @@ {"id": "meta_animation", "nameKey": "meta.animation", "descriptionKey": "meta.animation.desc", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta"], "metaUnlock": "BoxAnimation"}, {"id": "meta_crafting", "nameKey": "meta.crafting", "descriptionKey": "meta.crafting.desc", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "CraftingPanel"}, {"id": "meta_autosave", "nameKey": "meta.autosave", "descriptionKey": "meta.autosave.desc", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta"], "metaUnlock": "AutoSave"}, - {"id": "meta_resource_health", "nameKey": "resource.health", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Health"}, - {"id": "meta_resource_mana", "nameKey": "resource.mana", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Mana"}, - {"id": "meta_resource_food", "nameKey": "resource.food", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Food"}, - {"id": "meta_resource_stamina", "nameKey": "resource.stamina", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Stamina"}, {"id": "meta_resource_blood", "nameKey": "resource.blood", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Blood"}, {"id": "meta_resource_gold", "nameKey": "resource.gold", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Gold"}, - {"id": "meta_resource_oxygen", "nameKey": "resource.oxygen", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Oxygen"}, - {"id": "meta_resource_energy", "nameKey": "resource.energy", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Energy"}, {"id": "meta_stat_strength", "nameKey": "stat.strength", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "StatVisibility"], "statType": "Strength"}, {"id": "meta_stat_intelligence", "nameKey": "stat.intelligence", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "StatVisibility"], "statType": "Intelligence"}, {"id": "meta_stat_luck", "nameKey": "stat.luck", "category": "Meta", "rarity": "Rare", "tags": ["Meta", "StatVisibility"], "statType": "Luck"}, @@ -69,17 +63,8 @@ {"id": "tint_gold", "nameKey": "tint.gold", "category": "Cosmetic", "rarity": "Epic", "tags": ["Tint"], "tintColor": "Gold"}, {"id": "tint_void", "nameKey": "tint.void", "category": "Cosmetic", "rarity": "Legendary", "tags": ["Tint"], "tintColor": "Void"}, - {"id": "health_potion_small", "nameKey": "item.health_potion_small", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Health", "resourceAmount": 10}, - {"id": "health_potion_medium", "nameKey": "item.health_potion_medium", "category": "Consumable", "rarity": "Uncommon", "tags": ["Consumable"], "resourceType": "Health", "resourceAmount": 25}, - {"id": "health_potion_large", "nameKey": "item.health_potion_large", "category": "Consumable", "rarity": "Rare", "tags": ["Consumable"], "resourceType": "Health", "resourceAmount": 50}, - {"id": "mana_crystal_small", "nameKey": "item.mana_crystal_small", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Mana", "resourceAmount": 10}, - {"id": "mana_crystal_medium", "nameKey": "item.mana_crystal_medium", "category": "Consumable", "rarity": "Uncommon", "tags": ["Consumable"], "resourceType": "Mana", "resourceAmount": 25}, - {"id": "food_ration", "nameKey": "item.food_ration", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Food", "resourceAmount": 15}, - {"id": "stamina_drink", "nameKey": "item.stamina_drink", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Stamina", "resourceAmount": 20}, {"id": "blood_vial", "nameKey": "item.blood_vial", "category": "Consumable", "rarity": "Rare", "tags": ["Consumable"], "resourceType": "Blood", "resourceAmount": 5}, {"id": "gold_pouch", "nameKey": "item.gold_pouch", "category": "Consumable", "rarity": "Common", "tags": ["Consumable"], "resourceType": "Gold", "resourceAmount": 50}, - {"id": "oxygen_tank", "nameKey": "item.oxygen_tank", "category": "Consumable", "rarity": "Uncommon", "tags": ["Consumable"], "resourceType": "Oxygen", "resourceAmount": 30}, - {"id": "energy_cell", "nameKey": "item.energy_cell", "category": "Consumable", "rarity": "Uncommon", "tags": ["Consumable"], "resourceType": "Energy", "resourceAmount": 20}, {"id": "space_badge", "nameKey": "item.space.badge", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Space"], "adventureTheme": "Space"}, {"id": "space_phone", "nameKey": "item.space.phone", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Space", "PhoneNumber"], "adventureTheme": "Space"}, @@ -94,7 +79,7 @@ {"id": "pirate_map", "nameKey": "item.pirate.map", "category": "Map", "rarity": "Epic", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate"}, {"id": "pirate_compass", "nameKey": "item.pirate.compass", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate"}, {"id": "pirate_feather", "nameKey": "item.pirate.feather", "category": "AdventureToken", "rarity": "Common", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate"}, - {"id": "pirate_rum", "nameKey": "item.pirate.rum", "category": "Consumable", "rarity": "Uncommon", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate", "resourceType": "Stamina", "resourceAmount": 30}, + {"id": "pirate_rum", "nameKey": "item.pirate.rum", "category": "AdventureToken", "rarity": "Uncommon", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate"}, {"id": "pirate_key", "nameKey": "item.pirate.key", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "Pirate", "Key"], "adventureTheme": "Pirate"}, {"id": "contemporary_phone", "nameKey": "item.contemporary.phone", "category": "AdventureToken", "rarity": "Common", "tags": ["Adventure", "Contemporary", "PhoneNumber"], "adventureTheme": "Contemporary"}, {"id": "contemporary_card", "nameKey": "item.contemporary.card", "category": "AdventureToken", "rarity": "Uncommon", "tags": ["Adventure", "Contemporary"], "adventureTheme": "Contemporary"}, @@ -148,14 +133,8 @@ {"id": "material_carbonfiber_raw", "nameKey": "material.carbonfiber", "category": "Material", "rarity": "Rare", "tags": ["Material"], "materialType": "CarbonFiber", "materialForm": "Raw"}, {"id": "material_carbonfiber_sheet", "nameKey": "material.carbonfiber", "category": "Material", "rarity": "Epic", "tags": ["Material"], "materialType": "CarbonFiber", "materialForm": "Sheet"}, - {"id": "resource_max_health", "nameKey": "item.resource_max_health", "category": "Consumable", "rarity": "Uncommon", "tags": ["Improvement", "ResourceMax"], "resourceType": "Health", "resourceMaxIncrease": 10}, - {"id": "resource_max_mana", "nameKey": "item.resource_max_mana", "category": "Consumable", "rarity": "Uncommon", "tags": ["Improvement", "ResourceMax"], "resourceType": "Mana", "resourceMaxIncrease": 10}, - {"id": "resource_max_food", "nameKey": "item.resource_max_food", "category": "Consumable", "rarity": "Uncommon", "tags": ["Improvement", "ResourceMax"], "resourceType": "Food", "resourceMaxIncrease": 10}, - {"id": "resource_max_stamina", "nameKey": "item.resource_max_stamina", "category": "Consumable", "rarity": "Uncommon", "tags": ["Improvement", "ResourceMax"], "resourceType": "Stamina", "resourceMaxIncrease": 10}, {"id": "resource_max_gold", "nameKey": "item.resource_max_gold", "category": "Consumable", "rarity": "Rare", "tags": ["Improvement", "ResourceMax"], "resourceType": "Gold", "resourceMaxIncrease": 50}, {"id": "resource_max_blood", "nameKey": "item.resource_max_blood", "category": "Consumable", "rarity": "Rare", "tags": ["Improvement", "ResourceMax"], "resourceType": "Blood", "resourceMaxIncrease": 5}, - {"id": "resource_max_oxygen", "nameKey": "item.resource_max_oxygen", "category": "Consumable", "rarity": "Rare", "tags": ["Improvement", "ResourceMax"], "resourceType": "Oxygen", "resourceMaxIncrease": 15}, - {"id": "resource_max_energy", "nameKey": "item.resource_max_energy", "category": "Consumable", "rarity": "Rare", "tags": ["Improvement", "ResourceMax"], "resourceType": "Energy", "resourceMaxIncrease": 10}, {"id": "music_melody", "nameKey": "item.music_melody", "category": "Consumable", "rarity": "Rare", "tags": ["Music", "Fun"], "description": "A melody plays from the box. Console.Beep never sounded so good."}, {"id": "cookie_fortune", "nameKey": "item.cookie_fortune", "category": "Consumable", "rarity": "Common", "tags": ["Cookie", "Fun"], "description": "A fortune cookie with wisdom of questionable origin."}, @@ -169,7 +148,6 @@ {"id": "blueprint_workbench", "nameKey": "item.blueprint.workbench", "category": "Meta", "rarity": "Uncommon", "tags": ["Blueprint", "Workstation"], "workstationType": "Workbench"}, {"id": "blueprint_furnace", "nameKey": "item.blueprint.furnace", "category": "Meta", "rarity": "Uncommon", "tags": ["Blueprint", "Workstation"], "workstationType": "Furnace"}, {"id": "blueprint_forge", "nameKey": "item.blueprint.forge", "category": "Meta", "rarity": "Rare", "tags": ["Blueprint", "Workstation"], "workstationType": "Forge"}, - {"id": "blueprint_alchemy", "nameKey": "item.blueprint.alchemy", "category": "Meta", "rarity": "Rare", "tags": ["Blueprint", "Workstation"], "workstationType": "AlchemyTable"}, {"id": "blueprint_engineer", "nameKey": "item.blueprint.engineer", "category": "Meta", "rarity": "Rare", "tags": ["Blueprint", "Workstation"], "workstationType": "EngineerDesk"}, {"id": "blueprint_drawing", "nameKey": "item.blueprint.drawing", "category": "Meta", "rarity": "Uncommon", "tags": ["Blueprint", "Workstation"], "workstationType": "DrawingTable"}, {"id": "blueprint_engraving", "nameKey": "item.blueprint.engraving", "category": "Meta", "rarity": "Rare", "tags": ["Blueprint", "Workstation"], "workstationType": "EngravingBench"}, diff --git a/content/data/recipes.json b/content/data/recipes.json index a833d81..e7cf574 100644 --- a/content/data/recipes.json +++ b/content/data/recipes.json @@ -54,45 +54,6 @@ "result": {"itemDefinitionId": "material_carbonfiber_sheet", "quantity": 1} }, - { - "id": "brew_health_potion_medium", - "nameKey": "recipe.brew_health_potion_medium", - "workstation": "AlchemyTable", - "ingredients": [ - {"itemDefinitionId": "health_potion_small", "quantity": 3} - ], - "result": {"itemDefinitionId": "health_potion_medium", "quantity": 1} - }, - { - "id": "brew_mana_crystal_medium", - "nameKey": "recipe.brew_mana_crystal_medium", - "workstation": "AlchemyTable", - "ingredients": [ - {"itemDefinitionId": "mana_crystal_small", "quantity": 3} - ], - "result": {"itemDefinitionId": "mana_crystal_medium", "quantity": 1} - }, - { - "id": "synthesize_energy_cell", - "nameKey": "recipe.synthesize_energy_cell", - "workstation": "MatterSynthesizer", - "ingredients": [ - {"itemDefinitionId": "mana_crystal_small", "quantity": 2}, - {"itemDefinitionId": "material_iron_ingot", "quantity": 1} - ], - "result": {"itemDefinitionId": "energy_cell", "quantity": 1} - }, - { - "id": "pressurize_oxygen_tank", - "nameKey": "recipe.pressurize_oxygen_tank", - "workstation": "EngineerDesk", - "ingredients": [ - {"itemDefinitionId": "material_steel_ingot", "quantity": 1}, - {"itemDefinitionId": "energy_cell", "quantity": 1} - ], - "result": {"itemDefinitionId": "oxygen_tank", "quantity": 2} - }, - { "id": "craft_pilot_glasses", "nameKey": "recipe.craft_pilot_glasses", @@ -121,7 +82,7 @@ "ingredients": [ {"itemDefinitionId": "cosmetic_legs_short", "quantity": 1}, {"itemDefinitionId": "material_titanium_ingot", "quantity": 1}, - {"itemDefinitionId": "energy_cell", "quantity": 2} + {"itemDefinitionId": "cosmic_shard", "quantity": 2} ], "result": {"itemDefinitionId": "cosmetic_legs_rocketboots", "quantity": 1} }, @@ -204,16 +165,6 @@ ], "result": {"itemDefinitionId": "box_cool", "quantity": 1} }, - { - "id": "craft_box_supply", - "nameKey": "recipe.craft_box_supply", - "workstation": "Workbench", - "ingredients": [ - {"itemDefinitionId": "material_wood_raw", "quantity": 2}, - {"itemDefinitionId": "food_ration", "quantity": 2} - ], - "result": {"itemDefinitionId": "box_supply", "quantity": 1} - }, { "id": "craft_box_epic", "nameKey": "recipe.craft_box_epic", diff --git a/content/strings/en.json b/content/strings/en.json index c621729..ef69e1c 100644 --- a/content/strings/en.json +++ b/content/strings/en.json @@ -129,14 +129,8 @@ "item.rarity.legendary": "Legendary", "item.rarity.mythic": "Mythic", - "resource.health": "Health", - "resource.mana": "Mana", - "resource.food": "Food", - "resource.stamina": "Stamina", "resource.blood": "Blood", "resource.gold": "Gold", - "resource.oxygen": "Oxygen", - "resource.energy": "Energy", "stat.strength": "Strength", "stat.intelligence": "Intelligence", @@ -218,17 +212,8 @@ "material.form.dust": "Dust", "material.form.gem": "Gem", - "item.health_potion_small": "Small Health Potion", - "item.health_potion_medium": "Medium Health Potion", - "item.health_potion_large": "Large Health Potion", - "item.mana_crystal_small": "Small Mana Crystal", - "item.mana_crystal_medium": "Medium Mana Crystal", - "item.food_ration": "Food Ration", - "item.stamina_drink": "Stamina Drink", "item.blood_vial": "Blood Vial", "item.gold_pouch": "Gold Pouch", - "item.oxygen_tank": "Oxygen Tank", - "item.energy_cell": "Energy Cell", "item.space.badge": "Astronaut Badge", "item.space.phone": "Alien Phone Number", @@ -285,14 +270,8 @@ "item.resource_up": "{0} +1", "item.stat_boost": "{0} +1", - "item.resource_max_health": "Health Capacity Upgrade", - "item.resource_max_mana": "Mana Capacity Upgrade", - "item.resource_max_food": "Food Capacity Upgrade", - "item.resource_max_stamina": "Stamina Capacity Upgrade", "item.resource_max_gold": "Gold Capacity Upgrade", "item.resource_max_blood": "Blood Capacity Upgrade", - "item.resource_max_oxygen": "Oxygen Capacity Upgrade", - "item.resource_max_energy": "Energy Capacity Upgrade", "item.music_melody": "Box Melody", "item.cookie_fortune": "Fortune Cookie", @@ -396,10 +375,7 @@ "recipe.smelt_steel_ingot": "Smelt Steel Ingot", "recipe.smelt_titanium_ingot": "Smelt Titanium Ingot", "recipe.forge_carbonfiber_sheet": "Press Carbon Fiber Sheet", - "recipe.brew_health_potion_medium": "Brew Medium Health Potion", - "recipe.brew_mana_crystal_medium": "Refine Medium Mana Crystal", - "recipe.synthesize_energy_cell": "Synthesize Energy Cell", - "recipe.pressurize_oxygen_tank": "Pressurize Oxygen Tank", + "recipe.craft_pilot_glasses": "Craft Pilot Glasses", "recipe.forge_armored_plate": "Forge Armored Plate", "recipe.engineer_rocket_boots": "Engineer Rocket Boots", @@ -411,7 +387,7 @@ "recipe.preserve_amber": "Preserve Amber Stone", "recipe.craft_box_ok_tier": "Craft Okay-ish Box", "recipe.craft_box_cool": "Craft Cool Box", - "recipe.craft_box_supply": "Craft Supply Box", + "recipe.craft_box_epic": "Craft Epic Box", "action.collect_crafting": "Collect crafted items", @@ -425,7 +401,7 @@ "item.blueprint.workbench": "Workbench Blueprint", "item.blueprint.furnace": "Furnace Blueprint", "item.blueprint.forge": "Forge Blueprint", - "item.blueprint.alchemy": "Alchemy Table Blueprint", + "item.blueprint.engineer": "Engineer Desk Blueprint", "item.blueprint.drawing": "Drawing Table Blueprint", "item.blueprint.engraving": "Engraving Bench Blueprint", diff --git a/content/strings/fr.json b/content/strings/fr.json index 507b11e..a94a988 100644 --- a/content/strings/fr.json +++ b/content/strings/fr.json @@ -129,14 +129,8 @@ "item.rarity.legendary": "Légendaire", "item.rarity.mythic": "Mythique", - "resource.health": "Santé", - "resource.mana": "Mana", - "resource.food": "Nourriture", - "resource.stamina": "Endurance", "resource.blood": "Sang", "resource.gold": "Or", - "resource.oxygen": "Oxygène", - "resource.energy": "Énergie", "stat.strength": "Force", "stat.intelligence": "Intelligence", @@ -218,17 +212,8 @@ "material.form.dust": "Poudre", "material.form.gem": "Gemme", - "item.health_potion_small": "Petite Potion de Santé", - "item.health_potion_medium": "Potion de Santé Moyenne", - "item.health_potion_large": "Grande Potion de Santé", - "item.mana_crystal_small": "Petit Cristal de Mana", - "item.mana_crystal_medium": "Cristal de Mana Moyen", - "item.food_ration": "Ration alimentaire", - "item.stamina_drink": "Boisson d'endurance", "item.blood_vial": "Fiole de sang", "item.gold_pouch": "Bourse d'or", - "item.oxygen_tank": "Réservoir d'oxygène", - "item.energy_cell": "Cellule d'énergie", "item.space.badge": "Badge d'astronaute", "item.space.phone": "Numéro de téléphone alien", @@ -285,14 +270,8 @@ "item.resource_up": "{0} +1", "item.stat_boost": "{0} +1", - "item.resource_max_health": "Amélioration Capacité Santé", - "item.resource_max_mana": "Amélioration Capacité Mana", - "item.resource_max_food": "Amélioration Capacité Nourriture", - "item.resource_max_stamina": "Amélioration Capacité Endurance", "item.resource_max_gold": "Amélioration Capacité Or", "item.resource_max_blood": "Amélioration Capacité Sang", - "item.resource_max_oxygen": "Amélioration Capacité Oxygène", - "item.resource_max_energy": "Amélioration Capacité Énergie", "item.music_melody": "Mélodie de boîte", "item.cookie_fortune": "Fortune Cookie", @@ -396,10 +375,7 @@ "recipe.smelt_steel_ingot": "Fondre un lingot d'acier", "recipe.smelt_titanium_ingot": "Fondre un lingot de titane", "recipe.forge_carbonfiber_sheet": "Presser une feuille de fibre de carbone", - "recipe.brew_health_potion_medium": "Brasser une potion de santé moyenne", - "recipe.brew_mana_crystal_medium": "Raffiner un cristal de mana moyen", - "recipe.synthesize_energy_cell": "Synthétiser une cellule d'énergie", - "recipe.pressurize_oxygen_tank": "Pressuriser un réservoir d'oxygène", + "recipe.craft_pilot_glasses": "Fabriquer des lunettes d'aviateur", "recipe.forge_armored_plate": "Forger une armure", "recipe.engineer_rocket_boots": "Concevoir des bottes à réaction", @@ -411,7 +387,7 @@ "recipe.preserve_amber": "Conserver une pierre d'ambre", "recipe.craft_box_ok_tier": "Fabriquer une boîte ok tiers", "recipe.craft_box_cool": "Fabriquer une boîte coolos", - "recipe.craft_box_supply": "Fabriquer une boîte de fourniture", + "recipe.craft_box_epic": "Fabriquer une boîte épique", "action.collect_crafting": "Récupérer les fabrications", @@ -425,7 +401,7 @@ "item.blueprint.workbench": "Plan d'Établi", "item.blueprint.furnace": "Plan de Fourneau", "item.blueprint.forge": "Plan de Forge", - "item.blueprint.alchemy": "Plan de Table d'Alchimie", + "item.blueprint.engineer": "Plan de Bureau d'Ingénieur", "item.blueprint.drawing": "Plan de Table à Dessin", "item.blueprint.engraving": "Plan de Banc de Gravure", diff --git a/refactoring_plan.md b/refactoring_plan.md deleted file mode 100644 index 8242fd7..0000000 --- a/refactoring_plan.md +++ /dev/null @@ -1,165 +0,0 @@ -# Refactoring Plan — Black Box Sim Compliance - -## Objectif - -Rendre le code conforme au pattern **Black Box Sim** décrit dans le README : - -``` -Input → GameAction → GameSimulation (ZERO I/O, pure logic) → GameEvent[] → IRenderer -``` - -La simulation ne lit jamais d'entrée et n'écrit jamais de sortie. Elle reçoit des actions, elle retourne des événements. Program.cs orchestre l'entrée/sortie et connecte le tout. - ---- - -## Violations actuelles et corrections proposées - -### 1. CraftingEngine fonctionne hors de GameSimulation (CRITIQUE) - -**Problème** : `CraftingEngine` est instancié directement dans `Program.cs` (ligne 27) et appelé en dehors de `GameSimulation` via `TickCraftingJobs()` et `CollectCrafting()`. De plus, un `MetaEngine` est créé ad-hoc dans `CollectCrafting()` pour traiter les items craftés. - -**Solution** : -- Internaliser `CraftingEngine` dans `GameSimulation` -- Ajouter `TickCraftingAction` et `CollectCraftingAction` comme GameActions -- `GameSimulation.ProcessAction(TickCraftingAction)` appelle `CraftingEngine.TickJobs()` en interne -- `GameSimulation.ProcessAction(CollectCraftingAction)` appelle `CollectCompleted()` + `MetaEngine` + `AutoCraftCheck()` en interne -- Supprimer `_craftingEngine` de `Program.cs` - -**Fichiers** : `Program.cs`, `GameSimulation.cs`, `CraftingEngine.cs`, `GameAction.cs` - ---- - -### 2. AdventureEngine appelle directement le renderer (CRITIQUE) - -**Problème** : `AdventureEngine` reçoit un `IRenderer` dans son constructeur et appelle `ShowAdventureDialogue()`, `ShowAdventureChoice()`, `WaitForKeyPress()` directement. Ceci viole fondamentalement le pattern car la simulation fait de l'I/O. - -**Solution** : -- Introduire un `IAdventureUI` interface avec les méthodes de callback : - - `ShowDialogue(string? character, string text)` - - `int ShowChoice(List options, List? hints)` - - `ShowMessage(string message)` -- `Program.cs` fournit l'implémentation de `IAdventureUI` qui délègue au `IRenderer` -- `AdventureEngine` retourne des `AdventureEvent` pour les changements d'état (items, resources) au lieu d'appeler `ShowMessage()` pour les effets de jeu -- Alternative : Garder le callback pour le dialogue interactif (nécessaire car Loreline est bloquant) mais retirer tous les appels `ShowMessage` des fonctions custom Loreline (grantItem, addResource, removeItem, markSecretBranch) — ces effets sont déjà retournés comme `GameEventResult` - -**Fichiers** : `AdventureEngine.cs`, `Program.cs`, nouveau `IAdventureUI.cs` - ---- - -### 3. Program.cs mute directement GameState (IMPORTANT) - -**Problème** : Plusieurs endroits dans `Program.cs` modifient `GameState` sans passer par `GameSimulation` : -- `_state.AddItem(starterBox)` dans `NewGame()` (ligne 261) -- `_state.AddItem(...)` dans `RunAdventure()` (ligne 875) -- `_state.CurrentLocale = newLocale` dans `ChangeLanguage()` (ligne 358) -- `_state.EventLog.Add(message)` dans `AddEventLog()` (ligne 1054) - -**Solution** : -- Créer `NewGameAction` → retourne un `ItemReceivedEvent` avec la starter box -- Les événements d'aventure (ItemGranted) devraient être traités par `GameSimulation.ProcessAction(AdventureResultAction)` au lieu de muter directement l'inventaire -- Le changement de locale passe déjà par `ChangeLocaleAction` quand fait en jeu — s'assurer que `ChangeLanguage()` utilise aussi ce chemin -- L'EventLog devrait être alimenté par le renderer ou un composant de présentation séparé, pas en mutant `GameState` - -**Fichiers** : `Program.cs`, `GameSimulation.cs`, `GameAction.cs` - ---- - -### 4. Les renderers dépendent de ContentRegistry (IMPORTANT) - -**Problème** : `SpectreRenderer`, `TerminalGuiRenderer`, `InventoryPanel`, `CraftingPanel` reçoivent tous un `ContentRegistry?` et font des lookups d'`ItemDefinition` et `BoxDefinition` directement. - -**Solution** : -- Créer un modèle de vue (ViewModel) dans le `RenderContext` ou à côté de `GameState` : - ```csharp - record DisplayItem(string Name, string Rarity, string Category, int Qty, string? Description, ...); - record DisplayCraftingJob(string Name, string Station, int ProgressPercent, bool IsComplete); - ``` -- La résolution `DefinitionId → nom localisé + rarity + description` se fait UNE FOIS au moment du rendu, dans Program.cs ou un nouveau `ViewModelBuilder` -- Les renderers reçoivent des `DisplayItem[]` au lieu de `GameState + ContentRegistry` -- **Phase transitoire** : Garder le `ContentRegistry` dans les panels pour l'instant mais créer le `ViewModelBuilder` progressivement - -**Fichiers** : Nouveau `Rendering/ViewModels/`, `InventoryPanel.cs`, `CraftingPanel.cs`, `SpectreRenderer.cs`, `TerminalGuiRenderer.cs` - ---- - -### 5. RenderEvents mélange logique et présentation (MODÉRÉ) - -**Problème** : La méthode `RenderEvents()` dans `Program.cs` (lignes 475-605) fait : -- Du rendu (`_renderer.ShowXxx()`) -- De la logique (`_renderContext.Unlock()`, `RefreshRenderer()`) -- Du tracking (`AddEventLog()`) -- De l'orchestration (`await RunAdventure()`) - -**Solution** : -- Séparer en deux passes : - 1. **Pass logique** : Traite les événements qui modifient l'état du programme (unlock features, update render context) - 2. **Pass présentation** : Appelle le renderer pour chaque événement à afficher -- Déplacer `RefreshRenderer()` dans la pass logique, avant la pass présentation -- L'EventLog devrait être alimenté dans la pass logique, pas en parallèle du rendu - -**Fichiers** : `Program.cs` - ---- - -### 6. Program.cs connaît les types de domaine (MODÉRÉ) - -**Problème** : Le game loop inspecte directement `ItemCategory`, `ItemDefinition.ResourceType`, `CosmeticSlot`, etc. pour construire les menus et déterminer les actions disponibles. - -**Solution** : -- Ajouter une méthode `GameSimulation.GetAvailableActions(GameState) → List` qui retourne les actions possibles avec leurs labels -- Ou : ajouter des propriétés helper sur `GameState` : - - `HasBoxes`, `HasConsumables`, `HasCosmeticsToEquip`, `HasCompletedCrafting` -- L'objectif n'est pas d'éliminer toute connaissance des catégories dans Program.cs (c'est la couche d'orchestration) mais de réduire les requêtes directes au registre - -**Fichiers** : `GameSimulation.cs` ou `GameState.cs`, `Program.cs` - ---- - -### 7. Portrait dupliqué entre les renderers (FAIBLE) - -**Problème** : `TerminalGuiRenderer` (lignes 614-677) duplique tout l'art ASCII du portrait qui existe déjà dans `PortraitPanel.cs`. - -**Solution** : -- Extraire les méthodes `GetHairArt()`, `GetEyeArt()`, etc. dans un helper statique partagé `PortraitArt` -- `PortraitPanel` et `TerminalGuiRenderer` utilisent tous les deux `PortraitArt.GetHairArt()`, etc. -- Alternative : `PortraitPanel` expose une méthode `RenderPlainText()` que `TerminalGuiRenderer` peut appeler - -**Fichiers** : Nouveau `Rendering/Panels/PortraitArt.cs`, `PortraitPanel.cs`, `TerminalGuiRenderer.cs` - ---- - -### 8. EventLog est de l'état de présentation dans GameState (FAIBLE) - -**Problème** : `GameState.EventLog` est une liste de strings de présentation ("★ Text Colors", "+ Bronze [Uncommon]") stockée dans le modèle de simulation. Elle n'est pas persistée (pas dans les saves), confirmant qu'il s'agit d'état de présentation. - -**Solution** : -- Déplacer `EventLog` hors de `GameState` dans un composant de présentation séparé (ex: `ChatLog` dans le namespace `Rendering`) -- `Program.cs` alimente le `ChatLog` dans sa pass de rendu des événements -- Les renderers reçoivent le `ChatLog` au lieu de lire `state.EventLog` - -**Fichiers** : `GameState.cs`, nouveau `Rendering/ChatLog.cs`, `Program.cs`, `ChatPanel.cs`, `TerminalGuiRenderer.cs` - ---- - -## Ordre de priorité recommandé - -| Priorité | Tâche | Impact | Effort | -|----------|-------|--------|--------| -| 1 | CraftingEngine dans GameSimulation | Critique | Moyen | -| 2 | Mutations directes de GameState | Important | Faible | -| 3 | RenderEvents deux passes | Modéré | Moyen | -| 4 | ViewModels pour renderers | Important | Élevé | -| 5 | AdventureEngine I/O callbacks | Critique | Élevé | -| 6 | GetAvailableActions helper | Modéré | Faible | -| 7 | Portrait partagé | Faible | Faible | -| 8 | EventLog hors de GameState | Faible | Faible | - ---- - -## Principes directeurs - -1. **GameSimulation est une boîte noire** — elle reçoit des `GameAction`, elle retourne des `GameEvent[]`. Rien d'autre. -2. **Les renderers ne connaissent pas le domaine** — ils reçoivent des données pré-formatées (ViewModels) et les affichent. -3. **Program.cs est le câblage** — il connecte l'input au simulateur et le simulateur au renderer. Il ne contient pas de logique de jeu. -4. **Les mutations de GameState passent par GameSimulation** — aucune écriture directe depuis Program.cs. -5. **Pas de refactoring big-bang** — chaque tâche peut être faite indépendamment et testée. diff --git a/specifications.md b/specifications.md index 38ff281..a44d02f 100644 --- a/specifications.md +++ b/specifications.md @@ -12,7 +12,7 @@ Array of box definitions. Each box has: - `guaranteedRolls` — Array of item IDs always given - `rollCount` — Number of random rolls - `entries` — Weighted loot entries with optional `condition`: - - `condition.type`: `HasItem`, `HasNotItem`, `ResourceAbove`, `ResourceBelow`, `BoxesOpenedAbove`, `HasUIFeature`, `HasWorkstation`, `HasAdventure`, `HasCosmetic`, `AllResourcesVisible` + - `condition.type`: `HasItem`, `HasNotItem`, `ResourceAbove`, `ResourceBelow`, `BoxesOpenedAbove`, `HasUIFeature`, `HasWorkstation`, `HasAdventure`, `HasCosmetic` ### `items.json` Array of item definitions. Categories: Token, Consumable, Material, Cosmetic, Meta, Box. @@ -20,27 +20,27 @@ Special properties: - `adventureTheme` — Links token to an adventure theme - `cosmeticSlot` / `cosmeticValue` — Cosmetic equipment data - `statType` / `statValue` — Stat modification -- `resourceType` / `resourceValue` — Resource modification +- `resourceType` / `resourceAmount` — Resource modification (Gold, Blood only) - `metaUnlock` — UI feature to unlock - `workstationType` — Workstation blueprint -### `crafting_recipes.json` -Crafting recipes with inputs, outputs, duration, and workstation requirements. +### `recipes.json` +Crafting recipes with inputs, outputs, and workstation requirements. ## Adventures (`content/adventures/`) ### Folder Structure ``` content/adventures/ -├── space/ — Sci-fi theme (Key resource: Oxygen) -├── medieval/ — Fantasy theme (Key resources: Mana, Stamina) -├── pirate/ — Pirate theme (Key resources: Gold, Stamina) -├── contemporary/ — Modern urban theme (Key resources: Energy, Gold) -├── sentimental/ — Romance theme (Key resources: Health, Mana) -├── prehistoric/ — Stone age theme (Key resources: Food, Stamina) -├── cosmic/ — Cosmic/divine theme (Key resources: Mana, Energy) -├── microscopic/ — Micro-world theme (Key resources: Energy, Oxygen) -├── darkfantasy/ — Gothic horror theme (Key resources: Blood, Mana) +├── space/ — Sci-fi theme +├── medieval/ — Fantasy theme +├── pirate/ — Pirate theme +├── contemporary/ — Modern urban theme (Key resource: Gold) +├── sentimental/ — Romance theme +├── prehistoric/ — Stone age theme +├── cosmic/ — Cosmic/divine theme +├── microscopic/ — Micro-world theme +├── darkfantasy/ — Gothic horror theme (Key resource: Blood) └── destiny/ — Final adventure (acknowledges all other adventures) ``` @@ -80,6 +80,7 @@ Boxes ──► Items ──► Inventory │ ├──► Cosmetics ──► Appearance ──┐ │ ├──► Stat Items ──► Stats ──────┤ │ └──► Consumables ──► Resources ─┤ + │ (Gold, Blood only) │ │ │ │ ┌───────────────────────────────┘ │ ▼ @@ -105,10 +106,9 @@ Space, Medieval, Pirate, Contemporary, Sentimental, Prehistoric, Cosmic, Microsc Strength, Intelligence, Luck, Charisma, Dexterity, Wisdom ### ResourceType (Characteristics) -Health, Mana, Food, Stamina, Blood, Gold, Oxygen, Energy -Note: internally called "Resource" in code, displayed as "Characteristics" to the player. -Characteristics represent the character's attributes with current/max values (e.g., Health 40/100). -They are implicitly unlocked when the player receives an item referencing that resource type (e.g., a Health Potion reveals Health). +Blood, Gold +Only resources with actual adventure gates are kept. Blood gates the DarkFantasy secret branch (≥20), Gold gates the Contemporary secret branch (≥30). +Resources are displayed as "Characteristics" to the player with current/max values (e.g., Gold 50/100). ### CosmeticSlot Hair, Eyes, Body, Legs, Arms @@ -125,7 +125,7 @@ UI features are unlocked in a fixed order regardless of which meta box drops. Th 3. **AutoSave** — Automatic saving 4. **InventoryPanel** — Interactive inventory table 5. **StatsPanel** — Progression stats and character attributes -6. **ResourcePanel** — Characteristics bars (health, mana, etc.) +6. **ResourcePanel** — Characteristics bars (Gold, Blood) 7. **PortraitPanel** — ASCII art portrait 8. **ChatPanel** — Event log panel 9. **ExtendedColors** — 256-color palette @@ -138,3 +138,11 @@ Note: KeyboardShortcuts is merged into ArrowKeySelection. ### Pity System If the player opens 10 boxes without receiving a meta box, one is guaranteed on the next opening. + +## Item Utility Audit + +Run the snapshot test to generate a full item utility report: +``` +dotnet test --filter "ItemUtilitySnapshot" --logger "console;verbosity=detailed" +``` +The report is written to `tests/snapshots/item_utility_report.txt` and shows every item with its usage contexts (loot source, crafting, interactions, adventure gates, etc.). Items with no usage context are flagged as orphans. diff --git a/src/OpenTheBox/Core/Enums/ResourceType.cs b/src/OpenTheBox/Core/Enums/ResourceType.cs index 0e1ad8b..cabd90b 100644 --- a/src/OpenTheBox/Core/Enums/ResourceType.cs +++ b/src/OpenTheBox/Core/Enums/ResourceType.cs @@ -1,33 +1,14 @@ namespace OpenTheBox.Core.Enums; /// -/// The 8 player resources that govern survival, exploration, crafting, and combat. -/// Each resource has a maximum cap that can be upgraded via Boite d'amelioration. -/// Resources are unlocked progressively through gameplay. +/// Player resources used as adventure gates and consumable targets. +/// Only resources with actual gameplay impact (adventure secret branches) are kept. /// public enum ResourceType { - /// Hit points. Unlocked from the start. Used for survival and combat. - Health, - - /// Magic points. Unlocked on first encounter with a magical character. - Mana, - - /// Nourishment. Unlocked on first adventure explored. - Food, - - /// Physical endurance. Unlocked on first physical action (combat, craft). - Stamina, - - /// Blood resource. Unlocked with the DarkFantasy theme. Used for dark rituals. + /// Blood resource. Unlocked with the DarkFantasy theme. Gates the Blood Communion secret branch (≥20). Blood, - /// Currency. Unlocked when a trading location is discovered. - Gold, - - /// Breathable supply. Unlocked with Space theme or underwater adventures. - Oxygen, - - /// Power supply. Unlocked when the first crafting workstation is built. - Energy + /// Currency. Unlocked when a trading location is discovered. Gates the Corporate VIP secret branch (≥30). + Gold } diff --git a/src/OpenTheBox/Rendering/Panels/ResourcePanel.cs b/src/OpenTheBox/Rendering/Panels/ResourcePanel.cs index 7f9c57a..d274ccc 100644 --- a/src/OpenTheBox/Rendering/Panels/ResourcePanel.cs +++ b/src/OpenTheBox/Rendering/Panels/ResourcePanel.cs @@ -52,12 +52,10 @@ public static class ResourcePanel .Border(BoxBorder.Rounded); } - private static string GetResourceColor(ResourceType type) => type.ToString().ToLowerInvariant() switch + private static string GetResourceColor(ResourceType type) => type switch { - "gold" or "coins" => "gold1", - "energy" or "stamina" => "green", - "mana" or "magic" => "blue", - "health" or "hp" => "red", + ResourceType.Gold => "gold1", + ResourceType.Blood => "red", _ => "silver" }; } diff --git a/tests/OpenTheBox.Tests/CraftingTests.cs b/tests/OpenTheBox.Tests/CraftingTests.cs index 7eaa0e6..7ab77b9 100644 --- a/tests/OpenTheBox.Tests/CraftingTests.cs +++ b/tests/OpenTheBox.Tests/CraftingTests.cs @@ -559,8 +559,8 @@ public class RecipeLoadingTests ContentPath("recipes.json")); Assert.NotEmpty(registry.Recipes); - // recipes.json has 23 recipes (13 workstations) - Assert.True(registry.Recipes.Count >= 20, $"Expected >= 20 recipes, got {registry.Recipes.Count}"); + // recipes.json has 18 recipes after removing alchemy/supply/energy/oxygen recipes + Assert.True(registry.Recipes.Count >= 15, $"Expected >= 15 recipes, got {registry.Recipes.Count}"); } [Fact] diff --git a/tests/OpenTheBox.Tests/RendererTests.cs b/tests/OpenTheBox.Tests/RendererTests.cs index 04a44db..f2edaa1 100644 --- a/tests/OpenTheBox.Tests/RendererTests.cs +++ b/tests/OpenTheBox.Tests/RendererTests.cs @@ -209,8 +209,8 @@ public class ResourcePanelTests public void Render_ZeroCurrent_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); - state.VisibleResources.Add(ResourceType.Health); - state.Resources[ResourceType.Health] = new ResourceState(0, 100); + state.VisibleResources.Add(ResourceType.Gold); + state.Resources[ResourceType.Gold] = new ResourceState(0, 100); var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); } @@ -219,8 +219,8 @@ public class ResourcePanelTests public void Render_ZeroMax_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); - state.VisibleResources.Add(ResourceType.Mana); - state.Resources[ResourceType.Mana] = new ResourceState(0, 0); + state.VisibleResources.Add(ResourceType.Blood); + state.Resources[ResourceType.Blood] = new ResourceState(0, 0); var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); } @@ -239,7 +239,7 @@ public class ResourcePanelTests public void Render_VisibleButNotInDict_Skipped() { var state = GameState.Create("Test", Locale.EN); - state.VisibleResources.Add(ResourceType.Energy); + state.VisibleResources.Add(ResourceType.Blood); // Not adding to Resources dict → should skip without crash var result = RenderHelper.RenderToString(ResourcePanel.Render(state)); Assert.NotEmpty(result); @@ -805,10 +805,10 @@ public class SpectreRendererOutputTests : IDisposable LegStyle = LegStyle.Short, ArmStyle = ArmStyle.Regular }; - state.AddItem(ItemInstance.Create("health_potion_small")); + state.AddItem(ItemInstance.Create("gold_pouch")); state.AddItem(ItemInstance.Create("box_of_boxes")); - state.VisibleResources.Add(ResourceType.Health); - state.Resources[ResourceType.Health] = new ResourceState(75, 100); + state.VisibleResources.Add(ResourceType.Gold); + state.Resources[ResourceType.Gold] = new ResourceState(75, 100); state.VisibleStats.Add(StatType.Strength); state.Stats[StatType.Strength] = 15; state.TotalBoxesOpened = 42; diff --git a/tests/OpenTheBox.Tests/UnitTest1.cs b/tests/OpenTheBox.Tests/UnitTest1.cs index 115d9c8..54e6171 100644 --- a/tests/OpenTheBox.Tests/UnitTest1.cs +++ b/tests/OpenTheBox.Tests/UnitTest1.cs @@ -1701,4 +1701,229 @@ public class ContentValidationTests var json = File.ReadAllText(EnStringsPath); return JsonSerializer.Deserialize>(json)!; } + + // ══════════════════════════════════════════════════════════════════════════ + // Item Utility Snapshot Test + // ══════════════════════════════════════════════════════════════════════════ + + /// + /// Generates a comprehensive inventory report of all game items, their categories, + /// and all contexts where they can be used. Writes to tests/snapshots/item_utility_report.txt. + /// In DEBUG mode the snapshot is always overwritten. In RELEASE mode the test fails if + /// the content has changed (snapshot testing). + /// + [Fact] + public void ItemUtilitySnapshot() + { + var registry = ContentRegistry.LoadFromFiles(ItemsPath, BoxesPath, InteractionsPath, RecipesPath); + var loc = new LocalizationManager(Locale.FR); + var interactions = LoadInteractions(); + + // Build usage context maps + var lootSources = new Dictionary>(); // itemId → [boxId, ...] + var craftIngredientOf = new Dictionary>(); // itemId → [recipeId, ...] + var craftOutputOf = new Dictionary(); // itemId → recipeId + var interactionItems = new Dictionary>(); // itemId → [interactionId, ...] + + // Map loot sources + foreach (var box in registry.Boxes.Values) + { + foreach (var guaranteed in box.LootTable.GuaranteedRolls) + { + if (!lootSources.ContainsKey(guaranteed)) lootSources[guaranteed] = []; + lootSources[guaranteed].Add($"{box.Id}(G)"); + } + foreach (var entry in box.LootTable.Entries) + { + if (!lootSources.ContainsKey(entry.ItemDefinitionId)) lootSources[entry.ItemDefinitionId] = []; + lootSources[entry.ItemDefinitionId].Add(box.Id); + } + } + + // Map crafting + foreach (var recipe in registry.Recipes.Values) + { + craftOutputOf[recipe.Result.ItemDefinitionId] = recipe.Id; + foreach (var ingredient in recipe.Ingredients) + { + if (!craftIngredientOf.ContainsKey(ingredient.ItemDefinitionId)) + craftIngredientOf[ingredient.ItemDefinitionId] = []; + craftIngredientOf[ingredient.ItemDefinitionId].Add(recipe.Id); + } + } + + // Map interactions (by required item IDs and tag matching) + foreach (var interaction in interactions) + { + // Direct item ID requirements + foreach (var reqId in interaction.RequiredItemIds ?? []) + { + if (!interactionItems.ContainsKey(reqId)) + interactionItems[reqId] = []; + interactionItems[reqId].Add(interaction.Id); + } + // Tag-based matching: find items that have ALL required tags + if (interaction.RequiredItemTags.Count > 0) + { + foreach (var item in registry.Items.Values) + { + if (interaction.RequiredItemTags.All(tag => item.Tags.Contains(tag))) + { + if (!interactionItems.ContainsKey(item.Id)) + interactionItems[item.Id] = []; + if (!interactionItems[item.Id].Contains(interaction.Id)) + interactionItems[item.Id].Add(interaction.Id); + } + } + } + } + + // Build report + var report = new System.Text.StringBuilder(); + report.AppendLine("# Item Utility Report"); + report.AppendLine($"# Generated: {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC"); + report.AppendLine($"# Total items: {registry.Items.Count}"); + report.AppendLine($"# Total boxes: {registry.Boxes.Count}"); + report.AppendLine($"# Total recipes: {registry.Recipes.Count}"); + report.AppendLine(); + + // Group by category + var categories = registry.Items.Values + .GroupBy(i => i.Category) + .OrderBy(g => g.Key.ToString()); + + foreach (var cat in categories) + { + report.AppendLine($"## {cat.Key} ({cat.Count()} items)"); + report.AppendLine(new string('─', 80)); + + var sorted = cat.OrderByDescending(i => CountUsages(i, lootSources, craftIngredientOf, craftOutputOf, interactionItems)); + foreach (var item in sorted) + { + string name = loc.Get(item.NameKey); + var usages = new List(); + + // Loot source + if (lootSources.TryGetValue(item.Id, out var sources)) + usages.Add($"Loot: {string.Join(", ", sources.Distinct().Take(5))}{(sources.Count > 5 ? "..." : "")}"); + + // Consumable + if (item.ResourceType.HasValue && item.ResourceAmount > 0) + usages.Add($"Consume: +{item.ResourceAmount} {item.ResourceType}"); + + // Resource max increase (detected via tags) + if (item.Tags.Contains("ResourceMax") && item.ResourceType.HasValue) + usages.Add($"Upgrade: max {item.ResourceType}"); + + // Meta unlock + if (item.MetaUnlock.HasValue) + usages.Add($"Unlock: {item.MetaUnlock}"); + + // Workstation + if (item.WorkstationType.HasValue) + usages.Add($"Workstation: {item.WorkstationType}"); + + // Adventure theme + if (item.AdventureTheme.HasValue) + usages.Add($"Adventure: {item.AdventureTheme}"); + + // Cosmetic + if (item.CosmeticSlot.HasValue) + usages.Add($"Equip: {item.CosmeticSlot}={item.CosmeticValue}"); + + // Crafting ingredient + if (craftIngredientOf.TryGetValue(item.Id, out var recipes)) + usages.Add($"Craft ingredient: {string.Join(", ", recipes)}"); + + // Crafting output + if (craftOutputOf.TryGetValue(item.Id, out var outputRecipe)) + usages.Add($"Craft output: {outputRecipe}"); + + // Interaction + if (interactionItems.TryGetValue(item.Id, out var inters)) + usages.Add($"Interaction: {string.Join(", ", inters)}"); + + // Tags + if (item.Tags.Contains("Music")) + usages.Add("Ephemeral: plays melody"); + if (item.Tags.Contains("Cookie")) + usages.Add("Ephemeral: fortune message"); + + int usageCount = usages.Count; + string usageIndicator = usageCount switch + { + 0 => "[NO USE]", + 1 => "[*]", + 2 => "[**]", + 3 => "[***]", + _ => $"[{"*".PadRight(usageCount, '*')}]" + }; + + report.AppendLine($" {usageIndicator} {item.Id} ({item.Rarity}) — {name}"); + foreach (var usage in usages) + report.AppendLine($" {usage}"); + report.AppendLine(); + } + } + + // Orphan check: items with no usage at all + report.AppendLine("## Orphan Items (no usage context)"); + report.AppendLine(new string('─', 80)); + var orphans = registry.Items.Values + .Where(i => CountUsages(i, lootSources, craftIngredientOf, craftOutputOf, interactionItems) == 0) + .ToList(); + if (orphans.Count == 0) + report.AppendLine(" (none)"); + else + foreach (var orphan in orphans) + report.AppendLine($" {orphan.Id} ({orphan.Category}, {orphan.Rarity})"); + + string reportText = report.ToString(); + + // Write snapshot + var snapshotDir = Path.Combine("tests", "snapshots"); + Directory.CreateDirectory(snapshotDir); + var snapshotPath = Path.Combine(snapshotDir, "item_utility_report.txt"); + +#if DEBUG + File.WriteAllText(snapshotPath, reportText); + Console.WriteLine(reportText); + Assert.True(true, "Snapshot written (DEBUG mode)."); +#else + if (!File.Exists(snapshotPath)) + { + File.WriteAllText(snapshotPath, reportText); + Assert.Fail("Snapshot did not exist — created. Re-run to validate."); + } + else + { + var existing = File.ReadAllText(snapshotPath); + Assert.True(existing == reportText, + "Item utility snapshot has changed. Review the diff and update the snapshot with a DEBUG build."); + } +#endif + } + + private static int CountUsages( + ItemDefinition item, + Dictionary> lootSources, + Dictionary> craftIngredientOf, + Dictionary craftOutputOf, + Dictionary> interactionItems) + { + int count = 0; + if (lootSources.ContainsKey(item.Id)) count++; + if (item.ResourceType.HasValue && item.ResourceAmount > 0) count++; + if (item.Tags.Contains("ResourceMax") && item.ResourceType.HasValue) count++; + if (item.MetaUnlock.HasValue) count++; + if (item.WorkstationType.HasValue) count++; + if (item.AdventureTheme.HasValue) count++; + if (item.CosmeticSlot.HasValue) count++; + if (craftIngredientOf.ContainsKey(item.Id)) count++; + if (craftOutputOf.ContainsKey(item.Id)) count++; + if (interactionItems.ContainsKey(item.Id)) count++; + if (item.Tags.Contains("Music")) count++; + if (item.Tags.Contains("Cookie")) count++; + return count; + } }