Content Data Schemas¶
Overview¶
This document defines the authored data schemas for all content types that feed the game's runtime systems. Authored content lives in YAML files under content/ in the server repository. The server reads these files at startup (or on an explicit reload) and seeds them into D1. The runtime systems read from D1 and from KV projections — they never read YAML directly.
The schemas here are the single source of truth for content authoring. Any deviation between a YAML file and this schema is a data validation error.
Conventions¶
- All identifiers use
snake_caseand must be globally unique within their type namespace. - Every schema requires an
id,version, andstatusfield. statusis one ofdraft | active | deprecated. Onlyactiverecords are loaded at runtime.- Numeric ranges are written as
[min, max](inclusive). - Optional fields are marked
# optional. - Enum values are listed as
one_of: [...].
Creature Template¶
File location: content/creatures/<family>/<creature_id>.yaml
id: string # e.g. peltorotta_field_swarm
version: int # increment on every edit; server rejects older versions
status: draft | active | deprecated
display:
name: string # shown on board cards and codex
family: string # e.g. "Peltorotta", "Drake", "Ashwalker"
grade: F | E | D | C | B | A | S # difficulty grade; matches Hall rank access
icon: string # R2 asset path, e.g. "creatures/peltorotta_field_swarm.svg"
description: string # 1–2 sentence flavor text for codex and tooltips
habitat:
biomes: list[string] # e.g. [grassland, farmland, forest_edge]
spawn_hex_tags: list[string] # tile tags this creature can spawn on
climate_preference: string # e.g. temperate, arid, tundra; used by season/event filters
stats:
base_threat: int # 1–100; feeds area security pressure when present
pack_size: [int, int] # min/max group size for a single encounter instance
movement_range: int # hex radius the creature can act within per settlement tick
aggression: passive | territorial | aggressive | predatory
respawn_ticks: int # settlement ticks before a cleared zone can respawn this creature
combat:
power: int # base combat power contributed to encounter difficulty
morale: int # creature morale floor; at 0 it breaks and flees
injury_risk: low | moderate | high | extreme # injury severity modifier for party members
special_abilities: list[string] # optional; e.g. [swarm_blind, nest_regeneration]
loot:
drop_table: # at least one entry required
- item_id: string
chance: float # 0.0–1.0
quantity: [int, int] # min/max
quality: any | standard | fine | masterwork # optional; defaults to any
resource_class: string # reserve class affected on kill: food | fuel | building | medicine | none
board_generation:
eligible_orders: list[string] # order types this creature qualifies for; e.g. [cull_order, named_creature_elimination]
min_party_size: int # minimum committed members for a generated board task
reward_coin: [int, int] # coin reward range scaled to grade
reward_point_base: int # Hall guild point base for S-rank variants
duration_ticks: [int, int] # expedition ticks min/max
ecology:
predator_of: list[string] # creature ids this creature preys on; affects spawn ecology
prey_of: list[string] # creature ids that prey on this creature
resource_pressure:
food: float # negative = drains food supply per active tick; positive = none
security: float # added to area security pressure while present
Item and Material Template¶
File location: content/items/<category>/<item_id>.yaml
id: string # e.g. smoked_river_trout, iron_ingot, ashwort_poultice
version: int
status: draft | active | deprecated
display:
name: string
category: material | consumable | gear | tool | crafted_good | key_item
subcategory: string # e.g. food, medicine, metal, leather, weapon, armor
icon: string # R2 asset path
description: string # 1–2 sentences; appears in inventory tooltip and codex
economy:
base_price: int # coin; the base for market index calculations
weight: float # encumbrance units; contributes to expedition carry weight
stack_limit: int # max units per inventory slot; 1 for gear, up to 999 for materials
tradeable: true | false
market_category: string # market browse category key, e.g. "food.preserved", "metal.refined"
town_supply:
reserve_class: food | water | fuel | medicine | building | none
reserve_value: float # normalized units added per item when delivered to a town
crafting:
is_ingredient: true | false
is_output: true | false # true if this item can be produced by a recipe
discipline: string # e.g. Provisioning, Alchemy, Forging; leave blank if ingredient-only
gear_stats: # optional; only for category = gear or tool
slot: head | chest | legs | feet | hands | weapon | offhand | back | tool
power_contribution: int # added to party combat power when equipped
durability: int
repair_cost_base: int
skill_requirements: # optional; map of skill_id → minimum band index
- skill_id: string
min_band: int # 0=Untrained…6=Master
consumable_effect: # optional; only for category = consumable
effect_type: heal | restore | buff | cure
magnitude: int # effect size; interpreted by the resolver
duration_ticks: int # 0 for instant
reserve_class_boost: string # optional; directly adds to a town reserve on delivery
codex:
knowledge_tier: 0 | 1 | 2 | 3 # 0 = no codex entry; 1 = basic; 2 = detailed; 3 = rare lore
unlock_condition: string # optional; e.g. "gathered_once" or "crafter_rank_D"
Expedition Template¶
File location: content/expeditions/<type>/<expedition_id>.yaml
Expedition templates are the authored sources that the expedition generation model instantiates into live board_item records. A template defines the structure; the runtime fills in seed-specific variables.
id: string # e.g. cull_peltorotta_farmland, escort_caravan_north_road
version: int
status: draft | active | deprecated
meta:
type: combat | gather | escort | delivery | scouting | boss_support | civic
subtype: string # optional; e.g. cull, survey, extract, named_creature
display_name: string # shown on board card; may use template variables: {zone}, {target}
flavor_text: string # 1–2 sentences; flavor context for the task
eligibility:
min_hall_rank: F | E | D | C | B | A | S
min_party_size: int
max_party_size: int # optional; no cap if omitted
required_skills: list[string] # optional skill_ids that improve outcome quality
location_tags: list[string] # hex or zone tags this template can spawn in
season_filter: list[string] # optional; e.g. [summer, autumn]
trigger_conditions: # world-state conditions that make this template eligible
- field: string # e.g. "food_reserve_band", "security_pressure", "monster_pressure"
operator: gte | lte | eq | in
value: any # band name, numeric threshold, or list
duration:
ticks: [int, int] # min/max settlement ticks to complete
real_time_hours: [float, float] # approximate real-world duration for UI display
resolution:
success_formula: string # reference to resolver function name
partial_success_threshold: float # 0.0–1.0 quality score floor for partial success
failure_conditions: list[string] # conditions that force failure regardless of score
rewards:
coin: [int, int]
hall_points: int # optional; only for S-rank orders
items: # optional; resolved from loot tables
- source: string # creature_id or gather_node_id
quantity: [int, int]
town_supply_contribution: # optional; marks this expedition as a delivery
reserve_class: string
units: float
penalties:
injury_risk: none | low | moderate | high | extreme
supply_consumption: float # ration and water units consumed per tick
board_presentation:
card_tags: list[string] # e.g. [combat, escort, solo_ok, group_required]
icon: string # R2 asset path for card icon
urgency: normal | urgent | crisis # affects sort priority on board
Board Order Template¶
File location: content/board_orders/<guild>/<order_id>.yaml
Board order templates are the authored types that the board generation system selects from when populating guild boards. They are distinct from expedition templates: a board order is what the player sees and accepts; it references an expedition template to define the actual work.
id: string # e.g. hall_cull_order_F, hall_escort_short_E
version: int
status: draft | active | deprecated
guild: adventurers_hall | crafters_hall | town_board | caravan_board
rank: F | E | D | C | B | A | S
display:
title: string # e.g. "Cull Order — {zone}"
summary: string # 1 sentence; shown on collapsed board card
detail_text: string # full description shown when card is expanded
references:
expedition_template_id: string # links to an expedition template
generation:
weight: int # relative spawn weight when board is populated
cooldown_ticks: int # minimum ticks before this order type can appear again at this location
max_simultaneous: int # how many of this type can be active on the board at once
refresh_on: list[string] # world-state triggers that can bypass cooldown; e.g. [security_crisis]
completion_criteria:
resolver_outcome: string # resolver function output that marks this complete
quality_floor: float # 0.0–1.0; below this the order fails; does not count toward rank
reward_overrides: # optional; overrides expedition template reward if specified
coin_bonus: int
hall_point_bonus: int
expiry:
ticks_until_expiry: int # board order removed if not accepted within this window
notify_on_expiry: true | false
Skill Template¶
File location: content/skills/<skill_id>.yaml
id: string # e.g. foraging, trade, navigation, combat_melee
version: int
status: draft | active | deprecated
display:
name: string
category: combat | craft | social | survival | knowledge | movement
description: string
bands:
- band: 0 # Untrained
label: Untrained
xp_threshold: 0
unlocks: []
- band: 1
label: Familiar
xp_threshold: 100
unlocks: list[string] # feature or order type ids unlocked at this band
- band: 2
label: Practiced
xp_threshold: 300
unlocks: list[string]
- band: 3
label: Skilled
xp_threshold: 700
unlocks: list[string]
- band: 4
label: Veteran
xp_threshold: 1500
unlocks: list[string]
- band: 5
label: Expert
xp_threshold: 3000
unlocks: list[string]
- band: 6
label: Master
xp_threshold: 6000
unlocks: list[string]
experience_sources: # what activities earn xp in this skill
- source_type: string # e.g. expedition_type, order_type, crafting_discipline
source_id: string
xp_per_completion: int
quality_multiplier: true | false # if true, xp scales with outcome quality
decay:
enabled: true | false
grace_days: int # days of inactivity before decay starts
decay_per_day: float # xp lost per real day after grace period; optional
Authoring Pipeline Flow¶
Author writes YAML in content/<type>/<id>.yaml
↓
CI/CD validation step: schema lint + duplicate id check
↓
On server deploy: loader script reads all active YAML files
↓
Loader upserts into D1: uses (id, version) as upsert key
↓
KV projector writes read-model projections for client lookups
↓
Runtime systems read from D1 (generation, resolution)
Runtime client reads from KV (board display, codex, market catalog)
Validation Rules¶
The CI validation step enforces:
- No two files in the same type namespace share an
id versionmust be greater than the current D1 record's version (no rollback without explicit migration)- All foreign key references (
expedition_template_id,creature_id,item_id) must resolve to anactiverecord in the same content set base_pricemust be positive;weightmust be non-negativeduration_ticks[0]must be ≤duration_ticks[1]- At least one
trigger_conditionrequired for expedition templates drop_tableprobabilities summing to > 1.0 are allowed (independent rolls); they are not treated as a probability mass function
Reload Without Restart¶
The server exposes an admin-only Worker endpoint POST /admin/content/reload that triggers the loader script without restarting the process. This allows new authored content to go live without a full deploy. The endpoint requires a signed request with the ADMIN_SECRET binding and is not exposed to the public.
First-Boot Seed Strategy¶
A fresh D1 database (staging or production) requires an initial data set before TownSettlementDO alarms or board generation can run. "First-boot" means: the D1 migrations have run, but no town, town_reserve, or content rows exist yet.
Static World Records (SQL Migrations)¶
Town and location rows are seeded via a dedicated SQL migration file, not via the content loader. This is because town ids are foreign keys referenced by other tables and must exist before any character or expedition data is written.
Migration file: migrations/0100_seed_towns.sql
-- Primary towns
INSERT INTO town (id, name, region, population, health_state, created_at) VALUES
('trevalkaan', 'Trevalkaan', 'Verdant Reach', 850, 'stable', unixepoch()),
('talmaes', 'Talmaes', 'Ashfen Lowlands', 620, 'stable', unixepoch());
-- Villages
INSERT INTO town (id, name, region, population, health_state, created_at) VALUES
('herath_cross', 'Herath Crossing', 'Verdant Reach', 190, 'stable', unixepoch()),
('durn_hollow', 'Durn Hollow', 'Ashfen Lowlands', 140, 'stable', unixepoch()),
('veldmoor', 'Veldmoor', 'Verdant Reach', 210, 'stable', unixepoch()),
('cairn_rest', 'Cairn Rest', 'Ashfen Lowlands', 160, 'stable', unixepoch());
-- Initial town reserves (stable mid-band)
INSERT INTO town_reserve (town_id, class, units, band, updated_at)
SELECT t.id, c.class, c.units, 'stable', unixepoch()
FROM town t
CROSS JOIN (VALUES
('food', 500.0), ('water', 400.0), ('fuel', 300.0),
('medicine', 200.0), ('building', 350.0)
) AS c(class, units);
This migration runs via wrangler d1 migrations apply as part of the first deploy (and every subsequent deploy — it is idempotent via the wrangler migrations table).
Content Records (Admin Endpoint)¶
YAML content (creatures, items, expeditions, board orders, skills) is not seeded via SQL migrations. It is loaded via the same loader pipeline that handles live reloads.
First-boot flow:
- Deploy Worker (migrations run automatically via deploy hook or manual step)
- Run:
curl -X POST https://{worker-url}/admin/content/reload -H "Authorization: Bearer {ADMIN_SECRET}" - Loader reads all
activeYAML files fromcontent/, upserts into D1, writes KV projections TownSettlementDOalarm fires on first tick and finds valid town rows + content rows — normal operation begins
Staging: same process against wrangler dev local D1 or staging D1 database.
What TownSettlementDO finds on first alarm:
- Town rows exist (from migration seed)
- Reserves at mid-band
stable(from migration seed) - No
board_itemrows — first tick generates them from templates - No
town_eventrows — first tick starts with clean event state - Content templates loaded — board generation can run immediately
Deploy Script¶
The deploy script (scripts/deploy-pages.ps1) calls content reload automatically after a successful Worker deploy in production. For staging, reload is manual or triggered by CI.