Skip to main content

dreamwell_engine/waymark/
schema.rs

1// Dreamwell Waymark v1.0.0 — Unified Engine Configuration Schema.
2//
3// Single source of truth for all configurable engine parameters exposed to
4// the Waymark content management system. Every sub-config carries sensible
5// defaults so that a minimal pack.json (id + title) is a valid document.
6//
7// Backward compatible with existing pack.json format: all legacy fields
8// deserialize directly into the v1.0.0 struct hierarchy.
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13// =============================================================================
14// §0  SCHEMA VERSION
15// =============================================================================
16
17/// Current schema version string.
18pub const SCHEMA_VERSION: &str = "dreamwell_waymark_v1.0.0";
19
20fn default_schema_version() -> String {
21    SCHEMA_VERSION.to_string()
22}
23
24// =============================================================================
25// §1  ROOT — DreamwellPackV1
26// =============================================================================
27
28/// Dreamwell Waymark v1.0.0 — Unified Engine Configuration Schema.
29/// Single source of truth for all configurable engine parameters.
30/// Backward compatible with existing pack.json format.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DreamwellPackV1 {
33    // === Pack Identity ===
34    /// Unique pack identifier (kebab_case). Required, must be non-empty.
35    pub id: String,
36
37    /// Human-readable pack title.
38    pub title: String,
39
40    /// Semantic version string (e.g. "1.0.0").
41    #[serde(default)]
42    pub version: String,
43
44    /// Pack description.
45    #[serde(default)]
46    pub description: String,
47
48    /// Schema version. Defaults to "dreamwell_waymark_v1.0.0".
49    #[serde(default = "default_schema_version")]
50    pub schema_version: String,
51
52    /// Optional tags for categorization and search.
53    #[serde(default)]
54    pub tags: Vec<String>,
55
56    /// Optional world ID this pack targets (e.g. "world:braxxis:v1").
57    #[serde(default)]
58    pub world_id: Option<String>,
59
60    /// Optional theme identifier (e.g. "SURVIVAL", "FANTASY").
61    #[serde(default)]
62    pub theme: Option<String>,
63
64    // === Entry Point ===
65    /// Scenario enum variant or identifier.
66    #[serde(default)]
67    pub scenario: Option<String>,
68
69    /// Path to the Waymark scenario script (relative to pack root).
70    #[serde(default)]
71    pub scenario_script: Option<String>,
72
73    /// Starting map identifier.
74    #[serde(default)]
75    pub starting_map: Option<String>,
76
77    // === Grid & Spatial ===
78    /// Grid dimensions for map generation.
79    #[serde(default)]
80    pub grid: GridConfig,
81
82    /// Spatial engine parameters (cell size, FOV, pathfinding).
83    #[serde(default)]
84    pub spatial: SpatialConfig,
85
86    // === Display ===
87    /// Client display configuration.
88    #[serde(default)]
89    pub display: DisplayConfig,
90
91    // === Features ===
92    /// Feature flags controlling which engine subsystems are active.
93    #[serde(default)]
94    pub features: FeatureFlags,
95
96    // === Equipment ===
97    /// Ordered list of equipment slot names.
98    #[serde(default)]
99    pub equip_slots: Vec<String>,
100
101    // === Simulation ===
102    /// Core simulation tick and entity limits.
103    #[serde(default)]
104    pub simulation: SimulationConfig,
105
106    // === Combat ===
107    /// Combat formula parameters.
108    #[serde(default)]
109    pub combat: CombatConfig,
110
111    // === Economy ===
112    /// Economy and trade parameters.
113    #[serde(default)]
114    pub economy: EconomyConfig,
115
116    // === Progression ===
117    /// Leveling, XP, and reputation parameters.
118    #[serde(default)]
119    pub progression: ProgressionConfig,
120
121    // === AI & Agents ===
122    /// Agent cognition and inference parameters.
123    #[serde(default)]
124    pub agents: AgentConfig,
125
126    // === Tiles & Visual ===
127    /// Tile ID mappings, movement costs, and collision rules.
128    #[serde(default)]
129    pub tiles: TileConfig,
130
131    // === Canon Event ===
132    /// Canon event pipeline parameters.
133    #[serde(default)]
134    pub canon: CanonConfig,
135
136    // === Waymark Scripting ===
137    /// Waymark scripting engine limits.
138    #[serde(default)]
139    pub scripting: ScriptingConfig,
140
141    // === Content ===
142    /// Paths to content data files (items, enemies, abilities, etc.).
143    #[serde(default)]
144    pub content: ContentConfig,
145
146    // === Map Eviction ===
147    /// Map eviction and caching parameters.
148    #[serde(default)]
149    pub eviction: EvictionConfig,
150
151    // === Props ===
152    /// Prop definitions for this pack.
153    #[serde(default)]
154    pub props: Vec<PropDefinition>,
155
156    // === Generators ===
157    /// Map generator configurations (pack-specific, arbitrary structure).
158    #[serde(default)]
159    pub generators: serde_json::Value,
160
161    // === Connection Props ===
162    /// Mapping of connection types to prop IDs.
163    #[serde(default)]
164    pub connection_type_props: HashMap<String, String>,
165
166    // === Boon Triggers ===
167    /// Boon trigger definitions (pack-specific, arbitrary structure).
168    #[serde(default)]
169    pub boon_triggers: Vec<serde_json::Value>,
170
171    // === Companion Services ===
172    /// Whether this pack uses companion (AI) services.
173    #[serde(default)]
174    pub uses_companion_services: bool,
175
176    // === Scenario Config (arbitrary, pack-specific) ===
177    /// Pack-specific scenario configuration. Arbitrary JSON.
178    #[serde(default)]
179    pub scenario_config: serde_json::Value,
180
181    // === Encumbrance (legacy field from existing packs) ===
182    /// Optional encumbrance configuration (pack-specific).
183    #[serde(default)]
184    pub encumbrance: Option<serde_json::Value>,
185
186    // === AI Brains (legacy field from existing packs) ===
187    /// AI brain definitions for NPCs/creatures.
188    #[serde(default)]
189    pub ai_brains: HashMap<String, serde_json::Value>,
190
191    // === Rules (legacy field from existing packs) ===
192    /// Pack-specific game rules (arbitrary structure).
193    #[serde(default)]
194    pub rules: Option<serde_json::Value>,
195
196    // === Identity (legacy field from existing packs) ===
197    /// Identity system configuration (pack-specific).
198    #[serde(default)]
199    pub identity: Option<serde_json::Value>,
200
201    // === Boot Sequence (legacy field from existing packs) ===
202    /// Boot sequence display configuration (pack-specific).
203    #[serde(default)]
204    pub boot_sequence: Option<serde_json::Value>,
205
206    // === Topology (v1.0.0) ===
207    /// 9-layer topology configuration. Defines the cosmic-to-point hierarchy
208    /// for this content pack. If omitted, seed creates a minimal 1-of-each.
209    #[serde(default)]
210    pub topology: TopologyConfig,
211
212    // === Chronoshift (v1.0.0) ===
213    /// Chronoshift (timeline fork/replay) parameters.
214    #[serde(default)]
215    pub chronoshift: ChronoshiftConfig,
216
217    // === Forensics (v1.0.0) ===
218    /// Forensics and proof lane parameters.
219    #[serde(default)]
220    pub forensics: ForensicsConfig,
221
222    // === Physics (v1.0.0) ===
223    /// Physics environment parameters for pack-driven worlds.
224    #[serde(default)]
225    pub physics: PhysicsConfig,
226
227    // === GPU Pipeline (v1.0.0) ===
228    /// Meshlet LOD configuration for GPU-driven rendering.
229    #[serde(default)]
230    pub meshlet_lod: Option<MeshletLodConfig>,
231
232    /// Meshlet emission configuration for DreamMatter.
233    #[serde(default)]
234    pub meshlet_emission: Option<MeshletEmissionConfig>,
235
236    /// Observer configuration for Quantum Culling.
237    #[serde(default)]
238    pub observer_config: Option<WaymarkObserverConfig>,
239
240    /// Promotion targets for DreamMatter → server authority.
241    #[serde(default)]
242    pub promotion_targets: Vec<WaymarkPromotionTarget>,
243    /// Default avatar physics/locomotion configuration.
244    #[serde(default)]
245    pub avatar_defaults: Option<AvatarDefaults>,
246}
247
248/// Default avatar configuration for a content pack.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct AvatarDefaults {
251    #[serde(default = "default_avatar_radius")]
252    pub radius: f32,
253    #[serde(default = "default_avatar_height")]
254    pub height: f32,
255    #[serde(default = "default_avatar_step_height")]
256    pub step_height: f32,
257    #[serde(default = "default_avatar_slope_limit")]
258    pub slope_limit_degrees: f32,
259    #[serde(default = "default_avatar_mass")]
260    pub mass: f32,
261    #[serde(default = "default_avatar_input_scheme")]
262    pub input_scheme: String,
263}
264
265fn default_avatar_radius() -> f32 {
266    0.3
267}
268fn default_avatar_height() -> f32 {
269    1.8
270}
271fn default_avatar_step_height() -> f32 {
272    0.35
273}
274fn default_avatar_slope_limit() -> f32 {
275    45.0
276}
277fn default_avatar_mass() -> f32 {
278    70.0
279}
280fn default_avatar_input_scheme() -> String {
281    "ThirdPerson".into()
282}
283
284impl Default for AvatarDefaults {
285    fn default() -> Self {
286        Self {
287            radius: default_avatar_radius(),
288            height: default_avatar_height(),
289            step_height: default_avatar_step_height(),
290            slope_limit_degrees: default_avatar_slope_limit(),
291            mass: default_avatar_mass(),
292            input_scheme: default_avatar_input_scheme(),
293        }
294    }
295}
296
297// =============================================================================
298// §2  GRID CONFIG
299// =============================================================================
300
301/// Grid dimensions for map generation.
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct GridConfig {
304    /// Grid width in tiles. Default: 80.
305    #[serde(default = "default_grid_width")]
306    pub width: u32,
307
308    /// Grid height in tiles. Default: 50.
309    #[serde(default = "default_grid_height")]
310    pub height: u32,
311
312    /// Chunk edge size in tiles. Default: 32. Must be a power of two.
313    #[serde(default = "default_chunk_size")]
314    pub chunk_size: u32,
315}
316
317fn default_grid_width() -> u32 {
318    80
319}
320fn default_grid_height() -> u32 {
321    50
322}
323fn default_chunk_size() -> u32 {
324    32
325}
326
327impl Default for GridConfig {
328    fn default() -> Self {
329        Self {
330            width: default_grid_width(),
331            height: default_grid_height(),
332            chunk_size: default_chunk_size(),
333        }
334    }
335}
336
337// =============================================================================
338// §3  SPATIAL CONFIG
339// =============================================================================
340
341/// Spatial engine parameters: cell sizing, field-of-view, pathfinding, AOI.
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct SpatialConfig {
344    /// Cell size in world units. Default: 128.
345    #[serde(default = "default_cell_size")]
346    pub cell_size: i32,
347
348    /// Default field-of-view radius in tiles. Default: 8.
349    #[serde(default = "default_fov_default_radius")]
350    pub fov_default_radius: i32,
351
352    /// Maximum allowed FOV radius. Default: 24.
353    #[serde(default = "default_fov_max_radius")]
354    pub fov_max_radius: i32,
355
356    /// Maximum A* pathfinding iterations before aborting. Default: 512.
357    #[serde(default = "default_max_pathfind_steps")]
358    pub max_pathfind_steps: u32,
359
360    /// AOI neighbor depth. 1 = 3x3 grid of cells. Default: 1.
361    #[serde(default = "default_aoi_neighbor_depth")]
362    pub aoi_neighbor_depth: u32,
363
364    /// Whether entity-tile collision is enabled. Default: true.
365    #[serde(default = "default_true")]
366    pub collision_enabled: bool,
367}
368
369fn default_cell_size() -> i32 {
370    128
371}
372fn default_fov_default_radius() -> i32 {
373    8
374}
375fn default_fov_max_radius() -> i32 {
376    24
377}
378fn default_max_pathfind_steps() -> u32 {
379    512
380}
381fn default_aoi_neighbor_depth() -> u32 {
382    1
383}
384fn default_true() -> bool {
385    true
386}
387
388impl Default for SpatialConfig {
389    fn default() -> Self {
390        Self {
391            cell_size: default_cell_size(),
392            fov_default_radius: default_fov_default_radius(),
393            fov_max_radius: default_fov_max_radius(),
394            max_pathfind_steps: default_max_pathfind_steps(),
395            aoi_neighbor_depth: default_aoi_neighbor_depth(),
396            collision_enabled: true,
397        }
398    }
399}
400
401// =============================================================================
402// §4  DISPLAY CONFIG
403// =============================================================================
404
405/// Client-side display configuration.
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct DisplayConfig {
408    /// Display name for the mana resource. Default: "Mana".
409    #[serde(default = "default_mana_name")]
410    pub mana_name: String,
411
412    /// Subtitle shown on the title screen or HUD.
413    #[serde(default)]
414    pub subtitle: Option<String>,
415
416    /// Whether to show the minimap. Default: true.
417    #[serde(default = "default_true")]
418    pub show_minimap: bool,
419
420    /// Whether to show tile coordinates in the HUD. Default: false.
421    #[serde(default)]
422    pub show_coordinates: bool,
423
424    /// Visual theme identifier (e.g. "dark", "light", "terminal").
425    #[serde(default)]
426    pub theme: Option<String>,
427}
428
429fn default_mana_name() -> String {
430    "Mana".to_string()
431}
432
433impl Default for DisplayConfig {
434    fn default() -> Self {
435        Self {
436            mana_name: default_mana_name(),
437            subtitle: None,
438            show_minimap: true,
439            show_coordinates: false,
440            theme: None,
441        }
442    }
443}
444
445// =============================================================================
446// §5  FEATURE FLAGS
447// =============================================================================
448
449/// Feature flags controlling which engine subsystems are active for this pack.
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct FeatureFlags {
452    /// Enable content plan system. Default: false.
453    #[serde(default)]
454    pub content_plan: bool,
455
456    /// Enable boon trigger system. Default: false.
457    #[serde(default)]
458    pub boons: bool,
459
460    /// Enable shrine interactables. Default: false.
461    #[serde(default)]
462    pub shrines: bool,
463
464    /// Enable container/inventory on props. Default: false.
465    #[serde(default)]
466    pub containers: bool,
467
468    /// Enable carry-weight encumbrance. Default: false.
469    #[serde(default)]
470    pub encumbrance: bool,
471
472    /// Enable player-versus-player combat. Default: false.
473    #[serde(default)]
474    pub pvp: bool,
475
476    /// Enable dynasty/lineage system. Default: false.
477    #[serde(default)]
478    pub dynasties: bool,
479
480    /// Enable crafting system. Default: false.
481    #[serde(default)]
482    pub crafting: bool,
483
484    /// Enable market/auction house. Default: false.
485    #[serde(default)]
486    pub market: bool,
487
488    /// Enable instanced areas. Default: false.
489    #[serde(default)]
490    pub instances: bool,
491
492    /// Enable crime/reputation system. Default: false.
493    #[serde(default)]
494    pub crime_system: bool,
495
496    /// Enable dynamic weather. Default: false.
497    #[serde(default)]
498    pub weather: bool,
499
500    /// Enable day/night cycle. Default: false.
501    #[serde(default)]
502    pub day_night_cycle: bool,
503
504    /// Enable fog of war. Default: false.
505    #[serde(default)]
506    pub fog_of_war: bool,
507
508    /// Enable chronoshift (timeline fork/replay). Default: false.
509    #[serde(default)]
510    pub chronoshift_enabled: bool,
511
512    /// Enable proof lane (forensics). Default: false.
513    #[serde(default)]
514    pub proof_lane_enabled: bool,
515
516    /// Enable dungeon ambient soundtrack. Default: true.
517    #[serde(default = "default_true")]
518    pub dungeon_ambient: bool,
519
520    /// Enable line-of-sight computation. Default: true.
521    #[serde(default = "default_true")]
522    pub line_of_sight: bool,
523
524    /// Enable tile collisions. Default: true.
525    #[serde(default = "default_true")]
526    pub collisions: bool,
527
528    /// Enable identity system (Embersteel-style reprints). Default: false.
529    #[serde(default)]
530    pub identity_system: bool,
531
532    /// Enable death-reprint mechanic. Default: false.
533    #[serde(default)]
534    pub death_reprint: bool,
535}
536
537impl Default for FeatureFlags {
538    fn default() -> Self {
539        Self {
540            content_plan: false,
541            boons: false,
542            shrines: false,
543            containers: false,
544            encumbrance: false,
545            pvp: false,
546            dynasties: false,
547            crafting: false,
548            market: false,
549            instances: false,
550            crime_system: false,
551            weather: false,
552            day_night_cycle: false,
553            fog_of_war: false,
554            chronoshift_enabled: false,
555            proof_lane_enabled: false,
556            dungeon_ambient: true,
557            line_of_sight: true,
558            collisions: true,
559            identity_system: false,
560            death_reprint: false,
561        }
562    }
563}
564
565// =============================================================================
566// §6  SIMULATION CONFIG
567// =============================================================================
568
569/// Core simulation tick rate and entity limits.
570#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct SimulationConfig {
572    /// Milliseconds per simulation tick. Default: 100.
573    #[serde(default = "default_tick_rate_ms")]
574    pub tick_rate_ms: u32,
575
576    /// Number of ticks that constitute one in-game day. Default: 365.
577    #[serde(default = "default_ticks_per_day")]
578    pub ticks_per_day: u32,
579
580    /// Time dilation multiplier. 1.0 = real time. Default: 1.0.
581    #[serde(default = "default_time_dilation")]
582    pub time_dilation: f64,
583
584    /// Maximum entities allowed in a single area. Default: 256.
585    #[serde(default = "default_max_entities_per_area")]
586    pub max_entities_per_area: u32,
587
588    /// Maximum canon events emitted per tick. Default: 1024.
589    #[serde(default = "default_max_events_per_tick")]
590    pub max_events_per_tick: u32,
591
592    /// Deterministic seed for RNG. 0 = use ctx.rng (server-provided). Default: 0.
593    #[serde(default)]
594    pub deterministic_seed: u64,
595}
596
597fn default_tick_rate_ms() -> u32 {
598    100
599}
600fn default_ticks_per_day() -> u32 {
601    365
602}
603fn default_time_dilation() -> f64 {
604    1.0
605}
606fn default_max_entities_per_area() -> u32 {
607    256
608}
609fn default_max_events_per_tick() -> u32 {
610    1024
611}
612
613impl Default for SimulationConfig {
614    fn default() -> Self {
615        Self {
616            tick_rate_ms: default_tick_rate_ms(),
617            ticks_per_day: default_ticks_per_day(),
618            time_dilation: default_time_dilation(),
619            max_entities_per_area: default_max_entities_per_area(),
620            max_events_per_tick: default_max_events_per_tick(),
621            deterministic_seed: 0,
622        }
623    }
624}
625
626// =============================================================================
627// §7  COMBAT CONFIG
628// =============================================================================
629
630/// Combat formula parameters.
631///
632/// Note: `crit_multiplier` is for display/client reference only. The
633/// authoritative server uses fixed-point arithmetic for all combat math.
634#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct CombatConfig {
636    /// Base hit chance percentage. Default: 80.
637    #[serde(default = "default_base_hit_chance")]
638    pub base_hit_chance: u32,
639
640    /// Critical hit damage multiplier (display/client only). Default: 2.0.
641    #[serde(default = "default_crit_multiplier")]
642    pub crit_multiplier: f64,
643
644    /// Base dodge chance percentage. Default: 10.
645    #[serde(default = "default_dodge_base")]
646    pub dodge_base: u32,
647
648    /// Base parry chance percentage. Default: 5.
649    #[serde(default = "default_parry_base")]
650    pub parry_base: u32,
651
652    /// HP percentage threshold below which NPCs attempt to flee. Default: 20.
653    #[serde(default = "default_flee_threshold_hp_pct")]
654    pub flee_threshold_hp_pct: u32,
655
656    /// Maximum simultaneous threat targets per actor. Default: 8.
657    #[serde(default = "default_max_threat_targets")]
658    pub max_threat_targets: u32,
659
660    /// Ticks before a duel request expires. Default: 100.
661    #[serde(default = "default_duel_timeout_ticks")]
662    pub duel_timeout_ticks: u32,
663
664    /// HP percentage restored on revive. Default: 25.
665    #[serde(default = "default_revive_hp_pct")]
666    pub revive_hp_pct: u32,
667
668    /// Maximum status effect stacks per effect type. Default: 5.
669    #[serde(default = "default_status_max_stacks")]
670    pub status_max_stacks: u32,
671
672    /// Available damage type identifiers.
673    #[serde(default = "default_damage_types")]
674    pub damage_types: Vec<String>,
675
676    /// Maximum resistance percentage cap. Default: 75.
677    #[serde(default = "default_resistance_cap")]
678    pub resistance_cap: u32,
679}
680
681fn default_base_hit_chance() -> u32 {
682    80
683}
684fn default_crit_multiplier() -> f64 {
685    2.0
686}
687fn default_dodge_base() -> u32 {
688    10
689}
690fn default_parry_base() -> u32 {
691    5
692}
693fn default_flee_threshold_hp_pct() -> u32 {
694    20
695}
696fn default_max_threat_targets() -> u32 {
697    8
698}
699fn default_duel_timeout_ticks() -> u32 {
700    100
701}
702fn default_revive_hp_pct() -> u32 {
703    25
704}
705fn default_status_max_stacks() -> u32 {
706    5
707}
708fn default_damage_types() -> Vec<String> {
709    vec![
710        "physical".to_string(),
711        "fire".to_string(),
712        "ice".to_string(),
713        "lightning".to_string(),
714        "arcane".to_string(),
715        "poison".to_string(),
716        "holy".to_string(),
717        "shadow".to_string(),
718    ]
719}
720fn default_resistance_cap() -> u32 {
721    75
722}
723
724impl Default for CombatConfig {
725    fn default() -> Self {
726        Self {
727            base_hit_chance: default_base_hit_chance(),
728            crit_multiplier: default_crit_multiplier(),
729            dodge_base: default_dodge_base(),
730            parry_base: default_parry_base(),
731            flee_threshold_hp_pct: default_flee_threshold_hp_pct(),
732            max_threat_targets: default_max_threat_targets(),
733            duel_timeout_ticks: default_duel_timeout_ticks(),
734            revive_hp_pct: default_revive_hp_pct(),
735            status_max_stacks: default_status_max_stacks(),
736            damage_types: default_damage_types(),
737            resistance_cap: default_resistance_cap(),
738        }
739    }
740}
741
742// =============================================================================
743// §8  ECONOMY CONFIG
744// =============================================================================
745
746/// Economy and trade parameters.
747#[derive(Debug, Clone, Serialize, Deserialize)]
748pub struct EconomyConfig {
749    /// Starting gold for new characters. Default: 100.
750    #[serde(default = "default_starting_gold")]
751    pub starting_gold: u64,
752
753    /// Trade tax percentage applied to direct trades. Default: 5.
754    #[serde(default = "default_trade_tax_pct")]
755    pub trade_tax_pct: u32,
756
757    /// Market listing fee percentage. Default: 10.
758    #[serde(default = "default_market_fee_pct")]
759    pub market_fee_pct: u32,
760
761    /// Maximum active market listings per player. Default: 20.
762    #[serde(default = "default_max_listings_per_player")]
763    pub max_listings_per_player: u32,
764
765    /// Currency type identifiers (e.g. "gold", "silver", "embercore").
766    #[serde(default = "default_currency_types")]
767    pub currency_types: Vec<String>,
768
769    /// Base crafting failure chance percentage. Default: 10.
770    #[serde(default = "default_crafting_fail_chance_base")]
771    pub crafting_fail_chance_base: u32,
772}
773
774fn default_starting_gold() -> u64 {
775    100
776}
777fn default_trade_tax_pct() -> u32 {
778    5
779}
780fn default_market_fee_pct() -> u32 {
781    10
782}
783fn default_max_listings_per_player() -> u32 {
784    20
785}
786fn default_currency_types() -> Vec<String> {
787    vec!["gold".to_string()]
788}
789fn default_crafting_fail_chance_base() -> u32 {
790    10
791}
792
793impl Default for EconomyConfig {
794    fn default() -> Self {
795        Self {
796            starting_gold: default_starting_gold(),
797            trade_tax_pct: default_trade_tax_pct(),
798            market_fee_pct: default_market_fee_pct(),
799            max_listings_per_player: default_max_listings_per_player(),
800            currency_types: default_currency_types(),
801            crafting_fail_chance_base: default_crafting_fail_chance_base(),
802        }
803    }
804}
805
806// =============================================================================
807// §9  PROGRESSION CONFIG
808// =============================================================================
809
810/// Leveling, XP curve, and reputation parameters.
811#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct ProgressionConfig {
813    /// Maximum character level. Default: 100.
814    #[serde(default = "default_max_level")]
815    pub max_level: u32,
816
817    /// Exponential XP curve base. XP(n) = base_xp * xp_curve_base^(n-1). Default: 1.5.
818    #[serde(default = "default_xp_curve_base")]
819    pub xp_curve_base: f64,
820
821    /// Base XP required for level 2. Default: 100.
822    #[serde(default = "default_base_xp")]
823    pub base_xp: u64,
824
825    /// HP bonus gained per level. Default: 10.
826    #[serde(default = "default_level_hp_bonus")]
827    pub level_hp_bonus: u32,
828
829    /// Stat points gained per level. Default: 2.
830    #[serde(default = "default_level_stat_bonus")]
831    pub level_stat_bonus: u32,
832
833    /// Minimum reputation value. Default: -1000.
834    #[serde(default = "default_reputation_min")]
835    pub reputation_min: i64,
836
837    /// Maximum reputation value. Default: 1000.
838    #[serde(default = "default_reputation_max")]
839    pub reputation_max: i64,
840}
841
842fn default_max_level() -> u32 {
843    100
844}
845fn default_xp_curve_base() -> f64 {
846    1.5
847}
848fn default_base_xp() -> u64 {
849    100
850}
851fn default_level_hp_bonus() -> u32 {
852    10
853}
854fn default_level_stat_bonus() -> u32 {
855    2
856}
857fn default_reputation_min() -> i64 {
858    -1000
859}
860fn default_reputation_max() -> i64 {
861    1000
862}
863
864impl Default for ProgressionConfig {
865    fn default() -> Self {
866        Self {
867            max_level: default_max_level(),
868            xp_curve_base: default_xp_curve_base(),
869            base_xp: default_base_xp(),
870            level_hp_bonus: default_level_hp_bonus(),
871            level_stat_bonus: default_level_stat_bonus(),
872            reputation_min: default_reputation_min(),
873            reputation_max: default_reputation_max(),
874        }
875    }
876}
877
878// =============================================================================
879// §10  AGENT CONFIG
880// =============================================================================
881
882/// Agent cognition, perception, and inference parameters.
883#[derive(Debug, Clone, Serialize, Deserialize)]
884pub struct AgentConfig {
885    /// Maximum cognition actions per agent per tick. Default: 3.
886    #[serde(default = "default_cognition_budget_per_tick")]
887    pub cognition_budget_per_tick: u32,
888
889    /// Perception range in tiles. Default: 10.
890    #[serde(default = "default_perception_range")]
891    pub perception_range: u32,
892
893    /// Maximum memory entries per agent. Default: 32.
894    #[serde(default = "default_memory_capacity")]
895    pub memory_capacity: u32,
896
897    /// Default reasoning tier (0 = reactive, 1 = deliberative, 2 = planning). Default: 1.
898    #[serde(default = "default_reasoning_tier")]
899    pub reasoning_tier_default: u32,
900
901    /// Default motor tier (0 = static, 1 = mobile, 2 = acrobatic). Default: 1.
902    #[serde(default = "default_motor_tier")]
903    pub motor_tier_default: u32,
904
905    /// Maximum concurrently active agents. Default: 64.
906    #[serde(default = "default_max_concurrent_agents")]
907    pub max_concurrent_agents: u32,
908
909    /// Inference request timeout in milliseconds. Default: 5000.
910    #[serde(default = "default_inference_timeout_ms")]
911    pub inference_timeout_ms: u32,
912
913    /// Maximum tokens per inference response. Default: 512.
914    #[serde(default = "default_inference_max_tokens")]
915    pub inference_max_tokens: u32,
916}
917
918fn default_cognition_budget_per_tick() -> u32 {
919    3
920}
921fn default_perception_range() -> u32 {
922    10
923}
924fn default_memory_capacity() -> u32 {
925    32
926}
927fn default_reasoning_tier() -> u32 {
928    1
929}
930fn default_motor_tier() -> u32 {
931    1
932}
933fn default_max_concurrent_agents() -> u32 {
934    64
935}
936fn default_inference_timeout_ms() -> u32 {
937    5000
938}
939fn default_inference_max_tokens() -> u32 {
940    512
941}
942
943impl Default for AgentConfig {
944    fn default() -> Self {
945        Self {
946            cognition_budget_per_tick: default_cognition_budget_per_tick(),
947            perception_range: default_perception_range(),
948            memory_capacity: default_memory_capacity(),
949            reasoning_tier_default: default_reasoning_tier(),
950            motor_tier_default: default_motor_tier(),
951            max_concurrent_agents: default_max_concurrent_agents(),
952            inference_timeout_ms: default_inference_timeout_ms(),
953            inference_max_tokens: default_inference_max_tokens(),
954        }
955    }
956}
957
958// =============================================================================
959// §11  TILE CONFIG
960// =============================================================================
961
962/// Individual tile type definition: glyph, movement cost, blocking rules.
963#[derive(Debug, Clone, Serialize, Deserialize)]
964pub struct TileTypeDef {
965    /// Tile ID (u16).
966    pub glyph: u16,
967
968    /// Optional display character for rendering.
969    #[serde(default)]
970    pub display_char: Option<char>,
971
972    /// Movement cost for pathfinding. 999 = impassable.
973    #[serde(default = "default_move_cost_1")]
974    pub move_cost: u32,
975
976    /// Whether this tile blocks entity movement.
977    #[serde(default)]
978    pub blocks_move: bool,
979
980    /// Whether this tile blocks line of sight.
981    #[serde(default)]
982    pub blocks_sight: bool,
983}
984
985fn default_move_cost_1() -> u32 {
986    1
987}
988
989/// Tile ID (u16) mappings, movement costs, and collision rules.
990///
991/// Defaults match the constants in `tile.rs`:
992/// - Ground layer: `.` `,` `:` `` ` `` `~` `=` `b` `;` `'`
993/// - Structure layer: `#` `|` `_` `I` `*` `^` `v`
994/// - Object layer: `o` `C` `c` `S` `B` `!` `$` `K` `?`
995/// - Effect layer: `x` `%`
996#[derive(Debug, Clone, Serialize, Deserialize)]
997pub struct TileConfig {
998    // -- Ground layer --
999    /// Ground tile: b'.' — open floor. Cost 1, passable, transparent.
1000    #[serde(default = "default_tile_ground")]
1001    pub ground: TileTypeDef,
1002
1003    /// Dirt tile: b',' — soft earth. Cost 1, passable, transparent.
1004    #[serde(default = "default_tile_dirt")]
1005    pub dirt: TileTypeDef,
1006
1007    /// Sand tile: b':' — loose sand. Cost 2, passable, transparent.
1008    #[serde(default = "default_tile_sand")]
1009    pub sand: TileTypeDef,
1010
1011    /// Mud tile: b'`' — wet ground. Cost 2, passable, transparent.
1012    #[serde(default = "default_tile_mud")]
1013    pub mud: TileTypeDef,
1014
1015    /// Liquid tile: b'~' — shallow water. Cost 3, passable, transparent.
1016    #[serde(default = "default_tile_liquid")]
1017    pub liquid: TileTypeDef,
1018
1019    /// Deep water tile: b'=' — impassable water. Cost 999, blocks move, transparent.
1020    #[serde(default = "default_tile_deep_water")]
1021    pub deep_water: TileTypeDef,
1022
1023    /// Rocky ground tile: b'b' — rough terrain. Cost 2, passable, transparent.
1024    #[serde(default = "default_tile_rocky_ground")]
1025    pub rocky_ground: TileTypeDef,
1026
1027    /// Grass tile: b';' — grass. Cost 2, passable, transparent.
1028    #[serde(default = "default_tile_grass")]
1029    pub grass: TileTypeDef,
1030
1031    /// Debris tile: b'\'' — rubble. Cost 2, passable, transparent.
1032    #[serde(default = "default_tile_debris")]
1033    pub debris: TileTypeDef,
1034
1035    // -- Structure layer --
1036    /// Wall tile: b'#' — solid wall. Cost 999, blocks move, blocks sight.
1037    #[serde(default = "default_tile_wall")]
1038    pub wall: TileTypeDef,
1039
1040    /// Vertical wall tile: b'|' — vertical wall segment. Cost 999, blocks move, blocks sight.
1041    #[serde(default = "default_tile_wall_vert")]
1042    pub wall_vert: TileTypeDef,
1043
1044    /// Horizontal wall tile: b'_' — horizontal wall segment. Cost 999, blocks move, blocks sight.
1045    #[serde(default = "default_tile_wall_horiz")]
1046    pub wall_horiz: TileTypeDef,
1047
1048    /// Closed door tile: b'I' — closed door. Cost 999, blocks move, blocks sight.
1049    #[serde(default = "default_tile_door_closed")]
1050    pub door_closed: TileTypeDef,
1051
1052    /// Open door tile: b'*' — open door. Cost 1, passable, transparent.
1053    #[serde(default = "default_tile_door_open")]
1054    pub door_open: TileTypeDef,
1055
1056    /// Stairs up tile: b'^' — ascending stairs. Cost 1, passable, transparent.
1057    #[serde(default = "default_tile_stairs_up")]
1058    pub stairs_up: TileTypeDef,
1059
1060    /// Stairs down tile: b'v' — descending stairs. Cost 1, passable, transparent.
1061    #[serde(default = "default_tile_stairs_down")]
1062    pub stairs_down: TileTypeDef,
1063
1064    /// Bridge tile: b'=' — walkable bridge. Cost 1, passable, transparent.
1065    #[serde(default = "default_tile_bridge")]
1066    pub bridge: TileTypeDef,
1067
1068    // -- Object layer --
1069    /// Boulder tile: b'o' — large rock. Cost 999, blocks move, blocks sight.
1070    #[serde(default = "default_tile_boulder")]
1071    pub boulder: TileTypeDef,
1072
1073    /// Chest tile: b'C' — closed chest. Cost 999, blocks move, transparent.
1074    #[serde(default = "default_tile_chest")]
1075    pub chest: TileTypeDef,
1076
1077    /// Crate tile: b'c' — wooden crate. Cost 999, blocks move, transparent.
1078    #[serde(default = "default_tile_crate")]
1079    pub crate_obj: TileTypeDef,
1080
1081    /// Shrine tile: b'S' — interactive shrine. Cost 1, passable, transparent.
1082    #[serde(default = "default_tile_shrine")]
1083    pub shrine: TileTypeDef,
1084
1085    /// Bed tile: b'B' — bed. Cost 999, blocks move, transparent.
1086    #[serde(default = "default_tile_bed")]
1087    pub bed: TileTypeDef,
1088
1089    /// Consumable tile: b'!' — pickup item. Cost 1, passable, transparent.
1090    #[serde(default = "default_tile_consumable")]
1091    pub consumable: TileTypeDef,
1092
1093    /// Currency tile: b'$' — gold drop. Cost 1, passable, transparent.
1094    #[serde(default = "default_tile_currency")]
1095    pub currency: TileTypeDef,
1096
1097    /// Key item tile: b'K' — quest item pickup. Cost 1, passable, transparent.
1098    #[serde(default = "default_tile_key_item")]
1099    pub key_item: TileTypeDef,
1100
1101    /// Unknown object tile: b'?' — unidentified. Cost 999, blocks move, transparent.
1102    #[serde(default = "default_tile_unknown_obj")]
1103    pub unknown_obj: TileTypeDef,
1104
1105    // -- Effect layer --
1106    /// Hazard tile: b'x' — environmental hazard. Cost 2, passable, transparent.
1107    #[serde(default = "default_tile_hazard")]
1108    pub hazard: TileTypeDef,
1109
1110    /// Smoke tile: b'%' — obscuring smoke. Cost 2, passable, blocks sight.
1111    #[serde(default = "default_tile_smoke")]
1112    pub smoke: TileTypeDef,
1113
1114    /// Additional custom tile definitions. Key = tile name, value = definition.
1115    #[serde(default)]
1116    pub custom: HashMap<String, TileTypeDef>,
1117}
1118
1119// -- Ground layer defaults --
1120fn default_tile_ground() -> TileTypeDef {
1121    TileTypeDef {
1122        glyph: b'.' as u16,
1123        display_char: None,
1124        move_cost: 1,
1125        blocks_move: false,
1126        blocks_sight: false,
1127    }
1128}
1129fn default_tile_dirt() -> TileTypeDef {
1130    TileTypeDef {
1131        glyph: b',' as u16,
1132        display_char: None,
1133        move_cost: 1,
1134        blocks_move: false,
1135        blocks_sight: false,
1136    }
1137}
1138fn default_tile_sand() -> TileTypeDef {
1139    TileTypeDef {
1140        glyph: b':' as u16,
1141        display_char: None,
1142        move_cost: 2,
1143        blocks_move: false,
1144        blocks_sight: false,
1145    }
1146}
1147fn default_tile_mud() -> TileTypeDef {
1148    TileTypeDef {
1149        glyph: b'`' as u16,
1150        display_char: None,
1151        move_cost: 2,
1152        blocks_move: false,
1153        blocks_sight: false,
1154    }
1155}
1156fn default_tile_liquid() -> TileTypeDef {
1157    TileTypeDef {
1158        glyph: b'~' as u16,
1159        display_char: None,
1160        move_cost: 3,
1161        blocks_move: false,
1162        blocks_sight: false,
1163    }
1164}
1165fn default_tile_deep_water() -> TileTypeDef {
1166    TileTypeDef {
1167        glyph: b'=' as u16,
1168        display_char: None,
1169        move_cost: 999,
1170        blocks_move: true,
1171        blocks_sight: false,
1172    }
1173}
1174fn default_tile_rocky_ground() -> TileTypeDef {
1175    TileTypeDef {
1176        glyph: b'b' as u16,
1177        display_char: None,
1178        move_cost: 2,
1179        blocks_move: false,
1180        blocks_sight: false,
1181    }
1182}
1183fn default_tile_grass() -> TileTypeDef {
1184    TileTypeDef {
1185        glyph: b';' as u16,
1186        display_char: None,
1187        move_cost: 2,
1188        blocks_move: false,
1189        blocks_sight: false,
1190    }
1191}
1192fn default_tile_debris() -> TileTypeDef {
1193    TileTypeDef {
1194        glyph: b'\'' as u16,
1195        display_char: None,
1196        move_cost: 2,
1197        blocks_move: false,
1198        blocks_sight: false,
1199    }
1200}
1201// -- Structure layer defaults --
1202fn default_tile_wall() -> TileTypeDef {
1203    TileTypeDef {
1204        glyph: b'#' as u16,
1205        display_char: None,
1206        move_cost: 999,
1207        blocks_move: true,
1208        blocks_sight: true,
1209    }
1210}
1211fn default_tile_wall_vert() -> TileTypeDef {
1212    TileTypeDef {
1213        glyph: b'|' as u16,
1214        display_char: None,
1215        move_cost: 999,
1216        blocks_move: true,
1217        blocks_sight: true,
1218    }
1219}
1220fn default_tile_wall_horiz() -> TileTypeDef {
1221    TileTypeDef {
1222        glyph: b'_' as u16,
1223        display_char: None,
1224        move_cost: 999,
1225        blocks_move: true,
1226        blocks_sight: true,
1227    }
1228}
1229fn default_tile_door_closed() -> TileTypeDef {
1230    TileTypeDef {
1231        glyph: b'I' as u16,
1232        display_char: None,
1233        move_cost: 999,
1234        blocks_move: true,
1235        blocks_sight: true,
1236    }
1237}
1238fn default_tile_door_open() -> TileTypeDef {
1239    TileTypeDef {
1240        glyph: b'*' as u16,
1241        display_char: None,
1242        move_cost: 1,
1243        blocks_move: false,
1244        blocks_sight: false,
1245    }
1246}
1247fn default_tile_stairs_up() -> TileTypeDef {
1248    TileTypeDef {
1249        glyph: b'^' as u16,
1250        display_char: None,
1251        move_cost: 1,
1252        blocks_move: false,
1253        blocks_sight: false,
1254    }
1255}
1256fn default_tile_stairs_down() -> TileTypeDef {
1257    TileTypeDef {
1258        glyph: b'v' as u16,
1259        display_char: None,
1260        move_cost: 1,
1261        blocks_move: false,
1262        blocks_sight: false,
1263    }
1264}
1265fn default_tile_bridge() -> TileTypeDef {
1266    TileTypeDef {
1267        glyph: b'=' as u16,
1268        display_char: None,
1269        move_cost: 1,
1270        blocks_move: false,
1271        blocks_sight: false,
1272    }
1273}
1274// -- Object layer defaults --
1275fn default_tile_boulder() -> TileTypeDef {
1276    TileTypeDef {
1277        glyph: b'o' as u16,
1278        display_char: None,
1279        move_cost: 999,
1280        blocks_move: true,
1281        blocks_sight: true,
1282    }
1283}
1284fn default_tile_chest() -> TileTypeDef {
1285    TileTypeDef {
1286        glyph: b'C' as u16,
1287        display_char: None,
1288        move_cost: 999,
1289        blocks_move: true,
1290        blocks_sight: false,
1291    }
1292}
1293fn default_tile_crate() -> TileTypeDef {
1294    TileTypeDef {
1295        glyph: b'c' as u16,
1296        display_char: None,
1297        move_cost: 999,
1298        blocks_move: true,
1299        blocks_sight: false,
1300    }
1301}
1302fn default_tile_shrine() -> TileTypeDef {
1303    TileTypeDef {
1304        glyph: b'S' as u16,
1305        display_char: None,
1306        move_cost: 1,
1307        blocks_move: false,
1308        blocks_sight: false,
1309    }
1310}
1311fn default_tile_bed() -> TileTypeDef {
1312    TileTypeDef {
1313        glyph: b'B' as u16,
1314        display_char: None,
1315        move_cost: 999,
1316        blocks_move: true,
1317        blocks_sight: false,
1318    }
1319}
1320fn default_tile_consumable() -> TileTypeDef {
1321    TileTypeDef {
1322        glyph: b'!' as u16,
1323        display_char: None,
1324        move_cost: 1,
1325        blocks_move: false,
1326        blocks_sight: false,
1327    }
1328}
1329fn default_tile_currency() -> TileTypeDef {
1330    TileTypeDef {
1331        glyph: b'$' as u16,
1332        display_char: None,
1333        move_cost: 1,
1334        blocks_move: false,
1335        blocks_sight: false,
1336    }
1337}
1338fn default_tile_key_item() -> TileTypeDef {
1339    TileTypeDef {
1340        glyph: b'K' as u16,
1341        display_char: None,
1342        move_cost: 1,
1343        blocks_move: false,
1344        blocks_sight: false,
1345    }
1346}
1347fn default_tile_unknown_obj() -> TileTypeDef {
1348    TileTypeDef {
1349        glyph: b'?' as u16,
1350        display_char: None,
1351        move_cost: 999,
1352        blocks_move: true,
1353        blocks_sight: false,
1354    }
1355}
1356// -- Effect layer defaults --
1357fn default_tile_hazard() -> TileTypeDef {
1358    TileTypeDef {
1359        glyph: b'x' as u16,
1360        display_char: None,
1361        move_cost: 2,
1362        blocks_move: false,
1363        blocks_sight: false,
1364    }
1365}
1366fn default_tile_smoke() -> TileTypeDef {
1367    TileTypeDef {
1368        glyph: b'%' as u16,
1369        display_char: None,
1370        move_cost: 2,
1371        blocks_move: false,
1372        blocks_sight: true,
1373    }
1374}
1375
1376impl Default for TileConfig {
1377    fn default() -> Self {
1378        Self {
1379            ground: default_tile_ground(),
1380            dirt: default_tile_dirt(),
1381            sand: default_tile_sand(),
1382            mud: default_tile_mud(),
1383            liquid: default_tile_liquid(),
1384            deep_water: default_tile_deep_water(),
1385            rocky_ground: default_tile_rocky_ground(),
1386            grass: default_tile_grass(),
1387            debris: default_tile_debris(),
1388            wall: default_tile_wall(),
1389            wall_vert: default_tile_wall_vert(),
1390            wall_horiz: default_tile_wall_horiz(),
1391            door_closed: default_tile_door_closed(),
1392            door_open: default_tile_door_open(),
1393            stairs_up: default_tile_stairs_up(),
1394            stairs_down: default_tile_stairs_down(),
1395            bridge: default_tile_bridge(),
1396            boulder: default_tile_boulder(),
1397            chest: default_tile_chest(),
1398            crate_obj: default_tile_crate(),
1399            shrine: default_tile_shrine(),
1400            bed: default_tile_bed(),
1401            consumable: default_tile_consumable(),
1402            currency: default_tile_currency(),
1403            key_item: default_tile_key_item(),
1404            unknown_obj: default_tile_unknown_obj(),
1405            hazard: default_tile_hazard(),
1406            smoke: default_tile_smoke(),
1407            custom: HashMap::new(),
1408        }
1409    }
1410}
1411
1412// =============================================================================
1413// §12  CANON CONFIG
1414// =============================================================================
1415
1416/// Canon event pipeline parameters.
1417#[derive(Debug, Clone, Serialize, Deserialize)]
1418pub struct CanonConfig {
1419    /// Ticks between block hash checkpoints. Default: 100.
1420    #[serde(default = "default_block_hash_interval")]
1421    pub block_hash_interval: u64,
1422
1423    /// Ticks between tick-level hash accumulation. Default: 10.
1424    #[serde(default = "default_tick_hash_interval")]
1425    pub tick_hash_interval: u64,
1426
1427    /// Maximum BFS traversal depth for graph queries. Default: 10.
1428    #[serde(default = "default_max_traversal_depth")]
1429    pub max_traversal_depth: u32,
1430
1431    /// Default scope token budget. Default: 100.
1432    #[serde(default = "default_scope_budget")]
1433    pub scope_budget_default: u32,
1434
1435    /// Tokens refilled per tick. Default: 5.
1436    #[serde(default = "default_token_refill_per_tick")]
1437    pub token_refill_per_tick: u32,
1438
1439    /// Maximum token cap. Default: 50.
1440    #[serde(default = "default_max_tokens")]
1441    pub max_tokens: u32,
1442
1443    /// Maximum trigger chain depth (prevents infinite loops). Default: 3.
1444    #[serde(default = "default_max_trigger_depth")]
1445    pub max_trigger_depth: u32,
1446
1447    /// Maximum triggers fired per single canon event. Default: 5.
1448    #[serde(default = "default_max_triggers_per_event")]
1449    pub max_triggers_per_event: u32,
1450
1451    /// Maximum total triggers fired per tick across all events. Default: 50.
1452    #[serde(default = "default_max_triggers_per_tick")]
1453    pub max_triggers_per_tick: u32,
1454}
1455
1456fn default_block_hash_interval() -> u64 {
1457    100
1458}
1459fn default_tick_hash_interval() -> u64 {
1460    10
1461}
1462fn default_max_traversal_depth() -> u32 {
1463    10
1464}
1465fn default_scope_budget() -> u32 {
1466    100
1467}
1468fn default_token_refill_per_tick() -> u32 {
1469    5
1470}
1471fn default_max_tokens() -> u32 {
1472    50
1473}
1474fn default_max_trigger_depth() -> u32 {
1475    3
1476}
1477fn default_max_triggers_per_event() -> u32 {
1478    5
1479}
1480fn default_max_triggers_per_tick() -> u32 {
1481    50
1482}
1483
1484impl Default for CanonConfig {
1485    fn default() -> Self {
1486        Self {
1487            block_hash_interval: default_block_hash_interval(),
1488            tick_hash_interval: default_tick_hash_interval(),
1489            max_traversal_depth: default_max_traversal_depth(),
1490            scope_budget_default: default_scope_budget(),
1491            token_refill_per_tick: default_token_refill_per_tick(),
1492            max_tokens: default_max_tokens(),
1493            max_trigger_depth: default_max_trigger_depth(),
1494            max_triggers_per_event: default_max_triggers_per_event(),
1495            max_triggers_per_tick: default_max_triggers_per_tick(),
1496        }
1497    }
1498}
1499
1500// =============================================================================
1501// §13  SCRIPTING CONFIG
1502// =============================================================================
1503
1504/// Waymark scripting engine limits.
1505#[derive(Debug, Clone, Serialize, Deserialize)]
1506pub struct ScriptingConfig {
1507    /// Maximum rune (trigger) definitions. Default: 256.
1508    #[serde(default = "default_max_runes")]
1509    pub max_runes: u32,
1510
1511    /// Maximum omen (scheduled event) definitions. Default: 128.
1512    #[serde(default = "default_max_omens")]
1513    pub max_omens: u32,
1514
1515    /// Maximum chapter definitions. Default: 64.
1516    #[serde(default = "default_max_chapters")]
1517    pub max_chapters: u32,
1518
1519    /// Maximum dialogue definitions. Default: 64.
1520    #[serde(default = "default_max_dialogues")]
1521    pub max_dialogues: u32,
1522
1523    /// Maximum vision (cutscene) definitions. Default: 32.
1524    #[serde(default = "default_max_visions")]
1525    pub max_visions: u32,
1526
1527    /// Maximum gate (condition) definitions. Default: 64.
1528    #[serde(default = "default_max_gates")]
1529    pub max_gates: u32,
1530
1531    /// Capacity of the fire-once set (prevents re-triggering). Default: 512.
1532    #[serde(default = "default_fire_once_capacity")]
1533    pub fire_once_capacity: u32,
1534
1535    /// Whether meta-persistence (cross-session state) is enabled. Default: true.
1536    #[serde(default = "default_true")]
1537    pub meta_persistence_enabled: bool,
1538
1539    /// Whether hot-reload of scripts is enabled (false in production). Default: false.
1540    #[serde(default)]
1541    pub hot_reload_enabled: bool,
1542}
1543
1544fn default_max_runes() -> u32 {
1545    256
1546}
1547fn default_max_omens() -> u32 {
1548    128
1549}
1550fn default_max_chapters() -> u32 {
1551    64
1552}
1553fn default_max_dialogues() -> u32 {
1554    64
1555}
1556fn default_max_visions() -> u32 {
1557    32
1558}
1559fn default_max_gates() -> u32 {
1560    64
1561}
1562fn default_fire_once_capacity() -> u32 {
1563    512
1564}
1565
1566impl Default for ScriptingConfig {
1567    fn default() -> Self {
1568        Self {
1569            max_runes: default_max_runes(),
1570            max_omens: default_max_omens(),
1571            max_chapters: default_max_chapters(),
1572            max_dialogues: default_max_dialogues(),
1573            max_visions: default_max_visions(),
1574            max_gates: default_max_gates(),
1575            fire_once_capacity: default_fire_once_capacity(),
1576            meta_persistence_enabled: true,
1577            hot_reload_enabled: false,
1578        }
1579    }
1580}
1581
1582// =============================================================================
1583// §14  CONTENT CONFIG
1584// =============================================================================
1585
1586/// Paths to content data files. All paths are relative to the pack root.
1587#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1588pub struct ContentConfig {
1589    /// Path to items definition file (e.g. "content/items.json").
1590    #[serde(default)]
1591    pub items_path: Option<String>,
1592
1593    /// Path to enemies definition file (e.g. "content/enemies.json").
1594    #[serde(default)]
1595    pub enemies_path: Option<String>,
1596
1597    /// Path to abilities definition file (e.g. "content/abilities.json").
1598    #[serde(default)]
1599    pub abilities_path: Option<String>,
1600
1601    /// Path to loot tables definition file (e.g. "content/loot_tables.json").
1602    #[serde(default)]
1603    pub loot_tables_path: Option<String>,
1604
1605    /// Path to economy/shops definition file (e.g. "content/economy.json").
1606    #[serde(default)]
1607    pub economy_path: Option<String>,
1608
1609    /// Path to dialogue definition file (e.g. "content/dialogue.json").
1610    #[serde(default)]
1611    pub dialogue_path: Option<String>,
1612
1613    /// Path to balance tuning file (e.g. "content/balance.json").
1614    #[serde(default)]
1615    pub balance_path: Option<String>,
1616
1617    /// Path to stats/attributes definition file (e.g. "content/stats.json").
1618    #[serde(default)]
1619    pub stats_path: Option<String>,
1620
1621    /// Path to scenario text/strings file (e.g. "content/scenario_text.json").
1622    #[serde(default)]
1623    pub scenario_text_path: Option<String>,
1624
1625    /// Path to lore entries file (e.g. "content/lore.json").
1626    #[serde(default)]
1627    pub lore_path: Option<String>,
1628
1629    /// Path to crafting recipes file (e.g. "content/crafting.json").
1630    #[serde(default)]
1631    pub crafting_path: Option<String>,
1632
1633    /// Path to quest definitions file (e.g. "content/quests.json").
1634    #[serde(default)]
1635    pub quests_path: Option<String>,
1636
1637    /// Path to factions definitions file (e.g. "content/factions.json").
1638    #[serde(default)]
1639    pub factions_path: Option<String>,
1640
1641    /// Path to map templates directory (e.g. "maps/templates").
1642    #[serde(default)]
1643    pub maps_path: Option<String>,
1644
1645    /// Path to texture pack file (e.g. "textures/pack.json").
1646    #[serde(default)]
1647    pub textures_path: Option<String>,
1648}
1649
1650// =============================================================================
1651// §15  EVICTION CONFIG
1652// =============================================================================
1653
1654/// Map eviction and caching parameters.
1655#[derive(Debug, Clone, Serialize, Deserialize)]
1656pub struct EvictionConfig {
1657    /// Maximum number of cached maps before eviction. Default: 20.
1658    #[serde(default = "default_max_cached")]
1659    pub max_cached: u32,
1660
1661    /// Map IDs that are never evicted.
1662    #[serde(default)]
1663    pub keep_ids: Vec<String>,
1664
1665    /// Map tags that are never evicted.
1666    #[serde(default)]
1667    pub keep_tags: Vec<String>,
1668}
1669
1670fn default_max_cached() -> u32 {
1671    20
1672}
1673
1674impl Default for EvictionConfig {
1675    fn default() -> Self {
1676        Self {
1677            max_cached: default_max_cached(),
1678            keep_ids: Vec::new(),
1679            keep_tags: Vec::new(),
1680        }
1681    }
1682}
1683
1684// =============================================================================
1685// §16  PROP DEFINITION
1686// =============================================================================
1687
1688/// A single state entry for a multi-state prop.
1689#[derive(Debug, Clone, Serialize, Deserialize)]
1690pub struct PropStateDef {
1691    /// Glyph displayed in this state.
1692    #[serde(default)]
1693    pub glyph: Option<String>,
1694
1695    /// Whether the prop blocks movement in this state.
1696    #[serde(default)]
1697    pub blocking: Option<bool>,
1698
1699    /// Whether entities can walk through in this state.
1700    #[serde(default)]
1701    pub walkthrough: Option<bool>,
1702
1703    /// Description shown in this state.
1704    #[serde(default)]
1705    pub description: Option<String>,
1706
1707    /// State to transition to on primary interact.
1708    #[serde(default)]
1709    pub on_interact: Option<String>,
1710
1711    /// State to transition to on secondary interact.
1712    #[serde(default)]
1713    pub on_secondary_interact: Option<String>,
1714
1715    /// Bump messages shown in this state.
1716    #[serde(default)]
1717    pub bump_messages: Vec<String>,
1718}
1719
1720/// Prop definition. Props are interactive or decorative objects placed on maps.
1721#[derive(Debug, Clone, Serialize, Deserialize)]
1722pub struct PropDefinition {
1723    /// Unique prop identifier within the pack.
1724    pub id: String,
1725
1726    /// Human-readable prop name.
1727    pub name: String,
1728
1729    /// Prop description shown on examine.
1730    #[serde(default)]
1731    pub description: String,
1732
1733    /// Extended look description.
1734    #[serde(default)]
1735    pub look_description: Option<String>,
1736
1737    /// Display glyph (single character string or Unicode).
1738    #[serde(default)]
1739    pub glyph: Option<String>,
1740
1741    /// Whether the prop blocks entity movement. Default: false.
1742    #[serde(default)]
1743    pub blocking: bool,
1744
1745    /// Whether entities can walk through (overrides blocking for pathing). Default: false.
1746    #[serde(default)]
1747    pub walkthrough: bool,
1748
1749    /// Whether the prop can be targeted by abilities. Default: false.
1750    #[serde(default)]
1751    pub targetable: bool,
1752
1753    /// Whether the prop responds to interact actions. Default: false.
1754    #[serde(default)]
1755    pub interactive: bool,
1756
1757    /// Whether the prop has multiple states. Default: false.
1758    #[serde(default)]
1759    pub has_secondary_state: bool,
1760
1761    /// Default state name for multi-state props.
1762    #[serde(default)]
1763    pub default_state: Option<String>,
1764
1765    /// Foley sound stub identifier.
1766    #[serde(default)]
1767    pub foley_stub_id: Option<String>,
1768
1769    /// Messages shown when an entity bumps into this prop.
1770    #[serde(default)]
1771    pub bump_messages: Vec<String>,
1772
1773    /// Whether this prop has an inventory. Default: false.
1774    #[serde(default)]
1775    pub has_inventory: bool,
1776
1777    /// Maximum items this container can hold.
1778    #[serde(default)]
1779    pub container_capacity: Option<u32>,
1780
1781    /// Loot table identifier for container drops.
1782    #[serde(default)]
1783    pub loot_table: Option<String>,
1784
1785    /// State definitions for multi-state props. Key = state name.
1786    #[serde(default)]
1787    pub states: HashMap<String, PropStateDef>,
1788}
1789
1790// =============================================================================
1791// §17  CHRONOBREAK CONFIG
1792// =============================================================================
1793
1794/// Chronobreak (timeline fork and replay) parameters.
1795#[derive(Debug, Clone, Serialize, Deserialize)]
1796pub struct ChronoshiftConfig {
1797    /// Whether chronoshift is enabled for this pack. Default: false.
1798    #[serde(default)]
1799    pub enabled: bool,
1800
1801    /// Maximum checkpoint history depth. Default: 10.
1802    #[serde(default = "default_max_checkpoint_depth")]
1803    pub max_checkpoint_depth: u32,
1804
1805    /// Replay speed multiplier. 1.0 = real time. Default: 1.0.
1806    #[serde(default = "default_replay_speed_multiplier")]
1807    pub replay_speed_multiplier: f64,
1808
1809    /// Auto-checkpoint interval in ticks. 0 = disabled. Default: 0.
1810    #[serde(default)]
1811    pub auto_checkpoint_interval_ticks: u64,
1812
1813    /// Maximum concurrent timeline forks. Default: 4.
1814    #[serde(default = "default_max_fork_count")]
1815    pub max_fork_count: u32,
1816
1817    /// Maximum compute budget percentage allocated to forks. Default: 25.
1818    #[serde(default = "default_fork_compute_cap_pct")]
1819    pub fork_compute_cap_pct: u32,
1820}
1821
1822fn default_max_checkpoint_depth() -> u32 {
1823    10
1824}
1825fn default_replay_speed_multiplier() -> f64 {
1826    1.0
1827}
1828fn default_max_fork_count() -> u32 {
1829    4
1830}
1831fn default_fork_compute_cap_pct() -> u32 {
1832    25
1833}
1834
1835impl Default for ChronoshiftConfig {
1836    fn default() -> Self {
1837        Self {
1838            enabled: false,
1839            max_checkpoint_depth: default_max_checkpoint_depth(),
1840            replay_speed_multiplier: default_replay_speed_multiplier(),
1841            auto_checkpoint_interval_ticks: 0,
1842            max_fork_count: default_max_fork_count(),
1843            fork_compute_cap_pct: default_fork_compute_cap_pct(),
1844        }
1845    }
1846}
1847
1848// =============================================================================
1849// §18  FORENSICS CONFIG
1850// =============================================================================
1851
1852/// Forensics and proof lane parameters.
1853#[derive(Debug, Clone, Serialize, Deserialize)]
1854pub struct ForensicsConfig {
1855    /// Whether the proof lane is enabled. Default: false.
1856    #[serde(default)]
1857    pub proof_lane_enabled: bool,
1858
1859    /// Whether BLAKE3 domain separation is used for hashing. Default: true.
1860    #[serde(default = "default_true")]
1861    pub blake3_domain_separation: bool,
1862
1863    /// Whether Merkle state root computation is enabled. Default: false.
1864    #[serde(default)]
1865    pub merkle_state_root_enabled: bool,
1866
1867    /// Whether Ed25519 signing of canon events is enabled. Default: false.
1868    #[serde(default)]
1869    pub ed25519_signing_enabled: bool,
1870
1871    /// Whether public audit tables are exposed. Default: false.
1872    #[serde(default)]
1873    pub public_audit_tables: bool,
1874}
1875
1876impl Default for ForensicsConfig {
1877    fn default() -> Self {
1878        Self {
1879            proof_lane_enabled: false,
1880            blake3_domain_separation: true,
1881            merkle_state_root_enabled: false,
1882            ed25519_signing_enabled: false,
1883            public_audit_tables: false,
1884        }
1885    }
1886}
1887
1888// =============================================================================
1889// §19a  PHYSICS CONFIG
1890// =============================================================================
1891
1892/// Physics environment parameters for pack-driven worlds.
1893#[derive(Debug, Clone, Serialize, Deserialize)]
1894pub struct PhysicsConfig {
1895    /// Planetary gravity acceleration. Default: 9.81.
1896    #[serde(default = "default_gravity_planetary")]
1897    pub gravity_planetary: f32,
1898
1899    /// Local gravity override (if set, overrides planetary at region level).
1900    #[serde(default)]
1901    pub gravity_local: Option<f32>,
1902
1903    /// Atmospheric density multiplier. Default: 1.0.
1904    #[serde(default = "default_atmosphere_density")]
1905    pub atmosphere_density: f32,
1906
1907    /// Ambient temperature in Celsius. Default: 20.0.
1908    #[serde(default = "default_temperature_ambient")]
1909    pub temperature_ambient: f32,
1910
1911    /// Emitter preset IDs to pre-load.
1912    #[serde(default)]
1913    pub emitter_presets: Vec<String>,
1914
1915    /// Force field configurations.
1916    #[serde(default)]
1917    pub force_fields: Vec<ForceFieldConfig>,
1918
1919    /// Collision plane configurations.
1920    #[serde(default)]
1921    pub collision_planes: Vec<CollisionPlaneConfig>,
1922}
1923
1924/// Force field configuration for pack-driven physics.
1925#[derive(Debug, Clone, Serialize, Deserialize)]
1926pub struct ForceFieldConfig {
1927    /// Force field kind identifier.
1928    pub kind: String,
1929
1930    /// Position in world space.
1931    #[serde(default)]
1932    pub position: [f32; 3],
1933
1934    /// Field strength. Default: 9.81.
1935    #[serde(default = "default_ff_strength")]
1936    pub strength: f32,
1937
1938    /// Field radius. Default: 100.0.
1939    #[serde(default = "default_ff_radius")]
1940    pub radius: f32,
1941}
1942
1943/// Collision plane configuration for pack-driven physics.
1944#[derive(Debug, Clone, Serialize, Deserialize)]
1945pub struct CollisionPlaneConfig {
1946    /// Plane normal vector. Default: [0, 1, 0] (up).
1947    #[serde(default = "default_plane_normal")]
1948    pub normal: [f32; 3],
1949
1950    /// Plane offset from origin. Default: 0.0.
1951    #[serde(default)]
1952    pub offset: f32,
1953
1954    /// Coefficient of restitution (bounciness). Default: 0.5.
1955    #[serde(default = "default_restitution")]
1956    pub restitution: f32,
1957}
1958
1959fn default_gravity_planetary() -> f32 {
1960    9.81
1961}
1962fn default_atmosphere_density() -> f32 {
1963    1.0
1964}
1965fn default_temperature_ambient() -> f32 {
1966    20.0
1967}
1968fn default_ff_strength() -> f32 {
1969    9.81
1970}
1971fn default_ff_radius() -> f32 {
1972    100.0
1973}
1974fn default_plane_normal() -> [f32; 3] {
1975    [0.0, 1.0, 0.0]
1976}
1977fn default_restitution() -> f32 {
1978    0.5
1979}
1980
1981impl Default for PhysicsConfig {
1982    fn default() -> Self {
1983        Self {
1984            gravity_planetary: default_gravity_planetary(),
1985            gravity_local: None,
1986            atmosphere_density: default_atmosphere_density(),
1987            temperature_ambient: default_temperature_ambient(),
1988            emitter_presets: Vec::new(),
1989            force_fields: Vec::new(),
1990            collision_planes: Vec::new(),
1991        }
1992    }
1993}
1994
1995// =============================================================================
1996// §18b  GPU PIPELINE CONFIG (v1.0.0)
1997// =============================================================================
1998
1999/// Meshlet LOD configuration for GPU-driven rendering.
2000#[derive(Debug, Clone, Serialize, Deserialize)]
2001pub struct MeshletLodConfig {
2002    /// Maximum meshlets per object. Default: 1024.
2003    #[serde(default = "default_max_meshlets_per_object")]
2004    pub max_meshlets_per_object: u32,
2005    /// LOD distance thresholds (world units).
2006    #[serde(default)]
2007    pub lod_distances: Vec<f32>,
2008    /// Maximum vertices per meshlet. Default: 64.
2009    #[serde(default = "default_max_vertices_per_meshlet")]
2010    pub max_vertices_per_meshlet: u32,
2011    /// Maximum triangles per meshlet. Default: 126.
2012    #[serde(default = "default_max_triangles_per_meshlet")]
2013    pub max_triangles_per_meshlet: u32,
2014}
2015
2016fn default_max_meshlets_per_object() -> u32 {
2017    1024
2018}
2019fn default_max_vertices_per_meshlet() -> u32 {
2020    64
2021}
2022fn default_max_triangles_per_meshlet() -> u32 {
2023    126
2024}
2025
2026impl Default for MeshletLodConfig {
2027    fn default() -> Self {
2028        Self {
2029            max_meshlets_per_object: default_max_meshlets_per_object(),
2030            lod_distances: vec![50.0, 100.0, 200.0],
2031            max_vertices_per_meshlet: default_max_vertices_per_meshlet(),
2032            max_triangles_per_meshlet: default_max_triangles_per_meshlet(),
2033        }
2034    }
2035}
2036
2037/// Meshlet emission configuration for DreamMatter integration.
2038#[derive(Debug, Clone, Serialize, Deserialize)]
2039pub struct MeshletEmissionConfig {
2040    /// Maximum particles per emitter. Default: 1024.
2041    #[serde(default = "default_max_particles_per_emitter")]
2042    pub max_particles_per_emitter: u32,
2043    /// Default particle lifetime in seconds.
2044    #[serde(default = "default_particle_lifetime")]
2045    pub default_lifetime: f32,
2046    /// Default emission rate (particles/sec).
2047    #[serde(default = "default_emission_rate")]
2048    pub default_emission_rate: f32,
2049}
2050
2051fn default_max_particles_per_emitter() -> u32 {
2052    1024
2053}
2054fn default_particle_lifetime() -> f32 {
2055    2.0
2056}
2057fn default_emission_rate() -> f32 {
2058    100.0
2059}
2060
2061impl Default for MeshletEmissionConfig {
2062    fn default() -> Self {
2063        Self {
2064            max_particles_per_emitter: default_max_particles_per_emitter(),
2065            default_lifetime: default_particle_lifetime(),
2066            default_emission_rate: default_emission_rate(),
2067        }
2068    }
2069}
2070
2071/// Observer configuration for Quantum Culling.
2072#[derive(Debug, Clone, Serialize, Deserialize)]
2073pub struct WaymarkObserverConfig {
2074    /// Default FOV radius in cells. Default: 10.
2075    #[serde(default = "default_fov_radius")]
2076    pub default_fov_radius: i32,
2077    /// Default topology layer for the observer. Default: "area".
2078    #[serde(default = "default_observer_layer")]
2079    pub default_layer: String,
2080}
2081
2082fn default_fov_radius() -> i32 {
2083    10
2084}
2085fn default_observer_layer() -> String {
2086    "area".into()
2087}
2088
2089impl Default for WaymarkObserverConfig {
2090    fn default() -> Self {
2091        Self {
2092            default_fov_radius: default_fov_radius(),
2093            default_layer: default_observer_layer(),
2094        }
2095    }
2096}
2097
2098/// Promotion target for DreamMatter → server authority handoff.
2099#[derive(Debug, Clone, Serialize, Deserialize)]
2100pub struct WaymarkPromotionTarget {
2101    /// Kind identifier for the promotion event.
2102    pub kind: String,
2103    /// Entity type to create on promotion.
2104    pub entity_type: String,
2105    /// Minimum age before promotion (seconds).
2106    #[serde(default)]
2107    pub min_age: f32,
2108}
2109
2110/// Mesh source config for FBX/GLTF/primitive references in packs.
2111#[derive(Debug, Clone, Serialize, Deserialize)]
2112pub struct MeshSourceConfig {
2113    /// Built-in primitive type (e.g., "Cube", "Sphere"). Mutually exclusive with paths.
2114    #[serde(default)]
2115    pub primitive_type: Option<String>,
2116    /// Path to FBX binary file (relative to pack root).
2117    #[serde(default)]
2118    pub fbx_path: Option<String>,
2119    /// Path to GLTF file (relative to pack root).
2120    #[serde(default)]
2121    pub gltf_path: Option<String>,
2122    /// Topology layers this mesh is active on.
2123    #[serde(default)]
2124    pub active_layers: Vec<u32>,
2125}
2126
2127// =============================================================================
2128// §19  TOPOLOGY CONFIG
2129// =============================================================================
2130
2131/// 9-layer topology configuration for the Dreamwell spatial hierarchy.
2132///
2133/// Layers (0-9): Universe → Galaxy → Sector → World → Realm → Region →
2134/// Area → Location → Room → Point.
2135///
2136/// If omitted, the seed pipeline creates a minimal 1-of-each hierarchy.
2137/// Each layer entry defines the initial topology entities to create.
2138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2139pub struct TopologyConfig {
2140    /// Universe name. Defaults to pack title.
2141    #[serde(default)]
2142    pub universe_name: Option<String>,
2143
2144    /// Galaxy definitions. If empty, one galaxy is auto-created.
2145    #[serde(default)]
2146    pub galaxies: Vec<GalaxyDef>,
2147
2148    /// Sector definitions. If empty, one sector per galaxy is auto-created.
2149    #[serde(default)]
2150    pub sectors: Vec<SectorDef>,
2151
2152    /// World definitions. If empty, one world is created from pack identity.
2153    #[serde(default)]
2154    pub worlds: Vec<WorldDef>,
2155
2156    /// Realm definitions per world. If empty, one realm per world.
2157    #[serde(default)]
2158    pub realms: Vec<RealmDef>,
2159
2160    /// Region definitions per realm. If empty, one region per realm.
2161    #[serde(default)]
2162    pub regions: Vec<RegionDef>,
2163
2164    /// Area definitions per region. If empty, one area per region.
2165    #[serde(default)]
2166    pub areas: Vec<AreaDef>,
2167
2168    /// Location definitions per area.
2169    #[serde(default)]
2170    pub locations: Vec<LocationDef>,
2171
2172    /// Point definitions (POI/AOI spawn markers).
2173    #[serde(default)]
2174    pub points: Vec<PointDef>,
2175}
2176
2177/// Galaxy definition for topology seeding.
2178#[derive(Debug, Clone, Serialize, Deserialize)]
2179pub struct GalaxyDef {
2180    pub id: String,
2181    pub name: String,
2182    #[serde(default)]
2183    pub kind: String,
2184}
2185
2186/// Sector definition for topology seeding.
2187#[derive(Debug, Clone, Serialize, Deserialize)]
2188pub struct SectorDef {
2189    pub id: String,
2190    pub galaxy_id: String,
2191    pub name: String,
2192    #[serde(default)]
2193    pub governing_entity_id: String,
2194}
2195
2196/// World definition for topology seeding.
2197#[derive(Debug, Clone, Serialize, Deserialize)]
2198pub struct WorldDef {
2199    pub id: String,
2200    #[serde(default)]
2201    pub sector_id: String,
2202    pub name: String,
2203    #[serde(default)]
2204    pub theme: String,
2205}
2206
2207/// Realm definition for topology seeding.
2208#[derive(Debug, Clone, Serialize, Deserialize)]
2209pub struct RealmDef {
2210    pub id: String,
2211    pub world_id: String,
2212    pub name: String,
2213}
2214
2215/// Region definition for topology seeding.
2216#[derive(Debug, Clone, Serialize, Deserialize)]
2217pub struct RegionDef {
2218    pub id: String,
2219    pub realm_id: String,
2220    pub name: String,
2221    #[serde(default = "default_pressure")]
2222    pub pressure_security: i32,
2223    #[serde(default = "default_pressure")]
2224    pub pressure_scarcity: i32,
2225    #[serde(default = "default_pressure")]
2226    pub pressure_unrest: i32,
2227    #[serde(default = "default_pressure")]
2228    pub pressure_anomaly: i32,
2229}
2230
2231fn default_pressure() -> i32 {
2232    25
2233}
2234
2235/// Area definition for topology seeding.
2236#[derive(Debug, Clone, Serialize, Deserialize)]
2237pub struct AreaDef {
2238    pub id: String,
2239    pub region_id: String,
2240    pub name: String,
2241    #[serde(default)]
2242    pub kind: String,
2243    #[serde(default = "default_danger")]
2244    pub danger_rating: i32,
2245}
2246
2247fn default_danger() -> i32 {
2248    1
2249}
2250
2251/// Location definition for topology seeding.
2252#[derive(Debug, Clone, Serialize, Deserialize)]
2253pub struct LocationDef {
2254    pub id: String,
2255    pub area_id: String,
2256    pub name: String,
2257    #[serde(default)]
2258    pub kind: String,
2259    #[serde(default)]
2260    pub x: i32,
2261    #[serde(default)]
2262    pub y: i32,
2263}
2264
2265/// Point definition (POI/AOI markers) for topology seeding.
2266#[derive(Debug, Clone, Serialize, Deserialize)]
2267pub struct PointDef {
2268    pub id: String,
2269    #[serde(default)]
2270    pub location_id: String,
2271    #[serde(default)]
2272    pub room_id: String,
2273    #[serde(default)]
2274    pub x: i32,
2275    #[serde(default)]
2276    pub y: i32,
2277    #[serde(default)]
2278    pub kind: String,
2279    #[serde(default)]
2280    pub label: String,
2281    #[serde(default)]
2282    pub trigger_id: String,
2283    #[serde(default)]
2284    pub template_id: String,
2285}
2286
2287// =============================================================================
2288// §20  DEFAULT IMPLEMENTATION — DreamwellPackV1
2289// =============================================================================
2290
2291impl Default for DreamwellPackV1 {
2292    fn default() -> Self {
2293        Self {
2294            id: String::new(),
2295            title: String::new(),
2296            version: String::new(),
2297            description: String::new(),
2298            schema_version: default_schema_version(),
2299            tags: Vec::new(),
2300            world_id: None,
2301            theme: None,
2302            scenario: None,
2303            scenario_script: None,
2304            starting_map: None,
2305            grid: GridConfig::default(),
2306            spatial: SpatialConfig::default(),
2307            display: DisplayConfig::default(),
2308            features: FeatureFlags::default(),
2309            equip_slots: Vec::new(),
2310            simulation: SimulationConfig::default(),
2311            combat: CombatConfig::default(),
2312            economy: EconomyConfig::default(),
2313            progression: ProgressionConfig::default(),
2314            agents: AgentConfig::default(),
2315            tiles: TileConfig::default(),
2316            canon: CanonConfig::default(),
2317            scripting: ScriptingConfig::default(),
2318            content: ContentConfig::default(),
2319            eviction: EvictionConfig::default(),
2320            props: Vec::new(),
2321            generators: serde_json::Value::Null,
2322            connection_type_props: HashMap::new(),
2323            boon_triggers: Vec::new(),
2324            uses_companion_services: false,
2325            scenario_config: serde_json::Value::Null,
2326            encumbrance: None,
2327            ai_brains: HashMap::new(),
2328            rules: None,
2329            identity: None,
2330            boot_sequence: None,
2331            topology: TopologyConfig::default(),
2332            chronoshift: ChronoshiftConfig::default(),
2333            forensics: ForensicsConfig::default(),
2334            physics: PhysicsConfig::default(),
2335            meshlet_lod: None,
2336            meshlet_emission: None,
2337            observer_config: None,
2338            promotion_targets: Vec::new(),
2339            avatar_defaults: None,
2340        }
2341    }
2342}
2343
2344// =============================================================================
2345// §20  METHODS — Parsing, Serialization, Validation, Migration
2346// =============================================================================
2347
2348impl DreamwellPackV1 {
2349    /// Parse a JSON string into a DreamwellPackV1.
2350    ///
2351    /// Accepts both v1.0.0 format and legacy pack.json format. Missing fields
2352    /// receive their default values via serde defaults.
2353    pub fn from_json(json: &str) -> Result<Self, String> {
2354        serde_json::from_str::<Self>(json).map_err(|e| format!("pack_parse_error: {}", e))
2355    }
2356
2357    /// Serialize this pack to a pretty-printed JSON string.
2358    pub fn to_json(&self) -> String {
2359        serde_json::to_string_pretty(self).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
2360    }
2361
2362    /// Validate the pack configuration. Returns a list of validation errors.
2363    /// An empty list means the pack is valid.
2364    pub fn validate(&self) -> Vec<String> {
2365        let mut errors = Vec::new();
2366
2367        // -- Identity --
2368        if self.id.is_empty() {
2369            errors.push("id_required: pack id must be non-empty".to_string());
2370        } else if self.id.len() > 128 {
2371            errors.push(format!("id_too_long: {} chars (max 128)", self.id.len()));
2372        } else if !self
2373            .id
2374            .chars()
2375            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
2376        {
2377            errors.push("id_invalid_chars: pack id must contain only [a-zA-Z0-9_-]".to_string());
2378        }
2379
2380        if self.title.is_empty() {
2381            errors.push("title_required: pack title must be non-empty".to_string());
2382        } else if self.title.len() > 256 {
2383            errors.push(format!("title_too_long: {} chars (max 256)", self.title.len()));
2384        }
2385
2386        if self.description.len() > 4096 {
2387            errors.push(format!(
2388                "description_too_long: {} chars (max 4096)",
2389                self.description.len()
2390            ));
2391        }
2392
2393        // -- Grid --
2394        if self.grid.width == 0 {
2395            errors.push("grid_width_zero: width must be > 0".to_string());
2396        }
2397        if self.grid.height == 0 {
2398            errors.push("grid_height_zero: height must be > 0".to_string());
2399        }
2400        if self.grid.width > 4096 {
2401            errors.push(format!("grid_width_too_large: {} (max 4096)", self.grid.width));
2402        }
2403        if self.grid.height > 4096 {
2404            errors.push(format!("grid_height_too_large: {} (max 4096)", self.grid.height));
2405        }
2406        if self.grid.chunk_size == 0 || !self.grid.chunk_size.is_power_of_two() {
2407            errors.push(format!(
2408                "grid_chunk_size_invalid: {} (must be a non-zero power of two)",
2409                self.grid.chunk_size
2410            ));
2411        }
2412
2413        // -- Spatial --
2414        if self.spatial.cell_size <= 0 {
2415            errors.push(format!(
2416                "spatial_cell_size_invalid: {} (must be > 0)",
2417                self.spatial.cell_size
2418            ));
2419        }
2420        if self.spatial.fov_default_radius < 0 {
2421            errors.push(format!(
2422                "spatial_fov_default_radius_negative: {}",
2423                self.spatial.fov_default_radius
2424            ));
2425        }
2426        if self.spatial.fov_max_radius < self.spatial.fov_default_radius {
2427            errors.push(format!(
2428                "spatial_fov_max_radius_less_than_default: max={} default={}",
2429                self.spatial.fov_max_radius, self.spatial.fov_default_radius
2430            ));
2431        }
2432        if self.spatial.max_pathfind_steps == 0 {
2433            errors.push("spatial_max_pathfind_steps_zero".to_string());
2434        }
2435
2436        // -- Simulation --
2437        if self.simulation.tick_rate_ms == 0 {
2438            errors.push("simulation_tick_rate_ms_zero".to_string());
2439        }
2440        if self.simulation.ticks_per_day == 0 {
2441            errors.push("simulation_ticks_per_day_zero".to_string());
2442        }
2443        if self.simulation.time_dilation <= 0.0 {
2444            errors.push(format!(
2445                "simulation_time_dilation_invalid: {} (must be > 0.0)",
2446                self.simulation.time_dilation
2447            ));
2448        }
2449        if self.simulation.max_entities_per_area == 0 {
2450            errors.push("simulation_max_entities_per_area_zero".to_string());
2451        }
2452        if self.simulation.max_events_per_tick == 0 {
2453            errors.push("simulation_max_events_per_tick_zero".to_string());
2454        }
2455
2456        // -- Combat --
2457        if self.combat.base_hit_chance > 100 {
2458            errors.push(format!(
2459                "combat_base_hit_chance_out_of_range: {} (max 100)",
2460                self.combat.base_hit_chance
2461            ));
2462        }
2463        if self.combat.crit_multiplier < 1.0 {
2464            errors.push(format!(
2465                "combat_crit_multiplier_too_low: {} (min 1.0)",
2466                self.combat.crit_multiplier
2467            ));
2468        }
2469        if self.combat.dodge_base > 100 {
2470            errors.push(format!(
2471                "combat_dodge_base_out_of_range: {} (max 100)",
2472                self.combat.dodge_base
2473            ));
2474        }
2475        if self.combat.parry_base > 100 {
2476            errors.push(format!(
2477                "combat_parry_base_out_of_range: {} (max 100)",
2478                self.combat.parry_base
2479            ));
2480        }
2481        if self.combat.flee_threshold_hp_pct > 100 {
2482            errors.push(format!(
2483                "combat_flee_threshold_hp_pct_out_of_range: {} (max 100)",
2484                self.combat.flee_threshold_hp_pct
2485            ));
2486        }
2487        if self.combat.revive_hp_pct > 100 {
2488            errors.push(format!(
2489                "combat_revive_hp_pct_out_of_range: {} (max 100)",
2490                self.combat.revive_hp_pct
2491            ));
2492        }
2493        if self.combat.resistance_cap > 100 {
2494            errors.push(format!(
2495                "combat_resistance_cap_out_of_range: {} (max 100)",
2496                self.combat.resistance_cap
2497            ));
2498        }
2499
2500        // -- Economy --
2501        if self.economy.trade_tax_pct > 100 {
2502            errors.push(format!(
2503                "economy_trade_tax_pct_out_of_range: {} (max 100)",
2504                self.economy.trade_tax_pct
2505            ));
2506        }
2507        if self.economy.market_fee_pct > 100 {
2508            errors.push(format!(
2509                "economy_market_fee_pct_out_of_range: {} (max 100)",
2510                self.economy.market_fee_pct
2511            ));
2512        }
2513        if self.economy.crafting_fail_chance_base > 100 {
2514            errors.push(format!(
2515                "economy_crafting_fail_chance_base_out_of_range: {} (max 100)",
2516                self.economy.crafting_fail_chance_base
2517            ));
2518        }
2519
2520        // -- Progression --
2521        if self.progression.max_level == 0 {
2522            errors.push("progression_max_level_zero".to_string());
2523        }
2524        if self.progression.xp_curve_base <= 0.0 {
2525            errors.push(format!(
2526                "progression_xp_curve_base_invalid: {} (must be > 0.0)",
2527                self.progression.xp_curve_base
2528            ));
2529        }
2530        if self.progression.reputation_min > self.progression.reputation_max {
2531            errors.push(format!(
2532                "progression_reputation_range_inverted: min={} max={}",
2533                self.progression.reputation_min, self.progression.reputation_max
2534            ));
2535        }
2536
2537        // -- Agents --
2538        if self.agents.cognition_budget_per_tick == 0 {
2539            errors.push("agents_cognition_budget_per_tick_zero".to_string());
2540        }
2541        if self.agents.perception_range == 0 {
2542            errors.push("agents_perception_range_zero".to_string());
2543        }
2544        if self.agents.memory_capacity == 0 {
2545            errors.push("agents_memory_capacity_zero".to_string());
2546        }
2547        if self.agents.inference_timeout_ms == 0 {
2548            errors.push("agents_inference_timeout_ms_zero".to_string());
2549        }
2550        if self.agents.inference_max_tokens == 0 {
2551            errors.push("agents_inference_max_tokens_zero".to_string());
2552        }
2553
2554        // -- Canon --
2555        if self.canon.block_hash_interval == 0 {
2556            errors.push("canon_block_hash_interval_zero".to_string());
2557        }
2558        if self.canon.tick_hash_interval == 0 {
2559            errors.push("canon_tick_hash_interval_zero".to_string());
2560        }
2561        if self.canon.max_traversal_depth == 0 {
2562            errors.push("canon_max_traversal_depth_zero".to_string());
2563        }
2564
2565        // -- Scripting --
2566        if self.scripting.max_runes == 0 {
2567            errors.push("scripting_max_runes_zero".to_string());
2568        }
2569        if self.scripting.fire_once_capacity == 0 {
2570            errors.push("scripting_fire_once_capacity_zero".to_string());
2571        }
2572
2573        // -- Chronobreak --
2574        if self.chronoshift.enabled {
2575            if self.chronoshift.max_checkpoint_depth == 0 {
2576                errors.push("chronoshift_max_checkpoint_depth_zero".to_string());
2577            }
2578            if self.chronoshift.replay_speed_multiplier <= 0.0 {
2579                errors.push(format!(
2580                    "chronoshift_replay_speed_multiplier_invalid: {} (must be > 0.0)",
2581                    self.chronoshift.replay_speed_multiplier
2582                ));
2583            }
2584            if self.chronoshift.max_fork_count == 0 {
2585                errors.push("chronoshift_max_fork_count_zero".to_string());
2586            }
2587            if self.chronoshift.fork_compute_cap_pct > 100 {
2588                errors.push(format!(
2589                    "chronoshift_fork_compute_cap_pct_out_of_range: {} (max 100)",
2590                    self.chronoshift.fork_compute_cap_pct
2591                ));
2592            }
2593        }
2594
2595        // -- Props --
2596        let mut prop_ids = std::collections::HashSet::new();
2597        for (i, prop) in self.props.iter().enumerate() {
2598            if prop.id.is_empty() {
2599                errors.push(format!("prop[{}]_id_required", i));
2600            } else if !prop_ids.insert(&prop.id) {
2601                errors.push(format!("prop_id_duplicate: \"{}\"", prop.id));
2602            }
2603            if prop.name.is_empty() {
2604                errors.push(format!("prop[{}]_name_required (id: \"{}\")", i, prop.id));
2605            }
2606            if prop.has_secondary_state && prop.states.is_empty() {
2607                errors.push(format!("prop_has_secondary_state_but_no_states: \"{}\"", prop.id));
2608            }
2609            if let Some(ref default_state) = prop.default_state {
2610                if prop.has_secondary_state && !prop.states.contains_key(default_state) {
2611                    errors.push(format!(
2612                        "prop_default_state_not_found: \"{}\" state \"{}\"",
2613                        prop.id, default_state
2614                    ));
2615                }
2616            }
2617            if prop.has_inventory && (prop.container_capacity.is_none() || prop.container_capacity == Some(0)) {
2618                errors.push(format!("prop_has_inventory_but_no_capacity: \"{}\"", prop.id));
2619            }
2620        }
2621
2622        errors
2623    }
2624
2625    /// Detect whether a JSON value is in the legacy pack.json format
2626    /// (pre-v1.0.0, no `schema_version` field).
2627    pub fn is_legacy_format(json: &serde_json::Value) -> bool {
2628        if let Some(obj) = json.as_object() {
2629            // Legacy format: has "id" but no "schema_version" field.
2630            obj.contains_key("id") && !obj.contains_key("schema_version")
2631        } else {
2632            false
2633        }
2634    }
2635
2636    /// Migrate a legacy pack.json value into the v1.0.0 schema.
2637    ///
2638    /// Legacy fields are mapped directly since the v1.0.0 struct is a superset
2639    /// of the legacy format. The `schema_version` field is injected and the
2640    /// result is deserialized through the normal path.
2641    pub fn migrate_legacy(json: &serde_json::Value) -> Result<Self, String> {
2642        let obj = json
2643            .as_object()
2644            .ok_or_else(|| "migrate_legacy_error: expected JSON object".to_string())?;
2645
2646        let mut migrated = obj.clone();
2647
2648        // Inject schema version.
2649        migrated.insert(
2650            "schema_version".to_string(),
2651            serde_json::Value::String(SCHEMA_VERSION.to_string()),
2652        );
2653
2654        let json_str =
2655            serde_json::to_string(&migrated).map_err(|e| format!("migrate_legacy_serialize_error: {}", e))?;
2656
2657        Self::from_json(&json_str)
2658    }
2659}
2660
2661// =============================================================================
2662// §21  TESTS
2663// =============================================================================
2664
2665#[cfg(test)]
2666mod tests {
2667    use super::*;
2668
2669    #[test]
2670    fn default_pack_has_schema_version() {
2671        let pack = DreamwellPackV1::default();
2672        assert_eq!(pack.schema_version, SCHEMA_VERSION);
2673    }
2674
2675    #[test]
2676    fn default_pack_validation_requires_id_and_title() {
2677        let pack = DreamwellPackV1::default();
2678        let errors = pack.validate();
2679        assert!(errors.iter().any(|e| e.contains("id_required")));
2680        assert!(errors.iter().any(|e| e.contains("title_required")));
2681    }
2682
2683    #[test]
2684    fn minimal_valid_pack() {
2685        let json = r#"{"id": "test_pack", "title": "Test Pack"}"#;
2686        let pack = DreamwellPackV1::from_json(json).unwrap();
2687        assert_eq!(pack.id, "test_pack");
2688        assert_eq!(pack.title, "Test Pack");
2689        assert_eq!(pack.schema_version, SCHEMA_VERSION);
2690        assert_eq!(pack.grid.width, 80);
2691        assert_eq!(pack.grid.height, 50);
2692        assert_eq!(pack.grid.chunk_size, 32);
2693        assert_eq!(pack.spatial.cell_size, 128);
2694        assert_eq!(pack.combat.base_hit_chance, 80);
2695        assert_eq!(pack.economy.starting_gold, 100);
2696        assert!(pack.validate().is_empty());
2697    }
2698
2699    #[test]
2700    fn roundtrip_serialization() {
2701        let json = r#"{"id": "roundtrip", "title": "Roundtrip Test"}"#;
2702        let pack = DreamwellPackV1::from_json(json).unwrap();
2703        let serialized = pack.to_json();
2704        let reparsed = DreamwellPackV1::from_json(&serialized).unwrap();
2705        assert_eq!(reparsed.id, "roundtrip");
2706        assert_eq!(reparsed.grid.width, 80);
2707        assert_eq!(reparsed.combat.crit_multiplier, 2.0);
2708    }
2709
2710    #[test]
2711    fn legacy_format_detection() {
2712        let legacy: serde_json::Value =
2713            serde_json::from_str(r#"{"id": "arena", "title": "Training Arena", "grid": {"width": 32, "height": 16}}"#)
2714                .unwrap();
2715        assert!(DreamwellPackV1::is_legacy_format(&legacy));
2716
2717        let v1: serde_json::Value = serde_json::from_str(
2718            r#"{"id": "arena", "title": "Training Arena", "schema_version": "dreamwell_waymark_v1.0.0"}"#,
2719        )
2720        .unwrap();
2721        assert!(!DreamwellPackV1::is_legacy_format(&v1));
2722    }
2723
2724    #[test]
2725    fn legacy_migration() {
2726        let legacy: serde_json::Value = serde_json::from_str(
2727            r#"{
2728                "id": "arena",
2729                "title": "Training Arena",
2730                "version": "0.1.0",
2731                "grid": {"width": 32, "height": 16},
2732                "features": {"content_plan": false, "boons": false}
2733            }"#,
2734        )
2735        .unwrap();
2736
2737        let pack = DreamwellPackV1::migrate_legacy(&legacy).unwrap();
2738        assert_eq!(pack.id, "arena");
2739        assert_eq!(pack.schema_version, SCHEMA_VERSION);
2740        assert_eq!(pack.grid.width, 32);
2741        assert_eq!(pack.grid.height, 16);
2742        assert!(!pack.features.content_plan);
2743        assert!(!pack.features.boons);
2744        // Defaults applied for missing fields.
2745        assert_eq!(pack.combat.base_hit_chance, 80);
2746        assert_eq!(pack.spatial.cell_size, 128);
2747    }
2748
2749    #[test]
2750    fn legacy_ayora_pack_loads() {
2751        let json = r#"{
2752            "id": "ayora",
2753            "title": "Ayora: The Barracks",
2754            "scenario_script": "scenario/main.wm",
2755            "uses_companion_services": true,
2756            "scenario_config": {
2757                "companion_ids": ["kael", "ryn", "senna"],
2758                "boss_npc_id": "grimsby"
2759            },
2760            "version": "0.1.0",
2761            "grid": {"width": 120, "height": 80},
2762            "display": {"mana_name": "Grace"},
2763            "features": {"content_plan": false, "boons": false, "shrines": false},
2764            "equip_slots": ["Head", "Body", "Hands", "Feet", "Weapon", "Offhand"],
2765            "boon_triggers": [{"id": "pool_room_5", "type": "RoomEntry"}],
2766            "props": [
2767                {
2768                    "id": "weapon_rack",
2769                    "name": "Weapon Rack",
2770                    "glyph": "\u2551",
2771                    "description": "A training weapon rests in the rack.",
2772                    "blocking": true,
2773                    "targetable": true,
2774                    "interactive": false,
2775                    "bump_messages": ["The rack is bolted to the wall."]
2776                }
2777            ]
2778        }"#;
2779
2780        let pack = DreamwellPackV1::from_json(json).unwrap();
2781        assert_eq!(pack.id, "ayora");
2782        assert_eq!(pack.grid.width, 120);
2783        assert_eq!(pack.grid.height, 80);
2784        assert_eq!(pack.display.mana_name, "Grace");
2785        assert!(pack.uses_companion_services);
2786        assert_eq!(pack.equip_slots.len(), 6);
2787        assert_eq!(pack.props.len(), 1);
2788        assert_eq!(pack.props[0].id, "weapon_rack");
2789        assert!(pack.validate().is_empty());
2790    }
2791
2792    #[test]
2793    fn legacy_embersteel_pack_loads() {
2794        let json = r#"{
2795            "id": "embersteel",
2796            "title": "Operation: Embersteel",
2797            "description": "Restore an abandoned research outpost.",
2798            "world_id": "world:braxxis:v1",
2799            "theme": "SURVIVAL",
2800            "version": "1.0.0",
2801            "tags": ["ascii", "roguelike", "survival"],
2802            "ai_brains": {
2803                "passive_roamer": {"base_brain": "state_machine", "profile": "passive_roamer"}
2804            },
2805            "starting_map": "olympus_9",
2806            "scenario_script": "scenario/main.wm",
2807            "grid": {"width": 80, "height": 55},
2808            "features": {"containers": true, "line_of_sight": true, "collisions": true, "identity_system": true},
2809            "rules": {"tick_rate_hz": 20, "determinism": "STRICT"},
2810            "identity": {"format": "{glyph}#{suffix}"},
2811            "boot_sequence": {"lines": ["[INFO] Signal Received"]},
2812            "generators": {
2813                "olympus_9": {"generator_type": "template", "template": "olympus_9"}
2814            },
2815            "props": [
2816                {"id": "printer", "name": "Bioprinter", "glyph": "P", "description": "Still warm.", "blocking": true, "targetable": true, "interactive": true}
2817            ]
2818        }"#;
2819
2820        let pack = DreamwellPackV1::from_json(json).unwrap();
2821        assert_eq!(pack.id, "embersteel");
2822        assert_eq!(pack.world_id, Some("world:braxxis:v1".to_string()));
2823        assert_eq!(pack.theme, Some("SURVIVAL".to_string()));
2824        assert!(pack.features.containers);
2825        assert!(pack.features.identity_system);
2826        assert!(pack.features.line_of_sight);
2827        assert_eq!(pack.ai_brains.len(), 1);
2828        assert!(pack.rules.is_some());
2829        assert!(pack.identity.is_some());
2830        assert!(pack.boot_sequence.is_some());
2831        assert!(pack.validate().is_empty());
2832    }
2833
2834    #[test]
2835    fn tile_defaults_match_tile_rs() {
2836        let tiles = TileConfig::default();
2837        assert_eq!(tiles.ground.glyph, b'.' as u16);
2838        assert_eq!(tiles.dirt.glyph, b',' as u16);
2839        assert_eq!(tiles.sand.glyph, b':' as u16);
2840        assert_eq!(tiles.mud.glyph, b'`' as u16);
2841        assert_eq!(tiles.liquid.glyph, b'~' as u16);
2842        assert_eq!(tiles.deep_water.glyph, b'=' as u16);
2843        assert_eq!(tiles.rocky_ground.glyph, b'b' as u16);
2844        assert_eq!(tiles.grass.glyph, b';' as u16);
2845        assert_eq!(tiles.debris.glyph, b'\'' as u16);
2846        assert_eq!(tiles.wall.glyph, b'#' as u16);
2847        assert_eq!(tiles.wall_vert.glyph, b'|' as u16);
2848        assert_eq!(tiles.wall_horiz.glyph, b'_' as u16);
2849        assert_eq!(tiles.door_closed.glyph, b'I' as u16);
2850        assert_eq!(tiles.door_open.glyph, b'*' as u16);
2851        assert_eq!(tiles.stairs_up.glyph, b'^' as u16);
2852        assert_eq!(tiles.stairs_down.glyph, b'v' as u16);
2853        assert_eq!(tiles.boulder.glyph, b'o' as u16);
2854        assert_eq!(tiles.chest.glyph, b'C' as u16);
2855        assert_eq!(tiles.shrine.glyph, b'S' as u16);
2856        assert_eq!(tiles.hazard.glyph, b'x' as u16);
2857        assert_eq!(tiles.smoke.glyph, b'%' as u16);
2858
2859        // Movement costs match tile.rs move_cost()
2860        assert_eq!(tiles.ground.move_cost, 1);
2861        assert_eq!(tiles.sand.move_cost, 2);
2862        assert_eq!(tiles.liquid.move_cost, 3);
2863        assert_eq!(tiles.wall.move_cost, 999);
2864
2865        // Blocking rules match tile.rs blocks_move()
2866        assert!(tiles.wall.blocks_move);
2867        assert!(tiles.door_closed.blocks_move);
2868        assert!(tiles.deep_water.blocks_move);
2869        assert!(tiles.boulder.blocks_move);
2870        assert!(!tiles.ground.blocks_move);
2871        assert!(!tiles.liquid.blocks_move);
2872
2873        // Sight blocking matches tile.rs blocks_sight()
2874        assert!(tiles.wall.blocks_sight);
2875        assert!(tiles.door_closed.blocks_sight);
2876        assert!(tiles.boulder.blocks_sight);
2877        assert!(tiles.smoke.blocks_sight);
2878        assert!(!tiles.ground.blocks_sight);
2879        assert!(!tiles.deep_water.blocks_sight);
2880    }
2881
2882    #[test]
2883    fn prop_validation_catches_duplicates() {
2884        let json = r#"{
2885            "id": "test",
2886            "title": "Test",
2887            "props": [
2888                {"id": "p1", "name": "Prop One"},
2889                {"id": "p1", "name": "Prop One Dupe"}
2890            ]
2891        }"#;
2892        let pack = DreamwellPackV1::from_json(json).unwrap();
2893        let errors = pack.validate();
2894        assert!(errors.iter().any(|e| e.contains("prop_id_duplicate")));
2895    }
2896
2897    #[test]
2898    fn prop_validation_catches_state_mismatch() {
2899        let json = r#"{
2900            "id": "test",
2901            "title": "Test",
2902            "props": [
2903                {
2904                    "id": "broken",
2905                    "name": "Broken Prop",
2906                    "has_secondary_state": true,
2907                    "default_state": "missing_state"
2908                }
2909            ]
2910        }"#;
2911        let pack = DreamwellPackV1::from_json(json).unwrap();
2912        let errors = pack.validate();
2913        assert!(errors.iter().any(|e| e.contains("has_secondary_state_but_no_states")));
2914        assert!(errors.iter().any(|e| e.contains("default_state_not_found")));
2915    }
2916
2917    #[test]
2918    fn combat_range_validation() {
2919        let json = r#"{
2920            "id": "test",
2921            "title": "Test",
2922            "combat": {"base_hit_chance": 150, "dodge_base": 101, "resistance_cap": 200}
2923        }"#;
2924        let pack = DreamwellPackV1::from_json(json).unwrap();
2925        let errors = pack.validate();
2926        assert!(errors.iter().any(|e| e.contains("base_hit_chance_out_of_range")));
2927        assert!(errors.iter().any(|e| e.contains("dodge_base_out_of_range")));
2928        assert!(errors.iter().any(|e| e.contains("resistance_cap_out_of_range")));
2929    }
2930
2931    #[test]
2932    fn v1_full_config_loads() {
2933        let json = r#"{
2934            "id": "full",
2935            "title": "Full Config",
2936            "schema_version": "dreamwell_waymark_v1.0.0",
2937            "simulation": {"tick_rate_ms": 50, "ticks_per_day": 720, "time_dilation": 2.0},
2938            "combat": {"base_hit_chance": 75, "crit_multiplier": 2.5, "damage_types": ["physical", "fire"]},
2939            "economy": {"starting_gold": 500, "currency_types": ["gold", "silver"]},
2940            "progression": {"max_level": 50, "xp_curve_base": 1.8},
2941            "agents": {"cognition_budget_per_tick": 5, "max_concurrent_agents": 128},
2942            "canon": {"block_hash_interval": 200, "max_trigger_depth": 5},
2943            "scripting": {"max_runes": 512, "hot_reload_enabled": true},
2944            "chronoshift": {"enabled": true, "max_fork_count": 8},
2945            "forensics": {"proof_lane_enabled": true, "blake3_domain_separation": true}
2946        }"#;
2947        let pack = DreamwellPackV1::from_json(json).unwrap();
2948        assert_eq!(pack.simulation.tick_rate_ms, 50);
2949        assert_eq!(pack.simulation.ticks_per_day, 720);
2950        assert_eq!(pack.combat.base_hit_chance, 75);
2951        assert_eq!(pack.combat.damage_types.len(), 2);
2952        assert_eq!(pack.economy.starting_gold, 500);
2953        assert_eq!(pack.economy.currency_types.len(), 2);
2954        assert_eq!(pack.progression.max_level, 50);
2955        assert_eq!(pack.agents.max_concurrent_agents, 128);
2956        assert_eq!(pack.canon.block_hash_interval, 200);
2957        assert_eq!(pack.scripting.max_runes, 512);
2958        assert!(pack.scripting.hot_reload_enabled);
2959        assert!(pack.chronoshift.enabled);
2960        assert_eq!(pack.chronoshift.max_fork_count, 8);
2961        assert!(pack.forensics.proof_lane_enabled);
2962        assert!(pack.validate().is_empty());
2963    }
2964
2965    #[test]
2966    fn cartographers_toolkit_with_encumbrance_loads() {
2967        let json = r#"{
2968            "id": "cartographers_toolkit",
2969            "title": "The Cartographer's Toolkit",
2970            "version": "0.1.0",
2971            "grid": {"width": 120, "height": 80},
2972            "features": {"content_plan": false, "encumbrance": true, "containers": true},
2973            "encumbrance": {
2974                "capacity_stat": "strength",
2975                "capacity_multiplier": 1000,
2976                "tiers": [
2977                    {"id": "burdened", "threshold": 0.75, "speed_modifier": 0.5},
2978                    {"id": "overloaded", "threshold": 1.0, "speed_modifier": 0.0}
2979                ]
2980            },
2981            "equip_slots": ["Head", "Body", "Weapon", "Ring1", "Ring2"],
2982            "generators": {
2983                "cartographers_toolkit": {
2984                    "player_spawn": [60, 5],
2985                    "player_stats": {"hp": 25, "atk": 4, "def": 2, "mana": 20}
2986                }
2987            }
2988        }"#;
2989        let pack = DreamwellPackV1::from_json(json).unwrap();
2990        assert_eq!(pack.id, "cartographers_toolkit");
2991        assert!(pack.features.encumbrance);
2992        assert!(pack.features.containers);
2993        assert!(pack.encumbrance.is_some());
2994        assert_eq!(pack.equip_slots.len(), 5);
2995        assert!(pack.validate().is_empty());
2996    }
2997
2998    #[test]
2999    fn radiant_forest_with_ai_brains_loads() {
3000        let json = r#"{
3001            "id": "radiant_forest",
3002            "title": "The Radiant Forest",
3003            "ai_brains": {"wisp_brain": {"base_brain": "state_machine"}},
3004            "starting_map": "compound",
3005            "scenario_script": "scenario/main.wm",
3006            "version": "0.1.0",
3007            "grid": {"width": 80, "height": 55},
3008            "features": {"containers": true}
3009        }"#;
3010        let pack = DreamwellPackV1::from_json(json).unwrap();
3011        assert_eq!(pack.id, "radiant_forest");
3012        assert_eq!(pack.starting_map, Some("compound".to_string()));
3013        assert_eq!(pack.ai_brains.len(), 1);
3014        assert!(pack.ai_brains.contains_key("wisp_brain"));
3015        assert!(pack.validate().is_empty());
3016    }
3017}