1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13pub const SCHEMA_VERSION: &str = "dreamwell_waymark_v1.0.0";
19
20fn default_schema_version() -> String {
21 SCHEMA_VERSION.to_string()
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DreamwellPackV1 {
33 pub id: String,
36
37 pub title: String,
39
40 #[serde(default)]
42 pub version: String,
43
44 #[serde(default)]
46 pub description: String,
47
48 #[serde(default = "default_schema_version")]
50 pub schema_version: String,
51
52 #[serde(default)]
54 pub tags: Vec<String>,
55
56 #[serde(default)]
58 pub world_id: Option<String>,
59
60 #[serde(default)]
62 pub theme: Option<String>,
63
64 #[serde(default)]
67 pub scenario: Option<String>,
68
69 #[serde(default)]
71 pub scenario_script: Option<String>,
72
73 #[serde(default)]
75 pub starting_map: Option<String>,
76
77 #[serde(default)]
80 pub grid: GridConfig,
81
82 #[serde(default)]
84 pub spatial: SpatialConfig,
85
86 #[serde(default)]
89 pub display: DisplayConfig,
90
91 #[serde(default)]
94 pub features: FeatureFlags,
95
96 #[serde(default)]
99 pub equip_slots: Vec<String>,
100
101 #[serde(default)]
104 pub simulation: SimulationConfig,
105
106 #[serde(default)]
109 pub combat: CombatConfig,
110
111 #[serde(default)]
114 pub economy: EconomyConfig,
115
116 #[serde(default)]
119 pub progression: ProgressionConfig,
120
121 #[serde(default)]
124 pub agents: AgentConfig,
125
126 #[serde(default)]
129 pub tiles: TileConfig,
130
131 #[serde(default)]
134 pub canon: CanonConfig,
135
136 #[serde(default)]
139 pub scripting: ScriptingConfig,
140
141 #[serde(default)]
144 pub content: ContentConfig,
145
146 #[serde(default)]
149 pub eviction: EvictionConfig,
150
151 #[serde(default)]
154 pub props: Vec<PropDefinition>,
155
156 #[serde(default)]
159 pub generators: serde_json::Value,
160
161 #[serde(default)]
164 pub connection_type_props: HashMap<String, String>,
165
166 #[serde(default)]
169 pub boon_triggers: Vec<serde_json::Value>,
170
171 #[serde(default)]
174 pub uses_companion_services: bool,
175
176 #[serde(default)]
179 pub scenario_config: serde_json::Value,
180
181 #[serde(default)]
184 pub encumbrance: Option<serde_json::Value>,
185
186 #[serde(default)]
189 pub ai_brains: HashMap<String, serde_json::Value>,
190
191 #[serde(default)]
194 pub rules: Option<serde_json::Value>,
195
196 #[serde(default)]
199 pub identity: Option<serde_json::Value>,
200
201 #[serde(default)]
204 pub boot_sequence: Option<serde_json::Value>,
205
206 #[serde(default)]
210 pub topology: TopologyConfig,
211
212 #[serde(default)]
215 pub chronoshift: ChronoshiftConfig,
216
217 #[serde(default)]
220 pub forensics: ForensicsConfig,
221
222 #[serde(default)]
225 pub physics: PhysicsConfig,
226
227 #[serde(default)]
230 pub meshlet_lod: Option<MeshletLodConfig>,
231
232 #[serde(default)]
234 pub meshlet_emission: Option<MeshletEmissionConfig>,
235
236 #[serde(default)]
238 pub observer_config: Option<WaymarkObserverConfig>,
239
240 #[serde(default)]
242 pub promotion_targets: Vec<WaymarkPromotionTarget>,
243 #[serde(default)]
245 pub avatar_defaults: Option<AvatarDefaults>,
246}
247
248#[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#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct GridConfig {
304 #[serde(default = "default_grid_width")]
306 pub width: u32,
307
308 #[serde(default = "default_grid_height")]
310 pub height: u32,
311
312 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct SpatialConfig {
344 #[serde(default = "default_cell_size")]
346 pub cell_size: i32,
347
348 #[serde(default = "default_fov_default_radius")]
350 pub fov_default_radius: i32,
351
352 #[serde(default = "default_fov_max_radius")]
354 pub fov_max_radius: i32,
355
356 #[serde(default = "default_max_pathfind_steps")]
358 pub max_pathfind_steps: u32,
359
360 #[serde(default = "default_aoi_neighbor_depth")]
362 pub aoi_neighbor_depth: u32,
363
364 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct DisplayConfig {
408 #[serde(default = "default_mana_name")]
410 pub mana_name: String,
411
412 #[serde(default)]
414 pub subtitle: Option<String>,
415
416 #[serde(default = "default_true")]
418 pub show_minimap: bool,
419
420 #[serde(default)]
422 pub show_coordinates: bool,
423
424 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct FeatureFlags {
452 #[serde(default)]
454 pub content_plan: bool,
455
456 #[serde(default)]
458 pub boons: bool,
459
460 #[serde(default)]
462 pub shrines: bool,
463
464 #[serde(default)]
466 pub containers: bool,
467
468 #[serde(default)]
470 pub encumbrance: bool,
471
472 #[serde(default)]
474 pub pvp: bool,
475
476 #[serde(default)]
478 pub dynasties: bool,
479
480 #[serde(default)]
482 pub crafting: bool,
483
484 #[serde(default)]
486 pub market: bool,
487
488 #[serde(default)]
490 pub instances: bool,
491
492 #[serde(default)]
494 pub crime_system: bool,
495
496 #[serde(default)]
498 pub weather: bool,
499
500 #[serde(default)]
502 pub day_night_cycle: bool,
503
504 #[serde(default)]
506 pub fog_of_war: bool,
507
508 #[serde(default)]
510 pub chronoshift_enabled: bool,
511
512 #[serde(default)]
514 pub proof_lane_enabled: bool,
515
516 #[serde(default = "default_true")]
518 pub dungeon_ambient: bool,
519
520 #[serde(default = "default_true")]
522 pub line_of_sight: bool,
523
524 #[serde(default = "default_true")]
526 pub collisions: bool,
527
528 #[serde(default)]
530 pub identity_system: bool,
531
532 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct SimulationConfig {
572 #[serde(default = "default_tick_rate_ms")]
574 pub tick_rate_ms: u32,
575
576 #[serde(default = "default_ticks_per_day")]
578 pub ticks_per_day: u32,
579
580 #[serde(default = "default_time_dilation")]
582 pub time_dilation: f64,
583
584 #[serde(default = "default_max_entities_per_area")]
586 pub max_entities_per_area: u32,
587
588 #[serde(default = "default_max_events_per_tick")]
590 pub max_events_per_tick: u32,
591
592 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct CombatConfig {
636 #[serde(default = "default_base_hit_chance")]
638 pub base_hit_chance: u32,
639
640 #[serde(default = "default_crit_multiplier")]
642 pub crit_multiplier: f64,
643
644 #[serde(default = "default_dodge_base")]
646 pub dodge_base: u32,
647
648 #[serde(default = "default_parry_base")]
650 pub parry_base: u32,
651
652 #[serde(default = "default_flee_threshold_hp_pct")]
654 pub flee_threshold_hp_pct: u32,
655
656 #[serde(default = "default_max_threat_targets")]
658 pub max_threat_targets: u32,
659
660 #[serde(default = "default_duel_timeout_ticks")]
662 pub duel_timeout_ticks: u32,
663
664 #[serde(default = "default_revive_hp_pct")]
666 pub revive_hp_pct: u32,
667
668 #[serde(default = "default_status_max_stacks")]
670 pub status_max_stacks: u32,
671
672 #[serde(default = "default_damage_types")]
674 pub damage_types: Vec<String>,
675
676 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
748pub struct EconomyConfig {
749 #[serde(default = "default_starting_gold")]
751 pub starting_gold: u64,
752
753 #[serde(default = "default_trade_tax_pct")]
755 pub trade_tax_pct: u32,
756
757 #[serde(default = "default_market_fee_pct")]
759 pub market_fee_pct: u32,
760
761 #[serde(default = "default_max_listings_per_player")]
763 pub max_listings_per_player: u32,
764
765 #[serde(default = "default_currency_types")]
767 pub currency_types: Vec<String>,
768
769 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct ProgressionConfig {
813 #[serde(default = "default_max_level")]
815 pub max_level: u32,
816
817 #[serde(default = "default_xp_curve_base")]
819 pub xp_curve_base: f64,
820
821 #[serde(default = "default_base_xp")]
823 pub base_xp: u64,
824
825 #[serde(default = "default_level_hp_bonus")]
827 pub level_hp_bonus: u32,
828
829 #[serde(default = "default_level_stat_bonus")]
831 pub level_stat_bonus: u32,
832
833 #[serde(default = "default_reputation_min")]
835 pub reputation_min: i64,
836
837 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
884pub struct AgentConfig {
885 #[serde(default = "default_cognition_budget_per_tick")]
887 pub cognition_budget_per_tick: u32,
888
889 #[serde(default = "default_perception_range")]
891 pub perception_range: u32,
892
893 #[serde(default = "default_memory_capacity")]
895 pub memory_capacity: u32,
896
897 #[serde(default = "default_reasoning_tier")]
899 pub reasoning_tier_default: u32,
900
901 #[serde(default = "default_motor_tier")]
903 pub motor_tier_default: u32,
904
905 #[serde(default = "default_max_concurrent_agents")]
907 pub max_concurrent_agents: u32,
908
909 #[serde(default = "default_inference_timeout_ms")]
911 pub inference_timeout_ms: u32,
912
913 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
964pub struct TileTypeDef {
965 pub glyph: u16,
967
968 #[serde(default)]
970 pub display_char: Option<char>,
971
972 #[serde(default = "default_move_cost_1")]
974 pub move_cost: u32,
975
976 #[serde(default)]
978 pub blocks_move: bool,
979
980 #[serde(default)]
982 pub blocks_sight: bool,
983}
984
985fn default_move_cost_1() -> u32 {
986 1
987}
988
989#[derive(Debug, Clone, Serialize, Deserialize)]
997pub struct TileConfig {
998 #[serde(default = "default_tile_ground")]
1001 pub ground: TileTypeDef,
1002
1003 #[serde(default = "default_tile_dirt")]
1005 pub dirt: TileTypeDef,
1006
1007 #[serde(default = "default_tile_sand")]
1009 pub sand: TileTypeDef,
1010
1011 #[serde(default = "default_tile_mud")]
1013 pub mud: TileTypeDef,
1014
1015 #[serde(default = "default_tile_liquid")]
1017 pub liquid: TileTypeDef,
1018
1019 #[serde(default = "default_tile_deep_water")]
1021 pub deep_water: TileTypeDef,
1022
1023 #[serde(default = "default_tile_rocky_ground")]
1025 pub rocky_ground: TileTypeDef,
1026
1027 #[serde(default = "default_tile_grass")]
1029 pub grass: TileTypeDef,
1030
1031 #[serde(default = "default_tile_debris")]
1033 pub debris: TileTypeDef,
1034
1035 #[serde(default = "default_tile_wall")]
1038 pub wall: TileTypeDef,
1039
1040 #[serde(default = "default_tile_wall_vert")]
1042 pub wall_vert: TileTypeDef,
1043
1044 #[serde(default = "default_tile_wall_horiz")]
1046 pub wall_horiz: TileTypeDef,
1047
1048 #[serde(default = "default_tile_door_closed")]
1050 pub door_closed: TileTypeDef,
1051
1052 #[serde(default = "default_tile_door_open")]
1054 pub door_open: TileTypeDef,
1055
1056 #[serde(default = "default_tile_stairs_up")]
1058 pub stairs_up: TileTypeDef,
1059
1060 #[serde(default = "default_tile_stairs_down")]
1062 pub stairs_down: TileTypeDef,
1063
1064 #[serde(default = "default_tile_bridge")]
1066 pub bridge: TileTypeDef,
1067
1068 #[serde(default = "default_tile_boulder")]
1071 pub boulder: TileTypeDef,
1072
1073 #[serde(default = "default_tile_chest")]
1075 pub chest: TileTypeDef,
1076
1077 #[serde(default = "default_tile_crate")]
1079 pub crate_obj: TileTypeDef,
1080
1081 #[serde(default = "default_tile_shrine")]
1083 pub shrine: TileTypeDef,
1084
1085 #[serde(default = "default_tile_bed")]
1087 pub bed: TileTypeDef,
1088
1089 #[serde(default = "default_tile_consumable")]
1091 pub consumable: TileTypeDef,
1092
1093 #[serde(default = "default_tile_currency")]
1095 pub currency: TileTypeDef,
1096
1097 #[serde(default = "default_tile_key_item")]
1099 pub key_item: TileTypeDef,
1100
1101 #[serde(default = "default_tile_unknown_obj")]
1103 pub unknown_obj: TileTypeDef,
1104
1105 #[serde(default = "default_tile_hazard")]
1108 pub hazard: TileTypeDef,
1109
1110 #[serde(default = "default_tile_smoke")]
1112 pub smoke: TileTypeDef,
1113
1114 #[serde(default)]
1116 pub custom: HashMap<String, TileTypeDef>,
1117}
1118
1119fn 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}
1201fn 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}
1274fn 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}
1356fn 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#[derive(Debug, Clone, Serialize, Deserialize)]
1418pub struct CanonConfig {
1419 #[serde(default = "default_block_hash_interval")]
1421 pub block_hash_interval: u64,
1422
1423 #[serde(default = "default_tick_hash_interval")]
1425 pub tick_hash_interval: u64,
1426
1427 #[serde(default = "default_max_traversal_depth")]
1429 pub max_traversal_depth: u32,
1430
1431 #[serde(default = "default_scope_budget")]
1433 pub scope_budget_default: u32,
1434
1435 #[serde(default = "default_token_refill_per_tick")]
1437 pub token_refill_per_tick: u32,
1438
1439 #[serde(default = "default_max_tokens")]
1441 pub max_tokens: u32,
1442
1443 #[serde(default = "default_max_trigger_depth")]
1445 pub max_trigger_depth: u32,
1446
1447 #[serde(default = "default_max_triggers_per_event")]
1449 pub max_triggers_per_event: u32,
1450
1451 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
1506pub struct ScriptingConfig {
1507 #[serde(default = "default_max_runes")]
1509 pub max_runes: u32,
1510
1511 #[serde(default = "default_max_omens")]
1513 pub max_omens: u32,
1514
1515 #[serde(default = "default_max_chapters")]
1517 pub max_chapters: u32,
1518
1519 #[serde(default = "default_max_dialogues")]
1521 pub max_dialogues: u32,
1522
1523 #[serde(default = "default_max_visions")]
1525 pub max_visions: u32,
1526
1527 #[serde(default = "default_max_gates")]
1529 pub max_gates: u32,
1530
1531 #[serde(default = "default_fire_once_capacity")]
1533 pub fire_once_capacity: u32,
1534
1535 #[serde(default = "default_true")]
1537 pub meta_persistence_enabled: bool,
1538
1539 #[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1588pub struct ContentConfig {
1589 #[serde(default)]
1591 pub items_path: Option<String>,
1592
1593 #[serde(default)]
1595 pub enemies_path: Option<String>,
1596
1597 #[serde(default)]
1599 pub abilities_path: Option<String>,
1600
1601 #[serde(default)]
1603 pub loot_tables_path: Option<String>,
1604
1605 #[serde(default)]
1607 pub economy_path: Option<String>,
1608
1609 #[serde(default)]
1611 pub dialogue_path: Option<String>,
1612
1613 #[serde(default)]
1615 pub balance_path: Option<String>,
1616
1617 #[serde(default)]
1619 pub stats_path: Option<String>,
1620
1621 #[serde(default)]
1623 pub scenario_text_path: Option<String>,
1624
1625 #[serde(default)]
1627 pub lore_path: Option<String>,
1628
1629 #[serde(default)]
1631 pub crafting_path: Option<String>,
1632
1633 #[serde(default)]
1635 pub quests_path: Option<String>,
1636
1637 #[serde(default)]
1639 pub factions_path: Option<String>,
1640
1641 #[serde(default)]
1643 pub maps_path: Option<String>,
1644
1645 #[serde(default)]
1647 pub textures_path: Option<String>,
1648}
1649
1650#[derive(Debug, Clone, Serialize, Deserialize)]
1656pub struct EvictionConfig {
1657 #[serde(default = "default_max_cached")]
1659 pub max_cached: u32,
1660
1661 #[serde(default)]
1663 pub keep_ids: Vec<String>,
1664
1665 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
1690pub struct PropStateDef {
1691 #[serde(default)]
1693 pub glyph: Option<String>,
1694
1695 #[serde(default)]
1697 pub blocking: Option<bool>,
1698
1699 #[serde(default)]
1701 pub walkthrough: Option<bool>,
1702
1703 #[serde(default)]
1705 pub description: Option<String>,
1706
1707 #[serde(default)]
1709 pub on_interact: Option<String>,
1710
1711 #[serde(default)]
1713 pub on_secondary_interact: Option<String>,
1714
1715 #[serde(default)]
1717 pub bump_messages: Vec<String>,
1718}
1719
1720#[derive(Debug, Clone, Serialize, Deserialize)]
1722pub struct PropDefinition {
1723 pub id: String,
1725
1726 pub name: String,
1728
1729 #[serde(default)]
1731 pub description: String,
1732
1733 #[serde(default)]
1735 pub look_description: Option<String>,
1736
1737 #[serde(default)]
1739 pub glyph: Option<String>,
1740
1741 #[serde(default)]
1743 pub blocking: bool,
1744
1745 #[serde(default)]
1747 pub walkthrough: bool,
1748
1749 #[serde(default)]
1751 pub targetable: bool,
1752
1753 #[serde(default)]
1755 pub interactive: bool,
1756
1757 #[serde(default)]
1759 pub has_secondary_state: bool,
1760
1761 #[serde(default)]
1763 pub default_state: Option<String>,
1764
1765 #[serde(default)]
1767 pub foley_stub_id: Option<String>,
1768
1769 #[serde(default)]
1771 pub bump_messages: Vec<String>,
1772
1773 #[serde(default)]
1775 pub has_inventory: bool,
1776
1777 #[serde(default)]
1779 pub container_capacity: Option<u32>,
1780
1781 #[serde(default)]
1783 pub loot_table: Option<String>,
1784
1785 #[serde(default)]
1787 pub states: HashMap<String, PropStateDef>,
1788}
1789
1790#[derive(Debug, Clone, Serialize, Deserialize)]
1796pub struct ChronoshiftConfig {
1797 #[serde(default)]
1799 pub enabled: bool,
1800
1801 #[serde(default = "default_max_checkpoint_depth")]
1803 pub max_checkpoint_depth: u32,
1804
1805 #[serde(default = "default_replay_speed_multiplier")]
1807 pub replay_speed_multiplier: f64,
1808
1809 #[serde(default)]
1811 pub auto_checkpoint_interval_ticks: u64,
1812
1813 #[serde(default = "default_max_fork_count")]
1815 pub max_fork_count: u32,
1816
1817 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
1854pub struct ForensicsConfig {
1855 #[serde(default)]
1857 pub proof_lane_enabled: bool,
1858
1859 #[serde(default = "default_true")]
1861 pub blake3_domain_separation: bool,
1862
1863 #[serde(default)]
1865 pub merkle_state_root_enabled: bool,
1866
1867 #[serde(default)]
1869 pub ed25519_signing_enabled: bool,
1870
1871 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
1894pub struct PhysicsConfig {
1895 #[serde(default = "default_gravity_planetary")]
1897 pub gravity_planetary: f32,
1898
1899 #[serde(default)]
1901 pub gravity_local: Option<f32>,
1902
1903 #[serde(default = "default_atmosphere_density")]
1905 pub atmosphere_density: f32,
1906
1907 #[serde(default = "default_temperature_ambient")]
1909 pub temperature_ambient: f32,
1910
1911 #[serde(default)]
1913 pub emitter_presets: Vec<String>,
1914
1915 #[serde(default)]
1917 pub force_fields: Vec<ForceFieldConfig>,
1918
1919 #[serde(default)]
1921 pub collision_planes: Vec<CollisionPlaneConfig>,
1922}
1923
1924#[derive(Debug, Clone, Serialize, Deserialize)]
1926pub struct ForceFieldConfig {
1927 pub kind: String,
1929
1930 #[serde(default)]
1932 pub position: [f32; 3],
1933
1934 #[serde(default = "default_ff_strength")]
1936 pub strength: f32,
1937
1938 #[serde(default = "default_ff_radius")]
1940 pub radius: f32,
1941}
1942
1943#[derive(Debug, Clone, Serialize, Deserialize)]
1945pub struct CollisionPlaneConfig {
1946 #[serde(default = "default_plane_normal")]
1948 pub normal: [f32; 3],
1949
1950 #[serde(default)]
1952 pub offset: f32,
1953
1954 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
2001pub struct MeshletLodConfig {
2002 #[serde(default = "default_max_meshlets_per_object")]
2004 pub max_meshlets_per_object: u32,
2005 #[serde(default)]
2007 pub lod_distances: Vec<f32>,
2008 #[serde(default = "default_max_vertices_per_meshlet")]
2010 pub max_vertices_per_meshlet: u32,
2011 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
2039pub struct MeshletEmissionConfig {
2040 #[serde(default = "default_max_particles_per_emitter")]
2042 pub max_particles_per_emitter: u32,
2043 #[serde(default = "default_particle_lifetime")]
2045 pub default_lifetime: f32,
2046 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
2073pub struct WaymarkObserverConfig {
2074 #[serde(default = "default_fov_radius")]
2076 pub default_fov_radius: i32,
2077 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
2100pub struct WaymarkPromotionTarget {
2101 pub kind: String,
2103 pub entity_type: String,
2105 #[serde(default)]
2107 pub min_age: f32,
2108}
2109
2110#[derive(Debug, Clone, Serialize, Deserialize)]
2112pub struct MeshSourceConfig {
2113 #[serde(default)]
2115 pub primitive_type: Option<String>,
2116 #[serde(default)]
2118 pub fbx_path: Option<String>,
2119 #[serde(default)]
2121 pub gltf_path: Option<String>,
2122 #[serde(default)]
2124 pub active_layers: Vec<u32>,
2125}
2126
2127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2139pub struct TopologyConfig {
2140 #[serde(default)]
2142 pub universe_name: Option<String>,
2143
2144 #[serde(default)]
2146 pub galaxies: Vec<GalaxyDef>,
2147
2148 #[serde(default)]
2150 pub sectors: Vec<SectorDef>,
2151
2152 #[serde(default)]
2154 pub worlds: Vec<WorldDef>,
2155
2156 #[serde(default)]
2158 pub realms: Vec<RealmDef>,
2159
2160 #[serde(default)]
2162 pub regions: Vec<RegionDef>,
2163
2164 #[serde(default)]
2166 pub areas: Vec<AreaDef>,
2167
2168 #[serde(default)]
2170 pub locations: Vec<LocationDef>,
2171
2172 #[serde(default)]
2174 pub points: Vec<PointDef>,
2175}
2176
2177#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
2209pub struct RealmDef {
2210 pub id: String,
2211 pub world_id: String,
2212 pub name: String,
2213}
2214
2215#[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#[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#[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#[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
2287impl 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
2344impl DreamwellPackV1 {
2349 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 pub fn to_json(&self) -> String {
2359 serde_json::to_string_pretty(self).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
2360 }
2361
2362 pub fn validate(&self) -> Vec<String> {
2365 let mut errors = Vec::new();
2366
2367 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 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 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 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 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 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 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 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 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 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 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 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 pub fn is_legacy_format(json: &serde_json::Value) -> bool {
2628 if let Some(obj) = json.as_object() {
2629 obj.contains_key("id") && !obj.contains_key("schema_version")
2631 } else {
2632 false
2633 }
2634 }
2635
2636 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 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#[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 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 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 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 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}