feature: Initial commit

This commit is contained in:
Keith Solomon
2026-03-15 09:41:22 -05:00
commit 95c629a42e
8 changed files with 2112 additions and 0 deletions

829
Planning/DATA_MODEL.md Normal file
View File

@@ -0,0 +1,829 @@
# 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.
```ts
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.
```ts
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
```ts
type WeaponDefinition = {
id: string;
name: string;
category: "melee" | "ranged";
handedness: "one-handed" | "two-handed";
baseDamage: number;
allowedManoeuvreIds: string[];
tags: string[];
startingOption: boolean;
};
```
### ManoeuvreDefinition
```ts
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
```ts
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.
```ts
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
```ts
type PotionDefinition = {
id: string;
name: string;
tableSource: string;
useTiming: "combat" | "exploration" | "town" | "any";
effects: RuleEffect[];
valueGp?: number;
mvp: boolean;
};
```
### ScrollDefinition
```ts
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
```ts
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.
```ts
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
```ts
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
```ts
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.
```ts
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
```ts
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
```ts
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
```ts
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
```ts
type TownState = {
visits: number;
knownServices: string[];
stash: InventoryEntry[];
pendingSales: InventoryEntry[];
serviceFlags: string[];
};
```
### QuestState
```ts
type QuestState = {
id: string;
title: string;
status: "available" | "active" | "completed" | "failed";
progressFlags: string[];
rewardText?: string;
};
```
### RunSummary
```ts
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
```ts
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
```ts
type DungeonState = {
levels: Record<number, DungeonLevelState>;
revealedPercentByLevel: Record<number, number>;
globalFlags: string[];
};
```
### DungeonLevelState
```ts
type DungeonLevelState = {
level: number;
themeName?: string;
rooms: Record<string, RoomState>;
discoveredRoomOrder: string[];
stairsUpRoomId?: string;
stairsDownRoomId?: string;
secretDoorUsed?: boolean;
exhaustedExitSearch?: boolean;
};
```
### RoomState
```ts
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
```ts
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
```ts
type EncounterState = {
id: string;
sourceTableCode?: string;
creatureIds: string[];
resolved: boolean;
surprise?: boolean;
rewardPending?: boolean;
};
```
### RoomObjectState
```ts
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
```ts
type CombatState = {
id: string;
round: number;
actingSide: "player" | "enemy";
fatigueDie?: number;
player: CombatantState;
enemies: CombatantState[];
selectedManoeuvreId?: string;
lastRoll?: RollResult;
pendingInterrupt?: InterruptState;
combatLog: LogEntry[];
};
```
### CombatantState
```ts
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
```ts
type InterruptState = {
source: "player" | "enemy";
trigger: string;
effectText: string;
resolved: boolean;
};
```
## Rules Action/Result Models
### RollResult
```ts
type RollResult = {
diceKind: DiceKind;
rolls: number[];
primary?: number;
secondary?: number;
total?: number;
modifier?: number;
modifiedTotal?: number;
clamped?: boolean;
};
```
### TableLookupResult
```ts
type TableLookupResult = {
tableId: string;
entryKey: string;
roll: RollResult;
entry: TableEntry;
};
```
### RoomGenerationResult
```ts
type RoomGenerationResult = {
room: RoomState;
consumedLookups: TableLookupResult[];
createdConnections: string[];
warnings: string[];
};
```
### ActionResolution
```ts
type ActionResolution = {
success: boolean;
effects: RuleEffect[];
logEntries: LogEntry[];
warnings?: string[];
};
```
### StatusInstance
```ts
type StatusInstance = {
id: string;
source?: string;
duration?: "round" | "combat" | "room" | "run" | "permanent";
value?: number;
notes?: string;
};
```
### LogEntry
```ts
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
## Recommended File Layout
Suggested future code structure:
```text
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