Module dynamique NextDungeon implémentant un type de donjon procédural inspiré de Hades & Archero : enchaînement de salles avec choix de portes, icônes de récompense visibles au choix, boss tous les 10 paliers, mode infini avec checkpoints.
Le Memory Labyrinth est un type de donjon fourni par le module module-memory-labyrinth. Il se présente comme un donjon NextDungeon classique exposant plusieurs floors correspondant à des niveaux de difficulté ; chaque floor enchaîne des salles générées de manière procédurale à partir d'un pool de salles pré-construites.
Le donjon peut se jouer solo ou en groupe.
[Lobby] → [Salle 1] → choix porte (icônes visibles) → [Salle 2] → … → [Salle 9] → [Boss salle 10]
│
(checkpoint dans Infinite)
▼
[Salle 11] → … → [Boss 20] → …
- Lobby : 1ʳᵉ salle, sans mob, sert de point d'entrée et permet (en Infinite) de choisir entre
Reprendre une saveetNouvelle partie. - Salle de combat (1 à 9 modulo 10) : contient des mobs ; les portes restent verrouillées tant que tous les mobs ne sont pas tués. À la fin, deux portes sont proposées avec leur icône de récompense visible ; le leader de la party choisit (style Hades).
- Salle de blessing (optionnelle, peut être insérée aléatoirement entre les salles de combat) : pas de mob, une porte avec une icône de bénédiction (non implémentée en v1) offrant un bonus pour les salle suivante.
- Salle de boss (toutes les 10 salles, soit
roomIndex % 10 == 0) : une seule porte de sortie. Tuer le boss déclenche :- le revive d'un coéquipier mort (sélection via chat),
- la sauvegarde du checkpoint (Infinite uniquement).
| Floor | Type | Nombre de salles | Save |
|---|---|---|---|
easy |
Fini | N (ex. 30) | non |
normal |
Fini | N (ex. 50) | non |
hard |
Fini | N (ex. 70) | non |
infinite |
Infini | ∞ | oui (à chaque boss) |
Les valeurs
Nsont configurables par floor.
- Sauvegarde automatique à la fin de chaque combat de boss (toutes les 10 salles).
- La save mémorise : numéro de salle atteint, composition du groupe (UUIDs au démarrage), seed/route empruntée, modificateur de difficulté courant, compteurs d'icônes accumulés.
- Reprise : la save n'est rechargeable que si la composition de groupe est identique à celle du démarrage (mêmes UUIDs). Si un joueur du groupe initial ne revient jamais, la save reste bloquée — les autres peuvent toujours créer une nouvelle partie.
- Mort totale (solo : le joueur meurt / groupe : tout le groupe est mort sans pouvoir revive) → la save est invalidée et supprimée. Le joueur recommence à zéro.
- Mort en cours de salle : le joueur passe en mode fantôme (système existant
InstancePlayerDeathListener). - Après un boss : si un ou plusieurs joueurs sont morts, un message dans le chat propose la liste des joueurs morts ; un joueur vivant clique sur un pseudo pour le revive. Un seul revive par boss.
- Si tout le groupe meurt avant de tuer un boss → fail de l'instance, save Infinite supprimée, loot calculé sur les icônes accumulées depuis la dernière save.
Le floor à une loot table avec des items et leur pourcentage de drop de base. Inspiré de Hades : chaque salle de combat obtient une icône de récompense tirée aléatoirement, affichée au-dessus de la porte au moment du choix. Les icônes s'accumulent pendant la run et déterminent le loot en fin de run. Pour le donjon infini, le loot n'est donné qu'à la fin de la run (mort totale ou sortie volontaire -> [On ne pourra plus recommencer cette run, elle sera "delete"]); les icônes accumulées sont persistées dans la save et survivent aux reprises.
Icônes disponibles en v1 :
| Icône | Effet sur le loot final |
|---|---|
🪙 GOLD |
Multiplie l'or final |
✨ BLESSING |
Réservé — non implémenté en v1 |
Pas de drop au sol pendant la run. Le loot n'est calculé et donné qu'à la fin du run (mort, complétion, ou sortie d'un Infinite). Les icônes accumulées sont persistées dans la save en Infinite et survivent aux save/resume.
Aucune porte ne s'ouvre tant que tous les mobs de la salle ne sont pas morts. Exception : le lobby (pas de mobs).
Verdict : faisable comme module dynamique sur le modèle de module-cinematic / module-worldedit. L'architecture existante couvre la majorité des besoins ; la logique propre au labyrinthe (boucle de salle, portes, loot) vit entièrement dans le module.
| Besoin | Brique existante |
|---|---|
| Chargement dynamique de modules | ModuleLoader + NextDungeonModule (common/.../module/) |
| Enregistrement de blocks Blockly | ModuleBlockRegistry, ModuleBlockDescriptor, ModuleContext.fireTrigger() |
| Triggers à base de régions | RegionTrigger (spigot/.../workflow/trigger/impl/) |
| Détection de mort de mob | EntityDeathTrigger + InstanceMobKillListener |
| Modèle de salle | Step (CuboidRegion nommé dans FloorData) |
| Instance de donjon multi-joueurs | FloorInstance (players, stats, lives, originInstances) |
| Système de fantôme & revive | InstancePlayerDeathListener.revivePlayer() |
| Persistence Mongo/MySQL | DatabaseManager (async, retry, transaction) |
| Sync cross-server | Redis via DungeonService |
| Profil joueur (stats, complétions) | ProfileData |
| Parties | IDungeonParty (interne + Alessio) |
- Catalogue de salles (
RoomTemplate) — pool de salles pré-construites, avec géométrie, points de spawn, points de portes, type (combat/boss/lobby). - Générateur procédural — pioche 2 salles à chaque transition et roll une icône par salle.
- État de run (
LabyrinthRun) — décompte de salle, route empruntée, mobs vivants par salle, modificateur de difficulté, compteurs d'icônes. - Système de portes — verrou/déverrou en fonction des mobs vivants, hologramme d'icône au-dessus.
- Save Infinite (
LabyrinthSave) — stockée en DB, retrouvée par hash de composition de groupe. - Calcul de loot — RNG pondéré sur une
LootTablepar floor, à la fin du run uniquement. - UI revive post-boss + UI lobby resume/new (chat clickable).
- Hooks Blockly — triggers / conditions / values exposés à l'éditeur (Option C : aucune action exposée).
- Le système actuel n'a pas de notion de « salle qui se charge à la volée ». v1 : salles statiques dans un monde pré-fait (le picker téléporte les joueurs entre régions). Paste runtime via
module-worldeditreste ouvert pour une v2. - Les checkpoints n'existent pas dans
ProfileData; on ajoute une table dédiée au module (labyrinth_saves), qui garde le module isolé. - Logique « mobs morts → portes ouvertes » : primitive native du module, pas de Blockly à câbler.
Calquée sur module-cinematic/ :
module-memory-labyrinth/
├── pom.xml ← parent: NextDungeon, Common en provided
└── src/main/java/fr/perrier/dungeons/module/labyrinth/
├── MemoryLabyrinthModule.java ← implémente NextDungeonModule
├── manager/
│ ├── LabyrinthRunManager.java ← état des runs en cours (par instance)
│ ├── RoomTemplateRegistry.java ← chargement du pool de salles depuis DB
│ ├── LabyrinthSaveManager.java ← CRUD des saves Infinite
│ └── LootTableRegistry.java ← chargement des loot tables par floor
├── model/
│ ├── RoomTemplate.java
│ ├── RoomType.java ← LOBBY | COMBAT | BOSS
│ ├── RewardIcon.java ← GOLD | BLESSING (TODO v2)
│ ├── DoorChoice.java ← (RoomTemplate, RewardIcon) × 2
│ ├── LabyrinthRun.java ← runtime (currentRoom, route, iconCounts…)
│ ├── LabyrinthSave.java ← snapshot persistant Infinite
│ ├── LootTable.java ← config loot par floor
│ └── DifficultyModifier.java ← scaling HP/dmg par tier
├── generator/
│ ├── RoomPicker.java ← tirage des 2 salles (avec contraintes)
│ └── IconRoller.java ← roll d'icône à la proposition de porte
├── lifecycle/ ← orchestration interne (non exposée)
│ ├── LabyrinthRoomLifecycle.java ← entrée/spawn/cleared/transition
│ ├── DoorController.java ← lock/unlock + hologramme d'icône
│ └── BossEncounterHandler.java ← boss → revive UI → save → next
├── loot/
│ └── LootCalculator.java ← RNG en fin de run
├── trigger/ ← exposés à Blockly
│ ├── OnRoomEnteredTrigger.java
│ ├── OnRoomClearedTrigger.java
│ ├── OnBossKilledTrigger.java
│ ├── OnDoorsProposedTrigger.java
│ ├── OnRunEndedTrigger.java
│ ├── OnCheckpointSavedTrigger.java
│ └── OnSaveInvalidatedTrigger.java
├── condition/
│ ├── IsBossRoomCondition.java
│ ├── IsInfiniteFloorCondition.java
│ └── HasResumableSaveCondition.java
├── value/
│ ├── CurrentTierValue.java
│ ├── CurrentRoomIndexValue.java
│ ├── IconCountValue.java
│ ├── GoldEarnedValue.java
│ └── ItemsRolledValue.java
└── ui/
├── DoorIconHologram.java ← icône au-dessus des portes
├── ResumeOrNewPrompt.java ← chat clickable (lobby Infinite, leader only)
└── RevivePromptComponent.java ← chat clickable (post-boss)
┌─────────────────────────────────────────────┐
│ module-memory-labyrinth (JAR) │
│ ┌─────────────────────────────────────┐ │
│ │ MemoryLabyrinthModule.onEnable() │ │
│ │ • register triggers/conditions/ │ │
│ │ values (Option C - no actions) │ │
│ │ • subscribe Bukkit listeners │ │
│ │ (room transitions, mob death, │ │
│ │ boss death, player death) │ │
│ └─────────────────────────────────────┘ │
└────────────────────┬────────────────────────┘
▼
ModuleContext (host)
▼
┌──────────────────────────────────┐
│ Core NextDungeon │
│ FloorInstance · RegionTrigger │
│ DatabaseManager · IDungeonParty │
│ ProfileData │
└──────────────────────────────────┘
Le module ne reçoit jamais d'actions Blockly — il pilote sa boucle nativement et émet des triggers que les workflows admin peuvent écouter pour greffer des effets de bord (cinematic, message, son…).
{
"id": "combat_basic_01",
"type": "COMBAT",
"worldId": "labyrinth_pool",
"region": { "min": [0, 60, 0], "max": [16, 80, 16] },
"playerSpawn": { "x": 8, "y": 61, "z": 2 },
"doors": [
{ "id": "exit_left", "anchor": [3, 61, 14] },
{ "id": "exit_right", "anchor": [13, 61, 14] }
],
"mobSpawns": [
{ "mobId": "skeleton_t1", "x": 5, "y": 61, "z": 8, "count": 2 },
{ "mobId": "zombie_t1", "x": 11, "y": 61, "z": 8, "count": 3 }
],
"tags": ["t1", "combat", "easy", "normal"],
"fixedIcon": null
}type:LOBBY|COMBAT|BOSStags: utilisés par le picker pour filtrer (tier de difficulté + nom de floor + rôle).mobSpawns[].mobId: référence à un mob MMOCore/MythicMobs existant. Le module applique le scaling tier au runtime (voir §6.4).fixedIcon:nullpourCOMBAT→ le module roll une icône au moment de la proposition de porte (Q5.3).NONEpourLOBBY(forcé).RewardIconnon-null pourBOSS(Q5.6 — fixe par boss).
class LabyrinthRun {
UUID instanceId;
String floorId; // easy | normal | hard | infinite
int currentRoomIndex; // 0 = lobby, 1..N
RoomTemplate currentRoom;
RewardIcon currentRoomIcon; // rolled for COMBAT, fixed for BOSS, NONE for LOBBY
List<RoomTemplate> routeHistory;
DoorChoice pendingChoice; // null si pas en fin de salle
Map<UUID, Integer> aliveMobsByRoom;
Set<UUID> deadPlayers; // résolus à chaque boss
DifficultyModifier currentModifier;
long seed;
Map<RewardIcon, Integer> iconCounts; // accumulé pendant la run
Set<UUID> initialPlayerUuids; // figé au démarrage (Q2 = A)
}{
"id": "uuid",
"floorId": "infinite",
"partyHash": "sha256(sorted(initialPlayerUuids))",
"playerUuids": ["uuid1", "uuid2"],
"lastBossClearedRoom": 30,
"difficultyTier": 3,
"seed": 1234567890,
"iconCounts": { "GOLD": 7 },
"checksum": "sha256(payload)",
"createdAt": "2026-04-25T12:00:00Z",
"updatedAt": "2026-04-25T12:35:00Z"
}partyHashest calculé sur la composition initiale du run (Q2 = A). Si un joueur du groupe initial ne revient jamais, la save reste inaccessible — pas de reset automatique, les joueurs créent une nouvelle partie.iconCountsest persisté : les icônes accumulées survivent aux save/resume (Q5.4).lastBossClearedRoomest toujours un multiple de 10.checksumanti-tamper recalculé à chaque update.
{
"floorId": "infinite",
"baseGold": 100,
"goldPerIcon": 0.15,
"baseItemRolls": 1,
"items": [
{ "itemId": "epic_sword", "weight": 5, "minTier": 1 },
{ "itemId": "legendary_armor", "weight": 1, "minTier": 3 }
]
}goldPerIcon: multiplicateur appliqué par icôneGOLDaccumulée.items[].minTier: utilisé en Infinite pour gater les drops par palier de difficulté.- Pool séparé par floor (Q5.2) —
easyne drop pas les items légendaires d'infinite.
CREATE TABLE IF NOT EXISTS labyrinth_rooms (
id VARCHAR(64) PRIMARY KEY,
type VARCHAR(16) NOT NULL,
payload_json MEDIUMTEXT NOT NULL,
tags VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS labyrinth_saves (
id VARCHAR(36) PRIMARY KEY,
floor_id VARCHAR(64) NOT NULL,
party_hash CHAR(64) NOT NULL,
payload_json MEDIUMTEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_party (party_hash, floor_id)
);
CREATE TABLE IF NOT EXISTS labyrinth_loot_tables (
floor_id VARCHAR(64) PRIMARY KEY,
payload_json MEDIUMTEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);labyrinth_rooms—{ _id, type, payload, tags, updated_at }labyrinth_saves—{ _id, floor_id, party_hash, payload, updated_at }+ index sur(party_hash, floor_id)labyrinth_loot_tables—{ _id (= floor_id), payload, updated_at }
Aucune action. Le module pilote sa boucle nativement. Les workflows admin écoutent les triggers ci-dessous pour greffer des effets de bord (cinematic, son, titre, message global, etc.).
| Block ID | Variables exposées au workflow |
|---|---|
labyrinth.on_room_entered |
roomIndex, roomType, rewardIcon, playerUuid |
labyrinth.on_room_cleared |
roomIndex, rewardIcon, clearTimeMs |
labyrinth.on_doors_proposed |
iconLeft, iconRight, nextIsBoss (bool) |
labyrinth.on_boss_killed |
roomIndex, tier, playersAlive |
labyrinth.on_run_ended |
success (bool), goldEarned, itemsRolled (list), iconCounts, tier |
labyrinth.on_checkpoint_saved |
roomIndex, tier |
labyrinth.on_save_invalidated |
reason (ALL_DEAD) |
| Block ID | Description |
|---|---|
labyrinth.is_boss_room |
True si currentRoomIndex % 10 == 0 (et > 0) |
labyrinth.is_infinite_floor |
True si floor courant = infinite |
labyrinth.has_resumable_save |
True si une save existe pour la compo actuelle |
| Block ID | Description |
|---|---|
labyrinth.current_tier |
Palier de difficulté actuel |
labyrinth.current_room_index |
Numéro de salle courant |
labyrinth.icon_count |
Compteur pour une icône (input dropdown : GOLD, BLESSING) |
labyrinth.gold_earned |
Or final calculé (valide uniquement dans on_run_ended) |
labyrinth.items_rolled |
Liste d'items rollés (idem) |
Couleur/catégorie suggérée :
#3F51B5("Memory Labyrinth").
Joueur lance /dungeon … memory_labyrinth infinite
→ FloorInstance créée (cœur)
→ MemoryLabyrinthModule détecte une instance de type labyrinth
→ LabyrinthRunManager.startRun(instance)
├── initialPlayerUuids = current party
├── Si floor == infinite ET has_resumable_save :
│ ResumeOrNewPrompt → leader clique (Q1 = A)
│ ├── "Reprendre" → charge save, applique tier, skip à room (lastBossClearedRoom + 1)
│ └── "Nouvelle" → invalide save existante, démarre fresh
└── TP joueurs au lobby (RoomType.LOBBY, sans mob, 1 porte)
Entrée salle X
→ fire labyrinth.on_room_entered
→ spawn mobs (mobSpawns + tier scaling) (incrémente aliveMobs[X])
→ DoorController.lock(X)
→ InstanceMobKillListener (cœur) → décrémente aliveMobs[X]
→ quand aliveMobs[X] == 0 :
fire labyrinth.on_room_cleared
→ DoorController.unlock(X)
→ RoomPicker.pickNext(currentRoomIndex)
├── Si X+1 % 10 == 0 (prochaine = boss) → 1 porte vers BOSS room
└── Sinon → 2 portes COMBAT
→ IconRoller.roll() pour chaque porte (sauf boss = fixedIcon)
→ DoorIconHologram.show() au-dessus de chaque porte
→ fire labyrinth.on_doors_proposed
→ Joueur traverse la porte choisie (RegionTrigger sur la zone porte)
→ currentRoomIndex++
→ iconCounts[chosenIcon]++
→ TP au playerSpawn de la salle suivante
Salle boss : currentRoomIndex % 10 == 0
→ spawn boss (RoomTemplate.type=BOSS, fixedIcon visible mais pas comptabilisé)
→ boss meurt → fire labyrinth.on_boss_killed
├── BossEncounterHandler.run() :
│ ├── Si deadPlayers non-vide ET mode groupe :
│ │ RevivePromptComponent dans le chat
│ │ → 1er clic d'un joueur vivant → revive ce target
│ │ → réutilise InstancePlayerDeathListener.revivePlayer()
│ │ (1 seul revive par boss)
│ ├── currentModifier.tier++
│ └── Si floor == infinite :
│ LabyrinthSaveManager.upsert(run)
│ → fire labyrinth.on_checkpoint_saved
└── Une seule porte vers la salle suivante (icône rollée si COMBAT)
Joueur meurt → InstancePlayerDeathListener (cœur) → ghost
deadPlayers.add(uuid)
Tous morts (avant boss) :
→ FloorInstance.fail()
→ fire labyrinth.on_run_ended (success=false, loot calculé sur iconCounts actuels)
→ Si floor == infinite :
LabyrinthSaveManager.delete(saveId)
→ fire labyrinth.on_save_invalidated (reason=ALL_DEAD)
Run terminée (succès finite OU mort totale OU sortie volontaire infinite)
→ LootCalculator.compute(iconCounts, tier, lootTable)
gold = baseGold × (1 + goldPerIcon × iconCounts.GOLD) × tierMultiplier
itemRolls = baseItemRolls
for r in 0..itemRolls :
pickWeightedFromLootTable(filter: minTier <= currentTier)
→ distribuer à chaque joueur (loot individuel — chacun son roll)
→ fire labyrinth.on_run_ended
variables: goldEarned, itemsRolled, iconCounts, tier, success
Au spawn d'un mob :
attach metadata { instanceId, tier }
→ setMaxHealth(base × modifier.hpMult(tier))
→ setAttackDamage(base × modifier.dmgMult(tier))
DifficultyModifier table (proposition initiale, à équilibrer) :
tier 1 : ×1.0 / ×1.0
tier 2 : ×1.3 / ×1.15
tier 3 : ×1.7 / ×1.30
tier N : ×(1 + 0.3·(N-1)) / ×(1 + 0.15·(N-1))
| Méthode | Endpoint | Rôle |
|---|---|---|
| GET | /labyrinth/rooms |
Liste des RoomTemplate |
| GET | /labyrinth/rooms/{id} |
Détail d'une salle |
| POST | /labyrinth/rooms |
Créer |
| PUT | /labyrinth/rooms/{id} |
Modifier |
| DELETE | /labyrinth/rooms/{id} |
Supprimer |
| GET | /labyrinth/loot-tables/{floorId} |
Récupérer la loot table d'un floor |
| PUT | /labyrinth/loot-tables/{floorId} |
Upsert |
| GET | /labyrinth/saves?partyHash=… |
Lister les saves (debug/admin) |
Suivre le pattern existant des endpoints
/cinematics.
- Composition de groupe :
partyHash = sha256(sorted(initialPlayerUuids)). Composition figée au démarrage de la run. - Cross-server : la save est stockée en DB partagée ; récupérable depuis n'importe quel serveur.
- Concurrence :
LabyrinthRunest en mémoire sur le serveur de l'instance ; seule laLabyrinthSaveest synchronisée via DB. - Limite de pool : prévoir au moins 8 salles
COMBATdistinctes par tier pour limiter les répétitions visibles. Picker logge un warning si pool trop maigre. - Cas dégénéré : si le pool ne contient qu'une salle d'un type requis,
RoomPickerpeut proposer la même salle des deux côtés (warning, fallback acceptable). - TTL save Infinite : 30 jours d'inactivité → purge automatique en arrière-plan.
- Anti-cheat checkpoint :
LabyrinthSave.checksum = sha256(payload sans le champ checksum). Si modifiée à la main, refus de reprise. - Save bloquée : si un joueur du groupe initial ne revient jamais, la save reste inaccessible. Pas de reset admin — les joueurs créent une nouvelle partie. Comportement assumé.
- Loot vs déconnexion : si un joueur déconnecté n'est plus en ligne au moment du
on_run_ended, son loot est mis en pending sur sonProfileDataet délivré au prochain login.
| Phase | Lot | Contenu |
|---|---|---|
| P0 | Squelette module | pom.xml parent, MemoryLabyrinthModule.onEnable, scaffolding packages |
| P1 | Modèles + DB | RoomTemplate, LabyrinthSave, LootTable, DAO MySQL & Mongo, migrations |
| P2 | Pool & RoomPicker | Registry chargé au boot, picker avec contraintes (lobby/boss/combat) |
| P3 | Runtime de salle | LabyrinthRunManager, spawn mobs (avec scaling), tracking aliveMobs |
| P4 | Portes & navigation | DoorController (lock/unlock), IconRoller, DoorIconHologram |
| P5 | Boss & revive UI | BossEncounterHandler, RevivePromptComponent chat-clickable |
| P6 | Save Infinite | LabyrinthSaveManager, partyHash, checksum, ResumeOrNewPrompt |
| P7 | Difficulté infinie | DifficultyModifier (HP/dmg scaling par tier) appliqué au spawn |
| P8 | Loot & end-of-run | LootCalculator, distribution individuelle, pending offline |
| P9 | Hooks Blockly | Tous les triggers/conditions/values exposés au panel (Option C) |
| P10 | Endpoints panel | CRUD RoomTemplate + LootTable + listing saves |
| P11 | QA & polish | Tests bout-en-bout solo + groupe, doc utilisateur, logs admin |
- Icône
BLESSING: enum réservée mais non implémentée en v1. - Génération de salles à la volée (paste de schématiques runtime). À évaluer en v2 via
module-worldedit. - Mode PvP dans le labyrinthe.
- Marketplace de pools (partage de
RoomTemplateentre serveurs). - Skill tree / méta-progression (mirror of night à la Hades) — possible plus tard via un autre module abonné à
on_boss_killed/on_run_ended. - Reset admin de save — pas de besoin identifié, on s'en passe.
| # | Question | Décision |
|---|---|---|
| Q1 | Lobby Infinite : qui décide reprendre vs nouvelle ? | Leader de la party |
| Q2 | partyHash basé sur quelle composition ? |
Composition au démarrage (figée) |
| Q3 | Pool de RoomTemplate par floor ou partagé ? |
Pool unique + tags (tier, floor, combat/boss/lobby) |
| Q4 | Mobs : Mythic/MMO ref vs descriptor ? | Hybride : ref mobId + scaling auto via DifficultyModifier |
| Q5 | Récompenses : Blockly vs primitive ? | Système d'icônes natif : roll par salle, accumulation, RNG sur LootTable en fin de run, distribution individuelle |
| Q5.1 | Icônes disponibles | GOLD (v1), BLESSING (réservée, non implémentée) |
| Q5.2 | Pool d'items | Un par floor |
| Q5.3 | Icône d'une salle COMBAT | Tirée aléatoirement au moment de la proposition de porte |
| Q5.4 | Loot pendant la run | Non — calculé et distribué uniquement en fin de run ; iconCounts persistés dans la save |
| Q5.5 | Icône lobby | NONE |
| Q5.6 | Icône boss | Fixe, définie dans la RoomTemplate du boss |
claude --resume 27d06130-53ae-4ded-a081-4031b8a7ee54