feature: Initial code setup

This commit is contained in:
Keith Solomon
2026-03-15 10:26:20 -05:00
parent e052544989
commit e303373441
18 changed files with 3329 additions and 0 deletions

203
src/schemas/content.ts Normal file
View File

@@ -0,0 +1,203 @@
import { z } from "zod";
import {
contentReferenceSchema,
diceKindSchema,
ruleEffectSchema,
} from "./rules";
export const tableEntrySchema = z.object({
key: z.string().min(1),
min: z.number().int().optional(),
max: z.number().int().optional(),
exact: z.number().int().optional(),
d66: z.number().int().optional(),
label: z.string().min(1),
text: z.string().optional(),
effects: z.array(ruleEffectSchema).optional(),
references: z.array(contentReferenceSchema).optional(),
});
export const tableDefinitionSchema = z.object({
id: z.string().min(1),
code: z.string().min(1),
name: z.string().min(1),
category: z.enum(["generic", "random-list", "loot", "level", "optional", "town", "room"]),
level: z.number().int().positive().optional(),
page: z.number().int().positive(),
diceKind: diceKindSchema,
usesModifiedRangesRule: z.boolean().optional(),
entries: z.array(tableEntrySchema),
notes: z.array(z.string()).optional(),
mvp: z.boolean(),
});
export const weaponDefinitionSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
category: z.enum(["melee", "ranged"]),
handedness: z.enum(["one-handed", "two-handed"]),
baseDamage: z.number().int(),
allowedManoeuvreIds: z.array(z.string()),
tags: z.array(z.string()),
startingOption: z.boolean(),
});
export const manoeuvreDefinitionSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
weaponCategories: z.array(z.enum(["melee", "ranged"])),
shiftCost: z.number().int().optional(),
disciplineModifier: z.number().int().optional(),
precisionModifier: z.number().int().optional(),
damageModifier: z.number().int().optional(),
exactStrikeBonus: z.boolean().optional(),
interruptRule: z.string().optional(),
effectText: z.string().optional(),
mvp: z.boolean(),
});
export const armourDefinitionSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
armourValue: z.number().int(),
penalties: z
.object({
shift: z.number().int().optional(),
discipline: z.number().int().optional(),
precision: z.number().int().optional(),
})
.optional(),
deflectionRule: z.string().optional(),
startingOption: z.boolean(),
valueGp: z.number().int().optional(),
mvp: z.boolean(),
});
export const itemDefinitionSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
itemType: z.enum([
"gear",
"treasure",
"quest",
"herb",
"rune",
"misc",
"ration",
"light-source",
]),
stackable: z.boolean(),
consumable: z.boolean(),
valueGp: z.number().int().optional(),
weight: z.number().optional(),
rulesText: z.string().optional(),
effects: z.array(ruleEffectSchema).optional(),
mvp: z.boolean(),
});
export const potionDefinitionSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
tableSource: z.string().min(1),
useTiming: z.enum(["combat", "exploration", "town", "any"]),
effects: z.array(ruleEffectSchema),
valueGp: z.number().int().optional(),
mvp: z.boolean(),
});
export const scrollDefinitionSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
tableSource: z.string().min(1),
castCheck: z
.object({
diceKind: z.enum(["d6", "2d6"]),
successMin: z.number().int().optional(),
successMax: z.number().int().optional(),
})
.optional(),
onSuccess: z.array(ruleEffectSchema),
onFailureTableCode: z.string().optional(),
valueGp: z.number().int().optional(),
startingOption: z.boolean(),
mvp: z.boolean(),
});
export const creatureDefinitionSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
level: z.number().int().positive(),
category: z.string().min(1),
hp: z.number().int().positive(),
attackProfile: z.object({
discipline: z.number().int(),
precision: z.number().int(),
damage: z.number().int(),
numberAppearing: z.string().optional(),
}),
defenceProfile: z
.object({
armour: z.number().int().optional(),
specialRules: z.array(z.string()).optional(),
})
.optional(),
xpReward: z.number().int().optional(),
lootTableCodes: z.array(z.string()).optional(),
interruptRules: z.array(z.string()).optional(),
traits: z.array(z.string()).optional(),
sourcePage: z.number().int().positive(),
mvp: z.boolean(),
});
export const exitTemplateSchema = z.object({
direction: z.enum(["north", "east", "south", "west"]).optional(),
exitType: z.enum(["open", "door", "locked", "secret", "shaft", "stairs"]),
destinationLevel: z.number().int().optional(),
});
export const roomTemplateSchema = z.object({
id: z.string().min(1),
level: z.number().int().positive(),
roomClass: z.enum(["normal", "small", "large", "special", "start", "stairs"]),
tableCode: z.string().min(1),
tableEntryKey: z.string().min(1),
title: z.string().min(1),
text: z.string().optional(),
dimensions: z
.object({
width: z.number().int().positive(),
height: z.number().int().positive(),
})
.optional(),
exits: z.array(exitTemplateSchema).optional(),
encounterRefs: z.array(contentReferenceSchema).optional(),
objectRefs: z.array(contentReferenceSchema).optional(),
tags: z.array(z.string()),
mvp: z.boolean(),
});
export const townServiceDefinitionSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
serviceType: z.enum(["market", "temple", "tavern", "healer", "smith", "quest"]),
tableCodes: z.array(z.string()).optional(),
costRules: z.array(z.string()).optional(),
effects: z.array(ruleEffectSchema).optional(),
mvp: z.boolean(),
});
export const contentPackSchema = z.object({
version: z.string().min(1),
sourceBooks: z.array(z.string()),
tables: z.array(tableDefinitionSchema),
weapons: z.array(weaponDefinitionSchema),
manoeuvres: z.array(manoeuvreDefinitionSchema),
armour: z.array(armourDefinitionSchema),
items: z.array(itemDefinitionSchema),
potions: z.array(potionDefinitionSchema),
scrolls: z.array(scrollDefinitionSchema),
creatures: z.array(creatureDefinitionSchema),
roomTemplates: z.array(roomTemplateSchema),
townServices: z.array(townServiceDefinitionSchema),
});

71
src/schemas/rules.ts Normal file
View File

@@ -0,0 +1,71 @@
import { z } from "zod";
export const diceKindSchema = z.enum(["d3", "d6", "2d6", "d66"]);
export const contentReferenceTypeSchema = z.enum([
"table",
"weapon",
"manoeuvre",
"armour",
"item",
"potion",
"scroll",
"creature",
"room",
"service",
]);
export const contentReferenceSchema = z.object({
type: contentReferenceTypeSchema,
id: z.string().min(1),
});
export const ruleEffectSchema = z.object({
type: z.enum([
"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: z.number().optional(),
statusId: z.string().optional(),
target: z.enum(["self", "enemy", "room", "campaign"]).optional(),
referenceId: z.string().optional(),
notes: z.string().optional(),
});
export const rollResultSchema = z.object({
diceKind: diceKindSchema,
rolls: z.array(z.number().int()),
primary: z.number().int().optional(),
secondary: z.number().int().optional(),
total: z.number().int().optional(),
modifier: z.number().int().optional(),
modifiedTotal: z.number().int().optional(),
clamped: z.boolean().optional(),
});
export const logEntrySchema = z.object({
id: z.string().min(1),
at: z.string().min(1),
type: z.enum(["system", "roll", "combat", "loot", "room", "town", "progression"]),
text: z.string().min(1),
relatedIds: z.array(z.string()).optional(),
});
export const actionResolutionSchema = z.object({
success: z.boolean(),
effects: z.array(ruleEffectSchema),
logEntries: z.array(logEntrySchema),
warnings: z.array(z.string()).optional(),
});

217
src/schemas/state.ts Normal file
View File

@@ -0,0 +1,217 @@
import { z } from "zod";
import { logEntrySchema, rollResultSchema, ruleEffectSchema } from "./rules";
export const statusInstanceSchema = z.object({
id: z.string().min(1),
source: z.string().optional(),
duration: z.enum(["round", "combat", "room", "run", "permanent"]).optional(),
value: z.number().optional(),
notes: z.string().optional(),
});
export const inventoryEntrySchema = z.object({
definitionId: z.string().min(1),
quantity: z.number().int().nonnegative(),
identified: z.boolean().optional(),
charges: z.number().int().optional(),
notes: z.string().optional(),
});
export const inventoryStateSchema = z.object({
carried: z.array(inventoryEntrySchema),
equipped: z.array(inventoryEntrySchema),
stored: z.array(inventoryEntrySchema),
currency: z.object({
gold: z.number().int().nonnegative(),
}),
rationCount: z.number().int().nonnegative(),
lightSources: z.array(inventoryEntrySchema),
});
export const adventurerStateSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
level: z.number().int().positive(),
xp: z.number().int().nonnegative(),
hp: z.object({
current: z.number().int(),
max: z.number().int().positive(),
base: z.number().int().positive(),
}),
stats: z.object({
shift: z.number().int(),
discipline: z.number().int(),
precision: z.number().int(),
}),
weaponId: z.string().min(1),
manoeuvreIds: z.array(z.string()),
armourId: z.string().optional(),
favour: z.record(z.string(), z.number().int()),
statuses: z.array(statusInstanceSchema),
inventory: inventoryStateSchema,
progressionFlags: z.array(z.string()),
});
export const townStateSchema = z.object({
visits: z.number().int().nonnegative(),
knownServices: z.array(z.string()),
stash: z.array(inventoryEntrySchema),
pendingSales: z.array(inventoryEntrySchema),
serviceFlags: z.array(z.string()),
});
export const questStateSchema = z.object({
id: z.string().min(1),
title: z.string().min(1),
status: z.enum(["available", "active", "completed", "failed"]),
progressFlags: z.array(z.string()),
rewardText: z.string().optional(),
});
export const runSummarySchema = z.object({
runId: z.string().min(1),
startedAt: z.string().min(1),
endedAt: z.string().optional(),
deepestLevel: z.number().int().positive(),
roomsVisited: z.number().int().nonnegative(),
creaturesDefeated: z.array(z.string()),
xpGained: z.number().int().nonnegative(),
treasureValue: z.number().int().nonnegative(),
outcome: z.enum(["escaped", "defeated", "saved-in-progress"]),
});
export const campaignStateSchema = z.object({
id: z.string().min(1),
createdAt: z.string().min(1),
updatedAt: z.string().min(1),
rulesVersion: z.string().min(1),
contentVersion: z.string().min(1),
adventurer: adventurerStateSchema,
unlockedLevels: z.array(z.number().int().positive()),
completedLevels: z.array(z.number().int().positive()),
townState: townStateSchema,
questState: z.array(questStateSchema),
campaignFlags: z.array(z.string()),
runHistory: z.array(runSummarySchema),
});
export const roomExitStateSchema = z.object({
id: z.string().min(1),
direction: z.enum(["north", "east", "south", "west"]),
exitType: z.enum(["open", "door", "locked", "secret", "shaft", "stairs"]),
discovered: z.boolean(),
traversable: z.boolean(),
leadsToRoomId: z.string().optional(),
destinationLevel: z.number().int().optional(),
});
export const encounterStateSchema = z.object({
id: z.string().min(1),
sourceTableCode: z.string().optional(),
creatureIds: z.array(z.string()),
resolved: z.boolean(),
surprise: z.boolean().optional(),
rewardPending: z.boolean().optional(),
});
export const roomObjectStateSchema = z.object({
id: z.string().min(1),
objectType: z.enum(["container", "altar", "corpse", "hazard", "feature", "quest"]),
sourceTableCode: z.string().optional(),
interacted: z.boolean(),
hidden: z.boolean().optional(),
effects: z.array(ruleEffectSchema).optional(),
notes: z.string().optional(),
});
export const roomStateSchema = z.object({
id: z.string().min(1),
level: z.number().int().positive(),
templateId: z.string().optional(),
position: z.object({
x: z.number().int(),
y: z.number().int(),
}),
dimensions: z.object({
width: z.number().int().positive(),
height: z.number().int().positive(),
}),
roomClass: z.enum(["normal", "small", "large", "special", "start", "stairs"]),
exits: z.array(roomExitStateSchema),
discovery: z.object({
generated: z.boolean(),
entered: z.boolean(),
cleared: z.boolean(),
searched: z.boolean(),
}),
encounter: encounterStateSchema.optional(),
objects: z.array(roomObjectStateSchema),
notes: z.array(z.string()),
flags: z.array(z.string()),
});
export const dungeonLevelStateSchema = z.object({
level: z.number().int().positive(),
themeName: z.string().optional(),
rooms: z.record(z.string(), roomStateSchema),
discoveredRoomOrder: z.array(z.string()),
stairsUpRoomId: z.string().optional(),
stairsDownRoomId: z.string().optional(),
secretDoorUsed: z.boolean().optional(),
exhaustedExitSearch: z.boolean().optional(),
});
export const dungeonStateSchema = z.object({
levels: z.record(z.string(), dungeonLevelStateSchema),
revealedPercentByLevel: z.record(z.string(), z.number()),
globalFlags: z.array(z.string()),
});
export const combatantStateSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
sourceDefinitionId: z.string().optional(),
hpCurrent: z.number().int(),
hpMax: z.number().int().positive(),
shift: z.number().int(),
discipline: z.number().int(),
precision: z.number().int(),
armourValue: z.number().int().optional(),
statuses: z.array(statusInstanceSchema),
traits: z.array(z.string()),
});
export const interruptStateSchema = z.object({
source: z.enum(["player", "enemy"]),
trigger: z.string().min(1),
effectText: z.string().min(1),
resolved: z.boolean(),
});
export const combatStateSchema = z.object({
id: z.string().min(1),
round: z.number().int().positive(),
actingSide: z.enum(["player", "enemy"]),
fatigueDie: z.number().int().optional(),
player: combatantStateSchema,
enemies: z.array(combatantStateSchema),
selectedManoeuvreId: z.string().optional(),
lastRoll: rollResultSchema.optional(),
pendingInterrupt: interruptStateSchema.optional(),
combatLog: z.array(logEntrySchema),
});
export const runStateSchema = z.object({
id: z.string().min(1),
campaignId: z.string().min(1),
status: z.enum(["active", "paused", "completed", "failed"]),
startedAt: z.string().min(1),
currentLevel: z.number().int().positive(),
currentRoomId: z.string().optional(),
dungeon: dungeonStateSchema,
adventurerSnapshot: adventurerStateSchema,
activeCombat: combatStateSchema.optional(),
log: z.array(logEntrySchema),
pendingEffects: z.array(ruleEffectSchema),
});