feat(simulation): NpcBlueprint struct design, RON schema, and validator CLI (#611)
Define ZoneSpec, CultureProfile, and NpcBlueprint structs with serde/RON deserialization. Ship example RON files as schema contract for the copy team (#609, #610). Add validate-ron CLI for copy team to lint their files without compiling the server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
Generated
+20
-1
@@ -128,6 +128,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.21.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy_app"
|
name = "bevy_app"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
@@ -1001,6 +1007,18 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -1092,7 +1110,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "settled-reach-server"
|
name = "settled-reach-server"
|
||||||
version = "0.1.23"
|
version = "0.1.24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bevy_app",
|
"bevy_app",
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
@@ -1102,6 +1120,7 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ bevy_ecs = "0.18"
|
|||||||
bevy_app = "0.18"
|
bevy_app = "0.18"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
ron = "0.8"
|
||||||
rmp-serde = "1"
|
rmp-serde = "1"
|
||||||
bincode = "1"
|
bincode = "1"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
//! 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());
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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,340 @@
|
|||||||
|
//! 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)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
/// Observable behaviors the player can witness.
|
||||||
|
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],
|
||||||
|
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 awareness;
|
||||||
pub mod background;
|
pub mod background;
|
||||||
|
pub mod blueprint;
|
||||||
pub mod disclosure;
|
pub mod disclosure;
|
||||||
pub mod generate;
|
pub mod generate;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
|
|||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# Resolve file to absolute path before changing directory
|
||||||
|
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