Skip to content

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_case and must be globally unique within their type namespace.
  • Every schema requires an id, version, and status field.
  • status is one of draft | active | deprecated. Only active records 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
  • version must 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 an active record in the same content set
  • base_price must be positive; weight must be non-negative
  • duration_ticks[0] must be ≤ duration_ticks[1]
  • At least one trigger_condition required for expedition templates
  • drop_table probabilities 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:

  1. Deploy Worker (migrations run automatically via deploy hook or manual step)
  2. Run: curl -X POST https://{worker-url}/admin/content/reload -H "Authorization: Bearer {ADMIN_SECRET}"
  3. Loader reads all active YAML files from content/, upserts into D1, writes KV projections
  4. TownSettlementDO alarm 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_item rows — first tick generates them from templates
  • No town_event rows — 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.

wrangler deploy
wrangler d1 migrations apply --database-name aelghar-db
curl -X POST $env:WORKER_URL/admin/content/reload -H "Authorization: Bearer $env:ADMIN_SECRET"