import React from "react"; import { sampleContentPack } from "@/data/sampleContentPack"; import { createStartingAdventurer } from "@/rules/character"; import { deleteSavedRun, getBrowserStorage, listSavedRuns, loadSavedRun, saveRun, type SavedRunSummary, } from "@/rules/persistence"; import { createRunState, enterCurrentRoom, getAvailableMoves, isCurrentRoomCombatReady, resolveRunEnemyTurn, resolveRunPlayerTurn, resumeDungeon, returnToTown, startCombatInCurrentRoom, travelCurrentExit, } from "@/rules/runState"; import { getNextLevelXpThreshold, MAX_ADVENTURER_LEVEL } from "@/rules/progression"; import { getConsumableCounts, restWithRation, usePotion, useScroll, } from "@/rules/recovery"; import { grantDebugTreasure, queueTreasureForSale, sellPendingTreasure, stashCarriedTreasure, withdrawStashedTreasure, } from "@/rules/townInventory"; import { useTownService } from "@/rules/townServices"; 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 getTownServiceDescription(serviceId: string) { switch (serviceId) { case "service.healer": return "Restore HP to full for 2 gold."; case "service.market": return "Buy 1 ration for 1 gold."; case "service.tavern": return "Recover 2 HP for 1 gold."; default: return "Visit this service."; } } function getItemName(definitionId: string) { return sampleContentPack.items.find((item) => item.id === definitionId)?.name ?? definitionId; } function getItemValue(definitionId: string) { return sampleContentPack.items.find((item) => item.id === definitionId)?.valueGp ?? 0; } function getManoeuvreName(manoeuvreId: string) { return sampleContentPack.manoeuvres.find((entry) => entry.id === manoeuvreId)?.name ?? manoeuvreId; } function getCombatTargetNumber(enemyArmourValue = 0) { return 7 + enemyArmourValue; } function App() { const [run, setRun] = React.useState(() => createDemoRun()); const [savedRuns, setSavedRuns] = React.useState([]); 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 inTown = run.phase === "town"; const knownServices = sampleContentPack.townServices.filter((service) => run.townState.knownServices.includes(service.id), ); const carriedTreasure = run.adventurerSnapshot.inventory.carried.filter((entry) => sampleContentPack.items.find((item) => item.id === entry.definitionId)?.itemType === "treasure", ); const stashedTreasure = run.townState.stash; const pendingSales = run.townState.pendingSales; const pendingSaleValue = pendingSales.reduce( (total, entry) => total + getItemValue(entry.definitionId) * entry.quantity, 0, ); const consumableCounts = getConsumableCounts(run); const latestCombatLogs = run.activeCombat?.combatLog.slice(-3).reverse() ?? []; const nextLevelXpThreshold = run.adventurerSnapshot.level >= MAX_ADVENTURER_LEVEL ? undefined : getNextLevelXpThreshold(run.adventurerSnapshot.level); const xpToNextLevel = nextLevelXpThreshold === undefined ? 0 : Math.max(0, nextLevelXpThreshold - run.adventurerSnapshot.xp); React.useEffect(() => { const storage = getBrowserStorage(); if (!storage) { return; } setSavedRuns(listSavedRuns(storage)); }, []); const handleReset = () => { setRun(createDemoRun()); }; const refreshSavedRuns = React.useCallback(() => { const storage = getBrowserStorage(); if (!storage) { return; } setSavedRuns(listSavedRuns(storage)); }, []); 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 handleReturnToTown = () => { setRun((previous) => returnToTown(previous).run); }; const handleResumeDungeon = () => { setRun((previous) => resumeDungeon(previous).run); }; const handleUseTownService = (serviceId: string) => { setRun((previous) => useTownService({ content: sampleContentPack, run: previous, serviceId, }).run, ); }; const handleGrantTreasure = (definitionId: string) => { setRun((previous) => grantDebugTreasure({ content: sampleContentPack, run: previous, definitionId, }).run, ); }; const handleStashTreasure = (definitionId: string) => { setRun((previous) => stashCarriedTreasure({ content: sampleContentPack, run: previous, definitionId, }).run, ); }; const handleWithdrawTreasure = (definitionId: string) => { setRun((previous) => withdrawStashedTreasure({ content: sampleContentPack, run: previous, definitionId, }).run, ); }; const handleQueueTreasure = (definitionId: string, source: "carried" | "stash") => { setRun((previous) => queueTreasureForSale({ content: sampleContentPack, run: previous, definitionId, source, }).run, ); }; const handleSellPending = () => { setRun((previous) => sellPendingTreasure({ content: sampleContentPack, run: previous, }).run, ); }; const handleUsePotion = () => { setRun((previous) => usePotion({ content: sampleContentPack, run: previous, definitionId: "potion.healing", }).run, ); }; const handleUseScroll = () => { setRun((previous) => useScroll({ content: sampleContentPack, run: previous, definitionId: "scroll.lesser-heal", roller: () => 4, }).run, ); }; const handleRationRest = () => { setRun((previous) => restWithRation({ content: sampleContentPack, run: previous, definitionId: "item.ration", }).run, ); }; const handleSaveRun = () => { const storage = getBrowserStorage(); if (!storage) { return; } saveRun(storage, run); refreshSavedRuns(); }; const handleLoadRun = (saveId: string) => { const storage = getBrowserStorage(); if (!storage) { return; } setRun(loadSavedRun(storage, saveId)); refreshSavedRuns(); }; const handleDeleteSave = (saveId: string) => { const storage = getBrowserStorage(); if (!storage) { return; } setSavedRuns(deleteSavedRun(storage, saveId)); }; 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 Phase {run.phase}
{combatReadyEncounter && !run.activeCombat && !inTown ? (
Encounter Ready {currentRoom?.encounter?.resultLabel}

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

) : null} {run.lastCombatOutcome ? (
{run.lastCombatOutcome.result === "victory" ? "Last Victory" : "Last Defeat"} {run.lastCombatOutcome.summary}

{run.lastCombatOutcome.enemyNames.length > 0 ? `Opposition: ${run.lastCombatOutcome.enemyNames.join(", ")}.` : "No enemy details recorded."}

) : null} {run.lastLevelUp ? (
Latest Level Up {run.lastLevelUp.summary}

{run.lastLevelUp.unlockedManoeuvreIds.length > 0 ? `Unlocked: ${run.lastLevelUp.unlockedManoeuvreIds.map(getManoeuvreName).join(", ")}.` : "No additional manoeuvres unlocked on that level."}

) : 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}
Next Level {nextLevelXpThreshold === undefined ? "Max" : `${xpToNextLevel} to go`}

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

Run rewards: {run.xpGained} XP earned, {run.defeatedCreatureIds.length} foes defeated.

{nextLevelXpThreshold === undefined ? `${run.adventurerSnapshot.name} has reached the current maximum supported level.` : `Next level at ${nextLevelXpThreshold} XP. Current manoeuvres: ${run.adventurerSnapshot.manoeuvreIds.map(getManoeuvreName).join(", ")}.`}

{inTown ? `The party is currently in town${run.lastTownAt ? ` as of ${new Date(run.lastTownAt).toLocaleString()}` : ""}.` : "The party is still delving below ground."}

Recovery Kit

{inTown ? "Town-ready" : "Field-ready"}
Potion Healing Potion

Restore 3 HP. Carried: {consumableCounts.healingPotion}

Scroll Lesser Heal

Restore 2 HP on a successful cast. Carried: {consumableCounts.lesserHealScroll}

Ration Town Rest

Spend 1 ration in town to recover 2 HP. Carried: {consumableCounts.ration}

Save Archive

{savedRuns.length} saves
{savedRuns.length === 0 ? (

No saved runs yet. Save the current run to persist progress.

) : (
{savedRuns.map((save) => (
{save.phase} {save.label}

Saved {new Date(save.savedAt).toLocaleString()} · Level {save.currentLevel}

))}
)}
{inTown ? (

Town Hub

Between Delves

Safe Harbor

You are out of the dungeon. Review the current expedition, catch your breath, and then resume the delve from the same level when you are ready.

Current Gold {run.adventurerSnapshot.inventory.currency.gold}
Rooms Found {currentLevel?.discoveredRoomOrder.length ?? 0}
Foes Defeated {run.defeatedCreatureIds.length}
{knownServices.map((service) => (
{service.serviceType} {service.name}

{getTownServiceDescription(service.id)}

))}

Treasure Ledger

{pendingSaleValue} gp pending

Pack Treasure

{carriedTreasure.length === 0 ? (

No treasure currently in the pack.

) : ( carriedTreasure.map((entry) => (
{getItemName(entry.definitionId)}

Qty {entry.quantity} · {getItemValue(entry.definitionId)} gp each

)) )}

Town Stash

{stashedTreasure.length === 0 ? (

No treasure stored in town.

) : ( stashedTreasure.map((entry) => (
{getItemName(entry.definitionId)}

Qty {entry.quantity} · {getItemValue(entry.definitionId)} gp each

)) )}

Queued Sales

{pendingSales.length === 0 ? (

Nothing is queued for sale yet.

) : ( pendingSales.map((entry) => (
{getItemName(entry.definitionId)}

Qty {entry.quantity} · {getItemValue(entry.definitionId) * entry.quantity} gp total

)) )}
) : ( <>

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}
Player HP {run.activeCombat.player.hpCurrent}/{run.activeCombat.player.hpMax}
Enemies Standing {run.activeCombat.enemies.filter((enemy) => enemy.hpCurrent > 0).length}
Last Roll {run.activeCombat.lastRoll?.total ?? "-"}
{run.activeCombat.enemies.map((enemy) => (
{enemy.name} HP {enemy.hpCurrent}/{enemy.hpMax} Target {getCombatTargetNumber(enemy.armourValue ?? 0)}
))}
{latestCombatLogs.map((entry) => (
{entry.type}

{entry.text}

))}
) : (

{run.lastCombatOutcome ? "No active combat. Review the last battle above, then continue the delve or recover in town." : "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;