Implement chain reaction system replacing simple auto-activation

Redesign the interaction mechanic into a recursive chain reaction system
where items can trigger cascading reactions. Keys now open themed chests
which produce items that may trigger further reactions, with chain bonus
rewards for multi-step chains (x2/x3/x4+).

- Add 6 themed chests + mysterious chest + alchemist stone catalyst
- Rewrite InteractionEngine with recursive chain loop (max depth 10)
- Add ConsumeTrigger field to InteractionRule for catalyst support
- Add ChainBonusEvent and enrich InteractionTriggeredEvent with context
- Update rendering to show both reacting items and chain indicators
- Add item descriptions with anticipation hints for chain partners
- Update GDD Section 5 with full chain reaction specification
This commit is contained in:
Samuel Bouchet 2026-03-15 18:43:42 +01:00
parent bc1857a6ae
commit 41bfb54a2c
16 changed files with 490 additions and 212 deletions

View file

@ -15,7 +15,7 @@
2. [Mecanique principale : ouverture de boites](#2-mecanique-principale--ouverture-de-boites)
3. [Systeme de progression CLI (Phases 0-8)](#3-systeme-de-progression-cli-phases-0-8)
4. [Systeme de boites](#4-systeme-de-boites)
5. [Auto-activation (cle + coffre, interactions automatiques)](#5-auto-activation-cle--coffre-interactions-automatiques)
5. [Reactions en chaine (interactions automatiques)](#5-reactions-en-chaine-interactions-automatiques)
6. [Personnalisation](#6-personnalisation)
7. [Materiaux et Craft](#7-materiaux-et-craft)
8. [Ressources](#8-ressources)
@ -400,49 +400,116 @@ Les fragments de lore se collectionnent et, une fois assembles, revelent des pan
---
## 5. Auto-activation (cle + coffre, interactions automatiques)
## 5. Reactions en chaine (interactions automatiques)
### Principe
L'auto-activation est un systeme central d'Open The Box. Lorsqu'un objet est ajoute a l'inventaire du joueur, le systeme verifie automatiquement si cet objet peut interagir avec un autre objet deja present.
Les reactions en chaine sont le systeme spectaculaire d'Open The Box. Lorsqu'un objet est ajoute a l'inventaire du joueur, le systeme verifie automatiquement si cet objet peut reagir avec un autre objet deja present. Si une reaction se produit, elle peut generer de nouveaux objets qui declenchent eux-memes d'autres reactions -- creant une cascade visible et excitante.
### Types d'interactions automatiques
### Philosophie
| Interaction | Declencheur | Resultat |
|---------------------------|--------------------------------------|-------------------------------------|
| **Cle + Coffre** | Cle et coffre correspondants | Le coffre s'ouvre, revelant son contenu |
| **Carte + Lieu** | Carte d'un lieu + badge d'exploration| Le lieu est deverrouille |
| **Token + Aventure** | AdventureToken + seuil de progression| L'aventure devient accessible |
| **Blueprint + Materiaux** | Blueprint + materiaux requis | La station de craft est construite |
| **Fragment + Fragment** | Fragments complementaires | Un lore complet est revele |
| **Consommable + Ressource** | Consommable + ressource cible | La ressource est restauree |
Le joueur n'initie jamais une reaction manuellement. Il ouvre une boite, recoit des objets, et observe les consequences. Les reactions en chaine sont le "feu d'artifice" du jeu : imprevisibles, spectaculaires, et recompensant l'accumulation strategique d'objets dans l'inventaire.
### Flux d'auto-activation
Les descriptions des objets reactifs indiquent toujours leur potentiel de combinaison, creant de l'anticipation chez le joueur qui possede un objet en attente de son partenaire.
### Coffres thematiques
Chaque theme d'aventure possede un **coffre** (tag `Openable`) qui apparait dans les boites d'aventure correspondantes. Les coffres sont des objets inertes tant que le joueur ne possede pas la cle correspondante.
| Coffre | Cle correspondante | Contenu a l'ouverture |
|-------------------------|-----------------------|----------------------------------------------------|
| Coffre spatial | Cle spatiale | Carte stellaire + Coordonnees spatiales |
| Coffre medieval | Cle medievale | Blason royal + Parchemin ancien |
| Coffre pirate | Cle pirate | Carte au tresor + Boussole pirate |
| Coffre contemporain | Cle contemporaine | Badge VIP + Cle USB |
| Coffre dark fantasy | Cle dark fantasy | Grimoire maudit + Anneau sombre |
| Coffre mysterieux | Cle mysterieuse | Objet aleatoire legendaire |
> **Note** : La cle mysterieuse (`mysterious_key`) reagit avec n'importe quel coffre. C'est un joker rare.
### Catalyseurs
Les **catalyseurs** sont des objets rares qui participent aux reactions sans etre consommes. Ils restent dans l'inventaire et peuvent declencher plusieurs chaines au fil du temps.
| Catalyseur | Effet | Source |
|-------------------------|-----------------------------------------------------|----------------------------|
| Pierre alchimique | Participe a toute reaction de type Combine | Boite legendaire |
### Flux de reaction en chaine
```
1. Objet ajoute a l'inventaire
2. Pour chaque objet de l'inventaire :
a. Verifier si une interaction est possible avec le nouvel objet
b. Si oui, executer l'interaction
c. L'interaction peut produire de nouveaux objets
d. Si de nouveaux objets sont produits, repeter depuis l'etape 2
3. Fin de la chaine d'auto-activation
2. Le systeme cherche des reactions possibles avec les objets existants
3. Si une reaction est trouvee :
a. Les objets reactifs sont consommes (sauf les catalyseurs)
b. La reaction produit de nouveaux objets et/ou deblocages
c. Compteur de chaine : +1
d. Les nouveaux objets sont re-injectes dans l'etape 2
4. Fin de la chaine quand plus aucune reaction n'est possible
5. Si chaine >= 2 : bonus de chaine applique
```
### Chaines d'activation
### Compteur de chaine et bonus
Les auto-activations peuvent creer des **chaines** : ouvrir un coffre peut reveler une cle qui ouvre un autre coffre, qui contient un fragment qui complete un lore, qui debloque un personnage. Ces chaines sont l'un des moments les plus satisfaisants du jeu.
Le compteur de chaine est affiche en temps reel pendant la cascade.
### Type de resultat d'interaction (InteractionResultType)
| Longueur de chaine | Affichage | Bonus |
|---------------------|-------------------|-------------------------------------------------|
| x1 | *(pas de bonus)* | Reaction simple, pas de bonus |
| x2 | ⚡ Chaine x2 ! | Objet bonus aleatoire (rarete Uncommon+) |
| x3 | ⚡⚡ Chaine x3 ! | Boite rare en bonus |
| x4+ | ⚡⚡⚡ Chaine x4+ ! | Boite legendaire en bonus |
Chaque interaction produit un resultat type parmi :
- **OpenBox** -- ouverture d'une boite ou d'un coffre
- **Craft** -- fabrication d'un objet
- **Transform** -- transformation d'un objet en un autre
- **Consume** -- consommation d'un objet (potion, nourriture)
- **Unlock** -- deblocage d'un contenu (lieu, aventure, personnage)
- **Combine** -- combinaison de plusieurs objets en un seul
- **Teleport** -- deplacement vers un nouveau lieu
### Types de reaction (InteractionResultType)
Chaque reaction produit un resultat parmi :
- **OpenBox** -- la cle ouvre le coffre, revelant son contenu (les objets produits peuvent declencher d'autres reactions)
- **Unlock** -- deblocage d'un contenu (aventure, personnage)
- **Combine** -- combinaison de plusieurs objets en un resultat (carte + boussole = localisation du tresor)
### Regles de reaction
Chaque regle dans `interactions.json` definit :
- `requiredItemTags` : tags que le nouvel objet doit posseder pour declencher la regle
- `requiredItemIds` : objets specifiques devant etre presents dans l'inventaire
- `consumeTrigger` : si `false`, l'objet declencheur n'est pas consomme (catalyseur)
- `resultData` : les objets/effets produits par la reaction
- `descriptionKey` : le message narratif affiche au joueur (avec placeholders `{0}` et `{1}` pour les noms des objets impliques)
- `isAutomatic` : toujours `true` pour les reactions en chaine
- `priority` : ordre de resolution quand plusieurs reactions sont possibles
### Descriptions d'objets et anticipation
Les objets reactifs portent dans leur description un indice de combinaison. Exemples :
| Objet | Description |
|------------------|--------------------------------------------------------------------------|
| Cle medievale | *"Une cle ornee d'un blason. Elle cherche un coffre a sa mesure..."* |
| Coffre medieval | *"Un coffre verrouille, orne du meme blason qu'une certaine cle..."* |
| Carte au tresor | *"X marque l'emplacement... mais sans boussole, impossible de s'orienter."* |
| Boussole pirate | *"L'aiguille pointe vers un tresor... si seulement tu avais la carte."* |
| Cle mysterieuse | *"Cette cle semble pouvoir ouvrir n'importe quel coffre..."* |
### Exemple de chaine complete
```
Boite d'aventure pirate ouverte !
Tu as recu :
- Coffre pirate [Rare]
- Plume de perroquet [Commun]
⚡ Reaction ! Ta Cle pirate et ton Coffre pirate reagissent...
La cle tourne, le coffre s'ouvre !
→ Carte au tresor [Epic]
→ Boussole pirate [Rare]
⚡⚡ Chaine x2 ! Ta Carte au tresor et ta Boussole pirate s'alignent...
Les coordonnees se revelent !
→ 🎉 Aventure 'Iles Perdues' debloquee !
⚡⚡ Bonus de chaine x2 : Boite coolos en recompense !
```
---

View file

@ -103,7 +103,7 @@ openthebox/
| +-- data/ # boxes.json, items.json, interactions.json, recipes.json
| +-- strings/ # en.json, fr.json
| +-- adventures/ # 9 themes, each with .lor + .fr.lor files
+-- docs/GDD.md # Game Design Document (French)
+-- GDD.md # Game Design Document (French)
+-- init.ps1 # Setup script
+-- global.json # Pins .NET 10 SDK
```

View file

@ -167,7 +167,7 @@ Combattre le dragon
Essayer de se faufiler pendant que le dragon est distrait
#opt-charm // "Flash your most dazzling smile at the dragon|||Something about your magnetic personality might work here..."
Adresser votre plus beau sourire au dragon|||Quelque chose dans votre personnalité magnétique pourrait fonctionner ici...
Adresser votre plus beau sourire au dragon|||Pas assez de charisme…
#charm-smile // "You step forward, lock eyes with Scorchtangle, and deliver the most radiant smile in the kingdom's history."
Vous faites un pas en avant, croisez le regard de Scorchtangle, et offrez le sourire le plus radieux de l'histoire du royaume.

View file

@ -124,7 +124,7 @@ beat DragonApproach
-> DragonFight
Try to sneak past while the dragon is distracted #opt-sneak
-> SneakPast
Flash your most dazzling smile at the dragon|||Something about your magnetic personality might work here... #opt-charm if hasStat("Charisma", 10)
Flash your most dazzling smile at the dragon|||Not enough charisma… #opt-charm if hasStat("Charisma", 10)
-> DragonCharmer
beat DragonCharmer

View file

@ -176,6 +176,7 @@
{"itemDefinitionId": "cosmetic_arms_wings", "weight": 1},
{"itemDefinitionId": "tint_void", "weight": 1},
{"itemDefinitionId": "tint_rainbow", "weight": 1},
{"itemDefinitionId": "alchemist_stone", "weight": 1},
{"itemDefinitionId": "lore_6", "weight": 1},
{"itemDefinitionId": "lore_10", "weight": 1},
{"itemDefinitionId": "box_meta_basics", "weight": 2},
@ -347,9 +348,8 @@
"entries": [
{"itemDefinitionId": "space_badge", "weight": 3},
{"itemDefinitionId": "space_phone", "weight": 3},
{"itemDefinitionId": "space_coordinates", "weight": 3},
{"itemDefinitionId": "space_key", "weight": 2},
{"itemDefinitionId": "space_map", "weight": 2}
{"itemDefinitionId": "space_chest", "weight": 3},
{"itemDefinitionId": "space_key", "weight": 3}
]
}
},
@ -364,10 +364,10 @@
"guaranteedRolls": ["box_of_boxes"],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "medieval_crest", "weight": 3},
{"itemDefinitionId": "medieval_scroll", "weight": 3},
{"itemDefinitionId": "medieval_chest", "weight": 3},
{"itemDefinitionId": "medieval_seal", "weight": 2},
{"itemDefinitionId": "medieval_key", "weight": 3},
{"itemDefinitionId": "medieval_sword", "weight": 2},
{"itemDefinitionId": "mysterious_key", "weight": 2}
]
}
@ -380,13 +380,13 @@
"isAutoOpen": false,
"adventureTheme": "Pirate",
"lootTable": {
"guaranteedRolls": ["pirate_map", "box_of_boxes"],
"guaranteedRolls": ["box_of_boxes"],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "pirate_compass", "weight": 3},
{"itemDefinitionId": "pirate_chest", "weight": 3},
{"itemDefinitionId": "pirate_feather", "weight": 4},
{"itemDefinitionId": "pirate_rum", "weight": 3},
{"itemDefinitionId": "pirate_key", "weight": 2},
{"itemDefinitionId": "pirate_key", "weight": 3},
{"itemDefinitionId": "mysterious_key", "weight": 2},
{"itemDefinitionId": "gold_pouch", "weight": 4}
]
@ -405,9 +405,8 @@
"entries": [
{"itemDefinitionId": "contemporary_phone", "weight": 3},
{"itemDefinitionId": "contemporary_card", "weight": 3},
{"itemDefinitionId": "contemporary_usb", "weight": 2},
{"itemDefinitionId": "contemporary_chest", "weight": 3},
{"itemDefinitionId": "contemporary_key", "weight": 3},
{"itemDefinitionId": "contemporary_badge", "weight": 3},
{"itemDefinitionId": "mysterious_key", "weight": 2}
]
}
@ -494,8 +493,7 @@
"guaranteedRolls": ["box_of_boxes"],
"rollCount": 2,
"entries": [
{"itemDefinitionId": "darkfantasy_ring", "weight": 3},
{"itemDefinitionId": "darkfantasy_grimoire", "weight": 2},
{"itemDefinitionId": "darkfantasy_chest", "weight": 3},
{"itemDefinitionId": "darkfantasy_gem", "weight": 1},
{"itemDefinitionId": "darkfantasy_key", "weight": 3},
{"itemDefinitionId": "mysterious_key", "weight": 2},
@ -549,6 +547,7 @@
"rollCount": 3,
"entries": [
{"itemDefinitionId": "mysterious_key", "weight": 3},
{"itemDefinitionId": "mysterious_chest", "weight": 2},
{"itemDefinitionId": "lore_10", "weight": 2},
{"itemDefinitionId": "cosmetic_gender_error", "weight": 1},
{"itemDefinitionId": "tint_void", "weight": 2},

View file

@ -1,33 +1,63 @@
[
{
"id": "key_chest_auto",
"requiredItemTags": ["Key"],
"requiredItemIds": null,
"id": "key_chest_space",
"requiredItemTags": ["Key", "Space"],
"requiredItemIds": ["space_chest"],
"resultType": "OpenBox",
"resultData": null,
"resultData": "space_map,space_coordinates",
"isAutomatic": true,
"priority": 10,
"descriptionKey": "interaction.key_chest"
},
{
"id": "badge_adventure_space",
"requiredItemTags": ["Badge", "Space"],
"requiredItemIds": null,
"resultType": "Unlock",
"resultData": "adventure:Space",
"id": "key_chest_medieval",
"requiredItemTags": ["Key", "Medieval"],
"requiredItemIds": ["medieval_chest"],
"resultType": "OpenBox",
"resultData": "medieval_crest,medieval_scroll",
"isAutomatic": true,
"priority": 5,
"descriptionKey": "adventure.start"
"priority": 10,
"descriptionKey": "interaction.key_chest"
},
{
"id": "phone_character_encounter",
"requiredItemTags": ["PhoneNumber"],
"requiredItemIds": null,
"resultType": "Unlock",
"resultData": "character",
"isAutomatic": false,
"priority": 3,
"descriptionKey": "interaction.phone_call"
"id": "key_chest_pirate",
"requiredItemTags": ["Key", "Pirate"],
"requiredItemIds": ["pirate_chest"],
"resultType": "OpenBox",
"resultData": "pirate_map,pirate_compass",
"isAutomatic": true,
"priority": 10,
"descriptionKey": "interaction.key_chest"
},
{
"id": "key_chest_contemporary",
"requiredItemTags": ["Key", "Contemporary"],
"requiredItemIds": ["contemporary_chest"],
"resultType": "OpenBox",
"resultData": "contemporary_badge,contemporary_usb",
"isAutomatic": true,
"priority": 10,
"descriptionKey": "interaction.key_chest"
},
{
"id": "key_chest_darkfantasy",
"requiredItemTags": ["Key", "DarkFantasy"],
"requiredItemIds": ["darkfantasy_chest"],
"resultType": "OpenBox",
"resultData": "darkfantasy_grimoire,darkfantasy_ring",
"isAutomatic": true,
"priority": 10,
"descriptionKey": "interaction.key_chest"
},
{
"id": "key_chest_mysterious",
"requiredItemTags": ["Key"],
"requiredItemIds": ["mysterious_chest"],
"resultType": "OpenBox",
"resultData": "lore_6,tint_void",
"isAutomatic": true,
"priority": 5,
"descriptionKey": "interaction.key_chest"
},
{
"id": "coordinates_map_combine",
@ -43,10 +73,30 @@
"id": "pirate_map_compass",
"requiredItemTags": [],
"requiredItemIds": ["pirate_map", "pirate_compass"],
"resultType": "Unlock",
"resultType": "Combine",
"resultData": "adventure:Pirate",
"isAutomatic": true,
"priority": 8,
"descriptionKey": "interaction.treasure_located"
},
{
"id": "badge_adventure_space",
"requiredItemTags": ["Badge", "Space"],
"requiredItemIds": null,
"resultType": "Unlock",
"resultData": "adventure:Space",
"isAutomatic": true,
"priority": 5,
"descriptionKey": "adventure.start"
},
{
"id": "badge_adventure_contemporary",
"requiredItemTags": ["Badge", "Contemporary"],
"requiredItemIds": null,
"resultType": "Unlock",
"resultData": "adventure:Contemporary",
"isAutomatic": true,
"priority": 5,
"descriptionKey": "adventure.start"
}
]

View file

@ -67,23 +67,27 @@
{"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"},
{"id": "space_key", "nameKey": "item.space.key", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "Space", "Key"], "adventureTheme": "Space"},
{"id": "space_map", "nameKey": "item.space.map", "category": "Map", "rarity": "Epic", "tags": ["Adventure", "Space"], "adventureTheme": "Space"},
{"id": "space_coordinates", "nameKey": "item.space.coordinates", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Space", "Coordinates"], "adventureTheme": "Space"},
{"id": "space_key", "nameKey": "item.space.key", "descriptionKey": "item.space.key.desc", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "Space", "Key"], "adventureTheme": "Space"},
{"id": "space_chest", "nameKey": "item.space.chest", "descriptionKey": "item.space.chest.desc", "category": "Chest", "rarity": "Rare", "tags": ["Adventure", "Space", "Openable"], "adventureTheme": "Space"},
{"id": "space_map", "nameKey": "item.space.map", "descriptionKey": "item.space.map.desc", "category": "Map", "rarity": "Epic", "tags": ["Adventure", "Space"], "adventureTheme": "Space"},
{"id": "space_coordinates", "nameKey": "item.space.coordinates", "descriptionKey": "item.space.coordinates.desc", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Space", "Coordinates"], "adventureTheme": "Space"},
{"id": "medieval_crest", "nameKey": "item.medieval.crest", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Medieval"], "adventureTheme": "Medieval"},
{"id": "medieval_sword", "nameKey": "item.medieval.sword", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Medieval"], "adventureTheme": "Medieval"},
{"id": "medieval_scroll", "nameKey": "item.medieval.scroll", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Medieval"], "adventureTheme": "Medieval"},
{"id": "medieval_seal", "nameKey": "item.medieval.seal", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "Medieval"], "adventureTheme": "Medieval"},
{"id": "medieval_key", "nameKey": "item.medieval.key", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "Medieval", "Key"], "adventureTheme": "Medieval"},
{"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": "medieval_key", "nameKey": "item.medieval.key", "descriptionKey": "item.medieval.key.desc", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "Medieval", "Key"], "adventureTheme": "Medieval"},
{"id": "medieval_chest", "nameKey": "item.medieval.chest", "descriptionKey": "item.medieval.chest.desc", "category": "Chest", "rarity": "Rare", "tags": ["Adventure", "Medieval", "Openable"], "adventureTheme": "Medieval"},
{"id": "pirate_map", "nameKey": "item.pirate.map", "descriptionKey": "item.pirate.map.desc", "category": "Map", "rarity": "Epic", "tags": ["Adventure", "Pirate"], "adventureTheme": "Pirate"},
{"id": "pirate_compass", "nameKey": "item.pirate.compass", "descriptionKey": "item.pirate.compass.desc", "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": "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": "pirate_key", "nameKey": "item.pirate.key", "descriptionKey": "item.pirate.key.desc", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "Pirate", "Key"], "adventureTheme": "Pirate"},
{"id": "pirate_chest", "nameKey": "item.pirate.chest", "descriptionKey": "item.pirate.chest.desc", "category": "Chest", "rarity": "Rare", "tags": ["Adventure", "Pirate", "Openable"], "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"},
{"id": "contemporary_usb", "nameKey": "item.contemporary.usb", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Contemporary"], "adventureTheme": "Contemporary"},
{"id": "contemporary_key", "nameKey": "item.contemporary.key", "category": "Key", "rarity": "Uncommon", "tags": ["Adventure", "Contemporary", "Key"], "adventureTheme": "Contemporary"},
{"id": "contemporary_key", "nameKey": "item.contemporary.key", "descriptionKey": "item.contemporary.key.desc", "category": "Key", "rarity": "Uncommon", "tags": ["Adventure", "Contemporary", "Key"], "adventureTheme": "Contemporary"},
{"id": "contemporary_chest", "nameKey": "item.contemporary.chest", "descriptionKey": "item.contemporary.chest.desc", "category": "Chest", "rarity": "Rare", "tags": ["Adventure", "Contemporary", "Openable"], "adventureTheme": "Contemporary"},
{"id": "contemporary_badge", "nameKey": "item.contemporary.badge", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Contemporary", "Badge"], "adventureTheme": "Contemporary"},
{"id": "sentimental_letter", "nameKey": "item.sentimental.letter", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "Sentimental"], "adventureTheme": "Sentimental"},
{"id": "sentimental_flower", "nameKey": "item.sentimental.flower", "category": "AdventureToken", "rarity": "Common", "tags": ["Adventure", "Sentimental"], "adventureTheme": "Sentimental"},
@ -101,9 +105,13 @@
{"id": "darkfantasy_ring", "nameKey": "item.darkfantasy.ring", "category": "AdventureToken", "rarity": "Rare", "tags": ["Adventure", "DarkFantasy"], "adventureTheme": "DarkFantasy"},
{"id": "darkfantasy_grimoire", "nameKey": "item.darkfantasy.grimoire", "category": "AdventureToken", "rarity": "Epic", "tags": ["Adventure", "DarkFantasy"], "adventureTheme": "DarkFantasy"},
{"id": "darkfantasy_gem", "nameKey": "item.darkfantasy.gem", "category": "AdventureToken", "rarity": "Legendary", "tags": ["Adventure", "DarkFantasy"], "adventureTheme": "DarkFantasy"},
{"id": "darkfantasy_key", "nameKey": "item.darkfantasy.key", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "DarkFantasy", "Key"], "adventureTheme": "DarkFantasy"},
{"id": "darkfantasy_key", "nameKey": "item.darkfantasy.key", "descriptionKey": "item.darkfantasy.key.desc", "category": "Key", "rarity": "Rare", "tags": ["Adventure", "DarkFantasy", "Key"], "adventureTheme": "DarkFantasy"},
{"id": "darkfantasy_chest", "nameKey": "item.darkfantasy.chest", "descriptionKey": "item.darkfantasy.chest.desc", "category": "Chest", "rarity": "Rare", "tags": ["Adventure", "DarkFantasy", "Openable"], "adventureTheme": "DarkFantasy"},
{"id": "mysterious_key", "nameKey": "item.mysterious_key", "descriptionKey": "item.mysterious_key.desc", "category": "Key", "rarity": "Rare", "tags": ["Key"]},
{"id": "mysterious_chest", "nameKey": "item.mysterious_chest", "descriptionKey": "item.mysterious_chest.desc", "category": "Chest", "rarity": "Epic", "tags": ["Openable"]},
{"id": "alchemist_stone", "nameKey": "item.alchemist_stone", "descriptionKey": "item.alchemist_stone.desc", "category": "AdventureToken", "rarity": "Legendary", "tags": ["Catalyst"]},
{"id": "lore_1", "nameKey": "lore.name_1", "category": "LoreFragment", "rarity": "Uncommon", "tags": ["Lore"]},
{"id": "lore_2", "nameKey": "lore.name_2", "category": "LoreFragment", "rarity": "Uncommon", "tags": ["Lore"]},

View file

@ -276,7 +276,7 @@
"item.cookie_fortune": "Fortune Cookie",
"item.mysterious_key": "Mysterious Key",
"item.mysterious_key.desc": "A key to... something. The box knows, but the box isn't talking.",
"item.mysterious_key.desc": "A key that seems to open any chest... if you can find one.",
"lore.fragment_1": "In the beginning, there was a box. The box contained another box. And so it was, and so it shall be.",
"lore.fragment_2": "The Ancient Order of Box-Openers has a single commandment: Open thy boxes.",
@ -321,6 +321,32 @@
"cookie.19": "Today is a good day to open boxes. Tomorrow too. Every day, really.",
"cookie.20": "Your spirit animal is a box. Your power move is opening.",
"item.space.key.desc": "A key shaped like a star. It's looking for a chest somewhere out there...",
"item.space.chest.desc": "A sealed container from a distant galaxy. A star-shaped keyhole glows faintly...",
"item.space.map.desc": "A star chart showing hyperspace routes... but coordinates are needed to navigate.",
"item.space.coordinates.desc": "Encrypted coordinates. Combined with a star chart, they reveal the path...",
"item.medieval.key.desc": "An ornate key bearing a royal crest. It seeks a chest worthy of its lock...",
"item.medieval.chest.desc": "A locked chest adorned with the same royal crest as a certain key...",
"item.pirate.key.desc": "A rusty key found in a bottle. It smells of salt and buried treasure...",
"item.pirate.chest.desc": "A barnacle-encrusted chest. The lock matches the teeth of a certain rusty key...",
"item.pirate.map.desc": "X marks the spot... but without a compass, the treasure stays hidden.",
"item.pirate.compass.desc": "The needle points to treasure... if only you had the map.",
"item.contemporary.key.desc": "A modern keycard with a magnetic strip. It's looking for a secure box...",
"item.contemporary.chest.desc": "A reinforced safe with a card reader. Waiting for the right keycard...",
"item.darkfantasy.key.desc": "A key forged in shadow. It whispers of a cursed chest...",
"item.darkfantasy.chest.desc": "A chest bound in dark chains. The lock pulses with the same shadow as a certain key...",
"item.mysterious_chest": "Mysterious Chest",
"item.mysterious_chest.desc": "An enigmatic chest with no markings. Any key might work...",
"item.alchemist_stone": "Alchemist's Stone",
"item.alchemist_stone.desc": "A shimmering philosopher's stone. It catalyzes reactions without being consumed...",
"item.space.chest": "Space Lockbox",
"item.medieval.chest": "Royal Chest",
"item.pirate.chest": "Pirate Chest",
"item.contemporary.chest": "Secure Safe",
"item.darkfantasy.chest": "Cursed Chest",
"interaction.chain_reaction": "{0} and {1} react in your inventory!",
"interaction.chain_bonus": "Chain x{0} bonus!",
"character.farah": "Farah",
"character.malkith": "Malkith",
"character.linu": "Linu",
@ -343,7 +369,7 @@
"adventure.item_removed": "Lost: {0}",
"adventure.resource_added": "{0} +{1}",
"interaction.key_chest": "You use {0} — the key fits! The chest opens automatically!",
"interaction.key_chest": "{0} and {1} react! The key turns, the chest opens!",
"interaction.key_no_match": "This key seems to fit something... but you don't have it yet. Perhaps a future box will provide.",
"interaction.treasure_located": "The map and compass align! Treasure located!",
"interaction.map_coordinates": "The map reveals mysterious coordinates...",
@ -548,5 +574,6 @@
"category.badge": "Badges",
"category.map": "Maps",
"category.storyitem": "Story Items",
"category.questitem": "Quest Items"
"category.questitem": "Quest Items",
"category.chest": "Chests"
}

View file

@ -276,7 +276,7 @@
"item.cookie_fortune": "Fortune Cookie",
"item.mysterious_key": "Clé mystérieuse",
"item.mysterious_key.desc": "Une clé pour... quelque chose. La boîte sait, mais la boîte ne parle pas.",
"item.mysterious_key.desc": "Une cle qui semble pouvoir ouvrir n'importe quel coffre... encore faut-il en trouver un.",
"lore.fragment_1": "Au commencement, il y avait une boîte. La boîte contenait une autre boîte. Et c'est ainsi que ça a été, et que ça sera.",
"lore.fragment_2": "L'Ancien Ordre des Ouvreurs de Boîtes n'a qu'un seul commandement : Tu ouvriras tes boîtes.",
@ -321,6 +321,32 @@
"cookie.19": "Aujourd'hui est un bon jour pour ouvrir des boîtes. Demain aussi. Tous les jours, en fait.",
"cookie.20": "Ton animal totem est une boîte. Ton pouvoir spécial c'est l'ouverture.",
"item.space.key.desc": "Une cle en forme d'etoile. Elle cherche un coffre quelque part dans l'espace...",
"item.space.chest.desc": "Un conteneur scelle venu d'une galaxie lointaine. Un trou de serrure en forme d'etoile brille faiblement...",
"item.space.map.desc": "Une carte stellaire montrant des routes hyperspaciales... mais il faut des coordonnees pour naviguer.",
"item.space.coordinates.desc": "Des coordonnees chiffrees. Combinees a une carte stellaire, elles revelent le chemin...",
"item.medieval.key.desc": "Une cle ornee d'un blason royal. Elle cherche un coffre a sa mesure...",
"item.medieval.chest.desc": "Un coffre verrouille, orne du meme blason royal qu'une certaine cle...",
"item.pirate.key.desc": "Une cle rouillee trouvee dans une bouteille. Elle sent le sel et le tresor enfoui...",
"item.pirate.chest.desc": "Un coffre couvert de bernacles. La serrure correspond aux dents d'une certaine cle rouillee...",
"item.pirate.map.desc": "X marque l'emplacement... mais sans boussole, le tresor reste cache.",
"item.pirate.compass.desc": "L'aiguille pointe vers un tresor... si seulement tu avais la carte.",
"item.contemporary.key.desc": "Un badge magnetique moderne. Il cherche un coffre-fort...",
"item.contemporary.chest.desc": "Un coffre-fort renforce avec un lecteur de badge. En attente de la bonne carte...",
"item.darkfantasy.key.desc": "Une cle forgee dans l'ombre. Elle murmure a propos d'un coffre maudit...",
"item.darkfantasy.chest.desc": "Un coffre enchaine dans les tenebres. La serrure pulse de la meme ombre qu'une certaine cle...",
"item.mysterious_chest": "Coffre mysterieux",
"item.mysterious_chest.desc": "Un coffre enigmatique sans aucune marque. N'importe quelle cle pourrait fonctionner...",
"item.alchemist_stone": "Pierre alchimique",
"item.alchemist_stone.desc": "Une pierre philosophale chatoyante. Elle catalyse les reactions sans etre consommee...",
"item.space.chest": "Coffre spatial",
"item.medieval.chest": "Coffre royal",
"item.pirate.chest": "Coffre pirate",
"item.contemporary.chest": "Coffre-fort",
"item.darkfantasy.chest": "Coffre maudit",
"interaction.chain_reaction": "{0} et {1} reagissent dans ton inventaire !",
"interaction.chain_bonus": "Bonus de chaine x{0} !",
"character.farah": "Farah",
"character.malkith": "Malkith",
"character.linu": "Linu",
@ -343,7 +369,7 @@
"adventure.item_removed": "Perdu : {0}",
"adventure.resource_added": "{0} +{1}",
"interaction.key_chest": "Tu utilises {0} — la clé rentre ! Le coffre s'ouvre automatiquement !",
"interaction.key_chest": "{0} et {1} reagissent ! La cle tourne, le coffre s'ouvre !",
"interaction.key_no_match": "Cette clé semble ouvrir quelque chose... mais tu ne l'as pas encore. Peut-être qu'une future boîte le fournira.",
"interaction.treasure_located": "La carte et la boussole s'alignent ! Trésor localisé !",
"interaction.map_coordinates": "La carte révèle des coordonnées mystérieuses...",
@ -549,5 +575,6 @@
"category.badge": "Badges",
"category.map": "Cartes",
"category.storyitem": "Objets d'histoire",
"category.questitem": "Objets de quête"
"category.questitem": "Objets de quête",
"category.chest": "Coffres"
}

View file

@ -53,5 +53,8 @@ public enum ItemCategory
CraftedItem,
/// <summary>An item required to complete a character's personal quest.</summary>
QuestItem
QuestItem,
/// <summary>A locked chest that can be opened with a matching key via chain reactions.</summary>
Chest
}

View file

@ -14,5 +14,6 @@ public sealed record InteractionRule(
string? ResultData,
bool IsAutomatic,
int Priority,
string DescriptionKey
string DescriptionKey,
bool ConsumeTrigger = true
);

View file

@ -521,13 +521,37 @@ public static class Program
break;
case InteractionTriggeredEvent interEvt:
// Defer interaction display until after loot reveal, with trigger item context
var interMsg = interEvt.TriggerItemId is not null
? _loc.Get(interEvt.DescriptionKey, GetLocalizedName(interEvt.TriggerItemId))
: _loc.Get(interEvt.DescriptionKey);
// Defer interaction display until after loot reveal
// Build message with both trigger and partner item names
string interMsg;
if (interEvt.TriggerItemId is not null && interEvt.PartnerItemId is not null)
{
var triggerName = GetLocalizedName(interEvt.TriggerItemId);
var partnerName = GetLocalizedName(interEvt.PartnerItemId);
// First show which items react, then show the rule-specific message
var chainIndicator = interEvt.ChainStep > 0
? new string(UnicodeSupport.IsUtf8 ? '⚡' : '!', interEvt.ChainStep) + " "
: "";
var reactionMsg = _loc.Get("interaction.chain_reaction", triggerName, partnerName);
var detailMsg = _loc.Get(interEvt.DescriptionKey, triggerName, partnerName);
interMsg = $"{chainIndicator}{reactionMsg}\n {detailMsg}";
}
else if (interEvt.TriggerItemId is not null)
{
interMsg = _loc.Get(interEvt.DescriptionKey, GetLocalizedName(interEvt.TriggerItemId));
}
else
{
interMsg = _loc.Get(interEvt.DescriptionKey);
}
deferredInteractions.Add(interMsg);
break;
case ChainBonusEvent chainEvt:
var bonusName = GetLocalizedName(chainEvt.BonusItemId);
deferredInteractions.Add(_loc.Get("interaction.chain_bonus", chainEvt.ChainLength) + $" {bonusName}");
break;
case ResourceChangedEvent resEvt:
var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");

View file

@ -27,9 +27,14 @@ public sealed record ItemReceivedEvent(ItemInstance Item) : GameEvent;
public sealed record ItemConsumedEvent(Guid InstanceId) : GameEvent;
/// <summary>
/// An interaction rule was triggered.
/// An interaction rule was triggered as part of a chain reaction.
/// </summary>
public sealed record InteractionTriggeredEvent(string RuleId, string DescriptionKey, string? TriggerItemId = null) : GameEvent;
public sealed record InteractionTriggeredEvent(string RuleId, string DescriptionKey, string? TriggerItemId = null, string? PartnerItemId = null, int ChainStep = 0) : GameEvent;
/// <summary>
/// A chain reaction bonus was awarded after a multi-step chain.
/// </summary>
public sealed record ChainBonusEvent(int ChainLength, string BonusItemId) : GameEvent;
/// <summary>
/// A UI feature was unlocked via a meta item.

View file

@ -101,8 +101,8 @@ public class GameSimulation
// Collect newly received items for post-processing
var newItems = boxEvents.OfType<ItemReceivedEvent>().Select(e => e.Item).ToList();
// Run auto-activation pass
events.AddRange(_interactionEngine.CheckAutoActivations(newItems, state));
// Run chain reaction pass
events.AddRange(_interactionEngine.RunChainReactions(newItems, state, _rng));
// Run meta pass
events.AddRange(_metaEngine.ProcessNewItems(newItems, state, _registry));
@ -139,7 +139,7 @@ public class GameSimulation
}
// Otherwise, check interaction rules
var interactionEvents = _interactionEngine.CheckAutoActivations([item], state);
var interactionEvents = _interactionEngine.RunChainReactions([item], state, _rng);
events.AddRange(interactionEvents);
return events;

View file

@ -9,19 +9,28 @@ namespace OpenTheBox.Simulation;
/// <summary>
/// Evaluates interaction rules against newly received items and the current game state,
/// triggering automatic interactions or requesting player choices when multiple apply.
/// triggering automatic chain reactions that can cascade recursively.
/// </summary>
public class InteractionEngine(ContentRegistry registry)
{
/// <summary>
/// Checks all interaction rules against newly received items and returns events for
/// any auto-activations or choice prompts.
/// </summary>
public List<GameEvent> CheckAutoActivations(List<ItemInstance> newItems, GameState state)
{
var events = new List<GameEvent>();
private const int MaxChainDepth = 10;
foreach (var newItem in newItems)
/// <summary>
/// Runs the full chain reaction loop: checks new items for interactions,
/// executes them, and re-checks any produced items until no more reactions fire.
/// Returns all events produced across the entire chain.
/// </summary>
public List<GameEvent> RunChainReactions(List<ItemInstance> newItems, GameState state, Random rng)
{
var allEvents = new List<GameEvent>();
var itemsToCheck = new List<ItemInstance>(newItems);
int chainStep = 0;
while (itemsToCheck.Count > 0 && chainStep < MaxChainDepth)
{
var producedItems = new List<ItemInstance>();
foreach (var newItem in itemsToCheck)
{
var itemDef = registry.GetItem(newItem.DefinitionId);
if (itemDef is null)
@ -31,7 +40,7 @@ public class InteractionEngine(ContentRegistry registry)
if (matchingRules.Count == 0)
{
// Special case: key without a matching openable item injects a box into future loot tables
// Special case: key without a matching openable item
if (itemDef.Tags.Contains("Key"))
{
var hasMatchingOpenable = state.Inventory.Any(i =>
@ -42,11 +51,7 @@ public class InteractionEngine(ContentRegistry registry)
if (!hasMatchingOpenable)
{
events.Add(new LootTableModifiedEvent(
BoxId: "starter_box",
AddedEntryId: newItem.DefinitionId,
Reason: $"Key '{newItem.DefinitionId}' has no matching Openable item; injecting into future loot tables"
));
allEvents.Add(new MessageEvent("interaction.key_no_match"));
}
}
@ -58,27 +63,42 @@ public class InteractionEngine(ContentRegistry registry)
.OrderByDescending(r => r.Priority)
.ToList();
if (automaticRules.Count == 1)
if (automaticRules.Count >= 1)
{
// Single automatic match: auto-execute
var rule = automaticRules[0];
events.AddRange(ExecuteRule(rule, newItem, state));
}
else if (automaticRules.Count > 1)
var ruleEvents = ExecuteRule(rule, newItem, state, chainStep);
allEvents.AddRange(ruleEvents);
// Collect items produced by this reaction for the next chain iteration
foreach (var evt in ruleEvents)
{
// Multiple automatic matches: let the player choose
events.Add(new ChoiceRequiredEvent(
Prompt: "prompt.choose_interaction",
Options: automaticRules.Select(r => r.DescriptionKey).ToList()
));
if (evt is ItemReceivedEvent received)
producedItems.Add(received.Item);
}
else
{
// Non-automatic rules found but none are automatic -- no auto-activation
}
}
return events;
if (producedItems.Count == 0)
break;
itemsToCheck = producedItems;
chainStep++;
}
// Award chain bonus if chain was 2+ steps
if (chainStep >= 2)
{
string bonusItemId = chainStep >= 4 ? "box_legendary"
: chainStep >= 3 ? "box_cool"
: "box_ok_tier";
var bonusInstance = ItemInstance.Create(bonusItemId);
state.AddItem(bonusInstance);
allEvents.Add(new ItemReceivedEvent(bonusInstance));
allEvents.Add(new ChainBonusEvent(chainStep, bonusItemId));
}
return allEvents;
}
/// <summary>
@ -95,7 +115,7 @@ public class InteractionEngine(ContentRegistry registry)
if (!tagsMatch)
continue;
// Check required item ids (if specified, at least one must be in inventory)
// Check required item ids (if specified, all must be in inventory)
if (rule.RequiredItemIds is not null && rule.RequiredItemIds.Count > 0)
{
var hasRequiredItem = rule.RequiredItemIds.All(id => state.HasItem(id));
@ -110,22 +130,48 @@ public class InteractionEngine(ContentRegistry registry)
}
/// <summary>
/// Executes a single interaction rule, consuming the trigger item and producing results.
/// Executes a single interaction rule, consuming items and producing results.
/// </summary>
private List<GameEvent> ExecuteRule(InteractionRule rule, ItemInstance triggerItem, GameState state)
private List<GameEvent> ExecuteRule(InteractionRule rule, ItemInstance triggerItem, GameState state, int chainStep)
{
var events = new List<GameEvent>();
// Consume the trigger item
// Find the partner item (first matching required item id) for the interaction message
string? partnerItemId = null;
if (rule.RequiredItemIds is not null && rule.RequiredItemIds.Count > 0)
{
partnerItemId = rule.RequiredItemIds[0];
}
// Consume the trigger item (unless it's a catalyst)
if (rule.ConsumeTrigger)
{
state.RemoveItem(triggerItem.Id);
events.Add(new ItemConsumedEvent(triggerItem.Id));
}
// Handle result based on ResultType
// Consume required inventory items (the partner items)
if (rule.RequiredItemIds is not null)
{
foreach (var requiredId in rule.RequiredItemIds)
{
var invItem = state.Inventory.FirstOrDefault(i => i.DefinitionId == requiredId);
if (invItem is not null)
{
state.RemoveItem(invItem.Id);
events.Add(new ItemConsumedEvent(invItem.Id));
}
}
}
// Emit the interaction event with context
events.Add(new InteractionTriggeredEvent(rule.Id, rule.DescriptionKey, triggerItem.DefinitionId, partnerItemId, chainStep));
// Handle result based on ResultData
if (rule.ResultData is not null)
{
if (rule.ResultData.StartsWith("adventure:") || rule.ResultData.StartsWith("adventure_unlock:"))
{
// Unlock an adventure theme (e.g., "adventure:Pirate" or "adventure_unlock:Space")
var themeName = rule.ResultData.Contains("adventure_unlock:")
? rule.ResultData["adventure_unlock:".Length..]
: rule.ResultData["adventure:".Length..];
@ -137,7 +183,7 @@ public class InteractionEngine(ContentRegistry registry)
}
else
{
// Produce result items (item definition ids, comma-separated)
// Produce result items
var resultItemIds = rule.ResultData.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var resultItemId in resultItemIds)
{
@ -148,8 +194,6 @@ public class InteractionEngine(ContentRegistry registry)
}
}
events.Add(new InteractionTriggeredEvent(rule.Id, rule.DescriptionKey, triggerItem.DefinitionId));
return events;
}
}

View file

@ -1,51 +1,20 @@
# Item Utility Report
# Total items: 145
# Total items: 152
# Total boxes: 31
# Total recipes: 18
## AdventureToken (31 items)
## AdventureToken (32 items)
────────────────────────────────────────────────────────────────────────────────
[****] space_coordinates (Epic) — Coordonnées mystérieuses
Loot: box_adventure_space
[***] space_coordinates (Epic) — Coordonnées mystérieuses
Adventure: Space
Craft ingredient: chart_star_navigation @ DrawingTable
Interaction: coordinates_map_combine
[***] space_phone (Rare) — Numéro de téléphone alien
Loot: box_adventure_space
Adventure: Space
Interaction: phone_character_encounter
[***] medieval_crest (Rare) — Blason de chevalier
Loot: box_adventure_medieval
Adventure: Medieval
Craft ingredient: engrave_royal_seal @ EngravingBench
[***] medieval_scroll (Rare) — Parchemin ancien
Loot: box_adventure_medieval
Adventure: Medieval
Craft ingredient: engrave_royal_seal @ EngravingBench
[***] medieval_seal (Epic) — Sceau royal
Loot: box_adventure_medieval
Adventure: Medieval
Craft output: engrave_royal_seal @ EngravingBench
[***] pirate_compass (Rare) — Boussole enchantée
Loot: box_adventure_pirate
Adventure: Pirate
Interaction: pirate_map_compass
[***] contemporary_phone (Common) — Smartphone
Loot: box_adventure_contemporary
Adventure: Contemporary
Interaction: phone_character_encounter
[***] sentimental_phone (Rare) — Numéro de l'ex
Loot: box_adventure_sentimental
Adventure: Sentimental
Interaction: phone_character_encounter
[***] prehistoric_tooth (Uncommon) — Dent de dinosaure
Loot: box_adventure_prehistoric
Adventure: Prehistoric
@ -82,20 +51,30 @@
Adventure: Microscopic
Craft ingredient: splice_glowing_dna @ GeneticModStation
[***] darkfantasy_ring (Rare) — Anneau maudit
Loot: box_adventure_darkfantasy
Adventure: DarkFantasy
Craft ingredient: enchant_dark_grimoire @ TransformationPentacle
[***] darkfantasy_grimoire (Epic) — Grimoire du nécromancien
Loot: box_adventure_darkfantasy
Adventure: DarkFantasy
Craft output: enchant_dark_grimoire @ TransformationPentacle
[**] space_badge (Rare) — Badge d'astronaute
Loot: box_adventure_space
Adventure: Space
[**] space_phone (Rare) — Numéro de téléphone alien
Loot: box_adventure_space
Adventure: Space
[**] medieval_crest (Rare) — Blason de chevalier
Adventure: Medieval
Craft ingredient: engrave_royal_seal @ EngravingBench
[**] medieval_sword (Epic) — Réplique d'Excalibur
Loot: box_adventure_medieval
Adventure: Medieval
[**] medieval_scroll (Rare) — Parchemin ancien
Adventure: Medieval
Craft ingredient: engrave_royal_seal @ EngravingBench
[**] pirate_compass (Rare) — Boussole enchantée
Adventure: Pirate
Interaction: pirate_map_compass
[**] pirate_feather (Common) — Plume de perroquet
Loot: box_adventure_pirate
Adventure: Pirate
@ -104,17 +83,17 @@
Loot: box_adventure_pirate
Adventure: Pirate
[**] contemporary_phone (Common) — Smartphone
Loot: box_adventure_contemporary
Adventure: Contemporary
[**] contemporary_card (Uncommon) — Carte de crédit
Loot: box_adventure_contemporary
Adventure: Contemporary
[**] contemporary_usb (Rare) — Clé USB suspecte
Loot: box_adventure_contemporary
Adventure: Contemporary
[**] contemporary_badge (Rare) — Badge d'entreprise
Loot: box_adventure_contemporary
Adventure: Contemporary
Interaction: badge_adventure_contemporary
[**] sentimental_letter (Rare) — Lettre d'amour
Loot: box_adventure_sentimental
@ -128,6 +107,10 @@
Loot: box_adventure_sentimental
Adventure: Sentimental
[**] sentimental_phone (Rare) — Numéro de l'ex
Loot: box_adventure_sentimental
Adventure: Sentimental
[**] prehistoric_fossil (Epic) — Fossile de trilobite
Loot: box_adventure_prehistoric
Adventure: Prehistoric
@ -136,6 +119,14 @@
Loot: box_adventure_cosmic, box_black
Adventure: Cosmic
[**] darkfantasy_ring (Rare) — Anneau maudit
Adventure: DarkFantasy
Craft ingredient: enchant_dark_grimoire @ TransformationPentacle
[**] darkfantasy_grimoire (Epic) — Grimoire du nécromancien
Adventure: DarkFantasy
Craft output: enchant_dark_grimoire @ TransformationPentacle
[**] darkfantasy_gem (Legendary) — Gemme d'âme
Loot: box_adventure_darkfantasy, box_black
Adventure: DarkFantasy
@ -144,8 +135,42 @@
Loot: box_endgame(G)
Adventure: Destiny
[*] medieval_sword (Epic) — Réplique d'Excalibur
[*] contemporary_usb (Rare) — Clé USB suspecte
Adventure: Contemporary
[*] alchemist_stone (Legendary) — Pierre alchimique
Loot: box_legendary
## Chest (6 items)
────────────────────────────────────────────────────────────────────────────────
[***] space_chest (Rare) — Coffre spatial
Loot: box_adventure_space
Adventure: Space
Interaction: key_chest_space
[***] medieval_chest (Rare) — Coffre royal
Loot: box_adventure_medieval
Adventure: Medieval
Interaction: key_chest_medieval
[***] pirate_chest (Rare) — Coffre pirate
Loot: box_adventure_pirate
Adventure: Pirate
Interaction: key_chest_pirate
[***] contemporary_chest (Rare) — Coffre-fort
Loot: box_adventure_contemporary
Adventure: Contemporary
Interaction: key_chest_contemporary
[***] darkfantasy_chest (Rare) — Coffre maudit
Loot: box_adventure_darkfantasy
Adventure: DarkFantasy
Interaction: key_chest_darkfantasy
[**] mysterious_chest (Epic) — Coffre mysterieux
Loot: box_black
Interaction: key_chest_mysterious
## Consumable (6 items)
────────────────────────────────────────────────────────────────────────────────
@ -345,31 +370,31 @@
Loot: box_adventure_space
Adventure: Space
Craft output: chart_star_navigation @ DrawingTable
Interaction: key_chest_auto
Interaction: key_chest_space, key_chest_mysterious
[***] medieval_key (Rare) — Clé du donjon
Loot: box_adventure_medieval
Adventure: Medieval
Interaction: key_chest_auto
Interaction: key_chest_medieval, key_chest_mysterious
[***] pirate_key (Rare) — Clé du coffre
Loot: box_adventure_pirate
Adventure: Pirate
Interaction: key_chest_auto
Interaction: key_chest_pirate, key_chest_mysterious
[***] contemporary_key (Uncommon) — Clé d'appartement
Loot: box_adventure_contemporary
Adventure: Contemporary
Interaction: key_chest_auto
Interaction: key_chest_contemporary, key_chest_mysterious
[***] darkfantasy_key (Rare) — Clé en os
Loot: box_adventure_darkfantasy
Adventure: DarkFantasy
Interaction: key_chest_auto
Interaction: key_chest_darkfantasy, key_chest_mysterious
[**] mysterious_key (Rare) — Clé mystérieuse
Loot: box_adventure_medieval, box_adventure_pirate, box_adventure_contemporary, box_adventure_darkfantasy, box_black
Interaction: key_chest_auto
Interaction: key_chest_mysterious
## LoreFragment (10 items)
────────────────────────────────────────────────────────────────────────────────
@ -405,14 +430,12 @@
## Map (2 items)
────────────────────────────────────────────────────────────────────────────────
[****] space_map (Epic) — Carte stellaire
Loot: box_adventure_space
[***] space_map (Epic) — Carte stellaire
Adventure: Space
Craft ingredient: chart_star_navigation @ DrawingTable
Interaction: coordinates_map_combine
[***] pirate_map (Epic) — Carte au trésor
Loot: box_adventure_pirate(G)
[**] pirate_map (Epic) — Carte au trésor
Adventure: Pirate
Interaction: pirate_map_compass