import React from "react"; import { sampleContentPack } from "@/data/sampleContentPack"; import { createStartingAdventurer } from "@/rules/character"; import { createRunState, enterCurrentRoom, getAvailableMoves, isCurrentRoomCombatReady, resolveRunEnemyTurn, resolveRunPlayerTurn, startCombatInCurrentRoom, travelCurrentExit, } from "@/rules/runState"; import { queueTreasureForSale, sellPendingTreasure, sendTreasureToStash } from "@/rules/town"; import type { RunState } from "@/types/state"; function createDemoRun() { const adventurer = createStartingAdventurer(sampleContentPack, { name: "Aster", weaponId: "weapon.short-sword", armourId: "armour.leather-vest", scrollId: "scroll.lesser-heal", }); return createRunState({ content: sampleContentPack, campaignId: "campaign.demo", adventurer, }); } function getRoomTitle(run: RunState, roomId?: string) { if (!roomId) { return "Unknown Room"; } const room = run.dungeon.levels[run.currentLevel]?.rooms[roomId]; if (!room) { return "Unknown Room"; } return ( sampleContentPack.roomTemplates.find((template) => template.id === room.templateId)?.title ?? room.notes[0] ?? room.templateId ?? room.id ); } function getDefinitionName(definitionId: string) { const item = sampleContentPack.items.find((candidate) => candidate.id === definitionId) ?? sampleContentPack.weapons.find((candidate) => candidate.id === definitionId) ?? sampleContentPack.armour.find((candidate) => candidate.id === definitionId) ?? sampleContentPack.scrolls.find((candidate) => candidate.id === definitionId) ?? sampleContentPack.potions.find((candidate) => candidate.id === definitionId); return item?.name ?? definitionId; } function formatInventoryEntry(definitionId: string, quantity: number) { const name = getDefinitionName(definitionId); return quantity > 1 ? `${quantity}x ${name}` : name; } function getTreasureItemIds() { return new Set( sampleContentPack.items .filter((item) => item.itemType === "treasure") .map((item) => item.id), ); } function getSupportItemIds() { return new Set( sampleContentPack.items .filter((item) => item.itemType !== "treasure") .map((item) => item.id), ); } function getConsumableItemIds() { return new Set( sampleContentPack.items .filter((item) => item.consumable || item.itemType === "ration") .map((item) => item.id), ); } const treasureItemIds = getTreasureItemIds(); const supportItemIds = getSupportItemIds(); const consumableItemIds = getConsumableItemIds(); function App() { const [run, setRun] = React.useState(() => createDemoRun()); const currentLevel = run.dungeon.levels[run.currentLevel]; const currentRoom = run.currentRoomId ? currentLevel?.rooms[run.currentRoomId] : undefined; const availableMoves = getAvailableMoves(run); const combatReadyEncounter = isCurrentRoomCombatReady(run); const carriedTreasure = run.adventurerSnapshot.inventory.carried.filter((entry) => treasureItemIds.has(entry.definitionId), ); const carriedConsumables = run.adventurerSnapshot.inventory.carried.filter( (entry) => consumableItemIds.has(entry.definitionId) || entry.definitionId.startsWith("potion.") || entry.definitionId.startsWith("scroll."), ); const carriedGear = run.adventurerSnapshot.inventory.carried.filter( (entry) => supportItemIds.has(entry.definitionId) && !consumableItemIds.has(entry.definitionId), ); const equippedItems = run.adventurerSnapshot.inventory.equipped; const latestLoot = run.lootedItems.slice(-4).reverse(); const pendingSales = run.townState.pendingSales; const stash = run.townState.stash; const handleReset = () => { setRun(createDemoRun()); }; const handleEnterRoom = () => { setRun((previous) => enterCurrentRoom({ content: sampleContentPack, run: previous }).run); }; const handleStartCombat = () => { setRun((previous) => startCombatInCurrentRoom({ content: sampleContentPack, run: previous }).run, ); }; const handleTravel = (direction: "north" | "east" | "south" | "west") => { setRun((previous) => travelCurrentExit({ content: sampleContentPack, run: previous, exitDirection: direction, }).run, ); }; const handlePlayerTurn = (manoeuvreId: string, targetEnemyId: string) => { setRun((previous) => resolveRunPlayerTurn({ content: sampleContentPack, run: previous, manoeuvreId, targetEnemyId, }).run, ); }; const handleEnemyTurn = () => { setRun((previous) => resolveRunEnemyTurn({ content: sampleContentPack, run: previous }).run, ); }; const handleQueueSale = (definitionId: string) => { setRun((previous) => queueTreasureForSale(sampleContentPack, previous, definitionId).run); }; const handleStashTreasure = (definitionId: string) => { setRun((previous) => sendTreasureToStash(sampleContentPack, previous, definitionId).run); }; const handleSellPending = () => { setRun((previous) => sellPendingTreasure(sampleContentPack, previous).run); }; return (

2D6 Dungeon Web

Dungeon Loop Shell

Traverse generated rooms, auto-resolve room entry, and engage combat when a room reveals a real encounter.

Run Status {run.status}
{combatReadyEncounter && !run.activeCombat ? (
Encounter Ready {currentRoom?.encounter?.resultLabel}

This room contains a combat-ready encounter. Engage now to enter tactical resolution.

) : null}

Adventurer

Level {run.adventurerSnapshot.level}
HP {run.adventurerSnapshot.hp.current}/{run.adventurerSnapshot.hp.max}
Shift {run.adventurerSnapshot.stats.shift}
Discipline {run.adventurerSnapshot.stats.discipline}
Precision {run.adventurerSnapshot.stats.precision}
XP {run.adventurerSnapshot.xp}

{run.adventurerSnapshot.name} is equipped with a{" "} {sampleContentPack.weapons.find( (weapon) => weapon.id === run.adventurerSnapshot.weaponId, )?.name ?? "weapon"} .

Run rewards: {run.xpGained} XP, {run.goldGained} gold,{" "} {run.defeatedCreatureIds.length} foes defeated.

Inventory

{run.adventurerSnapshot.inventory.carried.length} carried entries
Gold {run.adventurerSnapshot.inventory.currency.gold}
Rations {run.adventurerSnapshot.inventory.rationCount}
Treasure {carriedTreasure.length}
Latest Loot {run.lootedItems.reduce((total, entry) => total + entry.quantity, 0)}
Equipped
{equippedItems.map((entry) => (
{formatInventoryEntry(entry.definitionId, entry.quantity)} Ready for use
))}
Consumables
{carriedConsumables.length === 0 ? (

No consumables carried.

) : ( carriedConsumables.map((entry) => (
{formatInventoryEntry(entry.definitionId, entry.quantity)} Combat or run utility
)) )}
Pack Gear
{carriedGear.length === 0 ? (

No general gear carried.

) : ( carriedGear.map((entry) => (
{formatInventoryEntry(entry.definitionId, entry.quantity)} Travel and exploration kit
)) )}
Treasure Stash
{carriedTreasure.length === 0 ? (

No treasure recovered yet.

) : ( carriedTreasure.map((entry) => (
{formatInventoryEntry(entry.definitionId, entry.quantity)} Sellable dungeon spoils
)) )}
Recent Spoils {latestLoot.length === 0 ? (

Win a fight to populate the loot ribbon.

) : (
{latestLoot.map((entry) => (
{formatInventoryEntry(entry.definitionId, entry.quantity)}
))}
)}

Town Market

{run.townState.visits} town actions
Known Services {run.townState.knownServices.length}
Queued Sales {pendingSales.length}
Stash {stash.length}
Treasure In Pack
{carriedTreasure.length === 0 ? (

No treasure available for town actions.

) : ( carriedTreasure.map((entry) => (
{formatInventoryEntry(entry.definitionId, entry.quantity)} Choose whether to sell or stash this treasure.
)) )}
Pending Sales
{pendingSales.length === 0 ? (

Nothing queued at the market.

) : ( pendingSales.map((entry) => (
{formatInventoryEntry(entry.definitionId, entry.quantity)} Ready to convert into gold
)) )}
Town Stash
{stash.length === 0 ? (

The stash is empty.

) : ( stash.map((entry) => (
{formatInventoryEntry(entry.definitionId, entry.quantity)} Held safely in town storage
)) )}

Current Room

Level {run.currentLevel}

{getRoomTitle(run, run.currentRoomId)}

{currentRoom?.notes[1] ?? "No encounter notes available yet."}

Entered: {currentRoom?.discovery.entered ? "Yes" : "No"} Cleared: {currentRoom?.discovery.cleared ? "Yes" : "No"} Exits: {currentRoom?.exits.length ?? 0}
Encounter {currentRoom?.encounter?.resultLabel ?? "None"}

Navigation

{availableMoves.length} exits
{availableMoves.map((move) => ( ))}
{currentLevel?.discoveredRoomOrder.map((roomId) => { const room = currentLevel.rooms[roomId]; const active = roomId === run.currentRoomId; return (
{room.position.x},{room.position.y} {getRoomTitle(run, roomId)}
); })}

Combat

{run.activeCombat ? `Round ${run.activeCombat.round}` : "Inactive"}
{run.activeCombat ? ( <>
Acting Side {run.activeCombat.actingSide}
{run.activeCombat.enemies.map((enemy) => (
{enemy.name} HP {enemy.hpCurrent}/{enemy.hpMax}
))}
) : (

No active combat. Travel until a room reveals a hostile encounter, then engage it from the banner or room panel.

)}

Run Log

{run.log.length} entries
{run.log.length === 0 ? (

No events recorded yet.

) : ( run.log .slice() .reverse() .map((entry) => (
{entry.type}

{entry.text}

)) )}
); } export default App;