14 KiB
2D6 Dungeon Data Model
Purpose
This document defines the data structures for a web-based version of 2D6 Dungeon.
It translates the rules and content inventory into implementation-ready models that can be mirrored in:
- TypeScript interfaces
- Zod schemas
- JSON content files
- application state stores
The main design rule is:
- Core Rules become behavior
- Tables Codex becomes data
Modeling Principles
- Keep static content separate from mutable game state.
- Prefer normalized content with stable IDs over embedded freeform blobs.
- Preserve raw dice values when rules depend on primary vs secondary die.
- Make table references explicit so missing codex data can be validated.
- Represent rules outcomes as structured effects where possible.
- Keep room generation state deterministic enough to save, reload, and replay.
Data Layers
1. Static Content
Read-only data encoded from the books.
Examples:
- weapons
- manoeuvres
- armour
- potions
- scrolls
- room tables
- loot tables
- creature cards
- town service tables
2. Campaign State
Long-lived player progress across multiple delves.
Examples:
- adventurer progression
- unlocked levels
- stored treasure
- completed side quests
- campaign log
3. Run State
The current dungeon expedition.
Examples:
- current level
- generated rooms
- current room
- active combat
- carried inventory
- temporary effects
4. Rules Action/Result Layer
Transient structures used by the engine.
Examples:
- roll results
- table lookups
- combat actions
- generated room results
- effect application results
ID Conventions
Use stable string IDs everywhere.
Recommended formats:
weapon.swordmanoeuvre.exact-strikearmour.leather-cuirassscroll.healing-wordtable.l1srcreature.level1.giant-ratroom.level1.001run.current
Benefits:
- easier JSON authoring
- easier save/load compatibility
- simpler references across content files
Static Content Models
ContentPack
Top-level grouping for encoded codex content.
type ContentPack = {
version: string;
sourceBooks: string[];
tables: TableDefinition[];
weapons: WeaponDefinition[];
manoeuvres: ManoeuvreDefinition[];
armour: ArmourDefinition[];
items: ItemDefinition[];
potions: PotionDefinition[];
scrolls: ScrollDefinition[];
creatures: CreatureDefinition[];
roomTemplates: RoomTemplate[];
townServices: TownServiceDefinition[];
};
TableDefinition
Generic representation for codex tables.
type DiceKind = "d3" | "d6" | "2d6" | "d66";
type TableDefinition = {
id: string;
code: string;
name: string;
category:
| "generic"
| "random-list"
| "loot"
| "level"
| "optional"
| "town"
| "room";
level?: number;
page: number;
diceKind: DiceKind;
usesModifiedRangesRule?: boolean;
entries: TableEntry[];
notes?: string[];
mvp: boolean;
};
type TableEntry = {
key: string;
min?: number;
max?: number;
exact?: number;
d66?: number;
label: string;
text?: string;
effects?: RuleEffect[];
references?: ContentReference[];
};
WeaponDefinition
type WeaponDefinition = {
id: string;
name: string;
category: "melee" | "ranged";
handedness: "one-handed" | "two-handed";
baseDamage: number;
allowedManoeuvreIds: string[];
tags: string[];
startingOption: boolean;
};
ManoeuvreDefinition
type ManoeuvreDefinition = {
id: string;
name: string;
weaponCategories: string[];
shiftCost?: number;
disciplineModifier?: number;
precisionModifier?: number;
damageModifier?: number;
exactStrikeBonus?: boolean;
interruptRule?: string;
effectText?: string;
mvp: boolean;
};
ArmourDefinition
type ArmourDefinition = {
id: string;
name: string;
armourValue: number;
penalties?: {
shift?: number;
discipline?: number;
precision?: number;
};
deflectionRule?: string;
startingOption: boolean;
valueGp?: number;
mvp: boolean;
};
ItemDefinition
Base model for loot and utility items.
type ItemDefinition = {
id: string;
name: string;
itemType:
| "gear"
| "treasure"
| "quest"
| "herb"
| "rune"
| "misc"
| "ration"
| "light-source";
stackable: boolean;
consumable: boolean;
valueGp?: number;
weight?: number;
rulesText?: string;
effects?: RuleEffect[];
mvp: boolean;
};
PotionDefinition
type PotionDefinition = {
id: string;
name: string;
tableSource: string;
useTiming: "combat" | "exploration" | "town" | "any";
effects: RuleEffect[];
valueGp?: number;
mvp: boolean;
};
ScrollDefinition
type ScrollDefinition = {
id: string;
name: string;
tableSource: string;
castCheck?: {
diceKind: "d6" | "2d6";
successMin?: number;
successMax?: number;
};
onSuccess: RuleEffect[];
onFailureTableCode?: string;
valueGp?: number;
startingOption: boolean;
mvp: boolean;
};
CreatureDefinition
type CreatureDefinition = {
id: string;
name: string;
level: number;
category: string;
hp: number;
attackProfile: {
discipline: number;
precision: number;
damage: number;
numberAppearing?: string;
};
defenceProfile?: {
armour?: number;
specialRules?: string[];
};
xpReward?: number;
lootTableCodes?: string[];
interruptRules?: string[];
traits?: string[];
sourcePage: number;
mvp: boolean;
};
RoomTemplate
Structured output for room generation tables.
type RoomTemplate = {
id: string;
level: number;
roomClass: "normal" | "small" | "large" | "special" | "start" | "stairs";
tableCode: string;
tableEntryKey: string;
title: string;
text?: string;
dimensions?: {
width: number;
height: number;
};
exits?: ExitTemplate[];
encounterRefs?: ContentReference[];
objectRefs?: ContentReference[];
tags: string[];
mvp: boolean;
};
type ExitTemplate = {
direction?: "north" | "east" | "south" | "west";
exitType: "open" | "door" | "locked" | "secret" | "shaft" | "stairs";
destinationLevel?: number;
};
TownServiceDefinition
type TownServiceDefinition = {
id: string;
name: string;
serviceType: "market" | "temple" | "tavern" | "healer" | "smith" | "quest";
tableCodes?: string[];
costRules?: string[];
effects?: RuleEffect[];
mvp: boolean;
};
Shared Supporting Models
ContentReference
type ContentReference = {
type:
| "table"
| "weapon"
| "manoeuvre"
| "armour"
| "item"
| "potion"
| "scroll"
| "creature"
| "room"
| "service";
id: string;
};
RuleEffect
Structured effects should be preferred over plain text whenever the outcome is mechanical.
type RuleEffect = {
type:
| "gain-xp"
| "gain-gold"
| "heal"
| "take-damage"
| "modify-shift"
| "modify-discipline"
| "modify-precision"
| "apply-status"
| "remove-status"
| "add-item"
| "remove-item"
| "start-combat"
| "reveal-exit"
| "move-level"
| "log-only";
amount?: number;
statusId?: string;
target?: "self" | "enemy" | "room" | "campaign";
referenceId?: string;
notes?: string;
};
Campaign State Models
CampaignState
type CampaignState = {
id: string;
createdAt: string;
updatedAt: string;
rulesVersion: string;
contentVersion: string;
adventurer: AdventurerState;
unlockedLevels: number[];
completedLevels: number[];
townState: TownState;
questState: QuestState[];
campaignFlags: string[];
runHistory: RunSummary[];
};
AdventurerState
type AdventurerState = {
id: string;
name: string;
level: number;
xp: number;
hp: {
current: number;
max: number;
base: number;
};
stats: {
shift: number;
discipline: number;
precision: number;
};
weaponId: string;
manoeuvreIds: string[];
armourId?: string;
favour: Record<string, number>;
statuses: StatusInstance[];
inventory: InventoryState;
progressionFlags: string[];
};
InventoryState
type InventoryState = {
carried: InventoryEntry[];
equipped: InventoryEntry[];
stored: InventoryEntry[];
currency: {
gold: number;
};
rationCount: number;
lightSources: InventoryEntry[];
};
type InventoryEntry = {
definitionId: string;
quantity: number;
identified?: boolean;
charges?: number;
notes?: string;
};
TownState
type TownState = {
visits: number;
knownServices: string[];
stash: InventoryEntry[];
pendingSales: InventoryEntry[];
serviceFlags: string[];
};
QuestState
type QuestState = {
id: string;
title: string;
status: "available" | "active" | "completed" | "failed";
progressFlags: string[];
rewardText?: string;
};
RunSummary
type RunSummary = {
runId: string;
startedAt: string;
endedAt?: string;
deepestLevel: number;
roomsVisited: number;
creaturesDefeated: string[];
xpGained: number;
treasureValue: number;
outcome: "escaped" | "defeated" | "saved-in-progress";
};
Run State Models
RunState
type RunState = {
id: string;
campaignId: string;
status: "active" | "paused" | "completed" | "failed";
startedAt: string;
currentLevel: number;
currentRoomId?: string;
dungeon: DungeonState;
adventurerSnapshot: AdventurerState;
activeCombat?: CombatState;
log: LogEntry[];
pendingEffects: RuleEffect[];
};
DungeonState
type DungeonState = {
levels: Record<number, DungeonLevelState>;
revealedPercentByLevel: Record<number, number>;
globalFlags: string[];
};
DungeonLevelState
type DungeonLevelState = {
level: number;
themeName?: string;
rooms: Record<string, RoomState>;
discoveredRoomOrder: string[];
stairsUpRoomId?: string;
stairsDownRoomId?: string;
secretDoorUsed?: boolean;
exhaustedExitSearch?: boolean;
};
RoomState
type RoomState = {
id: string;
level: number;
templateId?: string;
position: {
x: number;
y: number;
};
dimensions: {
width: number;
height: number;
};
roomClass: "normal" | "small" | "large" | "special" | "start" | "stairs";
exits: RoomExitState[];
discovery: {
generated: boolean;
entered: boolean;
cleared: boolean;
searched: boolean;
};
encounter?: EncounterState;
objects: RoomObjectState[];
notes: string[];
flags: string[];
};
RoomExitState
type RoomExitState = {
id: string;
direction: "north" | "east" | "south" | "west";
exitType: "open" | "door" | "locked" | "secret" | "shaft" | "stairs";
discovered: boolean;
traversable: boolean;
leadsToRoomId?: string;
destinationLevel?: number;
};
EncounterState
type EncounterState = {
id: string;
sourceTableCode?: string;
creatureIds: string[];
resolved: boolean;
surprise?: boolean;
rewardPending?: boolean;
};
RoomObjectState
type RoomObjectState = {
id: string;
objectType: "container" | "altar" | "corpse" | "hazard" | "feature" | "quest";
sourceTableCode?: string;
interacted: boolean;
hidden?: boolean;
effects?: RuleEffect[];
notes?: string;
};
Combat State Models
CombatState
type CombatState = {
id: string;
round: number;
actingSide: "player" | "enemy";
fatigueDie?: number;
player: CombatantState;
enemies: CombatantState[];
selectedManoeuvreId?: string;
lastRoll?: RollResult;
pendingInterrupt?: InterruptState;
combatLog: LogEntry[];
};
CombatantState
type CombatantState = {
id: string;
name: string;
sourceDefinitionId?: string;
hpCurrent: number;
hpMax: number;
shift: number;
discipline: number;
precision: number;
armourValue?: number;
statuses: StatusInstance[];
traits: string[];
};
InterruptState
type InterruptState = {
source: "player" | "enemy";
trigger: string;
effectText: string;
resolved: boolean;
};
Rules Action/Result Models
RollResult
type RollResult = {
diceKind: DiceKind;
rolls: number[];
primary?: number;
secondary?: number;
total?: number;
modifier?: number;
modifiedTotal?: number;
clamped?: boolean;
};
TableLookupResult
type TableLookupResult = {
tableId: string;
entryKey: string;
roll: RollResult;
entry: TableEntry;
};
RoomGenerationResult
type RoomGenerationResult = {
room: RoomState;
consumedLookups: TableLookupResult[];
createdConnections: string[];
warnings: string[];
};
ActionResolution
type ActionResolution = {
success: boolean;
effects: RuleEffect[];
logEntries: LogEntry[];
warnings?: string[];
};
StatusInstance
type StatusInstance = {
id: string;
source?: string;
duration?: "round" | "combat" | "room" | "run" | "permanent";
value?: number;
notes?: string;
};
LogEntry
type LogEntry = {
id: string;
at: string;
type:
| "system"
| "roll"
| "combat"
| "loot"
| "room"
| "town"
| "progression";
text: string;
relatedIds?: string[];
};
MVP Priority Models
For the first playable version, fully define these first:
WeaponDefinitionManoeuvreDefinitionArmourDefinitionPotionDefinitionScrollDefinitionCreatureDefinitionTableDefinitionCampaignStateRunStateRoomStateCombatStateRollResult
The following can start simplified:
QuestStateTownServiceDefinitionRoomObjectState- favour tracking
- optional table effects
Recommended File Layout
Suggested future code structure:
src/
data/
content-pack.ts
tables/
creatures/
rooms/
items/
rules/
dice/
tables/
dungeon/
combat/
progression/
state/
campaign/
run/
combat/
types/
content.ts
state.ts
rules.ts
Validation Rules
Add schema validation for these cases early:
- Every referenced content ID must exist.
- Every table code used by rules must be present in the content pack.
- Every room exit must be reciprocal after generation.
- HP values must never exceed max unless a rule explicitly allows it.
- Inventory quantities must never drop below zero.
- Level-specific tables must match their dungeon level.
- Creature source pages and table codes should remain traceable to the books.
Immediate Next Build Step
After this document, the most useful implementation artifact is:
IMPLEMENTATION_NOTES.md
That file should capture the digital-only rulings the books leave ambiguous, especially:
- map generation heuristics
- secret door fallback behavior
- room geometry/rendering rules
- handling of subjective or flavor-heavy outcomes
- what gets automated versus explicitly player-confirmed