Compare commits
13
Commits
v0.1.24
...
a7b7d3e31a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7b7d3e31a | ||
|
|
c3a8dcc481 | ||
|
|
c12005843b | ||
|
|
10b1a4252e | ||
|
|
4b75ec3495 | ||
|
|
a6c857b174 | ||
|
|
bdad91d3d0 | ||
|
|
95c0da9cf5 | ||
|
|
4b57006a5c | ||
|
|
a54b9d24e0 | ||
|
|
0f83c64e8f | ||
|
|
fb3ebf4313 | ||
|
|
4cbaf0cb57 |
@@ -87,6 +87,48 @@ tooling/db/sprint stop
|
||||
This marks the active sprint as completed and lists carry-over candidates.
|
||||
Note the sprint number (N) from the output.
|
||||
|
||||
#### A1b. Sprint retrospective and review
|
||||
|
||||
Before bumping the version, run a brief retro. Present the following to
|
||||
the user:
|
||||
|
||||
1. **What shipped** — list completed tickets with one-line summaries
|
||||
2. **What didn't ship** — carry-overs and why (blocked, cut, deprioritized)
|
||||
3. **What we learned** — open questions raised during the sprint (new Q-NNN
|
||||
items), review findings that surfaced design gaps, and any assumptions
|
||||
that turned out to be wrong
|
||||
4. **Process notes** — what worked well, what was friction (e.g. dependency
|
||||
chains that blocked teams, specs that were over/under-specified,
|
||||
review cycles that caught real issues vs busywork)
|
||||
|
||||
5. **Process improvements** — this is the most important section. Do NOT
|
||||
skip it. Look for:
|
||||
- Dependency chains that blocked teams — could the sprint have been
|
||||
structured differently to avoid the bottleneck?
|
||||
- Specs that were over-specified (wasted planning) or under-specified
|
||||
(wasted iteration) — what's the right level of detail for this
|
||||
project's current stage?
|
||||
- Review cycles — did they catch real issues or create busywork?
|
||||
- Agent coordination — were agents stuck, duplicating work, or idle?
|
||||
- **Dig into the deeper why.** Don't stop at "the dependency chain
|
||||
blocked the copy team." Ask: why was there a dependency chain? Was
|
||||
the sprint structured wrong, or was the work inherently sequential?
|
||||
Could Phase 0 have been done pre-sprint? Should we change how we
|
||||
plan sprints going forward?
|
||||
- If something went rough, understand the root cause — not just what
|
||||
happened, but why the process allowed it to happen.
|
||||
- If a concrete process change follows naturally, propose it. But do
|
||||
NOT force improvements. If nothing was broken, say so and move on.
|
||||
Unnecessary process changes are worse than no changes.
|
||||
|
||||
Keep each section concise — a few bullet points, not a document. The
|
||||
retro is a conversation checkpoint, not a report. Use `AskUserQuestion`
|
||||
to let the user add their own observations and push back before proceeding.
|
||||
|
||||
If the user raises items that should be tracked, create Q-NNN entries
|
||||
or backlog tickets on the spot. If process changes are agreed, update
|
||||
the relevant skill files or CLAUDE.md immediately — don't defer them.
|
||||
|
||||
#### A2. Bump the version
|
||||
|
||||
The project version scheme is `v0.1.{sprint_number}`. After closing
|
||||
|
||||
@@ -6,6 +6,14 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Name pool first-pick bias — generator spike produced "Dav" as NPC 1 across all seeds; now uses derived RNG per zone+culture (#628)
|
||||
- Behavior dedup — same behavior string no longer assigned to multiple NPCs in one zone run (#629)
|
||||
|
||||
### Added
|
||||
- Relationship-to-behavior pipeline — NPC behavior lines now reflect social connections (rivals ignore each other, friends gravitate, subordinates defer) (#631)
|
||||
- Want/State layer — NPCs have internal motives (Bored, Alert, Suspicious, AvoidingSomeone, LookingForInfo) that leak through observable micro-tells (#632)
|
||||
|
||||
## [v0.1.24] — 2026-03-06
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Krenn Culture Profile — example file
|
||||
//
|
||||
// Schema: server/src/npc/blueprint.rs :: CultureProfile
|
||||
// Real content: ticket #610 (copy team fills this)
|
||||
// Validate: tooling/validate-ron content/global/culture-krenn.example.ron culture
|
||||
//
|
||||
// One file per culture. The generator reads this to give NPCs culturally
|
||||
// appropriate names, speech patterns, and personality bias.
|
||||
//
|
||||
// Source: D-036 (Krenn System canonical setting), D-128 (culture implicit in location),
|
||||
// D-121 (voice culture-driven), D-123 (AI content templating via culture vectors).
|
||||
|
||||
(
|
||||
id: "krenn",
|
||||
name: "Krenn System Culture",
|
||||
description: "Working-class pragmatic culture. ~180 years settled, mid-Reach G3V system. Community-oriented, suspicious of distant authority, values competence and reliability over credentials. First-name-primary in social contexts.",
|
||||
|
||||
naming: (
|
||||
style: "compact, consonant-heavy, first-name-primary in social contexts",
|
||||
given_names: [
|
||||
"Kael", "Voss", "Lera", "Torek", "Drin",
|
||||
"Maret", "Naia", "Sera", "Nils", "Pael",
|
||||
"Tev", "Ren", "Sess", "Renn", "Olin",
|
||||
"Tav", "Resha", "Harek", "Sabel", "Pell",
|
||||
],
|
||||
family_names: [
|
||||
"Davan", "Sessik", "Korr", "Tamm", "Venn",
|
||||
"Lintar", "Darvo", "Kosse",
|
||||
],
|
||||
// Krenn culture is first-name-primary. Family names exist but are
|
||||
// used mainly in formal/institutional contexts.
|
||||
family_name_used_socially: false,
|
||||
),
|
||||
|
||||
speech: (
|
||||
register: "direct, minimal pleasantries, gets to the point",
|
||||
filler_words: [
|
||||
"look",
|
||||
"right",
|
||||
"yeah",
|
||||
"so",
|
||||
],
|
||||
greetings: [
|
||||
"hey",
|
||||
"morning",
|
||||
"shift treating you alright?",
|
||||
],
|
||||
farewells: [
|
||||
"shift's calling",
|
||||
"gotta move",
|
||||
"catch you later",
|
||||
],
|
||||
exclamations: [
|
||||
"void take it",
|
||||
"stars",
|
||||
"unbelievable",
|
||||
],
|
||||
),
|
||||
|
||||
values: (
|
||||
description: "Pragmatic, community-oriented, suspicious of authority. Competence earns respect. Showing up and doing the work matters more than rank or credentials. Outsiders are tolerated but watched.",
|
||||
// Traits more common in Krenn culture — generator biases toward these.
|
||||
favored_traits: [Bold, Honest, Curious],
|
||||
// Traits less common — generator biases away from these.
|
||||
disfavored_traits: [Reclusive, Deceptive],
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
// Krenn Culture Profile
|
||||
//
|
||||
// Schema: server/src/npc/blueprint.rs :: CultureProfile
|
||||
// Ticket: #610 (copy team)
|
||||
// Validate: tooling/validate-ron content/global/culture-krenn.ron culture
|
||||
//
|
||||
// Sources: D-036 (Sova Transit District / Krenn System setting, amended post-workshop),
|
||||
// D-121 (voice is culture-driven, job as modifier),
|
||||
// D-128 (culture implicit in starting location).
|
||||
//
|
||||
// Krenn: ~180 years settled. Mid-Reach G3V system. Working-class pragmatic.
|
||||
// Community-oriented, suspicious of distant authority. Competence earns respect.
|
||||
// Showing up and doing the work matters more than rank or credentials.
|
||||
// First-name-primary in social contexts — family names are institutional.
|
||||
|
||||
(
|
||||
id: "krenn",
|
||||
name: "Krenn System Culture",
|
||||
description: "Working-class pragmatic culture. ~180 years settled, mid-Reach G3V system. Community-oriented, suspicious of distant authority, values competence and reliability over credentials. Atmosphere: quotidian-with-undertow — comfortable enough to be complacent, tight enough that extra income is tempting.",
|
||||
|
||||
naming: (
|
||||
style: "compact, consonant-heavy, first-name-primary in social contexts",
|
||||
// Pool the generator draws from. More names = more variety across runs.
|
||||
// Source: D-036 canonical examples + extended set following same phoneme rules.
|
||||
// Rule: compact (1-2 syllables), consonant clusters welcome, hard endings preferred.
|
||||
given_names: [
|
||||
// D-036 canonical set
|
||||
"Kael", "Voss", "Lera", "Torek", "Drin",
|
||||
"Maret", "Naia", "Sera", "Nils", "Pael",
|
||||
"Tev", "Ren", "Sess", "Renn", "Olin",
|
||||
"Tav", "Resha", "Harek", "Sabel", "Pell",
|
||||
// Extended — same phoneme pattern
|
||||
"Dav", "Tork", "Ness", "Pren", "Vel",
|
||||
"Orin", "Mael", "Torra", "Sorek", "Sev",
|
||||
"Bren", "Linn", "Vrek", "Darek", "Pess",
|
||||
"Nell", "Kren", "Sorel", "Tavek", "Rask",
|
||||
],
|
||||
family_names: [
|
||||
// D-036 canonical + extended
|
||||
"Davan", "Sessik", "Korr", "Tamm", "Venn",
|
||||
"Lintar", "Darvo", "Kosse",
|
||||
// Extended
|
||||
"Pellan", "Sorren", "Tessik", "Morek", "Brav",
|
||||
"Ossel", "Rennick", "Harven", "Tollek", "Dass",
|
||||
],
|
||||
// Krenn culture is first-name-primary.
|
||||
// Family names exist but belong to institutional contexts: contracts, registrations, arrest records.
|
||||
family_name_used_socially: false,
|
||||
),
|
||||
|
||||
speech: (
|
||||
// Direct. Minimal pleasantries. Gets to the point — not because they're rude,
|
||||
// but because time is real and everyone's short of it.
|
||||
register: "direct, minimal pleasantries, gets to the point",
|
||||
filler_words: [
|
||||
"look",
|
||||
"right",
|
||||
"yeah",
|
||||
"so",
|
||||
"listen",
|
||||
"well",
|
||||
],
|
||||
greetings: [
|
||||
"hey",
|
||||
"morning",
|
||||
"shift treating you alright?",
|
||||
"all good?",
|
||||
"what's the word?",
|
||||
"all in one piece?",
|
||||
],
|
||||
farewells: [
|
||||
"shift's calling",
|
||||
"gotta move",
|
||||
"catch you later",
|
||||
"take it easy",
|
||||
"see you around",
|
||||
"stay out of trouble",
|
||||
],
|
||||
// Krenn oaths are void-adjacent — space is real here, and hostile.
|
||||
// They don't swear by gods or governments. They swear by what kills you.
|
||||
exclamations: [
|
||||
"void take it",
|
||||
"stars",
|
||||
"blood and void",
|
||||
"void's sake",
|
||||
"damn all",
|
||||
"cold vacuum",
|
||||
],
|
||||
),
|
||||
|
||||
values: (
|
||||
description: "Pragmatic, community-oriented, suspicious of authority. Competence earns respect. Showing up and doing the work matters more than rank or credentials. Outsiders are tolerated but watched. Loyalty runs narrow and deep — to your crew, your shift, your street.",
|
||||
// Traits more common in Krenn culture — generator biases toward these.
|
||||
// Bold: Krenn people speak their mind. Honest: community trust is load-bearing.
|
||||
// Curious: 180 years of problem-solving breeds intellectual appetite.
|
||||
// Social: community-oriented culture; isolation is a warning sign.
|
||||
favored_traits: [Bold, Honest, Curious, Social],
|
||||
// Traits less common — generator biases away from these.
|
||||
// Reclusive: red flag in a community that depends on showing up.
|
||||
// Deceptive: betrayal of trust is the worst thing you can do here.
|
||||
disfavored_traits: [Reclusive, Deceptive],
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,124 @@
|
||||
// Zone Identity Spec — Industrial
|
||||
//
|
||||
// Schema: server/src/npc/blueprint.rs :: ZoneSpec
|
||||
// Ticket: #609 (copy team)
|
||||
// Validate: tooling/validate-ron content/global/industrial-zone-spec.ron zone
|
||||
//
|
||||
// Character: freight handling, manufacturing, maintenance. High throughput.
|
||||
// Shift rhythms. Functional over comfortable. Nobody lingers — unless they're on break.
|
||||
//
|
||||
// Generator contrast target: industrial vs rural (Sprint 25 proof).
|
||||
// If these produce NPCs that look the same, something is wrong.
|
||||
//
|
||||
// Heritage roots (D-104/D-105) overlay these base specs at generation time,
|
||||
// modifying role weights, social site composition, and behavioral flavoring.
|
||||
|
||||
(
|
||||
zone_type: "industrial",
|
||||
label: "Industrial Zone",
|
||||
description: "Freight handling, manufacturing, and maintenance. High throughput, shift-based work rhythms, functional over comfortable. Faces are known by role and bay number more than name. The work doesn't stop between shifts — people do.",
|
||||
|
||||
// 1-10. Industrial Krenn: steady credit flow, but it all goes somewhere.
|
||||
economic_level: 7,
|
||||
// 1-10. Dense. Multiple shifts overlap. Crowds at handover.
|
||||
population_density: 6,
|
||||
|
||||
roles: [
|
||||
(
|
||||
id: "dock_worker",
|
||||
label: "Dock Worker",
|
||||
// Most common. The zone runs on their backs.
|
||||
weight: 5,
|
||||
skill_focus: ["technical"],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: [
|
||||
"guides a freight container into position with hand signals",
|
||||
"checks a manifest against a handheld scanner, lips moving",
|
||||
"waits at the loading bay apron with arms crossed, watching the clock",
|
||||
"calls bay numbers to a colleague across the noise of the floor",
|
||||
"hooks a cargo sling and steps clear before signaling the lift",
|
||||
"stacks empty pallets against a wall with mechanical efficiency",
|
||||
"wipes sweat from her face with a forearm and keeps moving",
|
||||
"slumps into a break room chair and stares at nothing for a full minute before reaching for a drink",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "technician",
|
||||
label: "Systems Technician",
|
||||
// Keeps the infrastructure running. Never enough of them.
|
||||
weight: 4,
|
||||
skill_focus: ["technical"],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: [
|
||||
"runs a diagnostic routine on a floor console, eyes on the readout",
|
||||
"traces a conduit run along the ceiling with a handheld light",
|
||||
"swaps a panel module with practiced speed and no wasted motion",
|
||||
"logs a fault code into a datapad before moving on",
|
||||
"presses an ear to a vibrating duct and listens",
|
||||
"argues quietly with a display that isn't giving the right numbers",
|
||||
"queries a fault log through her insert without touching the terminal, eyes briefly unfocused",
|
||||
"stretches both arms overhead in the break room doorway, blocking it without noticing",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "foreman",
|
||||
label: "Shift Foreman",
|
||||
// Fewer of them. High visibility. Everyone knows where they are.
|
||||
weight: 2,
|
||||
skill_focus: ["observation", "persuasion"],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: [
|
||||
"walks the floor with a datapad under one arm and says nothing",
|
||||
"pulls a worker aside for a quiet word near the far wall",
|
||||
"marks a line off a production board and moves to the next one",
|
||||
"stands at the mezzanine rail watching throughput without expression",
|
||||
"reviews shift handover notes and circles something with a stylus",
|
||||
"speaks to a dock crew in a low voice — they listen without nodding",
|
||||
"sits alone in the break room rubbing the back of her neck, datapad face-down on the table",
|
||||
"laughs at something a technician says, then catches herself and goes quiet",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "security",
|
||||
label: "Facility Security",
|
||||
// Present, watchful. Not looking for trouble — cataloguing it.
|
||||
weight: 2,
|
||||
skill_focus: ["combat", "observation"],
|
||||
combat_eligible: true,
|
||||
typical_behaviors: [
|
||||
"sweeps the access corridor on a timed circuit, same path each time",
|
||||
"waves a regular through the checkpoint on sight — scans the one behind them out of procedure",
|
||||
"leans at the restricted bay entrance, arms folded — been watching this corridor since the shift started",
|
||||
"notes something in a shift log without reacting to it outwardly",
|
||||
"watches a handover between crews from across the loading floor",
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
social_sites: [
|
||||
(
|
||||
site_type: "break_room",
|
||||
label: "Worker Break Room",
|
||||
// Between shifts. Decompression. People stop performing for a moment.
|
||||
roles: ["dock_worker", "technician", "foreman"],
|
||||
min_npcs: 2,
|
||||
max_npcs: 5,
|
||||
),
|
||||
(
|
||||
site_type: "maintenance_bay",
|
||||
label: "Maintenance Bay",
|
||||
// Active work site. Technicians and dock workers cross paths here.
|
||||
roles: ["technician", "dock_worker"],
|
||||
min_npcs: 2,
|
||||
max_npcs: 4,
|
||||
),
|
||||
(
|
||||
site_type: "loading_platform",
|
||||
label: "Loading Platform",
|
||||
// The operational center. Busy, loud, coordinated.
|
||||
roles: ["dock_worker", "foreman", "security"],
|
||||
min_npcs: 3,
|
||||
max_npcs: 8,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,119 @@
|
||||
// Zone Identity Spec — Rural
|
||||
//
|
||||
// Schema: server/src/npc/blueprint.rs :: ZoneSpec
|
||||
// Ticket: #609 (copy team)
|
||||
// Validate: tooling/validate-ron content/global/rural-zone-spec.ron zone
|
||||
//
|
||||
// Character: scattered homesteads and small workshops. Unhurried. Community-bound.
|
||||
// Low throughput, high familiarity. Everyone knows who belongs here.
|
||||
//
|
||||
// Generator contrast target: rural vs industrial (Sprint 25 proof).
|
||||
// If these produce NPCs that look the same, something is wrong.
|
||||
//
|
||||
// Heritage roots (D-104/D-105) overlay these base specs at generation time,
|
||||
// modifying role weights, social site composition, and behavioral flavoring.
|
||||
|
||||
(
|
||||
zone_type: "rural",
|
||||
label: "Rural Settlement",
|
||||
description: "Scattered homesteads, small workshops, and communal gathering points. Low population density, strong community bonds, subsistence-plus economy. Work is seasonal and visible — everyone knows what everyone else is doing.",
|
||||
|
||||
// 1-10. Rural Krenn: self-sufficient, low cash flow, barter supplements credits.
|
||||
economic_level: 3,
|
||||
// 1-10. Sparse. Faces are familiar. Strangers stand out.
|
||||
population_density: 2,
|
||||
|
||||
roles: [
|
||||
(
|
||||
id: "farmer",
|
||||
label: "Farmer",
|
||||
// Most common. The settlement runs on food production.
|
||||
weight: 5,
|
||||
skill_focus: ["technical"],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: [
|
||||
"tends rows of low-growing crops with a long-handled hoe",
|
||||
"lifts a crate of produce onto a flatbed with practiced ease",
|
||||
"checks the section's light cycle timer before deciding whether to water",
|
||||
"patches a cracked irrigation pipe with strips of bonding tape",
|
||||
"calls across a field to a neighbor without looking up from work",
|
||||
"hauls produce to the market stall before the morning exchange opens",
|
||||
"checks seedling trays in a low prefab greenhouse",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "mechanic",
|
||||
label: "Settlement Mechanic",
|
||||
// One or two per settlement. Everyone knows who to call.
|
||||
weight: 3,
|
||||
skill_focus: ["technical"],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: [
|
||||
"pulls a drive unit from a tiller and examines the worn housing",
|
||||
"wipes grease on the thigh of her coveralls between jobs",
|
||||
"explains a repair in clipped shorthand without looking up",
|
||||
"lines up salvaged parts on a workbench and assesses them",
|
||||
"welds a seam on a cracked water tank with slow, careful strokes",
|
||||
"borrows a tool from a neighbor and returns it without being asked",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "trader",
|
||||
label: "Itinerant Trader",
|
||||
// Passes through. Outsider-familiar — not local, not stranger.
|
||||
weight: 2,
|
||||
skill_focus: ["persuasion", "observation"],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: [
|
||||
"squares goods on a fold-out portable display with deliberate care",
|
||||
"leans back on a stool and watches the foot traffic",
|
||||
"flicks credit chits across the counter with one thumb, barely glancing down",
|
||||
"holds eye contact through a long pause, waiting for the price to land",
|
||||
"watches a regular browse the same shelf as last time and says nothing",
|
||||
"packs unsold goods with no visible frustration",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "militia",
|
||||
label: "Settlement Militia",
|
||||
// Rare. Part-time. Knows everyone, trusted because of it.
|
||||
weight: 1,
|
||||
skill_focus: ["combat", "observation"],
|
||||
combat_eligible: true,
|
||||
typical_behaviors: [
|
||||
"walks the fence line at a measured, unhurried pace",
|
||||
"leans on the gate post with rifle slung, watching the road",
|
||||
"waves a familiar face through without checking credentials",
|
||||
"sits in the shade of the gatehouse with a local newsline",
|
||||
"stops to talk with a passing farmer, eyes still scanning the perimeter",
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
social_sites: [
|
||||
(
|
||||
site_type: "tavern",
|
||||
label: "Local Tavern",
|
||||
// The Last Shift equivalent for rural Krenn: functional, familiar, slow.
|
||||
roles: ["farmer", "mechanic", "trader", "militia"],
|
||||
min_npcs: 3,
|
||||
max_npcs: 6,
|
||||
),
|
||||
(
|
||||
site_type: "workshop",
|
||||
label: "Community Workshop",
|
||||
// Shared space. People fix things together.
|
||||
roles: ["mechanic", "farmer"],
|
||||
min_npcs: 2,
|
||||
max_npcs: 4,
|
||||
),
|
||||
(
|
||||
site_type: "market_stall",
|
||||
label: "Settlement Market",
|
||||
// Weekly or daily trading post. Commerce and gossip combined.
|
||||
roles: ["trader", "farmer"],
|
||||
min_npcs: 1,
|
||||
max_npcs: 3,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
// Zone Identity Spec — example file
|
||||
//
|
||||
// Schema: server/src/npc/blueprint.rs :: ZoneSpec
|
||||
// Real content: ticket #609 (copy team fills this)
|
||||
// Validate: tooling/validate-ron content/global/zone-identity-spec.example.ron zone
|
||||
//
|
||||
// One file per zone type. The generator reads this to decide NPC count,
|
||||
// role distribution, and social site placement.
|
||||
//
|
||||
// Format: RON (Rusty Object Notation) — the Rust structs ARE the schema.
|
||||
// Comments are allowed. Trailing commas are allowed.
|
||||
|
||||
(
|
||||
zone_type: "rural",
|
||||
label: "Rural Settlement",
|
||||
description: "Scattered homesteads, small workshops, and communal gathering spots. Low population density, strong community bonds, subsistence-plus economy.",
|
||||
|
||||
// 1-10 scale. Rural = low economic activity, mostly self-sufficient.
|
||||
economic_level: 3,
|
||||
// 1-10 scale. Rural = sparse, everyone knows everyone.
|
||||
population_density: 2,
|
||||
|
||||
roles: [
|
||||
(
|
||||
id: "farmer",
|
||||
label: "Farmer",
|
||||
weight: 5,
|
||||
skill_focus: ["technical"],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: [
|
||||
"tends crops in the field",
|
||||
"hauls produce to the market stall",
|
||||
"repairs equipment by hand",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "mechanic",
|
||||
label: "Settlement Mechanic",
|
||||
weight: 3,
|
||||
skill_focus: ["technical"],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: [
|
||||
"works on machinery with focused intensity",
|
||||
"wipes grease on coveralls between tasks",
|
||||
"explains repairs in terse technical shorthand",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "trader",
|
||||
label: "Itinerant Trader",
|
||||
weight: 2,
|
||||
skill_focus: ["persuasion", "observation"],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: [
|
||||
"arranges goods on a portable display",
|
||||
"haggles with quiet persistence",
|
||||
"watches foot traffic from market stall",
|
||||
],
|
||||
),
|
||||
(
|
||||
id: "militia",
|
||||
label: "Settlement Militia",
|
||||
weight: 1,
|
||||
skill_focus: ["combat", "observation"],
|
||||
combat_eligible: true,
|
||||
typical_behaviors: [
|
||||
"patrols the settlement perimeter",
|
||||
"checks credentials at the gate",
|
||||
"leans on rifle while scanning the horizon",
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
social_sites: [
|
||||
(
|
||||
site_type: "tavern",
|
||||
label: "Local Tavern",
|
||||
roles: ["farmer", "mechanic", "trader", "militia"],
|
||||
min_npcs: 3,
|
||||
max_npcs: 6,
|
||||
),
|
||||
(
|
||||
site_type: "workshop",
|
||||
label: "Community Workshop",
|
||||
roles: ["mechanic", "farmer"],
|
||||
min_npcs: 2,
|
||||
max_npcs: 4,
|
||||
),
|
||||
(
|
||||
site_type: "market_stall",
|
||||
label: "Market Stall",
|
||||
roles: ["trader", "farmer"],
|
||||
min_npcs: 1,
|
||||
max_npcs: 3,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -176,4 +176,18 @@ Narrative, NPCs, dialogue, templates, setting, worldbuilding, and storyteller me
|
||||
|
||||
---
|
||||
|
||||
*19 questions (5 resolved, 2 partially resolved, 12 open). Last updated: 2026-03-05 (Q-033 partially resolved/reframed — Where's the Fun? Workshop)*
|
||||
---
|
||||
|
||||
### Q-056: Zone spec needs location_context field (surface/station/vessel)
|
||||
|
||||
- **Status:** Open
|
||||
- **Raised:** Sprint 25 PR #88 review (Miri)
|
||||
- **Context:** Rural zone spec behaviors reference sky, weather, and diurnal heat — only valid on a planetary surface, not inside a station. The current `ZoneSpec` struct has no field for environment context. Without it, the generator can't distinguish surface-rural from station-rural, and behavior strings may be incoherent for the location.
|
||||
- **Question:** Should `ZoneSpec` include a `location_context` enum (Surface/Station/Vessel) that the generator uses to filter or modify environment-specific behaviors? Or should zone specs be authored per-context (e.g. `rural-surface.ron`, `rural-station.ron`)?
|
||||
- **Implications:** Affects all zone spec authoring going forward. The generator's ability to extrapolate from minimal input depends on knowing whether "rural" means open sky or sealed corridors.
|
||||
- **Cross-reference:** D-012 (chunk-based map), D-036 (Krenn/Sova setting), D-104/D-105 (heritage roots), #609 (zone identity spec)
|
||||
- **Assigned to:** Tyre, Miri
|
||||
|
||||
---
|
||||
|
||||
*20 questions (5 resolved, 2 partially resolved, 13 open). Last updated: 2026-03-07 (Q-056 added — Sprint 25 review)*
|
||||
|
||||
@@ -8,7 +8,7 @@ Tracked questions awaiting discussion or resolution. Split by domain, mirroring
|
||||
|------|--------|-----------|
|
||||
| [questions-architecture.md](questions-architecture.md) | Technical foundation | Q-001, Q-006, Q-009, Q-018, Q-019, Q-020, Q-021, Q-022, Q-023, Q-029, Q-030, Q-046 |
|
||||
| [questions-perception.md](questions-perception.md) | Player observation | Q-003, Q-014, Q-016, Q-024, Q-025, Q-026, Q-051, Q-053, Q-054 |
|
||||
| [questions-content.md](questions-content.md) | Narrative, NPCs, setting | Q-010, Q-012, Q-013, Q-015, Q-017, Q-028, Q-031, Q-033, Q-040, Q-041, Q-042, Q-043, Q-044, Q-045, Q-047, Q-048, Q-049, Q-050, Q-052 |
|
||||
| [questions-content.md](questions-content.md) | Narrative, NPCs, setting | Q-010, Q-012, Q-013, Q-015, Q-017, Q-028, Q-031, Q-033, Q-040, Q-041, Q-042, Q-043, Q-044, Q-045, Q-047, Q-048, Q-049, Q-050, Q-052, Q-056 |
|
||||
| [questions-scope.md](questions-scope.md) | Game concept, prototype | Q-002, Q-004, Q-005, Q-007, Q-008, Q-011, Q-027, Q-032, Q-034, Q-035, Q-036, Q-037, Q-038, Q-039 |
|
||||
|
||||
## Status Summary
|
||||
|
||||
@@ -24,36 +24,38 @@ Use `tooling/db/ticket show <id>` for full details.
|
||||
|
||||
**How this sprint works for copy**
|
||||
|
||||
Server starts first. Tyre (#611) defines `ZoneSpec`, `CultureProfile`, and `NpcBlueprint` as Rust structs and writes example YAML showing the expected format. That YAML is the schema contract. Copy fills real content into that schema — not the other way around.
|
||||
Server starts first. Tyre (#611) defines `ZoneSpec`, `CultureProfile`, and `NpcBlueprint` as Rust structs and writes example RON files showing the expected format. The Rust structs ARE the schema — no separate schema file to maintain. Copy fills real content into that format.
|
||||
|
||||
Wait for #611 to deliver its example YAML before writing the real files. Coordinate with Tyre at sprint start to agree on file locations (`content/global/zone-identity-spec.yaml` and `content/global/culture-krenn.yaml` are the expected paths, but Tyre's struct design is authoritative).
|
||||
Wait for #611 to deliver its example RON before writing the real files. Tyre also ships a **RON validator CLI** (`tooling/validate-content <file.ron>`) that deserializes into the actual Rust structs and prints errors. Use it to lint your files before submitting.
|
||||
|
||||
Coordinate with Tyre at sprint start to agree on file locations (`content/global/zone-identity-spec.ron` and `content/global/culture-krenn.ron` are the expected paths, but Tyre's struct design is authoritative).
|
||||
|
||||
**#609 — Zone identity spec**
|
||||
|
||||
- Output: YAML file the generator deserializes at runtime. Schema defined by Tyre's `ZoneSpec` struct from #611.
|
||||
- Output: RON file the generator deserializes at runtime. Schema defined by Tyre's `ZoneSpec` struct from #611.
|
||||
- Minimum two zone types with real content: **rural** and **industrial**. These are the two the sprint proof runs. Remaining types can be stubs with plausible values.
|
||||
- The taxonomy must make the generator produce visibly different output per zone type — if rural and industrial look the same, it has failed.
|
||||
- Content scope: what varies between zone types (density, pace, social site mix, NPC role distribution). Not prose worldbuilding — structured parameters that the Rust generator can read.
|
||||
- Do not invent the schema. Read #611's example YAML first.
|
||||
- Do not invent the schema. Read #611's example RON first.
|
||||
|
||||
**#610 — Krenn culture profile**
|
||||
|
||||
- Output: YAML file the generator deserializes at runtime. Schema defined by Tyre's `CultureProfile` struct from #611.
|
||||
- Output: RON file the generator deserializes at runtime. Schema defined by Tyre's `CultureProfile` struct from #611.
|
||||
- Must provide enough cultural signal that generated NPCs feel Krenn, not generic-space-village.
|
||||
- Sprint scope: **name lists** (not phoneme generation rules — Tyre's generator picks from lists), speech markers, economic values, social norms.
|
||||
- Phoneme-based name generation is explicitly out of scope for this sprint. A curated list of Krenn-sounding names is sufficient.
|
||||
- Existing Krenn atmosphere and naming examples in `decisions/content.md` D-036 (amended post-workshop) are a starting point. Go deeper on concrete values (specific speech markers, actual name examples) not broader on atmospheric description.
|
||||
- Do not invent the schema. Read #611's example YAML first.
|
||||
- Do not invent the schema. Read #611's example RON first.
|
||||
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
server #611 (schema contract) ──> #609 (zone spec YAML) ──┐
|
||||
├──> server #612 (generator)
|
||||
──> #610 (culture YAML) ───────┘
|
||||
server #611 (schema + validator) ──> #609 (zone spec RON) ──┐
|
||||
├──> server #612 (generator)
|
||||
──> #610 (culture RON) ──────┘
|
||||
```
|
||||
|
||||
Server defines the shape. Copy fills it. Both #609 and #610 can be written in parallel once #611 delivers its example YAML.
|
||||
Server defines the shape. Copy fills it. Both #609 and #610 can be written in parallel once #611 delivers its example RON.
|
||||
|
||||
## PR Workflow
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ Use `tooling/db/ticket show <id>` for full details.
|
||||
|
||||
This sprint discovers the right spec — it does not implement a known one. Three phases:
|
||||
|
||||
- **Phase 0 (#611):** Define the Rust structs (`ZoneSpec`, `CultureProfile`, `NpcBlueprint`) and write example YAML. Share with copy team immediately — this unblocks #609 and #610.
|
||||
- **Phase 1 (#612, early):** Build the generator binary with hardcoded test data. Do not wait for copy to finish their YAML. Hardcode two zone profiles (rural, industrial stub) and a Krenn culture stub in Rust. Get the generation pipeline and stdout output working end-to-end.
|
||||
- **Phase 2 (#612, late):** Swap hardcoded stubs for real YAML loading from disk. Wire in copy's actual #609 and #610 files. Run the two-zone proof.
|
||||
- **Phase 0 (#611):** Define the Rust structs (`ZoneSpec`, `CultureProfile`, `NpcBlueprint`) and write example RON files. Build the RON validator CLI. Share with copy team immediately — this unblocks #609 and #610.
|
||||
- **Phase 1 (#612, early):** Build the generator binary with hardcoded test data. Do not wait for copy to finish their RON files. Hardcode two zone profiles (rural, industrial stub) and a Krenn culture stub in Rust. Get the generation pipeline and stdout output working end-to-end.
|
||||
- **Phase 2 (#612, late):** Swap hardcoded stubs for real RON loading from disk. Wire in copy's actual #609 and #610 files. Run the two-zone proof.
|
||||
|
||||
This phasing means the copy team's blocking relationship is on the final integration, not the generator build. Server can move through Phase 0 and Phase 1 in parallel with copy writing #609/#610.
|
||||
|
||||
@@ -43,21 +43,22 @@ This phasing means the copy team's blocking relationship is on the final integra
|
||||
|
||||
- Starts immediately. No blockers.
|
||||
- Define three structs in `server/src/npc/blueprint.rs` (new file):
|
||||
- `ZoneSpec` — deserializes from zone-identity-spec.yaml
|
||||
- `CultureProfile` — deserializes from culture-krenn.yaml
|
||||
- `ZoneSpec` — deserializes from zone-identity-spec.ron
|
||||
- `CultureProfile` — deserializes from culture-krenn.ron
|
||||
- `NpcBlueprint` — generator output for a single NPC
|
||||
- All three derive `Serialize`, `Deserialize` (serde + serde_yaml).
|
||||
- All three derive `Serialize`, `Deserialize` (serde + `ron`). **Use RON format, not YAML/JSON.** RON is Rust-native, struct-aware, supports enums and comments. The Rust structs ARE the schema — no separate schema file to maintain.
|
||||
- `NpcBlueprint` fields: name (String), role (occupation), traits (Vec of trait enum), observable_behaviors (Vec<String>), cultural_markers (speech register, filler words from culture profile), relationships (Vec of (npc_id, relationship_type, valence)).
|
||||
- Use a spike-specific `SpikeOutput` struct for the binary's top-level output — do NOT couple to `DistrictSkeleton` for the proof. Keep the spike isolated.
|
||||
- Key deliverable: write `content/global/zone-identity-spec.example.yaml` and `content/global/culture-krenn.example.yaml` showing the schema copy must fill. Share these with Miri before copy starts writing real content.
|
||||
- Key deliverable: write `content/global/zone-identity-spec.example.ron` and `content/global/culture-krenn.example.ron` showing the schema copy must fill. Share these with Miri before copy starts writing real content.
|
||||
- Add a note in the file header pointing to the tickets (#609, #610) that fill these schemas with real content.
|
||||
- **Build a RON validator CLI** (`tooling/validate-content <file.ron>`) that deserializes into the actual Rust structs and prints errors. This is the copy team's lint tool — they run it to check their files without needing to compile the server. ~20 lines of Rust, ship it as part of #611.
|
||||
|
||||
**#612 — Template assembly generator (absorbs #613)**
|
||||
|
||||
- Blocked by #611. Build Phase 1 before #609/#610 arrive; integrate in Phase 2.
|
||||
- Binary: `cargo run --bin generator-spike -- --zone <type> --seed <n>` (new binary in `server/src/bin/`).
|
||||
- Phase 1: hardcoded `ZoneSpec` and `CultureProfile` stubs in Rust. Focus on the generation logic and output formatting.
|
||||
- Phase 2: load zone spec and culture YAML from disk at runtime. Zone taxonomy is the file — adding a new zone type requires zero Rust changes.
|
||||
- Phase 2: load zone spec and culture RON from disk at runtime. Zone taxonomy is the file — adding a new zone type requires zero Rust changes.
|
||||
- Determinism: `SimRng` seeded from the `--seed` flag. Same inputs = same output.
|
||||
- NPC generation: use the existing `npc/generate.rs` pipeline. `NpcBlueprint` maps to `RoleDefinition` via a conversion method. The blueprint's cultural markers bias trait selection.
|
||||
- Stdout output per invocation: zone type header, NPC list (name, role, traits, one observable behavior), relationship pairs ("A knows B as colleague (positive)").
|
||||
@@ -66,9 +67,9 @@ This phasing means the copy team's blocking relationship is on the final integra
|
||||
## Dependency Chain
|
||||
|
||||
```
|
||||
#611 (structs + example YAML) ──> copy #609 (zone spec) ──┐
|
||||
──> copy #610 (culture) ──┴──> #612 (generator, Phase 2)
|
||||
#611 ──────────────────────────────────────────────────────────> #612 (generator, Phase 1 — no YAML needed)
|
||||
#611 (structs + example RON + validator) ──> copy #609 (zone spec) ──┐
|
||||
──> copy #610 (culture) ──┴──> #612 (generator, Phase 2)
|
||||
#611 ──────────────────────────────────────────────────────────────────> #612 (generator, Phase 1 — no RON needed)
|
||||
```
|
||||
|
||||
#611 first. Phase 1 of #612 runs in parallel with copy writing #609/#610. Phase 2 of #612 waits for both.
|
||||
@@ -83,7 +84,7 @@ From Troblum's pre-sprint review. Read before starting.
|
||||
|
||||
3. **`DayPhase` name collision.** `server/src/simulation/generator.rs` defines `DayPhase = String` as a stub type alias, shadowing the real `DayPhase` enum in `server/src/simulation/time.rs`. Use the real enum explicitly or alias the stub out of scope before the spike binary sees both. Do not let the collision silently compile to the wrong type.
|
||||
|
||||
4. **Schema negotiation takes rounds.** The first YAML draft from copy will not deserialize cleanly. Build in slack between Phase 1 and Phase 2 — expect at least one round of struct adjustments after seeing real content.
|
||||
4. **Schema negotiation takes rounds.** The first RON draft from copy will not deserialize cleanly — the validator will catch this early. Build in slack between Phase 1 and Phase 2 — expect at least one round of struct adjustments after seeing real content.
|
||||
|
||||
## PR Workflow
|
||||
|
||||
|
||||
Generated
+20
-1
@@ -128,6 +128,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "bevy_app"
|
||||
version = "0.18.0"
|
||||
@@ -1001,6 +1007,18 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bitflags",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
@@ -1092,7 +1110,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "settled-reach-server"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
dependencies = [
|
||||
"bevy_app",
|
||||
"bevy_ecs",
|
||||
@@ -1102,6 +1120,7 @@ dependencies = [
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rmp-serde",
|
||||
"ron",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
|
||||
@@ -8,6 +8,7 @@ bevy_ecs = "0.18"
|
||||
bevy_app = "0.18"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
ron = "0.8"
|
||||
rmp-serde = "1"
|
||||
bincode = "1"
|
||||
rand = "0.9"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
//! RON content validator CLI (#611).
|
||||
//!
|
||||
//! Deserializes a RON file into the actual Rust structs and prints errors.
|
||||
//! This is the copy team's lint tool — run it to check RON files without
|
||||
//! needing to compile the full server.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```sh
|
||||
//! # Via wrapper script (recommended):
|
||||
//! tooling/validate-ron content/global/zone-identity-spec.example.ron zone
|
||||
//! tooling/validate-ron content/global/culture-krenn.example.ron culture
|
||||
//!
|
||||
//! # Direct:
|
||||
//! cargo run --bin validate_ron -- <file.ron> <zone|culture>
|
||||
//! ```
|
||||
|
||||
use std::process;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use settled_reach_server::npc::blueprint::{CultureProfile, ZoneSpec};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "validate_ron",
|
||||
about = "Validate RON content files against Rust struct schemas"
|
||||
)]
|
||||
struct Args {
|
||||
/// Path to the RON file to validate.
|
||||
file: String,
|
||||
/// Schema type: "zone" (ZoneSpec) or "culture" (CultureProfile).
|
||||
schema: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
let content = match std::fs::read_to_string(&args.file) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading {}: {}", args.file, e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
match args.schema.as_str() {
|
||||
"zone" => match ron::from_str::<ZoneSpec>(&content) {
|
||||
Ok(spec) => {
|
||||
println!("Valid ZoneSpec: {} ({})", spec.label, spec.zone_type);
|
||||
println!(" {} roles, {} social sites", spec.roles.len(), spec.social_sites.len());
|
||||
let mut warnings = 0;
|
||||
if spec.roles.is_empty() {
|
||||
eprintln!(" WARNING: no roles defined — generator will reject this");
|
||||
warnings += 1;
|
||||
}
|
||||
if spec.population_density < 1 {
|
||||
eprintln!(" WARNING: population_density is 0 — generator requires >= 1");
|
||||
warnings += 1;
|
||||
}
|
||||
for site in &spec.social_sites {
|
||||
if site.min_npcs > site.max_npcs {
|
||||
eprintln!(
|
||||
" WARNING: social site '{}' has min_npcs ({}) > max_npcs ({})",
|
||||
site.site_type, site.min_npcs, site.max_npcs
|
||||
);
|
||||
warnings += 1;
|
||||
}
|
||||
}
|
||||
if warnings > 0 {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Invalid ZoneSpec in {}:", args.file);
|
||||
eprintln!(" {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
"culture" => match ron::from_str::<CultureProfile>(&content) {
|
||||
Ok(profile) => {
|
||||
println!("Valid CultureProfile: {} ({})", profile.name, profile.id);
|
||||
println!(
|
||||
" {} given names, {} family names",
|
||||
profile.naming.given_names.len(),
|
||||
profile.naming.family_names.len()
|
||||
);
|
||||
println!(
|
||||
" {} filler words, {} favored traits",
|
||||
profile.speech.filler_words.len(),
|
||||
profile.values.favored_traits.len()
|
||||
);
|
||||
let mut warnings = 0;
|
||||
if profile.naming.given_names.is_empty() {
|
||||
eprintln!(" WARNING: no given_names — generator will reject this");
|
||||
warnings += 1;
|
||||
}
|
||||
if warnings > 0 {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Invalid CultureProfile in {}:", args.file);
|
||||
eprintln!(" {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
other => {
|
||||
eprintln!("Unknown schema type: '{}'. Use 'zone' or 'culture'.", other);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
//! NPC blueprint structs for the generator spike (#611).
|
||||
//!
|
||||
//! Three input/output structs for the generator proof-of-life:
|
||||
//! - `ZoneSpec` — zone identity specification (deserializes from zone-identity-spec RON)
|
||||
//! - `CultureProfile` — culture profile (deserializes from culture RON)
|
||||
//! - `NpcBlueprint` — generator output for a single NPC
|
||||
//!
|
||||
//! Plus `SpikeOutput` as the top-level binary output container.
|
||||
//! These are spike-specific and intentionally decoupled from `DistrictSkeleton`.
|
||||
//!
|
||||
//! ## Content contract
|
||||
//!
|
||||
//! The Rust structs ARE the schema. Copy team fills RON files to match these structs:
|
||||
//! - Zone identity spec: ticket #609
|
||||
//! - Culture profile (Krenn): ticket #610
|
||||
//!
|
||||
//! ## Format
|
||||
//!
|
||||
//! RON (Rusty Object Notation) — struct-aware, supports enums and comments.
|
||||
//! Validate with: `tooling/validate-ron <file.ron>`
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::npc::PersonalityTrait;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zone identity specification (input — filled by copy team, ticket #609)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Top-level zone identity spec. One file per zone type.
|
||||
///
|
||||
/// Describes what a zone IS — its purpose, population shape, and social sites.
|
||||
/// The generator reads this to decide how many NPCs to create, what roles they
|
||||
/// fill, and what social sites exist.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ZoneSpec {
|
||||
/// Zone type identifier (e.g. "rural", "industrial", "commercial").
|
||||
pub zone_type: String,
|
||||
/// Human-readable label for output headers.
|
||||
pub label: String,
|
||||
/// Short description of this zone type's character.
|
||||
pub description: String,
|
||||
/// Economic activity level (1-10). Affects NPC count and role distribution.
|
||||
pub economic_level: u8,
|
||||
/// Population density hint (1-10). Generator scales NPC count from this.
|
||||
pub population_density: u8,
|
||||
/// Role definitions available in this zone type.
|
||||
/// Each role has a weight (relative frequency) and properties.
|
||||
pub roles: Vec<RoleSpec>,
|
||||
/// Social site templates that can appear in this zone type.
|
||||
pub social_sites: Vec<SocialSiteSpec>,
|
||||
}
|
||||
|
||||
/// A role that NPCs can fill in a zone.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoleSpec {
|
||||
/// Role identifier (e.g. "dock_worker", "merchant", "guard").
|
||||
pub id: String,
|
||||
/// Human-readable label.
|
||||
pub label: String,
|
||||
/// Relative weight for role selection (higher = more common).
|
||||
pub weight: u8,
|
||||
/// Skills biased toward for this role.
|
||||
pub skill_focus: Vec<String>,
|
||||
/// Whether this role may have combat capability.
|
||||
pub combat_eligible: bool,
|
||||
/// Observable behaviors typical for this role (generator picks from these).
|
||||
pub typical_behaviors: Vec<String>,
|
||||
}
|
||||
|
||||
/// A social site template within a zone.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SocialSiteSpec {
|
||||
/// Site type identifier (e.g. "bar", "workshop", "market_stall").
|
||||
pub site_type: String,
|
||||
/// Human-readable label.
|
||||
pub label: String,
|
||||
/// Roles that staff or frequent this site (references RoleSpec.id).
|
||||
pub roles: Vec<String>,
|
||||
/// Minimum NPCs associated with this site.
|
||||
pub min_npcs: u8,
|
||||
/// Maximum NPCs associated with this site.
|
||||
pub max_npcs: u8,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Culture profile (input — filled by copy team, ticket #610)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Culture profile for a star system or region.
|
||||
///
|
||||
/// Drives the cultural texture of generated NPCs: how they speak, what names
|
||||
/// they have, what values shape their personality distribution.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CultureProfile {
|
||||
/// Culture identifier (e.g. "krenn").
|
||||
pub id: String,
|
||||
/// Human-readable culture name.
|
||||
pub name: String,
|
||||
/// Short cultural description for generator context.
|
||||
pub description: String,
|
||||
/// Naming conventions for this culture.
|
||||
pub naming: NamingConventions,
|
||||
/// Speech patterns — how NPCs from this culture talk.
|
||||
pub speech: SpeechPatterns,
|
||||
/// Cultural values that bias personality trait selection.
|
||||
pub values: CulturalValues,
|
||||
}
|
||||
|
||||
/// Naming conventions for NPC name generation.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NamingConventions {
|
||||
/// Style description (e.g. "compact, consonant-heavy, first-name-primary").
|
||||
pub style: String,
|
||||
/// Pool of given names the generator draws from.
|
||||
pub given_names: Vec<String>,
|
||||
/// Pool of family names (may be empty if culture is first-name-primary).
|
||||
pub family_names: Vec<String>,
|
||||
/// Whether family names are commonly used in social contexts.
|
||||
pub family_name_used_socially: bool,
|
||||
}
|
||||
|
||||
/// Speech patterns that color NPC dialogue and observable behaviors.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpeechPatterns {
|
||||
/// Speech register description (e.g. "direct, minimal pleasantries").
|
||||
pub register: String,
|
||||
/// Filler words and verbal tics drawn from this culture.
|
||||
pub filler_words: Vec<String>,
|
||||
/// Common greetings.
|
||||
pub greetings: Vec<String>,
|
||||
/// Common farewells.
|
||||
pub farewells: Vec<String>,
|
||||
/// Oath or exclamation phrases.
|
||||
pub exclamations: Vec<String>,
|
||||
}
|
||||
|
||||
/// Cultural values that influence personality trait distribution.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CulturalValues {
|
||||
/// Brief description of the culture's value system.
|
||||
pub description: String,
|
||||
/// Traits that are more common in this culture (biased toward).
|
||||
pub favored_traits: Vec<PersonalityTrait>,
|
||||
/// Traits that are less common in this culture (biased against).
|
||||
pub disfavored_traits: Vec<PersonalityTrait>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NPC blueprint (output — generator produces these)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Internal motive/state driving an NPC's micro-behavior (#632).
|
||||
///
|
||||
/// The player never sees this directly — it leaks through the observable
|
||||
/// behavior as a tell. The gap between what an NPC is doing and WHY is
|
||||
/// the asymmetric information mechanic (D-010 principle 2).
|
||||
///
|
||||
/// `Neutral` means no notable motive — the NPC is simply doing their job.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum NpcWant {
|
||||
/// Nothing stands out; routine work mode.
|
||||
Neutral,
|
||||
/// Disengaged from current task; attention elsewhere.
|
||||
Bored,
|
||||
/// Heightened attention to the environment; scanning, tracking.
|
||||
Alert,
|
||||
/// Something is wrong or off; watching without being obvious about it.
|
||||
Suspicious,
|
||||
/// Steering clear of a specific person in the zone.
|
||||
AvoidingSomeone,
|
||||
/// Actively trying to pick up information by proximity or conversation.
|
||||
LookingForInfo,
|
||||
}
|
||||
|
||||
/// Generator output for a single NPC.
|
||||
///
|
||||
/// Maps to the 10-axis model (D-024) but as a serializable data record,
|
||||
/// not ECS components. The spike binary prints these; the full pipeline
|
||||
/// will convert `NpcBlueprint` → `RoleDefinition` → ECS entity.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NpcBlueprint {
|
||||
/// Generated NPC name.
|
||||
pub name: String,
|
||||
/// Role identifier (matches RoleSpec.id from the zone spec).
|
||||
pub role: String,
|
||||
/// Personality traits (2-3, no contradictory pairs).
|
||||
pub traits: Vec<PersonalityTrait>,
|
||||
/// Internal motive that biases observable behavior (#632).
|
||||
/// The player never sees this label — they infer it from the behavior.
|
||||
pub want: NpcWant,
|
||||
/// Observable behaviors the player can witness.
|
||||
/// Index 0 is the primary role/relationship-driven action.
|
||||
/// Index 1 (if present) is the Want tell — the leak of the internal state.
|
||||
pub observable_behaviors: Vec<String>,
|
||||
/// Cultural markers derived from the culture profile.
|
||||
pub cultural_markers: CulturalMarkers,
|
||||
/// Relationship slots (0-3 per D-024).
|
||||
pub relationships: Vec<BlueprintRelationship>,
|
||||
}
|
||||
|
||||
/// Cultural markers attached to a generated NPC.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CulturalMarkers {
|
||||
/// Speech register inherited from culture.
|
||||
pub speech_register: String,
|
||||
/// Filler words this NPC uses (subset of culture's pool).
|
||||
pub filler_words: Vec<String>,
|
||||
/// Greeting this NPC tends to use.
|
||||
pub greeting: String,
|
||||
}
|
||||
|
||||
/// A relationship in the blueprint (pre-ECS, uses string IDs).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BlueprintRelationship {
|
||||
/// Target NPC name (resolved to StableId at spawn time).
|
||||
pub target_name: String,
|
||||
/// Relationship type.
|
||||
pub relationship_type: String,
|
||||
/// Valence: positive, negative, or neutral.
|
||||
pub valence: RelationshipValence,
|
||||
}
|
||||
|
||||
/// Relationship valence for blueprint output.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RelationshipValence {
|
||||
Positive,
|
||||
Negative,
|
||||
Neutral,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spike output container
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Top-level output for the generator spike binary.
|
||||
///
|
||||
/// Intentionally decoupled from `DistrictSkeleton` — this is a proof-of-life
|
||||
/// container, not production architecture.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpikeOutput {
|
||||
/// Zone type that was generated.
|
||||
pub zone_type: String,
|
||||
/// Seed used for generation.
|
||||
pub seed: u64,
|
||||
/// Culture profile used.
|
||||
pub culture: String,
|
||||
/// Generated NPCs.
|
||||
pub npcs: Vec<NpcBlueprint>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn zone_spec_round_trips_through_ron() {
|
||||
let spec = ZoneSpec {
|
||||
zone_type: "rural".into(),
|
||||
label: "Rural Settlement".into(),
|
||||
description: "Scattered homesteads and small workshops".into(),
|
||||
economic_level: 3,
|
||||
population_density: 2,
|
||||
roles: vec![RoleSpec {
|
||||
id: "farmer".into(),
|
||||
label: "Farmer".into(),
|
||||
weight: 5,
|
||||
skill_focus: vec!["technical".into()],
|
||||
combat_eligible: false,
|
||||
typical_behaviors: vec!["tends crops".into()],
|
||||
}],
|
||||
social_sites: vec![SocialSiteSpec {
|
||||
site_type: "tavern".into(),
|
||||
label: "Local Tavern".into(),
|
||||
roles: vec!["farmer".into()],
|
||||
min_npcs: 2,
|
||||
max_npcs: 5,
|
||||
}],
|
||||
};
|
||||
|
||||
let ron_str = ron::ser::to_string_pretty(&spec, ron::ser::PrettyConfig::default()).unwrap();
|
||||
let deserialized: ZoneSpec = ron::from_str(&ron_str).unwrap();
|
||||
assert_eq!(deserialized.zone_type, "rural");
|
||||
assert_eq!(deserialized.roles.len(), 1);
|
||||
assert_eq!(deserialized.social_sites.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn culture_profile_round_trips_through_ron() {
|
||||
let profile = CultureProfile {
|
||||
id: "krenn".into(),
|
||||
name: "Krenn System Culture".into(),
|
||||
description: "Working-class pragmatic culture".into(),
|
||||
naming: NamingConventions {
|
||||
style: "compact, consonant-heavy".into(),
|
||||
given_names: vec!["Kael".into(), "Voss".into()],
|
||||
family_names: vec!["Davan".into()],
|
||||
family_name_used_socially: false,
|
||||
},
|
||||
speech: SpeechPatterns {
|
||||
register: "direct, minimal pleasantries".into(),
|
||||
filler_words: vec!["look".into(), "right".into()],
|
||||
greetings: vec!["hey".into()],
|
||||
farewells: vec!["shift's calling".into()],
|
||||
exclamations: vec!["void take it".into()],
|
||||
},
|
||||
values: CulturalValues {
|
||||
description: "Pragmatic, community-oriented, suspicious of authority".into(),
|
||||
favored_traits: vec![PersonalityTrait::Bold, PersonalityTrait::Honest],
|
||||
disfavored_traits: vec![PersonalityTrait::Reclusive],
|
||||
},
|
||||
};
|
||||
|
||||
let ron_str =
|
||||
ron::ser::to_string_pretty(&profile, ron::ser::PrettyConfig::default()).unwrap();
|
||||
let deserialized: CultureProfile = ron::from_str(&ron_str).unwrap();
|
||||
assert_eq!(deserialized.id, "krenn");
|
||||
assert_eq!(deserialized.naming.given_names.len(), 2);
|
||||
assert_eq!(deserialized.values.favored_traits.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npc_blueprint_round_trips_through_ron() {
|
||||
let blueprint = NpcBlueprint {
|
||||
name: "Kael".into(),
|
||||
role: "dock_worker".into(),
|
||||
traits: vec![PersonalityTrait::Bold, PersonalityTrait::Honest],
|
||||
want: NpcWant::Alert,
|
||||
observable_behaviors: vec!["works efficiently".into()],
|
||||
cultural_markers: CulturalMarkers {
|
||||
speech_register: "direct".into(),
|
||||
filler_words: vec!["look".into()],
|
||||
greeting: "hey".into(),
|
||||
},
|
||||
relationships: vec![BlueprintRelationship {
|
||||
target_name: "Voss".into(),
|
||||
relationship_type: "colleague".into(),
|
||||
valence: RelationshipValence::Positive,
|
||||
}],
|
||||
};
|
||||
|
||||
let ron_str =
|
||||
ron::ser::to_string_pretty(&blueprint, ron::ser::PrettyConfig::default()).unwrap();
|
||||
let deserialized: NpcBlueprint = ron::from_str(&ron_str).unwrap();
|
||||
assert_eq!(deserialized.name, "Kael");
|
||||
assert_eq!(deserialized.traits.len(), 2);
|
||||
assert_eq!(deserialized.relationships.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spike_output_round_trips_through_ron() {
|
||||
let output = SpikeOutput {
|
||||
zone_type: "rural".into(),
|
||||
seed: 42,
|
||||
culture: "krenn".into(),
|
||||
npcs: vec![],
|
||||
};
|
||||
|
||||
let ron_str =
|
||||
ron::ser::to_string_pretty(&output, ron::ser::PrettyConfig::default()).unwrap();
|
||||
let deserialized: SpikeOutput = ron::from_str(&ron_str).unwrap();
|
||||
assert_eq!(deserialized.zone_type, "rural");
|
||||
assert_eq!(deserialized.seed, 42);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
pub mod awareness;
|
||||
pub mod background;
|
||||
pub mod blueprint;
|
||||
pub mod disclosure;
|
||||
pub mod generate;
|
||||
pub mod interaction;
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# RON content validator — wrapper for the Rust validate_ron binary (#611).
|
||||
#
|
||||
# Usage:
|
||||
# tooling/validate-ron <file.ron> <zone|culture>
|
||||
#
|
||||
# Examples:
|
||||
# tooling/validate-ron content/global/zone-identity-spec.example.ron zone
|
||||
# tooling/validate-ron content/global/culture-krenn.example.ron culture
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: tooling/validate-ron <file.ron> <zone|culture>"
|
||||
echo ""
|
||||
echo "Validates a RON file against the Rust struct schema."
|
||||
echo "Schema types:"
|
||||
echo " zone — ZoneSpec (zone identity spec)"
|
||||
echo " culture — CultureProfile (culture profile)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check file exists before resolving — realpath gives unhelpful errors otherwise
|
||||
if [ ! -f "$1" ]; then
|
||||
echo "Error: file not found: $1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILE="$(realpath "$1")"
|
||||
SCHEMA="$2"
|
||||
|
||||
cd "$(dirname "$0")/../server"
|
||||
exec cargo run --quiet --bin validate_ron -- "$FILE" "$SCHEMA"
|
||||
Reference in New Issue
Block a user