Files
2D6-Dungeon/Planning/DATA_MODEL.md
2026-03-15 09:41:22 -05:00

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

  1. Keep static content separate from mutable game state.
  2. Prefer normalized content with stable IDs over embedded freeform blobs.
  3. Preserve raw dice values when rules depend on primary vs secondary die.
  4. Make table references explicit so missing codex data can be validated.
  5. Represent rules outcomes as structured effects where possible.
  6. 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.sword
  • manoeuvre.exact-strike
  • armour.leather-cuirass
  • scroll.healing-word
  • table.l1sr
  • creature.level1.giant-rat
  • room.level1.001
  • run.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:

  • WeaponDefinition
  • ManoeuvreDefinition
  • ArmourDefinition
  • PotionDefinition
  • ScrollDefinition
  • CreatureDefinition
  • TableDefinition
  • CampaignState
  • RunState
  • RoomState
  • CombatState
  • RollResult

The following can start simplified:

  • QuestState
  • TownServiceDefinition
  • RoomObjectState
  • favour tracking
  • optional table effects

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:

  1. Every referenced content ID must exist.
  2. Every table code used by rules must be present in the content pack.
  3. Every room exit must be reciprocal after generation.
  4. HP values must never exceed max unless a rule explicitly allows it.
  5. Inventory quantities must never drop below zero.
  6. Level-specific tables must match their dungeon level.
  7. 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