1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6pub type EntityId = u64;
7pub type Tick = u64;
8pub type Seq = u32;
9pub type SessionId = u64;
10
11#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct WorldCoord {
14 pub x: f32,
15 pub y: f32,
16 pub z: f32,
17 pub w: u32,
18 pub t: u32,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
23pub struct AimPoint {
24 pub x: f32,
25 pub y: f32,
26 #[serde(default)]
27 pub z: f32,
28}
29
30impl AimPoint {
31 pub fn xy(x: f32, y: f32) -> Self {
32 Self { x, y, z: 0.0 }
33 }
34}
35
36impl WorldCoord {
37 pub fn surface(x: f32, y: f32) -> Self {
38 Self {
39 x,
40 y,
41 z: 0.0,
42 w: 0,
43 t: 0,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
49pub struct Velocity2D {
50 pub vx: f32,
51 pub vy: f32,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
55pub struct Transform {
56 pub position: WorldCoord,
57 pub yaw: f32,
58 pub velocity: Velocity2D,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum LifeState {
64 Alive,
65 Dead,
66}
67
68impl Default for LifeState {
69 fn default() -> Self {
70 Self::Alive
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum TimeOfDayPhase {
78 Night,
79 Dawn,
80 Morning,
81 Midday,
82 Afternoon,
83 Evening,
84}
85
86impl TimeOfDayPhase {
87 pub fn label(self) -> &'static str {
88 match self {
89 Self::Night => "Night",
90 Self::Dawn => "Dawn",
91 Self::Morning => "Morning",
92 Self::Midday => "Midday",
93 Self::Afternoon => "Afternoon",
94 Self::Evening => "Evening",
95 }
96 }
97
98 pub fn from_name(name: &str) -> Self {
99 match name.to_ascii_lowercase().as_str() {
100 "dawn" => Self::Dawn,
101 "morning" => Self::Morning,
102 "midday" | "mid_day" | "noon" => Self::Midday,
103 "afternoon" => Self::Afternoon,
104 "evening" | "dusk" => Self::Evening,
105 _ => Self::Night,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
112pub struct WorldClock {
113 pub day: u64,
115 pub hour: u8,
116 pub minute: u8,
117 pub phase: TimeOfDayPhase,
118}
119
120impl Default for WorldClock {
121 fn default() -> Self {
122 Self {
123 day: 0,
124 hour: 8,
125 minute: 0,
126 phase: TimeOfDayPhase::Morning,
127 }
128 }
129}
130
131impl WorldClock {
132 pub fn display_time(self) -> String {
133 format!("{:02}:{:02}", self.hour, self.minute)
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
139pub struct PrimaryAttributes {
140 pub strength: u16,
141 pub dexterity: u16,
142 pub intelligence: u16,
143 pub stamina: u16,
144 pub vitality: u16,
145 pub wisdom: u16,
146 pub charisma: u16,
147}
148
149impl Default for PrimaryAttributes {
150 fn default() -> Self {
151 Self {
152 strength: 150,
153 dexterity: 150,
154 intelligence: 150,
155 stamina: 150,
156 vitality: 150,
157 wisdom: 150,
158 charisma: 150,
159 }
160 }
161}
162
163impl PrimaryAttributes {
164 pub fn display(value: u16) -> u16 {
166 (value / 10).clamp(1, 100)
167 }
168
169 pub fn derived_preview(&self) -> DerivedPreview {
171 let str_d = Self::display(self.strength) as f32;
172 let dex_d = Self::display(self.dexterity) as f32;
173 let int_d = Self::display(self.intelligence) as f32;
174 let wis_d = Self::display(self.wisdom) as f32;
175 DerivedPreview {
176 attack_power: str_d * 1.2 + dex_d * 0.3,
177 spell_power: int_d * 1.1 + wis_d * 0.4,
178 evasion: dex_d * 0.8 + wis_d * 0.2,
179 carry_mass_max: str_d * 2.5,
180 sight_range_m: 12.0 + wis_d * 0.15 + dex_d * 0.05,
181 fov_deg: 120.0 + wis_d * 0.2,
182 hearing_range_m: 6.0
183 + wis_d * 0.08
184 + (PrimaryAttributes::display(self.stamina) as f32) * 0.04,
185 }
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq)]
191pub struct DerivedPreview {
192 pub attack_power: f32,
193 pub spell_power: f32,
194 pub evasion: f32,
195 pub carry_mass_max: f32,
196 pub sight_range_m: f32,
197 pub fov_deg: f32,
198 pub hearing_range_m: f32,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203pub struct SkillProgress {
204 pub level: u16,
205 #[serde(default)]
206 pub last_trained_tick: u64,
207}
208
209impl Default for SkillProgress {
210 fn default() -> Self {
211 Self {
212 level: 0,
213 last_trained_tick: 0,
214 }
215 }
216}
217
218impl SkillProgress {
219 pub fn display_tier(&self) -> u16 {
221 (self.level / 100).min(10)
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
227#[serde(default)]
228pub struct ProgressionXp {
229 pub strength: f64,
230 pub dexterity: f64,
231 pub intelligence: f64,
232 pub stamina: f64,
233 pub vitality: f64,
234 pub wisdom: f64,
235 pub charisma: f64,
236 pub logging: f64,
237 pub mining: f64,
238 pub evocation: f64,
239 pub restoration: f64,
240 pub swords: f64,
241 pub archery: f64,
242 pub crafting: f64,
243 pub alchemy: f64,
244 pub cartography: f64,
245 #[serde(default)]
247 pub ability: std::collections::BTreeMap<String, f64>,
248}
249
250impl ProgressionXp {
251 pub fn bootstrap_new(baseline_display: u16, xp_base: f64, xp_growth: f64) -> Self {
253 let bootstrap = |display: f64| {
254 if display <= 1.0 {
255 0.0
256 } else {
257 xp_base * xp_growth.powf(display - 1.0)
258 }
259 };
260 let b = baseline_display as f64;
261 let primary = bootstrap(b);
262 Self {
263 strength: primary,
264 dexterity: primary,
265 intelligence: primary,
266 stamina: primary,
267 vitality: primary,
268 wisdom: primary,
269 charisma: primary,
270 ..Self::default()
271 }
272 }
273
274 pub fn is_empty(&self) -> bool {
275 self.strength == 0.0
276 && self.dexterity == 0.0
277 && self.intelligence == 0.0
278 && self.stamina == 0.0
279 && self.vitality == 0.0
280 && self.wisdom == 0.0
281 && self.charisma == 0.0
282 && self.logging == 0.0
283 && self.mining == 0.0
284 && self.evocation == 0.0
285 && self.restoration == 0.0
286 && self.swords == 0.0
287 && self.archery == 0.0
288 && self.crafting == 0.0
289 && self.alchemy == 0.0
290 && self.cartography == 0.0
291 && self.ability.is_empty()
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct AbilityMasteryHud {
298 pub ability_id: String,
299 pub tier: u16,
301 pub level: u16,
303 pub xp: f64,
305 pub xp_to_next: f64,
307}
308
309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311#[serde(default)]
312pub struct PlayerSkills {
313 pub logging: SkillProgress,
314 pub mining: SkillProgress,
315 pub evocation: SkillProgress,
316 #[serde(default)]
317 pub restoration: SkillProgress,
318 pub swords: SkillProgress,
319 #[serde(default)]
320 pub archery: SkillProgress,
321 pub crafting: SkillProgress,
322 #[serde(default)]
323 pub alchemy: SkillProgress,
324 pub cartography: SkillProgress,
325}
326
327impl Default for PlayerSkills {
328 fn default() -> Self {
329 Self {
330 logging: SkillProgress::default(),
331 mining: SkillProgress::default(),
332 evocation: SkillProgress::default(),
333 restoration: SkillProgress::default(),
334 swords: SkillProgress::default(),
335 archery: SkillProgress::default(),
336 crafting: SkillProgress::default(),
337 alchemy: SkillProgress::default(),
338 cartography: SkillProgress::default(),
339 }
340 }
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
345pub struct PlayerVitals {
346 pub health: f32,
347 pub health_max: f32,
348 pub mana: f32,
349 pub mana_max: f32,
350 pub stamina: f32,
351 pub stamina_max: f32,
352 #[serde(default = "default_survival_pool_max")]
353 pub hunger: f32,
354 #[serde(default = "default_survival_pool_max")]
355 pub hunger_max: f32,
356 #[serde(default = "default_survival_pool_max")]
357 pub thirst: f32,
358 #[serde(default = "default_survival_pool_max")]
359 pub thirst_max: f32,
360 #[serde(default)]
361 pub coins: u32,
362 #[serde(default)]
363 pub deaths: u32,
364 #[serde(default)]
365 pub life_state: LifeState,
366}
367
368fn default_survival_pool_max() -> f32 {
369 100.0
370}
371
372impl Default for PlayerVitals {
373 fn default() -> Self {
374 Self::from_attributes(PrimaryAttributes::default())
375 }
376}
377
378impl PlayerVitals {
379 pub fn from_attributes(attrs: PrimaryAttributes) -> Self {
386 let vit_d = PrimaryAttributes::display(attrs.vitality) as f32;
387 let int_d = PrimaryAttributes::display(attrs.intelligence) as f32;
388 let wis_d = PrimaryAttributes::display(attrs.wisdom) as f32;
389 let sta_d = PrimaryAttributes::display(attrs.stamina) as f32;
390
391 let health_max = 50.0 + vit_d * 2.0;
392 let stamina_max = 30.0 + sta_d * 1.4;
393 let mana_max = 25.0 + int_d * 1.1 + wis_d * 1.3;
394 let hunger_max = 100.0;
395 let thirst_max = 100.0;
396 Self {
397 health: health_max,
398 health_max,
399 mana: mana_max,
400 mana_max,
401 stamina: stamina_max,
402 stamina_max,
403 hunger: hunger_max,
404 hunger_max,
405 thirst: thirst_max,
406 thirst_max,
407 coins: 0,
408 deaths: 0,
409 life_state: LifeState::Alive,
410 }
411 }
412
413 pub fn legacy_maxima(attrs: PrimaryAttributes) -> (f32, f32, f32) {
415 (
416 attrs.vitality as f32 / 5.0,
417 attrs.stamina as f32 / 5.0,
418 (attrs.intelligence as f32 + attrs.wisdom as f32) / 20.0,
419 )
420 }
421}
422
423#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
425#[serde(default)]
426pub struct StoredVitalsState {
427 pub health: f32,
428 pub mana: f32,
429 pub stamina: f32,
430 pub hunger: f32,
431 pub thirst: f32,
432 pub coins: u32,
433 pub deaths: u32,
434 pub life_state: LifeState,
435}
436
437impl StoredVitalsState {
438 pub fn from_live(v: &PlayerVitals) -> Self {
439 Self {
440 health: v.health,
441 mana: v.mana,
442 stamina: v.stamina,
443 hunger: v.hunger,
444 thirst: v.thirst,
445 coins: v.coins,
446 deaths: v.deaths,
447 life_state: v.life_state,
448 }
449 }
450
451 pub fn is_pristine(&self) -> bool {
453 self.health == 0.0
454 && self.mana == 0.0
455 && self.stamina == 0.0
456 && self.hunger == 0.0
457 && self.thirst == 0.0
458 && self.coins == 0
459 && self.deaths == 0
460 && self.life_state == LifeState::Alive
461 }
462
463 pub fn apply_to(self, attrs: PrimaryAttributes) -> PlayerVitals {
464 if self.is_pristine() {
465 return PlayerVitals::from_attributes(attrs);
466 }
467 let fresh = PlayerVitals::from_attributes(attrs);
468 let (legacy_hp, legacy_sta, legacy_mana) = PlayerVitals::legacy_maxima(attrs);
469
470 let scale = |current: f32, legacy_max: f32, new_max: f32| {
471 if legacy_max > 0.0 && new_max > legacy_max * 1.05 && current >= legacy_max * 0.95 {
472 let ratio = (current / legacy_max).clamp(0.0, 1.0);
473 (new_max * ratio).min(new_max)
474 } else {
475 current.min(new_max)
476 }
477 };
478
479 let mut v = fresh;
480 v.health = scale(self.health, legacy_hp, fresh.health_max);
481 v.mana = scale(self.mana, legacy_mana, fresh.mana_max);
482 v.stamina = scale(self.stamina, legacy_sta, fresh.stamina_max);
483 v.hunger = self.hunger.min(v.hunger_max);
484 v.thirst = self.thirst.min(v.thirst_max);
485 v.coins = self.coins;
486 v.deaths = self.deaths;
487 v.life_state = self.life_state;
488 v
489 }
490}
491
492impl Default for StoredVitalsState {
493 fn default() -> Self {
494 Self::from_live(&PlayerVitals::default())
495 }
496}
497
498pub fn humanize_snake_id(id: &str) -> String {
502 id.split('_')
503 .filter(|part| !part.is_empty())
504 .map(|part| {
505 let mut chars = part.chars();
506 match chars.next() {
507 None => String::new(),
508 Some(first) => first.to_uppercase().chain(chars).collect(),
509 }
510 })
511 .collect::<Vec<_>>()
512 .join(" ")
513}
514
515#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
517pub struct KnownAbility {
518 pub ability_id: String,
519 #[serde(default = "default_known_permanent")]
521 pub permanent: bool,
522 #[serde(default)]
524 pub expires_at_tick: Option<u64>,
525}
526
527fn default_known_permanent() -> bool {
528 true
529}
530
531impl KnownAbility {
532 pub fn permanent(ability_id: impl Into<String>) -> Self {
533 Self {
534 ability_id: ability_id.into(),
535 permanent: true,
536 expires_at_tick: None,
537 }
538 }
539
540 pub fn is_active(&self, tick: u64) -> bool {
541 if self.permanent {
542 return true;
543 }
544 match self.expires_at_tick {
545 Some(exp) => tick < exp,
546 None => false,
547 }
548 }
549}
550
551#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
553pub struct RotationPreset {
554 pub id: String,
555 pub label: String,
556 #[serde(default)]
557 pub abilities: Vec<String>,
558}
559
560impl RotationPreset {
561 pub fn melee_default(ability_id: impl Into<String>) -> Self {
562 let id = ability_id.into();
563 Self {
564 id: "melee".into(),
565 label: "Weapon".into(),
567 abilities: vec![id],
568 }
569 }
570
571 pub fn is_weapon_preset(&self) -> bool {
572 self.id == "melee"
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
578pub struct StoredTargetSlot {
579 pub instance_id: Option<String>,
580 #[serde(default)]
581 pub preset_id: Option<String>,
582 #[serde(default)]
583 pub rotation_index: u32,
584 #[serde(default)]
585 pub auto_enabled: bool,
586}
587
588#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
589#[serde(default)]
590pub struct StoredCombatProfile {
591 pub combat_target_instance_id: Option<String>,
593 pub in_combat: bool,
594 pub last_combat_tick: u64,
595 pub last_attack_tick: u64,
596 pub cooldowns_until_tick: BTreeMap<String, u64>,
597 #[serde(default = "default_auto_attack")]
598 pub auto_attack_enabled: bool,
599 #[serde(default)]
601 pub mainhand_template_id: Option<String>,
602 #[serde(default)]
604 pub mainhand_instance_id: Option<Uuid>,
605 #[serde(default)]
607 pub offhand_template_id: Option<String>,
608 #[serde(default)]
610 pub offhand_instance_id: Option<Uuid>,
611 #[serde(default)]
614 pub worn: Vec<(BodySlot, ItemStack)>,
615 #[serde(default)]
617 pub rotation_presets: Vec<RotationPreset>,
618 #[serde(default)]
620 pub target_slots: Vec<StoredTargetSlot>,
621 #[serde(default)]
623 pub known_blueprint_ids: Vec<String>,
624 #[serde(default)]
626 pub keychain: Vec<ItemStack>,
627 #[serde(default)]
629 pub whisper_pouch: Vec<ItemStack>,
630 #[serde(default)]
632 pub known_abilities: Vec<KnownAbility>,
633 #[serde(default)]
635 pub hotbar: Vec<Option<String>>,
636 #[serde(default)]
638 pub abilities_schema_version: u32,
639 #[serde(default)]
641 pub bank_balance_copper: u64,
642}
643
644fn default_auto_attack() -> bool {
645 true
646}
647
648impl Default for StoredCombatProfile {
649 fn default() -> Self {
650 Self {
651 combat_target_instance_id: None,
652 in_combat: false,
653 last_combat_tick: 0,
654 last_attack_tick: 0,
655 cooldowns_until_tick: BTreeMap::new(),
656 auto_attack_enabled: true,
657 mainhand_template_id: None,
658 mainhand_instance_id: None,
659 offhand_template_id: None,
660 offhand_instance_id: None,
661 worn: Vec::new(),
662 rotation_presets: Vec::new(),
663 target_slots: Vec::new(),
664 known_blueprint_ids: Vec::new(),
665 keychain: Vec::new(),
666 whisper_pouch: Vec::new(),
667 known_abilities: Vec::new(),
668 hotbar: Vec::new(),
669 abilities_schema_version: 0,
670 bank_balance_copper: 0,
671 }
672 }
673}
674
675#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
677#[serde(rename_all = "snake_case")]
678pub enum CombatCueKind {
679 Dodge,
680 Block,
681 AttackTelegraph,
682}
683
684#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
685pub struct CombatCueView {
686 pub kind: CombatCueKind,
687 pub until_tick: Tick,
689 #[serde(default)]
691 pub start_tick: Tick,
692 #[serde(default)]
694 pub ability_id: Option<String>,
695 #[serde(default)]
697 pub telegraph_kind: Option<CombatFxKind>,
698 #[serde(default)]
699 pub origin_x: Option<f32>,
700 #[serde(default)]
701 pub origin_y: Option<f32>,
702 #[serde(default)]
703 pub origin_z: Option<f32>,
704 #[serde(default)]
705 pub end_x: Option<f32>,
706 #[serde(default)]
707 pub end_y: Option<f32>,
708 #[serde(default)]
709 pub end_z: Option<f32>,
710 #[serde(default)]
711 pub yaw: Option<f32>,
712 #[serde(default)]
713 pub reach_m: Option<f32>,
714 #[serde(default)]
715 pub arc_deg: Option<f32>,
716 #[serde(default)]
717 pub radius_m: Option<f32>,
718}
719
720impl CombatCueView {
721 pub fn timing(kind: CombatCueKind, until_tick: Tick, start_tick: Tick) -> Self {
723 Self {
724 kind,
725 until_tick,
726 start_tick,
727 ability_id: None,
728 telegraph_kind: None,
729 origin_x: None,
730 origin_y: None,
731 origin_z: None,
732 end_x: None,
733 end_y: None,
734 end_z: None,
735 yaw: None,
736 reach_m: None,
737 arc_deg: None,
738 radius_m: None,
739 }
740 }
741}
742
743#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
744pub struct EntityState {
745 pub id: EntityId,
746 pub transform: Transform,
747 #[serde(default)]
749 pub label: String,
750 #[serde(default)]
751 pub vitals: Option<PlayerVitals>,
752 #[serde(default)]
754 pub attributes: Option<PrimaryAttributes>,
755 #[serde(default)]
756 pub skills: Option<PlayerSkills>,
757 #[serde(default)]
759 pub inside_building: Option<String>,
760 #[serde(default)]
762 pub tile_id: Option<String>,
763 #[serde(default)]
765 pub paperdoll_ref: Option<String>,
766 #[serde(default = "default_draw_scale")]
768 pub draw_scale: f32,
769 #[serde(default)]
771 pub presentation_state: Option<String>,
772 #[serde(default)]
774 pub sprite_mode: Option<String>,
775 #[serde(default)]
777 pub progression_xp: Option<ProgressionXp>,
778 #[serde(default)]
780 pub combat_cues: Vec<CombatCueView>,
781 #[serde(default)]
783 pub statuses: Vec<StatusEffectHud>,
784}
785
786#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
788#[serde(rename_all = "snake_case")]
789pub enum ChatChannel {
790 Nearby,
792 Direct,
794 Whisper,
796 WhisperStone,
798}
799
800#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
802#[serde(rename_all = "snake_case")]
803pub enum ChatClarity {
804 #[default]
805 Clear,
806 Partial,
807 Heavy,
808}
809
810#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
811pub struct ChatMessage {
812 pub channel: ChatChannel,
813 pub from_entity: EntityId,
814 pub from_name: String,
815 pub text: String,
817 pub tick: Tick,
818 #[serde(default)]
820 pub to_entity: Option<EntityId>,
821 #[serde(default)]
822 pub clarity: ChatClarity,
823}
824
825#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
827pub enum Intent {
828 Move {
829 entity_id: EntityId,
830 forward: f32,
831 strafe: f32,
832 #[serde(default)]
834 vertical: f32,
835 #[serde(default)]
837 sprint: bool,
838 #[serde(default)]
840 sneak: bool,
841 seq: Seq,
842 },
843 Stop {
844 entity_id: EntityId,
845 seq: Seq,
846 },
847 Harvest {
848 entity_id: EntityId,
849 node_id: String,
850 seq: Seq,
851 },
852 Use {
853 entity_id: EntityId,
854 template_id: String,
855 seq: Seq,
856 },
857 UseGrant {
859 entity_id: EntityId,
860 grant_instance_id: Uuid,
861 target_instance_id: Uuid,
862 seq: Seq,
863 },
864 Say {
865 entity_id: EntityId,
866 channel: ChatChannel,
867 text: String,
868 #[serde(default)]
870 to_entity: Option<EntityId>,
871 seq: Seq,
872 },
873 Craft {
875 entity_id: EntityId,
876 blueprint_id: String,
877 #[serde(default)]
879 count: Option<u32>,
880 seq: Seq,
881 },
882 Interact {
884 entity_id: EntityId,
885 target_id: String,
886 seq: Seq,
887 },
888 ShopBuy {
890 entity_id: EntityId,
891 npc_id: String,
892 offer_id: String,
893 #[serde(default = "default_one")]
894 quantity: u32,
895 seq: Seq,
896 },
897 ShopSell {
899 entity_id: EntityId,
900 npc_id: String,
901 template_id: String,
902 #[serde(default = "default_one")]
903 quantity: u32,
904 seq: Seq,
905 },
906 ShopClose {
908 entity_id: EntityId,
909 npc_id: String,
910 seq: Seq,
911 },
912 TestDamage {
914 entity_id: EntityId,
915 amount: f32,
916 seq: Seq,
917 },
918 SetTarget {
920 entity_id: EntityId,
921 target_id: EntityId,
922 seq: Seq,
923 },
924 SetTargetSlot {
926 entity_id: EntityId,
927 slot_index: u8,
928 target_id: EntityId,
929 seq: Seq,
930 },
931 ClearTarget {
932 entity_id: EntityId,
933 seq: Seq,
934 },
935 ClearTargetSlot {
936 entity_id: EntityId,
937 slot_index: u8,
938 seq: Seq,
939 },
940 SetAutoAttack {
942 entity_id: EntityId,
943 slot_index: u8,
944 enabled: bool,
945 seq: Seq,
946 },
947 Attack {
949 entity_id: EntityId,
950 #[serde(default)]
951 target_id: Option<EntityId>,
952 #[serde(default)]
953 weapon_slot: Option<u32>,
954 seq: Seq,
955 },
956 Pickup {
958 entity_id: EntityId,
959 #[serde(default)]
960 drop_id: Option<String>,
961 seq: Seq,
962 },
963 Cast {
966 entity_id: EntityId,
967 ability_id: String,
968 target_id: EntityId,
969 #[serde(default)]
970 target_point: Option<AimPoint>,
971 seq: Seq,
972 },
973 BindActionSlot {
975 entity_id: EntityId,
976 slot_index: u8,
977 ability_id: String,
978 #[serde(default = "default_auto_attack")]
979 auto_enabled: bool,
980 seq: Seq,
981 },
982 UseActionSlot {
984 entity_id: EntityId,
985 slot_index: u8,
986 seq: Seq,
987 },
988 Dodge {
992 entity_id: EntityId,
993 #[serde(default)]
995 forward: f32,
996 #[serde(default)]
998 strafe: f32,
999 seq: Seq,
1000 },
1001 Lunge {
1003 entity_id: EntityId,
1004 #[serde(default)]
1006 forward: f32,
1007 #[serde(default)]
1009 strafe: f32,
1010 seq: Seq,
1011 },
1012 DirectionalJump {
1014 entity_id: EntityId,
1015 #[serde(default)]
1017 forward: f32,
1018 #[serde(default)]
1020 strafe: f32,
1021 seq: Seq,
1022 },
1023 Block {
1025 entity_id: EntityId,
1026 #[serde(default = "default_block_enabled")]
1027 enabled: bool,
1028 seq: Seq,
1029 },
1030 EquipMainhand {
1034 entity_id: EntityId,
1035 #[serde(default)]
1036 template_id: Option<String>,
1037 #[serde(default)]
1038 instance_id: Option<Uuid>,
1039 seq: Seq,
1040 },
1041 EquipOffhand {
1043 entity_id: EntityId,
1044 #[serde(default)]
1045 template_id: Option<String>,
1046 #[serde(default)]
1047 instance_id: Option<Uuid>,
1048 seq: Seq,
1049 },
1050 EquipWorn {
1053 entity_id: EntityId,
1054 slot: BodySlot,
1055 #[serde(default)]
1056 instance_id: Option<Uuid>,
1057 seq: Seq,
1058 },
1059 MoveItem {
1061 entity_id: EntityId,
1062 item_instance_id: Uuid,
1063 from: InventoryLocation,
1064 to: InventoryLocation,
1065 #[serde(default)]
1067 to_parent_instance_id: Option<Uuid>,
1068 #[serde(default)]
1070 quantity: Option<u32>,
1071 seq: Seq,
1072 },
1073 PlaceContainer {
1075 entity_id: EntityId,
1076 item_instance_id: Uuid,
1077 seq: Seq,
1078 },
1079 PickupContainer {
1081 entity_id: EntityId,
1082 container_id: String,
1083 seq: Seq,
1084 },
1085 MovePlacedContainer {
1087 entity_id: EntityId,
1088 container_id: String,
1089 x: f32,
1090 y: f32,
1091 seq: Seq,
1092 },
1093 SetContainerLocked {
1095 entity_id: EntityId,
1096 location: InventoryLocation,
1098 locked: bool,
1099 seq: Seq,
1100 },
1101 DropItem {
1103 entity_id: EntityId,
1104 item_instance_id: Uuid,
1105 from: InventoryLocation,
1106 seq: Seq,
1107 },
1108 DestroyItem {
1110 entity_id: EntityId,
1111 item_instance_id: Uuid,
1112 from: InventoryLocation,
1113 #[serde(default)]
1115 quantity: Option<u32>,
1116 seq: Seq,
1117 },
1118 RenameContainer {
1120 entity_id: EntityId,
1121 item_instance_id: Uuid,
1122 location: InventoryLocation,
1123 name: String,
1124 seq: Seq,
1125 },
1126 UpsertRotationPreset {
1128 entity_id: EntityId,
1129 preset: RotationPreset,
1130 seq: Seq,
1131 },
1132 DeleteRotationPreset {
1134 entity_id: EntityId,
1135 preset_id: String,
1136 seq: Seq,
1137 },
1138 AssignSlotPreset {
1140 entity_id: EntityId,
1141 slot_index: u8,
1142 preset_id: String,
1143 seq: Seq,
1144 },
1145 SetHotbarSlot {
1148 entity_id: EntityId,
1149 slot: u8,
1151 #[serde(default)]
1153 ability_id: Option<String>,
1154 seq: Seq,
1155 },
1156 AdvanceRotation {
1158 entity_id: EntityId,
1159 slot_index: u8,
1160 seq: Seq,
1161 },
1162 NpcTalkOpen {
1164 entity_id: EntityId,
1165 npc_id: String,
1166 #[serde(default)]
1168 quest_id: Option<String>,
1169 seq: Seq,
1170 },
1171 NpcTalkSay {
1173 entity_id: EntityId,
1174 npc_id: String,
1175 message: String,
1176 seq: Seq,
1177 },
1178 NpcTalkClose {
1180 entity_id: EntityId,
1181 npc_id: String,
1182 seq: Seq,
1183 },
1184 AcceptQuest {
1186 entity_id: EntityId,
1187 quest_id: String,
1188 seq: Seq,
1189 },
1190 WithdrawQuest {
1192 entity_id: EntityId,
1193 quest_id: String,
1194 seq: Seq,
1195 },
1196 TrackQuest {
1198 entity_id: EntityId,
1199 quest_id: String,
1200 seq: Seq,
1201 },
1202 QuestGiveItem {
1204 entity_id: EntityId,
1205 npc_id: String,
1206 template_id: String,
1207 #[serde(default = "default_one")]
1208 quantity: u32,
1209 seq: Seq,
1210 },
1211 HireWorker {
1213 entity_id: EntityId,
1214 def_id: String,
1215 wage_copper_per_interval: u32,
1216 #[serde(default)]
1217 lodging_container_id: Option<String>,
1218 #[serde(default)]
1219 job_yaml: Option<String>,
1220 seq: Seq,
1221 },
1222 DismissWorker {
1224 entity_id: EntityId,
1225 worker_instance_id: String,
1226 seq: Seq,
1227 },
1228 SetWorkerJob {
1230 entity_id: EntityId,
1231 worker_instance_id: String,
1232 job_yaml: String,
1233 seq: Seq,
1234 },
1235 AssignWorkerLodging {
1237 entity_id: EntityId,
1238 worker_instance_id: String,
1239 lodging_container_id: String,
1240 seq: Seq,
1241 },
1242 SetWorkerMode {
1244 entity_id: EntityId,
1245 worker_instance_id: String,
1246 mode: String,
1247 seq: Seq,
1248 },
1249 EquipWorkerItem {
1255 entity_id: EntityId,
1256 worker_instance_id: String,
1257 item_instance_id: uuid::Uuid,
1258 slot: String,
1259 seq: Seq,
1260 },
1261 GiveWorkerItem {
1264 entity_id: EntityId,
1265 worker_instance_id: String,
1266 item_instance_id: uuid::Uuid,
1267 #[serde(default)]
1268 quantity: Option<u32>,
1269 seq: Seq,
1270 },
1271 TakeWorkerItem {
1273 entity_id: EntityId,
1274 worker_instance_id: String,
1275 item_instance_id: uuid::Uuid,
1276 #[serde(default)]
1277 quantity: Option<u32>,
1278 seq: Seq,
1279 },
1280 RenameHiredWorker {
1282 entity_id: EntityId,
1283 worker_instance_id: String,
1284 name: String,
1285 seq: Seq,
1286 },
1287 RenamePropertyPlot {
1289 entity_id: EntityId,
1290 plot_id: Uuid,
1291 label: String,
1292 seq: Seq,
1293 },
1294 TeachWorkerBlueprint {
1296 entity_id: EntityId,
1297 worker_instance_id: String,
1298 blueprint_id: String,
1299 seq: Seq,
1300 },
1301 AttendHiredWorker {
1303 entity_id: EntityId,
1304 worker_instance_id: String,
1305 attending: bool,
1306 seq: Seq,
1307 },
1308 BuyPlot {
1310 entity_id: EntityId,
1311 zone_id: String,
1312 x0: f32,
1313 y0: f32,
1314 x1: f32,
1315 y1: f32,
1316 seq: Seq,
1317 },
1318 BuyPlotAllFree {
1320 entity_id: EntityId,
1321 zone_id: String,
1322 seq: Seq,
1323 },
1324 SellPlotToCrown {
1326 entity_id: EntityId,
1327 plot_id: Uuid,
1328 seq: Seq,
1329 },
1330 Cultivate {
1332 entity_id: EntityId,
1333 x: f32,
1335 y: f32,
1336 seq: Seq,
1337 },
1338 PlantSeeds {
1340 entity_id: EntityId,
1341 seed_template_id: String,
1342 quantity: u32,
1343 seq: Seq,
1344 },
1345 SetPlotFarmPublic {
1347 entity_id: EntityId,
1348 plot_id: Uuid,
1349 public: bool,
1350 #[serde(default)]
1351 public_tax_discount_bps: u32,
1352 seq: Seq,
1353 },
1354 PlotFarmAllowUpsert {
1356 entity_id: EntityId,
1357 plot_id: Uuid,
1358 #[serde(default)]
1360 character_id: Option<Uuid>,
1361 #[serde(default)]
1363 character_name: String,
1364 #[serde(default)]
1365 tax_discount_bps: u32,
1366 seq: Seq,
1367 },
1368 PlotFarmAllowRemove {
1370 entity_id: EntityId,
1371 plot_id: Uuid,
1372 character_id: Uuid,
1373 seq: Seq,
1374 },
1375 StartPlotBuild {
1378 entity_id: EntityId,
1379 plot_id: Uuid,
1380 wall_material_id: String,
1381 roof_material_id: String,
1382 seq: Seq,
1383 },
1384 CancelPlotBuild {
1385 entity_id: EntityId,
1386 seq: Seq,
1387 },
1388 SetDoorLocked {
1391 entity_id: EntityId,
1392 door_id: String,
1393 locked: bool,
1394 seq: Seq,
1395 },
1396 EnterBuildingDoor {
1399 entity_id: EntityId,
1400 door_id: String,
1401 seq: Seq,
1402 },
1403 ExitBuildingDoor {
1406 entity_id: EntityId,
1407 door_id: String,
1408 seq: Seq,
1409 },
1410 ConfirmInteriorEdit {
1412 entity_id: EntityId,
1413 building_id: String,
1414 rooms: Vec<InteriorRoomEdit>,
1415 room_doors: Vec<InteriorRoomDoorEdit>,
1416 seq: Seq,
1417 },
1418 CancelInteriorEdit {
1419 entity_id: EntityId,
1420 building_id: String,
1421 seq: Seq,
1422 },
1423 BankDeposit {
1425 entity_id: EntityId,
1426 npc_id: String,
1427 #[serde(default)]
1429 amount_copper: u64,
1430 seq: Seq,
1431 },
1432 BankWithdraw {
1434 entity_id: EntityId,
1435 npc_id: String,
1436 #[serde(default)]
1438 amount_copper: u64,
1439 seq: Seq,
1440 },
1441 BankClose {
1443 entity_id: EntityId,
1444 npc_id: String,
1445 seq: Seq,
1446 },
1447 BankTransfer {
1449 entity_id: EntityId,
1450 npc_id: String,
1451 #[serde(default)]
1453 to_character_id: Option<Uuid>,
1454 #[serde(default)]
1456 to_name: String,
1457 amount_copper: u64,
1459 seq: Seq,
1460 },
1461 StorageStore {
1463 entity_id: EntityId,
1464 npc_id: String,
1465 item_instance_id: Uuid,
1466 #[serde(default)]
1467 quantity: Option<u32>,
1468 seq: Seq,
1469 },
1470 StorageTake {
1472 entity_id: EntityId,
1473 npc_id: String,
1474 item_instance_id: Uuid,
1475 #[serde(default)]
1476 quantity: Option<u32>,
1477 seq: Seq,
1478 },
1479 StorageShip {
1481 entity_id: EntityId,
1482 npc_id: String,
1483 dest_building_id: String,
1484 item_instance_id: Uuid,
1485 #[serde(default)]
1486 quantity: Option<u32>,
1487 seq: Seq,
1488 },
1489 StorageClose {
1491 entity_id: EntityId,
1492 npc_id: String,
1493 seq: Seq,
1494 },
1495 MarketList {
1498 entity_id: EntityId,
1499 npc_id: String,
1500 source: GoodsLocation,
1501 item_instance_id: Uuid,
1502 #[serde(default)]
1503 quantity: Option<u32>,
1504 unit_price_copper: u64,
1505 #[serde(default)]
1507 npc_price: bool,
1508 seq: Seq,
1509 },
1510 MarketReprice {
1512 entity_id: EntityId,
1513 npc_id: String,
1514 listing_id: Uuid,
1515 unit_price_copper: u64,
1516 seq: Seq,
1517 },
1518 MarketDelist {
1520 entity_id: EntityId,
1521 npc_id: String,
1522 listing_id: Uuid,
1523 dest: GoodsLocation,
1524 seq: Seq,
1525 },
1526 MarketBuy {
1528 entity_id: EntityId,
1529 npc_id: String,
1530 listing_id: Uuid,
1531 #[serde(default = "default_one")]
1532 quantity: u32,
1533 dest: GoodsLocation,
1534 seq: Seq,
1535 },
1536 MarketClose {
1538 entity_id: EntityId,
1539 npc_id: String,
1540 seq: Seq,
1541 },
1542 TradeRequest {
1544 entity_id: EntityId,
1545 peer_entity_id: EntityId,
1546 seq: Seq,
1547 },
1548 TradeRespond {
1550 entity_id: EntityId,
1551 peer_entity_id: EntityId,
1552 accept: bool,
1553 seq: Seq,
1554 },
1555 TradePresent {
1557 entity_id: EntityId,
1558 item_instance_id: Uuid,
1559 #[serde(default)]
1560 quantity: Option<u32>,
1561 seq: Seq,
1562 },
1563 TradeUnpresent {
1565 entity_id: EntityId,
1566 item_instance_id: Uuid,
1567 seq: Seq,
1568 },
1569 TradeSetReady {
1571 entity_id: EntityId,
1572 ready: bool,
1573 seq: Seq,
1574 },
1575 TradeCancel {
1577 entity_id: EntityId,
1578 seq: Seq,
1579 },
1580 DestroyWhisperStone {
1582 entity_id: EntityId,
1583 item_instance_id: Uuid,
1584 seq: Seq,
1585 },
1586 StowWhisperStone {
1588 entity_id: EntityId,
1589 item_instance_id: Uuid,
1590 seq: Seq,
1591 },
1592 DeliverWorkerToNearestStorage {
1595 entity_id: EntityId,
1596 worker_instance_id: String,
1597 seq: Seq,
1598 },
1599 CancelWorkerDelivery {
1601 entity_id: EntityId,
1602 worker_instance_id: String,
1603 seq: Seq,
1604 },
1605}
1606
1607fn default_block_enabled() -> bool {
1608 true
1609}
1610
1611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1613pub struct StatusEffectHud {
1614 pub effect_id: String,
1615 pub label: String,
1616 #[serde(default)]
1617 pub polarity: String,
1618 #[serde(default)]
1619 pub icon_tile_id: Option<String>,
1620 #[serde(default)]
1622 pub dot_color: Option<String>,
1623 #[serde(default)]
1625 pub remaining_sec: Option<f32>,
1626 #[serde(default = "default_stack_count")]
1628 pub stack_count: u8,
1629}
1630
1631fn default_stack_count() -> u8 {
1632 1
1633}
1634
1635#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1637pub struct CombatTargetHud {
1638 pub entity_id: EntityId,
1639 #[serde(default)]
1640 pub label: String,
1641 #[serde(default)]
1642 pub level: u32,
1643 pub health: f32,
1644 pub health_max: f32,
1645 #[serde(default)]
1646 pub life_state: LifeState,
1647 #[serde(default)]
1648 pub distance_m: f32,
1649 #[serde(default)]
1650 pub statuses: Vec<StatusEffectHud>,
1651}
1652
1653#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1655#[serde(rename_all = "snake_case")]
1656pub enum TimedChannelKind {
1657 #[default]
1658 Cultivate,
1659 Plant,
1660 Harvest,
1661 Build,
1663 Craft,
1665}
1666
1667#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1669pub struct TimedChannelHud {
1670 #[serde(default)]
1671 pub label: String,
1672 #[serde(default)]
1673 pub channel: TimedChannelKind,
1674 #[serde(default)]
1675 pub cell_x: i32,
1676 #[serde(default)]
1677 pub cell_y: i32,
1678 #[serde(default)]
1680 pub x0: f32,
1681 #[serde(default)]
1682 pub y0: f32,
1683 #[serde(default)]
1684 pub x1: f32,
1685 #[serde(default)]
1686 pub y1: f32,
1687 #[serde(default)]
1688 pub ticks_remaining: u64,
1689 #[serde(default)]
1690 pub ticks_total: u64,
1691}
1692
1693impl TimedChannelHud {
1694 pub fn has_footprint(&self) -> bool {
1696 self.x1 > self.x0 && self.y1 > self.y0
1697 }
1698}
1699
1700#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1702#[serde(rename_all = "snake_case")]
1703pub enum PlotBuildMaterialSource {
1704 #[default]
1705 None,
1706 TownStorage,
1707 NearbyContainer,
1708}
1709
1710#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1712pub struct BuildingMaterialView {
1713 pub id: String,
1714 pub display_name: String,
1715 #[serde(default)]
1716 pub can_wall: bool,
1717 #[serde(default)]
1718 pub can_roof: bool,
1719 #[serde(default)]
1720 pub wall_set: String,
1721 #[serde(default)]
1722 pub roof_set: String,
1723 #[serde(default = "default_material_tick_mult")]
1724 pub tick_mult: f32,
1725 #[serde(default)]
1726 pub wall_bom: Vec<BuildingBomLineView>,
1727 #[serde(default)]
1728 pub roof_bom: Vec<BuildingBomLineView>,
1729}
1730
1731fn default_material_tick_mult() -> f32 {
1732 1.0
1733}
1734
1735#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1736pub struct BuildingBomLineView {
1737 pub template_id: String,
1738 #[serde(default)]
1739 pub display_name: String,
1740 pub per_m2: f32,
1741}
1742
1743#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1745pub struct PlotBuildStockView {
1746 pub template_id: String,
1747 #[serde(default)]
1748 pub display_name: String,
1749 pub quantity: u32,
1750}
1751
1752#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1754pub struct PlotBuildOfferHud {
1755 pub plot_id: Uuid,
1756 #[serde(default)]
1757 pub pad_width_m: f32,
1758 #[serde(default)]
1759 pub pad_depth_m: f32,
1760 #[serde(default)]
1761 pub pad_ok: bool,
1762 #[serde(default)]
1763 pub pad_error: String,
1764 #[serde(default)]
1765 pub source: PlotBuildMaterialSource,
1766 #[serde(default)]
1767 pub source_label: String,
1768 #[serde(default)]
1769 pub available: Vec<PlotBuildStockView>,
1770 #[serde(default)]
1771 pub base_ticks: u32,
1772 #[serde(default)]
1773 pub tick_per_m2: u32,
1774}
1775
1776#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1778pub struct CastProgressHud {
1779 #[serde(default)]
1780 pub ability_id: String,
1781 #[serde(default)]
1782 pub ability_label: String,
1783 #[serde(default)]
1784 pub ticks_remaining: u64,
1785 #[serde(default)]
1786 pub ticks_total: u64,
1787}
1788
1789#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1791pub struct AbilityCooldownHud {
1792 #[serde(default)]
1793 pub ability_id: String,
1794 #[serde(default)]
1795 pub label: String,
1796 #[serde(default)]
1797 pub cd_ticks: u64,
1798 #[serde(default)]
1799 pub cd_total_ticks: u64,
1800}
1801
1802#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1804pub struct CombatSlotHud {
1805 pub slot_index: u8,
1806 #[serde(default)]
1807 pub target_entity_id: Option<EntityId>,
1808 #[serde(default)]
1809 pub target_label: Option<String>,
1810 #[serde(default)]
1811 pub target: Option<CombatTargetHud>,
1812 #[serde(default)]
1813 pub preset_id: Option<String>,
1814 #[serde(default)]
1815 pub preset_label: Option<String>,
1816 #[serde(default)]
1817 pub rotation: Vec<String>,
1818 #[serde(default)]
1819 pub rotation_index: u32,
1820 #[serde(default)]
1821 pub next_ability_id: Option<String>,
1822 #[serde(default)]
1823 pub auto_enabled: bool,
1824}
1825
1826#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1828pub struct DefensePieceHud {
1829 pub slot: BodySlot,
1830 pub label: String,
1831 pub template_id: String,
1832 #[serde(default)]
1833 pub armor_physical: f32,
1834 #[serde(default)]
1835 pub resists: Vec<(String, f32)>,
1836}
1837
1838impl Default for DefensePieceHud {
1839 fn default() -> Self {
1840 Self {
1841 slot: BodySlot::Head,
1842 label: String::new(),
1843 template_id: String::new(),
1844 armor_physical: 0.0,
1845 resists: Vec::new(),
1846 }
1847 }
1848}
1849
1850#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1852pub struct DefenseHud {
1853 pub armor_physical: f32,
1854 pub vitality_contribution: f32,
1855 pub total_mitigation_rating: f32,
1856 pub estimated_physical_dr: f32,
1858 #[serde(default)]
1859 pub resists: Vec<(String, f32)>,
1860 #[serde(default)]
1861 pub pieces: Vec<DefensePieceHud>,
1862}
1863
1864#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1866pub struct CombatHud {
1867 pub in_combat: bool,
1868 pub auto_attack: bool,
1870 pub has_los: bool,
1871 pub attack_cd_ticks: u64,
1872 #[serde(default)]
1873 pub ability_id: String,
1874 #[serde(default)]
1875 pub target_entity_id: Option<EntityId>,
1876 #[serde(default)]
1877 pub target_label: Option<String>,
1878 #[serde(default)]
1879 pub max_target_slots: u8,
1880 #[serde(default)]
1881 pub slots: Vec<CombatSlotHud>,
1882 #[serde(default)]
1883 pub rotation_presets: Vec<RotationPreset>,
1884 #[serde(default)]
1885 pub gcd_ticks: u64,
1886 #[serde(default)]
1887 pub mainhand_template_id: Option<String>,
1888 #[serde(default)]
1889 pub mainhand_label: Option<String>,
1890 #[serde(default)]
1892 pub mainhand_instance_id: Option<Uuid>,
1893 #[serde(default)]
1894 pub offhand_template_id: Option<String>,
1895 #[serde(default)]
1896 pub offhand_label: Option<String>,
1897 #[serde(default)]
1899 pub offhand_instance_id: Option<Uuid>,
1900 #[serde(default)]
1902 pub mainhand_hand_slots: u8,
1903 #[serde(default)]
1905 pub worn: Vec<(BodySlot, ItemStack)>,
1906 #[serde(default)]
1908 pub defense: Option<DefenseHud>,
1909 #[serde(default)]
1910 pub carry_mass: f32,
1911 #[serde(default)]
1912 pub carry_mass_max: f32,
1913 #[serde(default)]
1914 pub encumbrance: EncumbranceState,
1915 #[serde(default)]
1917 pub keychain: Vec<ItemStack>,
1918 #[serde(default)]
1920 pub whisper_pouch: Vec<ItemStack>,
1921 #[serde(default)]
1922 pub target: Option<CombatTargetHud>,
1923 #[serde(default)]
1924 pub cast: Option<CastProgressHud>,
1925 #[serde(default)]
1927 pub timed_channel: Option<TimedChannelHud>,
1928 #[serde(default)]
1930 pub plot_build: Option<PlotBuildOfferHud>,
1931 #[serde(default)]
1932 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1933 #[serde(default)]
1934 pub blocking_active: bool,
1935 #[serde(default)]
1937 pub progression_xp: Option<ProgressionXp>,
1938 #[serde(default)]
1939 pub progression_baseline: u16,
1940 #[serde(default)]
1941 pub progression_xp_base: f64,
1942 #[serde(default)]
1943 pub progression_xp_growth: f64,
1944 #[serde(default)]
1945 pub attributes: Option<PrimaryAttributes>,
1946 #[serde(default)]
1947 pub skills: Option<PlayerSkills>,
1948 #[serde(default)]
1950 pub statuses: Vec<StatusEffectHud>,
1951 #[serde(default)]
1953 pub known_abilities: Vec<String>,
1954 #[serde(default)]
1956 pub ability_meta: Vec<AbilityMetaHud>,
1957 #[serde(default)]
1959 pub ability_mastery: Vec<AbilityMasteryHud>,
1960 #[serde(default)]
1963 pub hotbar: Vec<Option<String>>,
1964 #[serde(default)]
1966 pub max_abilities_per_rotation: u8,
1967 #[serde(default)]
1969 pub move_speed_mps: f32,
1970 #[serde(default)]
1972 pub move_speed_mult: f32,
1973}
1974
1975#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1977pub struct AbilityMetaHud {
1978 pub id: String,
1979 #[serde(default = "default_aim_mode_entity")]
1981 pub aim_mode: String,
1982 #[serde(default)]
1983 pub blast_radius_m: f32,
1984 #[serde(default)]
1985 pub allows_self: bool,
1986 #[serde(default)]
1987 pub is_heal: bool,
1988 #[serde(default = "default_auto_rotation_eligible")]
1991 pub auto_rotation_eligible: bool,
1992}
1993
1994fn default_auto_rotation_eligible() -> bool {
1995 true
1996}
1997
1998fn default_aim_mode_entity() -> String {
1999 "entity".into()
2000}
2001
2002pub const HOTBAR_ITEM_PREFIX: &str = "item:";
2004
2005pub fn hotbar_consumable_binding(template_id: &str) -> String {
2007 format!("{HOTBAR_ITEM_PREFIX}{}", template_id.trim())
2008}
2009
2010pub fn hotbar_consumable_template(binding: &str) -> Option<&str> {
2012 binding
2013 .strip_prefix(HOTBAR_ITEM_PREFIX)
2014 .map(str::trim)
2015 .filter(|id| !id.is_empty())
2016}
2017
2018pub fn hotbar_binding_is_consumable(binding: &str) -> bool {
2020 hotbar_consumable_template(binding).is_some()
2021}
2022
2023#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2025#[serde(rename_all = "snake_case")]
2026pub enum CombatFxKind {
2027 MeleeArc,
2028 Cone,
2029 Sphere,
2030 Beam,
2031 HitMarker,
2032}
2033
2034#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2036#[serde(rename_all = "snake_case")]
2037pub enum CombatFxHitOutcome {
2038 #[default]
2039 Hit,
2040 Blocked,
2041 Miss,
2042 Glance,
2043}
2044
2045#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2047pub struct CombatFxHit {
2048 pub entity_id: EntityId,
2049 pub x: f32,
2050 pub y: f32,
2051 pub z: f32,
2052 #[serde(default)]
2053 pub outcome: CombatFxHitOutcome,
2054}
2055
2056#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2058pub struct CombatFx {
2059 pub id: u64,
2060 pub kind: CombatFxKind,
2061 pub ability_id: String,
2062 pub caster_id: EntityId,
2063 pub origin_x: f32,
2064 pub origin_y: f32,
2065 pub origin_z: f32,
2066 #[serde(default)]
2067 pub end_x: Option<f32>,
2068 #[serde(default)]
2069 pub end_y: Option<f32>,
2070 #[serde(default)]
2071 pub end_z: Option<f32>,
2072 #[serde(default)]
2073 pub yaw: Option<f32>,
2074 #[serde(default)]
2075 pub reach_m: Option<f32>,
2076 #[serde(default)]
2077 pub arc_deg: Option<f32>,
2078 #[serde(default)]
2079 pub radius_m: Option<f32>,
2080 #[serde(default)]
2081 pub hits: Vec<CombatFxHit>,
2082 pub until_tick: u64,
2084 #[serde(default)]
2085 pub damage_type: String,
2086}
2087
2088#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2090pub struct GroundHazardView {
2091 pub x: f32,
2092 pub y: f32,
2093 pub z: f32,
2094 pub radius_m: f32,
2095 pub expires_at_tick: u64,
2096 #[serde(default)]
2097 pub damage_type: String,
2098}
2099
2100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2102#[serde(rename_all = "snake_case")]
2103pub enum WorkerModeView {
2104 Companion,
2105 Defender,
2106 JobLoop,
2107 Idle,
2110}
2111
2112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2114#[serde(rename_all = "snake_case")]
2115pub enum WorkerStateView {
2116 Idle,
2117 Traveling,
2118 Working,
2119 Resting,
2120 Waiting,
2121 Strike,
2122 Dismissed,
2123}
2124
2125#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
2127pub struct WorkerVitalsSummary {
2128 pub health_pct: f32,
2129 pub stamina_pct: f32,
2130 #[serde(default)]
2131 pub mana_pct: f32,
2132 #[serde(default)]
2133 pub hunger_pct: f32,
2134 #[serde(default)]
2135 pub thirst_pct: f32,
2136}
2137
2138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2140#[serde(rename_all = "snake_case")]
2141pub enum WorkerRouteKindView {
2142 #[default]
2143 HarvestLoop,
2144 Ordered,
2145}
2146
2147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2149pub struct WorkerRouteView {
2150 #[serde(default)]
2151 pub kind: WorkerRouteKindView,
2152 #[serde(default)]
2153 pub lodging_container_id: Option<String>,
2154 #[serde(default)]
2156 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
2157 #[serde(default)]
2159 pub harvest_nodes: Vec<String>,
2160 #[serde(default = "default_route_carry_ratio")]
2161 pub carry_return_ratio: f32,
2162 #[serde(default)]
2164 pub stops: Vec<WorkerRouteStopView>,
2165}
2166
2167fn default_route_carry_ratio() -> f32 {
2168 0.90
2169}
2170
2171fn default_true_view() -> bool {
2172 true
2173}
2174
2175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2177pub struct WorkerWithdrawItemView {
2178 pub template: String,
2179 #[serde(default)]
2181 pub qty: u32,
2182 #[serde(default)]
2184 pub all: bool,
2185}
2186
2187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2188pub struct WorkerRouteWaypointView {
2189 pub x: f32,
2190 pub y: f32,
2191 pub z: f32,
2192}
2193
2194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2203#[serde(rename_all = "snake_case")]
2204pub enum WorkerRouteStopView {
2205 Waypoint {
2206 x: f32,
2207 y: f32,
2208 #[serde(default)]
2209 z: f32,
2210 },
2211 HarvestNode {
2212 node_id: String,
2213 },
2214 DepositAt {
2215 container_id: String,
2216 #[serde(default)]
2217 filter: Option<Vec<String>>,
2218 },
2219 TradeWith {
2220 #[serde(default)]
2221 npc_id: Option<String>,
2222 template: String,
2223 #[serde(default = "default_true_view")]
2224 sell_all: bool,
2225 },
2226 WithdrawFrom {
2227 container_id: String,
2228 items: Vec<WorkerWithdrawItemView>,
2229 },
2230 CraftAt {
2231 device: String,
2232 blueprint: String,
2233 #[serde(default)]
2234 qty: Option<u32>,
2235 },
2236 CultivatePlot {
2237 plot_id: uuid::Uuid,
2238 },
2239 PlantPlot {
2240 plot_id: uuid::Uuid,
2241 seed_template: String,
2242 },
2243 HarvestPlot {
2244 plot_id: uuid::Uuid,
2245 },
2246 RestIfNeeded,
2247 Wait {
2248 #[serde(default)]
2249 wait_ticks: u64,
2250 },
2251}
2252
2253#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2255#[serde(rename_all = "snake_case")]
2256pub enum LedgerCategory {
2257 Workers,
2258 Hire,
2259 Train,
2260 ShopBuy,
2261 Taxes,
2262 WorkerSales,
2263 TraderSales,
2264 BankDeposit,
2265 BankWithdraw,
2266 BankTransferOut,
2267 BankTransferIn,
2268 BankTransferFee,
2269 StorageShipFee,
2270 PropertyBuy,
2272 PropertySell,
2274 TaxShare,
2276 MarketBuy,
2278 MarketSell,
2280 Other,
2281}
2282
2283impl LedgerCategory {
2284 pub fn as_str(self) -> &'static str {
2285 match self {
2286 Self::Workers => "workers",
2287 Self::Hire => "hire",
2288 Self::Train => "train",
2289 Self::ShopBuy => "shop_buy",
2290 Self::Taxes => "taxes",
2291 Self::WorkerSales => "worker_sales",
2292 Self::TraderSales => "trader_sales",
2293 Self::BankDeposit => "bank_deposit",
2294 Self::BankWithdraw => "bank_withdraw",
2295 Self::BankTransferOut => "bank_transfer_out",
2296 Self::BankTransferIn => "bank_transfer_in",
2297 Self::BankTransferFee => "bank_transfer_fee",
2298 Self::StorageShipFee => "storage_ship_fee",
2299 Self::PropertyBuy => "property_buy",
2300 Self::PropertySell => "property_sell",
2301 Self::TaxShare => "tax_share",
2302 Self::MarketBuy => "market_buy",
2303 Self::MarketSell => "market_sell",
2304 Self::Other => "other",
2305 }
2306 }
2307}
2308
2309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2310pub struct LedgerEntryView {
2311 pub id: uuid::Uuid,
2312 pub game_day: u64,
2313 pub signed_copper: i64,
2314 pub category: LedgerCategory,
2315 #[serde(default)]
2316 pub label: String,
2317}
2318
2319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2320pub struct LedgerPeriodTotals {
2321 #[serde(default)]
2323 pub expenses: std::collections::HashMap<String, u64>,
2324 #[serde(default)]
2326 pub income: std::collections::HashMap<String, u64>,
2327 pub expense_copper: u64,
2328 pub income_copper: u64,
2329 pub cash_flow_copper: i64,
2331}
2332
2333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2334pub struct PlayerLedgerView {
2335 pub current_game_day: u64,
2336 #[serde(default)]
2337 pub period_day: LedgerPeriodTotals,
2338 #[serde(default)]
2339 pub period_week: LedgerPeriodTotals,
2340 #[serde(default)]
2341 pub period_month: LedgerPeriodTotals,
2342 #[serde(default)]
2343 pub period_lifetime: LedgerPeriodTotals,
2344 #[serde(default)]
2345 pub recent: Vec<LedgerEntryView>,
2346 #[serde(default)]
2348 pub wealth_on_person_copper: u64,
2349 #[serde(default)]
2351 pub wealth_in_storage_copper: u64,
2352 #[serde(default)]
2354 pub wealth_in_bank_copper: u64,
2355 #[serde(default)]
2357 pub wealth_total_copper: u64,
2358 #[serde(default)]
2360 pub wealth_in_property_copper: u64,
2361 #[serde(default)]
2363 pub wealth_net_worth_copper: u64,
2364 #[serde(default)]
2366 pub property_assets: Vec<PropertyAssetView>,
2367 #[serde(default)]
2369 pub property_market_nearby: Vec<PropertyMarketCompView>,
2370 #[serde(default)]
2372 pub live_expense_per_interval_copper: u64,
2373 #[serde(default)]
2375 pub live_income_route_est_per_loop_copper: u64,
2376 #[serde(default)]
2378 pub live_income_avg_per_interval_copper: u64,
2379 #[serde(default)]
2381 pub live_income_avg_window_intervals: u32,
2382 #[serde(default)]
2384 pub live_net_avg_per_interval_copper: i64,
2385}
2386
2387#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2389pub struct PropertyAssetView {
2390 pub plot_id: Uuid,
2391 pub label: String,
2393 pub zone_id: String,
2394 #[serde(default)]
2395 pub zone_label: Option<String>,
2396 pub area_m2: f32,
2397 pub purchase_basis_copper: u64,
2399 pub upkeep_copper_per_day: u64,
2400}
2401
2402#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2404pub struct PropertyMarketCompView {
2405 pub day: u64,
2406 pub zone_id: String,
2407 #[serde(default)]
2408 pub zone_label: Option<String>,
2409 pub area_m2: f32,
2410 pub price_copper: u64,
2411 pub price_per_m2_copper: u64,
2413 pub kind: String,
2415 pub distance_m: f32,
2417}
2418
2419#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2421#[serde(rename_all = "snake_case")]
2422pub enum AnalyticsMetric {
2423 NpcKill,
2424 WildlifeKill,
2425 Harvest,
2426 QuestComplete,
2427 QuestAccept,
2428 QuestAbandon,
2429 PlayerDeath,
2430 Craft,
2431 WorkerHire,
2432 WorkerDismiss,
2433 WorkerTeach,
2434 NpcTalk,
2435 ShopBuy,
2436 ShopSell,
2437 PlaceContainer,
2438 PickupContainer,
2439 PickupDrop,
2440 ConsumableUse,
2441 AbilityUse,
2442 DistanceWalkedM,
2443 DoorUse,
2444 BuildingEnter,
2445}
2446
2447impl AnalyticsMetric {
2448 pub fn as_str(self) -> &'static str {
2449 match self {
2450 Self::NpcKill => "npc_kill",
2451 Self::WildlifeKill => "wildlife_kill",
2452 Self::Harvest => "harvest",
2453 Self::QuestComplete => "quest_complete",
2454 Self::QuestAccept => "quest_accept",
2455 Self::QuestAbandon => "quest_abandon",
2456 Self::PlayerDeath => "player_death",
2457 Self::Craft => "craft",
2458 Self::WorkerHire => "worker_hire",
2459 Self::WorkerDismiss => "worker_dismiss",
2460 Self::WorkerTeach => "worker_teach",
2461 Self::NpcTalk => "npc_talk",
2462 Self::ShopBuy => "shop_buy",
2463 Self::ShopSell => "shop_sell",
2464 Self::PlaceContainer => "place_container",
2465 Self::PickupContainer => "pickup_container",
2466 Self::PickupDrop => "pickup_drop",
2467 Self::ConsumableUse => "consumable_use",
2468 Self::AbilityUse => "ability_use",
2469 Self::DistanceWalkedM => "distance_walked_m",
2470 Self::DoorUse => "door_use",
2471 Self::BuildingEnter => "building_enter",
2472 }
2473 }
2474
2475 pub fn from_str_key(s: &str) -> Option<Self> {
2476 Some(match s {
2477 "npc_kill" => Self::NpcKill,
2478 "wildlife_kill" => Self::WildlifeKill,
2479 "harvest" => Self::Harvest,
2480 "quest_complete" => Self::QuestComplete,
2481 "quest_accept" => Self::QuestAccept,
2482 "quest_abandon" => Self::QuestAbandon,
2483 "player_death" => Self::PlayerDeath,
2484 "craft" => Self::Craft,
2485 "worker_hire" => Self::WorkerHire,
2486 "worker_dismiss" => Self::WorkerDismiss,
2487 "worker_teach" => Self::WorkerTeach,
2488 "npc_talk" => Self::NpcTalk,
2489 "shop_buy" => Self::ShopBuy,
2490 "shop_sell" => Self::ShopSell,
2491 "place_container" => Self::PlaceContainer,
2492 "pickup_container" => Self::PickupContainer,
2493 "pickup_drop" => Self::PickupDrop,
2494 "consumable_use" => Self::ConsumableUse,
2495 "ability_use" => Self::AbilityUse,
2496 "distance_walked_m" => Self::DistanceWalkedM,
2497 "door_use" => Self::DoorUse,
2498 "building_enter" => Self::BuildingEnter,
2499 _ => return None,
2500 })
2501 }
2502}
2503
2504#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2505pub struct CareerMetricRow {
2506 pub subject_id: String,
2507 pub amount: u64,
2508}
2509
2510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2512pub struct PlayerCareerView {
2513 pub current_game_day: u64,
2514 #[serde(default)]
2515 pub kills: Vec<CareerMetricRow>,
2516 #[serde(default)]
2517 pub harvests: Vec<CareerMetricRow>,
2518 pub quests_completed: u64,
2519 #[serde(default)]
2520 pub crafts: Vec<CareerMetricRow>,
2521 pub deaths: u64,
2522 pub npc_talks: u64,
2523 pub shop_buys: u64,
2524 pub shop_sells: u64,
2525 pub distance_m: u64,
2526 #[serde(default)]
2527 pub other: Vec<CareerMetricRow>,
2528}
2529
2530#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2535pub struct WorkerEquipmentView {
2536 #[serde(default)]
2537 pub mainhand: Option<ItemStack>,
2538 #[serde(default)]
2539 pub offhand: Option<ItemStack>,
2540 #[serde(default)]
2541 pub worn: Vec<(BodySlot, ItemStack)>,
2542}
2543
2544#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2546pub struct HiredWorkerView {
2547 pub instance_id: String,
2548 pub entity_id: EntityId,
2549 pub def_id: String,
2550 pub label: String,
2552 pub x: f32,
2553 pub y: f32,
2554 pub z: f32,
2555 pub mode: WorkerModeView,
2556 pub state: WorkerStateView,
2557 #[serde(default)]
2558 pub step_label: String,
2559 pub vitals: WorkerVitalsSummary,
2560 #[serde(default)]
2561 pub carry_pct: f32,
2562 #[serde(default)]
2563 pub last_error: Option<String>,
2564 pub wage_copper_per_interval: u32,
2565 #[serde(default)]
2567 pub effective_wage_copper: u32,
2568 #[serde(default)]
2570 pub wage_meters_walked: f32,
2571 #[serde(default)]
2573 pub lodging_container_id: Option<String>,
2574 #[serde(default)]
2576 pub route: Option<WorkerRouteView>,
2577 #[serde(default)]
2580 pub route_stop_index: Option<u32>,
2581 #[serde(default)]
2583 pub known_blueprint_ids: Vec<String>,
2584 #[serde(default = "default_worker_view_level")]
2586 pub level: u32,
2587 #[serde(default)]
2589 pub worker_xp: f64,
2590 #[serde(default)]
2592 pub inventory: Vec<ItemStack>,
2593 #[serde(default)]
2595 pub equipment: WorkerEquipmentView,
2596 #[serde(default)]
2599 pub issue_hint: Option<String>,
2600}
2601
2602fn default_worker_view_level() -> u32 {
2603 1
2604}
2605
2606#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2608pub struct TickDelta {
2609 pub tick: Tick,
2610 pub entities: Vec<EntityState>,
2611 #[serde(default)]
2612 pub resource_nodes: Vec<ResourceNodeView>,
2613 #[serde(default)]
2614 pub buildings: Vec<BuildingView>,
2615 #[serde(default)]
2616 pub doors: Vec<DoorView>,
2617 #[serde(default)]
2618 pub npcs: Vec<NpcView>,
2619 #[serde(default)]
2621 pub inventory: Vec<ItemStack>,
2622 #[serde(default)]
2623 pub blueprints: Vec<BlueprintView>,
2624 #[serde(default)]
2626 pub building_materials: Vec<BuildingMaterialView>,
2627 #[serde(default)]
2628 pub world_clock: WorldClock,
2629 #[serde(default)]
2630 pub ground_drops: Vec<GroundDropView>,
2631 #[serde(default)]
2632 pub placed_containers: Vec<PlacedContainerView>,
2633 #[serde(default)]
2634 pub combat: Option<CombatHud>,
2635 #[serde(default)]
2636 pub interior_map: Option<InteriorMapView>,
2637 #[serde(default)]
2638 pub quest_log: Vec<QuestLogEntry>,
2639 #[serde(default)]
2640 pub hired_workers: Vec<HiredWorkerView>,
2641 #[serde(default)]
2642 pub interactables: Vec<InteractableView>,
2643 #[serde(default)]
2644 pub ledger: Option<PlayerLedgerView>,
2645 #[serde(default)]
2646 pub career: Option<PlayerCareerView>,
2647 #[serde(default)]
2649 pub combat_fx: Vec<CombatFx>,
2650 #[serde(default)]
2652 pub ground_hazards: Vec<GroundHazardView>,
2653 #[serde(default)]
2655 pub property_plots: Vec<PropertyPlotView>,
2656 #[serde(default)]
2658 pub terrain_overlays: Vec<TerrainZoneView>,
2659}
2660#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2661pub struct GroundDropView {
2662 pub id: String,
2663 pub template_id: String,
2664 pub quantity: u32,
2665 pub x: f32,
2666 pub y: f32,
2667 pub z: f32,
2668 #[serde(default)]
2670 pub tile_id: Option<String>,
2671 #[serde(default)]
2673 pub display_name: Option<String>,
2674 #[serde(default)]
2676 pub yaw: f32,
2677 #[serde(default)]
2679 pub pitch: f32,
2680 #[serde(default)]
2682 pub roll: f32,
2683 #[serde(default = "default_draw_scale")]
2685 pub draw_scale: f32,
2686 #[serde(default)]
2688 pub item_instance_id: Option<Uuid>,
2689 #[serde(default)]
2691 pub props: std::collections::BTreeMap<String, String>,
2692 #[serde(default)]
2694 pub status_bindings: Vec<ItemStatusBinding>,
2695}
2696
2697fn default_draw_scale() -> f32 {
2698 1.0
2699}
2700
2701#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2703pub struct Snapshot {
2704 pub tick: Tick,
2705 pub chunk_rev: u64,
2706 #[serde(default)]
2708 pub content_rev: u64,
2709 #[serde(default)]
2711 pub publish_rev: u64,
2712 pub entities: Vec<EntityState>,
2713 #[serde(default)]
2714 pub resource_nodes: Vec<ResourceNodeView>,
2715 #[serde(default)]
2717 pub world_x0: f32,
2718 #[serde(default)]
2719 pub world_y0: f32,
2720 #[serde(default)]
2722 pub world_width_m: f32,
2723 #[serde(default)]
2724 pub world_height_m: f32,
2725 #[serde(default)]
2726 pub buildings: Vec<BuildingView>,
2727 #[serde(default)]
2728 pub doors: Vec<DoorView>,
2729 #[serde(default)]
2730 pub npcs: Vec<NpcView>,
2731 #[serde(default)]
2732 pub inventory: Vec<ItemStack>,
2733 #[serde(default)]
2734 pub blueprints: Vec<BlueprintView>,
2735 #[serde(default)]
2737 pub building_materials: Vec<BuildingMaterialView>,
2738 #[serde(default)]
2739 pub world_clock: WorldClock,
2740 #[serde(default)]
2741 pub terrain_zones: Vec<TerrainZoneView>,
2742 #[serde(default)]
2743 pub z_platforms: Vec<ZPlatformView>,
2744 #[serde(default)]
2745 pub z_transitions: Vec<ZTransitionView>,
2746 #[serde(default)]
2747 pub ground_drops: Vec<GroundDropView>,
2748 #[serde(default)]
2749 pub placed_containers: Vec<PlacedContainerView>,
2750 #[serde(default)]
2751 pub combat: Option<CombatHud>,
2752 #[serde(default)]
2753 pub interior_map: Option<InteriorMapView>,
2754 #[serde(default)]
2755 pub quest_log: Vec<QuestLogEntry>,
2756 #[serde(default)]
2757 pub hired_workers: Vec<HiredWorkerView>,
2758 #[serde(default)]
2759 pub interactables: Vec<InteractableView>,
2760 #[serde(default)]
2761 pub ledger: Option<PlayerLedgerView>,
2762 #[serde(default)]
2763 pub career: Option<PlayerCareerView>,
2764 #[serde(default)]
2766 pub combat_fx: Vec<CombatFx>,
2767 #[serde(default)]
2769 pub ground_hazards: Vec<GroundHazardView>,
2770 #[serde(default)]
2772 pub property_zones: Vec<PropertyZoneView>,
2773 #[serde(default)]
2775 pub tax_zones: Vec<TaxZoneView>,
2776 #[serde(default)]
2778 pub boundary_zones: Vec<BoundaryZoneView>,
2779 #[serde(default)]
2781 pub encounter_zones: Vec<EncounterZoneView>,
2782 #[serde(default)]
2784 pub growth_zones: Vec<GrowthZoneView>,
2785 #[serde(default)]
2787 pub biome_zones: Vec<BiomeZoneView>,
2788 #[serde(default)]
2790 pub terrain_kind_nav: Vec<TerrainKindNavView>,
2791 #[serde(default)]
2793 pub property_plots: Vec<PropertyPlotView>,
2794 #[serde(default)]
2796 pub property_plot_settings: Option<PropertyPlotSettingsView>,
2797 #[serde(default)]
2800 pub item_catalog: Vec<ItemCatalogEntryView>,
2801}
2802
2803#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2805pub struct ItemCatalogEntryView {
2806 pub template_id: String,
2807 #[serde(default)]
2808 pub display_name: String,
2809 #[serde(default)]
2810 pub category: String,
2811 #[serde(default)]
2813 pub seed_for: Option<String>,
2814}
2815
2816impl ItemCatalogEntryView {
2817 pub fn is_harvest_node(&self) -> bool {
2818 self.category == "harvest_node"
2819 }
2820
2821 pub fn is_depositable_stack(&self) -> bool {
2823 !self.is_harvest_node()
2824 }
2825
2826 pub fn is_farm_seed(&self) -> bool {
2827 self.seed_for
2828 .as_deref()
2829 .is_some_and(|s| !s.trim().is_empty())
2830 || self.category == "seed"
2831 }
2832}
2833
2834#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2836pub struct ResourceNodeView {
2837 pub id: String,
2838 pub label: String,
2839 pub x: f32,
2840 pub y: f32,
2841 pub z: f32,
2842 pub item_template: String,
2843 #[serde(default = "default_node_state")]
2844 pub state: ResourceNodeState,
2845 #[serde(default = "default_blocking_view")]
2847 pub blocking: bool,
2848 #[serde(default = "default_blocking_radius_view")]
2850 pub blocking_radius_m: f32,
2851 #[serde(default)]
2853 pub harvest_off: bool,
2854 #[serde(default)]
2856 pub tile_id: Option<String>,
2857 #[serde(default)]
2859 pub yaw: f32,
2860 #[serde(default)]
2862 pub pitch: f32,
2863 #[serde(default)]
2865 pub roll: f32,
2866 #[serde(default = "default_draw_scale")]
2868 pub draw_scale: f32,
2869 #[serde(default)]
2871 pub sprite_mode: Option<String>,
2872 #[serde(default)]
2874 pub presentation_state: Option<String>,
2875 #[serde(default)]
2878 pub growth_progress: Option<f32>,
2879 #[serde(default)]
2881 pub channel_start_tick: Option<Tick>,
2882 #[serde(default)]
2883 pub channel_end_tick: Option<Tick>,
2884 #[serde(default)]
2886 pub harvest_drop_templates: Vec<String>,
2887}
2888
2889fn default_blocking_radius_view() -> f32 {
2890 0.8
2891}
2892
2893fn default_blocking_view() -> bool {
2894 true
2895}
2896
2897#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2898#[serde(rename_all = "snake_case")]
2899pub enum ResourceNodeState {
2900 Available,
2901 Harvesting,
2902 Cooldown,
2903}
2904fn default_node_state() -> ResourceNodeState {
2905 ResourceNodeState::Available
2906}
2907
2908#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2910#[serde(rename_all = "snake_case")]
2911pub enum ItemSpawnStateView {
2912 Spawned,
2913 PickedUp { respawn_at_tick: u64 },
2914 Consumed,
2915}
2916
2917#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2919pub struct ItemSpawnView {
2920 pub id: String,
2921 pub label: String,
2922 pub item_template: String,
2923 pub quantity: u32,
2924 pub x: f32,
2925 pub y: f32,
2926 pub z: f32,
2927 pub respawn_ticks: u32,
2928 #[serde(default)]
2929 pub building_id: Option<String>,
2930 pub state: ItemSpawnStateView,
2931 #[serde(default)]
2933 pub once_per_character: bool,
2934 #[serde(default)]
2936 pub collected_count: u32,
2937}
2938
2939#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2940#[serde(rename_all = "snake_case")]
2941pub enum ItemStatusBindingMode {
2942 OnHit,
2943 WhileEquipped,
2944}
2945
2946impl Default for ItemStatusBindingMode {
2947 fn default() -> Self {
2948 Self::OnHit
2949 }
2950}
2951
2952#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2954pub struct ItemStatusBinding {
2955 pub effect_id: String,
2956 #[serde(default)]
2957 pub mode: ItemStatusBindingMode,
2958 #[serde(default)]
2960 pub source: String,
2961 #[serde(default)]
2962 pub applied_at_tick: u64,
2963 #[serde(default)]
2966 pub expires_at_tick: Option<u64>,
2967}
2968
2969#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2970pub struct ItemStack {
2971 pub template_id: String,
2972 pub quantity: u32,
2973 #[serde(default)]
2975 pub item_instance_id: Option<Uuid>,
2976 #[serde(default)]
2978 pub props: BTreeMap<String, String>,
2979 #[serde(default)]
2981 pub status_bindings: Vec<ItemStatusBinding>,
2982 #[serde(default)]
2984 pub contents: Vec<ItemStack>,
2985 #[serde(default)]
2987 pub display_name: Option<String>,
2988 #[serde(default)]
2990 pub category: Option<String>,
2991 #[serde(default)]
2993 pub base_mass: Option<f32>,
2994 #[serde(default)]
2996 pub base_volume: Option<f32>,
2997 #[serde(default)]
2999 pub capacity_volume: Option<f32>,
3000 #[serde(default)]
3002 pub stackable: Option<bool>,
3003 #[serde(default)]
3005 pub world_placeable: Option<bool>,
3006 #[serde(default)]
3008 pub worker_lodging_capacity: Option<u32>,
3009 #[serde(default)]
3011 pub equip_slot: Option<BodySlot>,
3012 #[serde(default)]
3014 pub armor_physical: Option<f32>,
3015 #[serde(default)]
3017 pub resists: Vec<(String, f32)>,
3018 #[serde(default)]
3020 pub hand_slots: Option<u8>,
3021 #[serde(default)]
3023 pub listable: Option<bool>,
3024 #[serde(default)]
3026 pub base_value_copper: Option<u32>,
3027}
3028
3029impl ItemStack {
3030 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
3031 Self {
3032 template_id: template_id.into(),
3033 quantity,
3034 ..Default::default()
3035 }
3036 }
3037}
3038
3039impl Default for ItemStack {
3040 fn default() -> Self {
3041 Self {
3042 template_id: String::new(),
3043 quantity: 0,
3044 item_instance_id: None,
3045 props: BTreeMap::new(),
3046 status_bindings: Vec::new(),
3047 contents: Vec::new(),
3048 display_name: None,
3049 category: None,
3050 base_mass: None,
3051 base_volume: None,
3052 capacity_volume: None,
3053 stackable: None,
3054 world_placeable: None,
3055 worker_lodging_capacity: None,
3056 equip_slot: None,
3057 armor_physical: None,
3058 resists: Vec::new(),
3059 hand_slots: None,
3060 listable: None,
3061 base_value_copper: None,
3062 }
3063 }
3064}
3065
3066#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3068#[serde(rename_all = "snake_case")]
3069pub enum EncumbranceState {
3070 #[default]
3071 Light,
3072 Heavy,
3073 Orange,
3074 Over,
3075}
3076
3077impl EncumbranceState {
3078 pub fn label(self) -> &'static str {
3080 match self {
3081 Self::Light => "Light",
3082 Self::Heavy => "Heavy",
3083 Self::Orange => "Overloaded",
3084 Self::Over => "Over",
3085 }
3086 }
3087
3088 pub fn allows_sprint(self) -> bool {
3090 !matches!(self, Self::Over)
3091 }
3092}
3093
3094#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
3098#[serde(rename_all = "snake_case")]
3099pub enum BodySlot {
3100 Head,
3101 #[serde(alias = "body")]
3103 Chest,
3104 #[serde(alias = "arms")]
3106 Forearms,
3107 Legs,
3108 Feet,
3109 Cloak,
3110 Back,
3111 Waist,
3112 Earrings,
3113 Necklace,
3114 Eyeglasses,
3115 #[serde(rename = "ring_left_1", alias = "ring_left1")]
3117 RingLeft1,
3118 #[serde(rename = "ring_left_2", alias = "ring_left2")]
3119 RingLeft2,
3120 #[serde(rename = "ring_right_1", alias = "ring_right1")]
3121 RingRight1,
3122 #[serde(rename = "ring_right_2", alias = "ring_right2")]
3123 RingRight2,
3124}
3125
3126impl BodySlot {
3127 pub const ALL: [BodySlot; 15] = [
3129 BodySlot::Head,
3130 BodySlot::Chest,
3131 BodySlot::Forearms,
3132 BodySlot::Legs,
3133 BodySlot::Feet,
3134 BodySlot::Cloak,
3135 BodySlot::Back,
3136 BodySlot::Waist,
3137 BodySlot::Earrings,
3138 BodySlot::Necklace,
3139 BodySlot::Eyeglasses,
3140 BodySlot::RingLeft1,
3141 BodySlot::RingLeft2,
3142 BodySlot::RingRight1,
3143 BodySlot::RingRight2,
3144 ];
3145
3146 pub fn as_str(self) -> &'static str {
3147 match self {
3148 BodySlot::Head => "head",
3149 BodySlot::Chest => "chest",
3150 BodySlot::Forearms => "forearms",
3151 BodySlot::Legs => "legs",
3152 BodySlot::Feet => "feet",
3153 BodySlot::Cloak => "cloak",
3154 BodySlot::Back => "back",
3155 BodySlot::Waist => "waist",
3156 BodySlot::Earrings => "earrings",
3157 BodySlot::Necklace => "necklace",
3158 BodySlot::Eyeglasses => "eyeglasses",
3159 BodySlot::RingLeft1 => "ring_left_1",
3160 BodySlot::RingLeft2 => "ring_left_2",
3161 BodySlot::RingRight1 => "ring_right_1",
3162 BodySlot::RingRight2 => "ring_right_2",
3163 }
3164 }
3165}
3166
3167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3169#[serde(rename_all = "snake_case")]
3170pub enum InventoryLocation {
3171 Root,
3173 Worn { slot: BodySlot },
3175 Placed { container_id: String },
3177 Keychain,
3179 WhisperPouch,
3181}
3182
3183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3185pub struct PlacedContainerView {
3186 pub id: String,
3187 pub template_id: String,
3188 pub display_name: String,
3189 pub x: f32,
3190 pub y: f32,
3191 pub z: f32,
3192 pub locked: bool,
3193 #[serde(default)]
3195 pub accessible: bool,
3196 #[serde(default)]
3197 pub owner_character_id: Option<Uuid>,
3198 #[serde(default)]
3200 pub contents: Vec<ItemStack>,
3201 #[serde(default)]
3203 pub lock_id: Option<String>,
3204 #[serde(default)]
3206 pub capacity_volume: Option<f32>,
3207 #[serde(default)]
3209 pub item_instance_id: Option<Uuid>,
3210 #[serde(default)]
3212 pub tile_id: Option<String>,
3213 #[serde(default)]
3215 pub worker_lodging_capacity: Option<u32>,
3216 #[serde(default)]
3218 pub blocking: bool,
3219 #[serde(default)]
3221 pub blocking_radius_m: f32,
3222 #[serde(default)]
3225 pub building_id: Option<String>,
3226}
3227
3228#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3229pub struct BlueprintIngredientView {
3230 pub template_id: String,
3231 pub quantity: u32,
3232 pub consumed: bool,
3234 #[serde(default)]
3236 pub display_name: String,
3237}
3238
3239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3240pub struct ToolRequirementView {
3241 pub item: String,
3242 pub consumed: bool,
3244 #[serde(default)]
3246 pub display_name: String,
3247}
3248
3249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3250pub struct SkillRequirementView {
3251 pub skill: String,
3252 pub level: u32,
3253}
3254
3255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3256pub struct BlueprintView {
3257 pub id: String,
3258 pub label: String,
3259 pub output: String,
3260 pub output_qty: u32,
3261 pub craft_ticks: u32,
3262 pub inputs: Vec<BlueprintIngredientView>,
3263 #[serde(default)]
3265 pub station: Option<String>,
3266 #[serde(default)]
3267 pub category: Option<String>,
3268 #[serde(default)]
3269 pub required_tools: Vec<ToolRequirementView>,
3270 #[serde(default)]
3271 pub skill: Option<SkillRequirementView>,
3272 #[serde(default)]
3273 pub failure_chance: f32,
3274 #[serde(default)]
3276 pub worker_train_copper: u64,
3277 #[serde(default)]
3279 pub output_display_name: String,
3280 #[serde(default)]
3282 pub craft_tier: u32,
3283}
3284
3285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3287pub struct TerrainKindNavView {
3288 pub kind: TerrainKindView,
3289 #[serde(default = "default_move_speed_mult_one")]
3290 pub move_speed_mult: f32,
3291 #[serde(default)]
3292 pub impassable: bool,
3293}
3294
3295fn default_move_speed_mult_one() -> f32 {
3296 1.0
3297}
3298
3299#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3301#[serde(rename_all = "snake_case")]
3302pub enum TerrainKindView {
3303 #[default]
3304 Grass,
3305 Dirt,
3306 Tilled,
3307 Desert,
3308 Hill,
3309 Bog,
3310 Beach,
3311 ShallowWater,
3312 DeepWater,
3313 Trail,
3314 Road,
3315 Rock,
3316}
3317
3318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3319pub struct TerrainZoneView {
3320 pub id: String,
3321 pub x0: f32,
3322 pub y0: f32,
3323 pub x1: f32,
3324 pub y1: f32,
3325 #[serde(default)]
3326 pub kind: TerrainKindView,
3327 #[serde(default)]
3329 pub elevation: f32,
3330 #[serde(default)]
3333 pub glyph: Option<String>,
3334 #[serde(default)]
3336 pub color: Option<String>,
3337 #[serde(default)]
3339 pub tile_id: Option<String>,
3340 #[serde(default)]
3342 pub z_order: i32,
3343 #[serde(default)]
3345 pub channel_start_tick: Option<Tick>,
3346 #[serde(default)]
3347 pub channel_end_tick: Option<Tick>,
3348}
3349
3350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3352pub struct ZoneRectView {
3353 pub x0: f32,
3354 pub y0: f32,
3355 pub x1: f32,
3356 pub y1: f32,
3357}
3358
3359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3361pub struct PropertyZoneView {
3362 pub id: String,
3363 #[serde(default)]
3365 pub label: Option<String>,
3366 pub rects: Vec<ZoneRectView>,
3367 #[serde(default)]
3368 pub z_order: i32,
3369 pub crown_price_copper: u64,
3370 pub upkeep_copper_per_day: u64,
3371 #[serde(default)]
3372 pub max_area_m2: Option<f32>,
3373 #[serde(default)]
3374 pub owner_tax_discount_bps: u32,
3375}
3376
3377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3379pub struct TaxZoneView {
3380 pub id: String,
3381 #[serde(default)]
3382 pub label: Option<String>,
3383 pub rects: Vec<ZoneRectView>,
3384 #[serde(default)]
3385 pub z_order: i32,
3386 pub rate_bps: u32,
3387 #[serde(default)]
3388 pub flat_copper: u64,
3389 #[serde(default)]
3391 pub market_sales_tax_bps: u32,
3392 #[serde(default)]
3394 pub market_sales_flat_copper: u32,
3395}
3396
3397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3399pub struct BoundaryZoneView {
3400 pub id: String,
3401 #[serde(default)]
3402 pub label: Option<String>,
3403 pub rects: Vec<ZoneRectView>,
3404 #[serde(default)]
3405 pub z_order: i32,
3406 #[serde(default, skip_serializing_if = "Option::is_none")]
3407 pub jurisdiction_id: Option<String>,
3408 #[serde(default = "default_true")]
3409 pub worker_logistics: bool,
3410 #[serde(default)]
3411 pub security_tier: String,
3412 #[serde(default)]
3413 pub pvp_mode: String,
3414 #[serde(default = "default_true")]
3415 pub crime_enabled: bool,
3416 #[serde(default)]
3417 pub guard_response: bool,
3418 #[serde(default)]
3420 pub presence_mode: String,
3421}
3422
3423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3425pub struct EncounterZoneView {
3426 pub id: String,
3427 #[serde(default)]
3428 pub label: Option<String>,
3429 pub rects: Vec<ZoneRectView>,
3430 #[serde(default)]
3431 pub z_order: i32,
3432}
3433
3434fn default_true() -> bool {
3435 true
3436}
3437
3438#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3440pub struct GrowthZoneView {
3441 pub id: String,
3442 #[serde(default)]
3443 pub label: Option<String>,
3444 pub rects: Vec<ZoneRectView>,
3445 #[serde(default)]
3446 pub z_order: i32,
3447 #[serde(default = "default_one_f32")]
3448 pub fertility: f32,
3449}
3450
3451fn default_one_f32() -> f32 {
3452 1.0
3453}
3454
3455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3457pub struct BiomeZoneView {
3458 pub id: String,
3459 #[serde(default)]
3460 pub label: Option<String>,
3461 pub rects: Vec<ZoneRectView>,
3462 #[serde(default)]
3463 pub z_order: i32,
3464 pub biome_id: String,
3465}
3466
3467#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3469pub struct FarmGrantView {
3470 pub character_id: Uuid,
3471 #[serde(default)]
3473 pub character_label: String,
3474 pub tax_discount_bps: u32,
3475}
3476
3477#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3479pub struct PropertyPlotView {
3480 pub plot_id: Uuid,
3481 pub property_zone_id: String,
3482 #[serde(default)]
3483 pub zone_label: Option<String>,
3484 pub deed_instance_id: Uuid,
3485 pub x0: f32,
3486 pub y0: f32,
3487 pub x1: f32,
3488 pub y1: f32,
3489 pub upkeep_copper_per_day: u64,
3490 pub arrears_days: u32,
3491 #[serde(default)]
3493 pub is_mine: bool,
3494 #[serde(default)]
3496 pub may_farm: bool,
3497 #[serde(default)]
3499 pub purchase_basis_copper: u64,
3500 #[serde(default)]
3501 pub farm_public: bool,
3502 #[serde(default)]
3503 pub public_tax_discount_bps: u32,
3504 #[serde(default)]
3505 pub farm_allow: Vec<FarmGrantView>,
3506 #[serde(default)]
3508 pub owner_character_id: Option<Uuid>,
3509 #[serde(default)]
3510 pub owner_label: Option<String>,
3511 #[serde(default)]
3513 pub building_id: Option<String>,
3514 #[serde(default)]
3516 pub plot_code: String,
3517 #[serde(default)]
3519 pub label: String,
3520}
3521
3522#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3524pub struct PropertyPlotSettingsView {
3525 pub min_plot_area_m2: f32,
3526 pub tax_premium_weight: f32,
3527 pub sellback_bps: u32,
3528}
3529
3530#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3532pub struct ZPlatformView {
3533 pub id: String,
3534 pub z: f32,
3535 pub x0: f32,
3536 pub y0: f32,
3537 pub x1: f32,
3538 pub y1: f32,
3539}
3540
3541#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3543pub struct ZTransitionView {
3544 pub id: String,
3545 pub z_from: f32,
3546 pub z_to: f32,
3547 pub x0: f32,
3548 pub y0: f32,
3549 pub x1: f32,
3550 pub y1: f32,
3551}
3552
3553#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3554pub struct BuildingView {
3555 pub id: String,
3556 pub label: String,
3557 pub x: f32,
3558 pub y: f32,
3559 pub width_m: f32,
3560 pub depth_m: f32,
3561 #[serde(default)]
3562 pub interior_blueprint: Option<String>,
3563 #[serde(default)]
3564 pub tags: Vec<String>,
3565 #[serde(default)]
3567 pub market_boundary_zone_ids: Vec<String>,
3568 #[serde(default)]
3570 pub market_max_volume: Option<f32>,
3571 #[serde(default)]
3574 pub wall_set: Option<String>,
3575 #[serde(default)]
3577 pub roof_set: Option<String>,
3578}
3579
3580pub const DEFAULT_BUILDING_ART_SET: &str = "classic_stone";
3583
3584impl BuildingView {
3585 pub fn effective_wall_set(&self) -> &str {
3586 self.wall_set
3587 .as_deref()
3588 .filter(|s| !s.is_empty())
3589 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3590 }
3591
3592 pub fn effective_roof_set(&self) -> &str {
3593 self.roof_set
3594 .as_deref()
3595 .filter(|s| !s.is_empty())
3596 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3597 }
3598}
3599
3600#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3601pub struct DoorView {
3602 pub id: String,
3603 pub building_id: String,
3604 pub x: f32,
3605 pub y: f32,
3606 #[serde(default)]
3607 pub open: bool,
3608 #[serde(default)]
3609 pub portal: Option<String>,
3610 #[serde(default)]
3613 pub locked: bool,
3614 #[serde(default = "default_door_accessible")]
3616 pub accessible: bool,
3617 #[serde(default)]
3618 pub lock_id: Option<Uuid>,
3619}
3620
3621fn default_door_accessible() -> bool {
3622 true
3623}
3624
3625#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3627pub struct InteriorRoomEdit {
3628 pub id: String,
3629 pub label: String,
3630 pub x0: f32,
3631 pub y0: f32,
3632 pub x1: f32,
3633 pub y1: f32,
3634}
3635
3636#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3637pub struct InteriorRoomDoorEdit {
3638 pub id: String,
3639 pub room_a: String,
3640 pub room_b: String,
3641 pub x: f32,
3642 pub y: f32,
3643}
3644
3645#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3646pub struct InteriorRoomView {
3647 pub id: String,
3648 pub label: String,
3649 pub floor: i32,
3650 pub x0: f32,
3651 pub y0: f32,
3652 pub x1: f32,
3653 pub y1: f32,
3654 #[serde(default)]
3655 pub floor_color: Option<String>,
3656 #[serde(default)]
3657 pub floor_glyph: Option<String>,
3658}
3659
3660#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3661pub struct InteriorDoorView {
3662 pub id: String,
3663 pub room_a: String,
3664 pub room_b: String,
3665 pub x: f32,
3666 pub y: f32,
3667 pub kind: String,
3668 #[serde(default)]
3669 pub x_a: Option<f32>,
3670 #[serde(default)]
3671 pub y_a: Option<f32>,
3672 #[serde(default)]
3673 pub x_b: Option<f32>,
3674 #[serde(default)]
3675 pub y_b: Option<f32>,
3676}
3677
3678#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3679pub struct InteriorMapView {
3680 pub building_id: String,
3681 pub blueprint_id: String,
3682 pub background_color: String,
3683 #[serde(default)]
3684 pub default_floor_color: Option<String>,
3685 #[serde(default = "default_floor_height_view")]
3686 pub floor_height_m: f32,
3687 #[serde(default)]
3689 pub z_platforms: Vec<ZPlatformView>,
3690 #[serde(default)]
3691 pub z_transitions: Vec<ZTransitionView>,
3692 pub rooms: Vec<InteriorRoomView>,
3693 #[serde(default)]
3694 pub room_doors: Vec<InteriorDoorView>,
3695}
3696
3697fn default_floor_height_view() -> f32 {
3698 3.0
3699}
3700
3701#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3702pub struct NpcView {
3703 pub id: String,
3704 pub label: String,
3705 pub role: String,
3706 pub x: f32,
3707 pub y: f32,
3708 #[serde(default)]
3710 pub building_id: Option<String>,
3711 #[serde(default)]
3713 pub entity_id: Option<EntityId>,
3714 #[serde(default)]
3715 pub life_state: Option<LifeState>,
3716 #[serde(default)]
3717 pub hp_pct: Option<f32>,
3718 #[serde(default)]
3720 pub can_trade: bool,
3721 #[serde(default)]
3723 pub buy_templates: Vec<String>,
3724 #[serde(default)]
3726 pub tile_id: Option<String>,
3727 #[serde(default)]
3729 pub behavior_state: Option<String>,
3730 #[serde(default)]
3732 pub presentation_state: Option<String>,
3733 #[serde(default)]
3735 pub sprite_mode: Option<String>,
3736 #[serde(default)]
3738 pub paperdoll_ref: Option<String>,
3739 #[serde(default = "default_draw_scale")]
3741 pub draw_scale: f32,
3742 #[serde(default)]
3744 pub yaw: Option<f32>,
3745 #[serde(default)]
3747 pub perception_fov_deg: Option<f32>,
3748 #[serde(default)]
3750 pub perception_sight_m: Option<f32>,
3751 #[serde(default)]
3753 pub perception_hear_m: Option<f32>,
3754 #[serde(default)]
3756 pub quest_verbs: Vec<NpcQuestVerb>,
3757}
3758
3759#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3761pub struct NpcQuestVerb {
3762 pub quest_id: String,
3763 pub label: String,
3765 pub kind: String,
3766}
3767
3768impl NpcQuestVerb {
3769 pub const KIND_OFFER: &'static str = "offer";
3770 pub const KIND_TALK: &'static str = "talk";
3771 pub const KIND_GIVE: &'static str = "give";
3772}
3773
3774#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3775pub struct UseResult {
3776 pub template_id: String,
3777 pub hunger_restored: f32,
3778 pub thirst_restored: f32,
3779 #[serde(default)]
3780 pub health_restored: f32,
3781 #[serde(default)]
3782 pub mana_restored: f32,
3783 #[serde(default)]
3784 pub cleared_dot_ids: Vec<String>,
3785}
3786
3787#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3788pub struct CraftResult {
3789 pub blueprint_id: String,
3790 pub outputs: Vec<ItemStack>,
3791 pub consumed: Vec<ItemStack>,
3792 #[serde(default = "default_one")]
3794 pub batch_index: u32,
3795 #[serde(default = "default_one")]
3797 pub batch_total: u32,
3798}
3799
3800fn default_one() -> u32 {
3801 1
3802}
3803
3804#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3805pub struct DeathNotice {
3806 pub entity_id: EntityId,
3807 pub respawn_x: f32,
3808 pub respawn_y: f32,
3809 pub message: String,
3810}
3811
3812#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3813pub struct InteractionNotice {
3814 pub target_id: String,
3815 pub message: String,
3816 #[serde(default)]
3817 pub coins_delta: i32,
3818 #[serde(default)]
3819 pub inventory_delta: Vec<ItemStack>,
3820}
3821
3822#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3823#[serde(rename_all = "snake_case")]
3824pub enum NpcTalkTrustFlag {
3825 Stranger,
3826 Acquainted,
3827 Trusted,
3828}
3829
3830#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3831#[serde(rename_all = "snake_case")]
3832pub enum NpcTalkDepth {
3833 #[default]
3834 Full,
3835 Brief,
3836 Unavailable,
3837}
3838
3839#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3840pub struct NpcTalkOpened {
3841 pub npc_id: String,
3842 pub npc_label: String,
3843 pub greeting: String,
3844 pub trust_flag: NpcTalkTrustFlag,
3845 #[serde(default)]
3846 pub talk_depth: NpcTalkDepth,
3847 #[serde(default = "default_true")]
3848 pub trade_allowed: bool,
3849 #[serde(default)]
3851 pub suggested_topics: Vec<String>,
3852}
3853
3854#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3855pub struct NpcTalkPending {
3856 pub npc_id: String,
3857}
3858
3859#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3860pub struct NpcTalkReply {
3861 pub npc_id: String,
3862 pub line: String,
3863 pub trust_flag: NpcTalkTrustFlag,
3864 #[serde(default)]
3865 pub wind_down: bool,
3866 #[serde(default)]
3867 pub trade_disabled: bool,
3868}
3869
3870#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3871pub struct NpcTalkClosed {
3872 pub npc_id: String,
3873}
3874
3875#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3876pub struct NpcTalkError {
3877 pub npc_id: String,
3878 pub reason: String,
3879}
3880
3881#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3882#[serde(rename_all = "snake_case")]
3883pub enum QuestStatusView {
3884 Available,
3885 Active,
3886 Completed,
3887}
3888
3889#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3890pub struct QuestObjectiveProgress {
3891 pub label: String,
3892 pub current: u32,
3893 pub required: u32,
3894 pub done: bool,
3895 #[serde(default)]
3899 pub kind: String,
3900 #[serde(default)]
3901 pub npc_ref: Option<String>,
3902 #[serde(default)]
3903 pub item_template: Option<String>,
3904 #[serde(default)]
3905 pub blueprint_id: Option<String>,
3906 #[serde(default)]
3907 pub building_id: Option<String>,
3908}
3909
3910#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3912pub struct QuestRewardItemView {
3913 pub template_id: String,
3914 #[serde(default)]
3916 pub display_name: String,
3917 pub quantity: u32,
3918}
3919
3920#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3922pub struct QuestRewardView {
3923 #[serde(default)]
3924 pub coins: u32,
3925 #[serde(default)]
3926 pub items: Vec<QuestRewardItemView>,
3927}
3928
3929impl QuestRewardView {
3930 pub fn is_empty(&self) -> bool {
3931 self.coins == 0 && self.items.is_empty()
3932 }
3933}
3934
3935#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3936#[serde(rename_all = "snake_case")]
3937pub enum QuestStepStatusView {
3938 #[default]
3939 Pending,
3940 Current,
3941 Done,
3942}
3943
3944#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3946pub struct QuestStepView {
3947 pub id: String,
3948 pub title: String,
3949 #[serde(default)]
3950 pub status: QuestStepStatusView,
3951 #[serde(default)]
3952 pub reward: QuestRewardView,
3953}
3954
3955#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3956pub struct QuestLogEntry {
3957 pub quest_id: String,
3958 pub title: String,
3959 pub description: String,
3960 pub status: QuestStatusView,
3961 #[serde(default)]
3962 pub current_step_id: Option<String>,
3963 #[serde(default)]
3964 pub current_step_title: String,
3965 #[serde(default)]
3966 pub current_step_index: u32,
3967 #[serde(default)]
3968 pub objectives: Vec<QuestObjectiveProgress>,
3969 #[serde(default)]
3971 pub current_step_reward: QuestRewardView,
3972 #[serde(default)]
3974 pub completion_reward: QuestRewardView,
3975 #[serde(default)]
3977 pub steps: Vec<QuestStepView>,
3978 #[serde(default)]
3979 pub is_tracked: bool,
3980 #[serde(default)]
3981 pub can_withdraw: bool,
3982}
3983
3984#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3985pub struct InteractableView {
3986 pub id: String,
3987 pub kind: String,
3988 pub label: String,
3989 pub x: f32,
3990 pub y: f32,
3991 pub z: f32,
3992 #[serde(default)]
3993 pub board_id: Option<String>,
3994}
3995
3996#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3997pub struct QuestOffer {
3998 pub quest_id: String,
3999 pub title: String,
4000 pub description: String,
4001 #[serde(default)]
4002 pub step_count: u32,
4003}
4004
4005#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4006pub struct QuestCatalogEntry {
4007 pub quest_id: String,
4008 pub title: String,
4009 pub description: String,
4010 pub step_count: u32,
4011 #[serde(default)]
4012 pub board_ids: Vec<String>,
4013}
4014
4015#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4016pub struct QuestCatalogUpdated {
4017 pub revision: u64,
4018 pub game_day: String,
4019 #[serde(default)]
4020 pub accepted: Vec<QuestCatalogEntry>,
4021 #[serde(default)]
4022 pub retired: Vec<String>,
4023}
4024
4025#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4026pub struct QuestNotice {
4027 pub quest_id: String,
4028 pub title: String,
4029 pub message: String,
4030}
4031
4032#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4033#[serde(rename_all = "snake_case")]
4034pub enum ShopOfferKind {
4035 Item,
4036 Blueprint,
4037}
4038
4039#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4040pub struct ShopOffer {
4041 pub offer_id: String,
4042 pub kind: ShopOfferKind,
4043 pub label: String,
4044 #[serde(default)]
4045 pub template_id: Option<String>,
4046 #[serde(default)]
4047 pub blueprint_id: Option<String>,
4048 pub price_copper: u32,
4049 #[serde(default)]
4050 pub affordable: bool,
4051 #[serde(default)]
4052 pub already_owned: bool,
4053}
4054
4055#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4056pub struct ShopBuyLine {
4057 pub template_id: String,
4058 pub label: String,
4059 pub quantity: u32,
4060 pub price_copper: u32,
4061}
4062
4063#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4065pub struct BankPanel {
4066 pub npc_id: String,
4067 pub npc_label: String,
4068 pub bank_balance_copper: u64,
4069 pub on_person_copper: u64,
4070 #[serde(default)]
4072 pub pending_outgoing_copper: u64,
4073 #[serde(default)]
4074 pub transfer_fee_bps: u32,
4075 #[serde(default)]
4076 pub transfer_clear_ticks: u64,
4077}
4078
4079#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4081pub struct StoragePanel {
4082 pub npc_id: String,
4083 pub npc_label: String,
4084 pub building_id: String,
4085 pub building_label: String,
4086 pub used_volume: f32,
4087 pub max_volume: f32,
4088 #[serde(default)]
4089 pub contents: Vec<ItemStack>,
4090 #[serde(default)]
4092 pub ship_destinations: Vec<StorageShipDest>,
4093}
4094
4095#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4096pub struct StorageShipDest {
4097 pub building_id: String,
4098 pub label: String,
4099 pub distance_m: f32,
4100 pub fee_copper: u64,
4101 pub travel_ticks: u64,
4102}
4103
4104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4107pub enum GoodsLocation {
4108 Person,
4110 TownStorage { building_id: String },
4113}
4114
4115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4118pub struct MarketListingView {
4119 pub listing_id: Uuid,
4120 pub seller_character_id: Uuid,
4121 pub seller_label: String,
4123 pub hall_building_id: String,
4124 pub hall_label: String,
4125 pub template_id: String,
4126 pub display_name: String,
4127 #[serde(default)]
4129 pub category: String,
4130 pub quantity: u32,
4131 pub unit_price_copper: u64,
4132 pub line_total_copper: u64,
4134 #[serde(default)]
4136 pub npc_price: bool,
4137 #[serde(default)]
4140 pub npc_dump_unit_copper: Option<u32>,
4141 pub mine: bool,
4143}
4144
4145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4147pub struct MarketListVault {
4148 pub building_id: String,
4149 pub building_label: String,
4151 #[serde(default)]
4152 pub contents: Vec<ItemStack>,
4153}
4154
4155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4158pub struct MarketPanel {
4159 pub npc_id: String,
4160 pub npc_label: String,
4161 pub building_id: String,
4162 pub building_label: String,
4163 pub used_volume: f32,
4165 pub max_volume: f32,
4166 #[serde(default)]
4169 pub listings: Vec<MarketListingView>,
4170 #[serde(default)]
4172 pub tax_bps: u32,
4173 #[serde(default)]
4174 pub tax_flat_copper: u32,
4175 #[serde(default)]
4177 pub list_vaults: Vec<MarketListVault>,
4178}
4179
4180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4181pub struct ShopCatalog {
4182 pub npc_id: String,
4183 pub npc_label: String,
4184 #[serde(default)]
4185 pub sells: Vec<ShopOffer>,
4186 #[serde(default)]
4187 pub buys: Vec<ShopBuyLine>,
4188}
4189
4190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4191pub struct HarvestResult {
4192 pub node_id: String,
4193 pub quantity: u32,
4195 pub item_template: String,
4196 #[serde(default)]
4199 pub item_instance_id: Option<Uuid>,
4200}
4201
4202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4204pub struct Envelope<T> {
4205 pub protocol_version: u16,
4206 pub payload: T,
4207}
4208
4209impl<T> Envelope<T> {
4210 pub fn new(payload: T) -> Self {
4211 Self {
4212 protocol_version: crate::PROTOCOL_VERSION,
4213 payload,
4214 }
4215 }
4216}
4217
4218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4220pub struct Hello {
4221 pub client_name: String,
4222 pub protocol_version: u16,
4223 #[serde(default)]
4224 pub auth: AuthCredential,
4225 #[serde(default)]
4227 pub character_id: Option<Uuid>,
4228}
4229
4230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4233#[serde(rename_all = "snake_case")]
4234pub enum AuthCredential {
4235 DevLocal,
4236 Session { token: String },
4237 ApiToken { token: String, character_id: Uuid },
4238}
4239
4240impl Default for AuthCredential {
4241 fn default() -> Self {
4242 Self::DevLocal
4243 }
4244}
4245
4246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4247pub struct Welcome {
4248 pub session_id: SessionId,
4249 pub entity_id: EntityId,
4250 pub snapshot: Snapshot,
4251}
4252
4253#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4254pub enum ServerMessage {
4255 Welcome(Welcome),
4256 ContentUpdated(Snapshot),
4258 Tick(TickDelta),
4259 IntentAck {
4260 entity_id: EntityId,
4261 seq: Seq,
4262 tick: Tick,
4263 },
4264 Chat(ChatMessage),
4265 HarvestResult(HarvestResult),
4266 UseResult(UseResult),
4267 CraftResult(CraftResult),
4268 Death(DeathNotice),
4269 Interaction(InteractionNotice),
4270 ShopOpened(ShopCatalog),
4271 NpcTalkOpened(NpcTalkOpened),
4272 NpcTalkPending(NpcTalkPending),
4273 NpcTalkReply(NpcTalkReply),
4274 NpcTalkClosed(NpcTalkClosed),
4275 NpcTalkError(NpcTalkError),
4276 QuestOffer(QuestOffer),
4277 QuestAccepted(QuestNotice),
4278 QuestWithdrawn(QuestNotice),
4279 QuestStepCompleted(QuestNotice),
4280 QuestCompleted(QuestNotice),
4281 QuestCatalogUpdated(QuestCatalogUpdated),
4282 BankOpened(BankPanel),
4284 StorageOpened(StoragePanel),
4286 MarketOpened(MarketPanel),
4288 TradeOpened(TradePanel),
4290 TradeClosed {
4292 reason: String,
4293 },
4294 ConnectRejected {
4297 reason: String,
4298 },
4299}
4300
4301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4303pub struct TradePanel {
4304 pub peer_entity_id: EntityId,
4305 pub peer_name: String,
4306 pub my_presented: Vec<ItemStack>,
4307 pub their_presented: Vec<ItemStack>,
4308 pub i_ready: bool,
4309 pub they_ready: bool,
4310 pub my_mass_after: f32,
4312 pub my_mass_max: f32,
4313 pub my_encumbrance_after: EncumbranceState,
4314 pub overburden_warning: bool,
4316}
4317
4318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4319pub enum ClientMessage {
4320 Hello(Hello),
4321 Intent(Intent),
4322 Disconnect,
4323}
4324
4325#[cfg(test)]
4326mod tests {
4327 use super::*;
4328
4329 #[test]
4330 fn pristine_vitals_state_yields_full_pools() {
4331 let attrs = PrimaryAttributes::default();
4332 let vitals = StoredVitalsState::default().apply_to(attrs);
4333 assert!(vitals.health > 0.0);
4334 assert_eq!(vitals.health, vitals.health_max);
4335 assert!((vitals.mana_max - 61.0).abs() < 0.01);
4336 }
4337
4338 #[test]
4339 fn humanize_snake_id_title_cases_parts() {
4340 assert_eq!(humanize_snake_id("heal_touch"), "Heal Touch");
4341 assert_eq!(humanize_snake_id("fireball"), "Fireball");
4342 assert_eq!(humanize_snake_id("cone_frost"), "Cone Frost");
4343 }
4344
4345 #[test]
4346 fn saved_vitals_scale_when_pool_max_increases() {
4347 let mut attrs = PrimaryAttributes::default();
4348 attrs.intelligence = 140;
4349 attrs.wisdom = 140;
4350 let saved = StoredVitalsState {
4351 health: 100.0,
4352 mana: 14.0,
4353 stamina: 100.0,
4354 ..StoredVitalsState::default()
4355 };
4356 let vitals = saved.apply_to(attrs);
4357 assert!(vitals.mana_max > 55.0);
4358 assert!(
4359 (vitals.mana - vitals.mana_max).abs() < 0.01,
4360 "full legacy mana bar migrates to full new bar"
4361 );
4362 }
4363
4364 #[test]
4365 fn empty_vitals_state_is_pristine() {
4366 let pristine = StoredVitalsState {
4367 health: 0.0,
4368 mana: 0.0,
4369 stamina: 0.0,
4370 hunger: 0.0,
4371 thirst: 0.0,
4372 coins: 0,
4373 deaths: 0,
4374 life_state: LifeState::Alive,
4375 };
4376 assert!(pristine.is_pristine());
4377 let vitals = pristine.apply_to(PrimaryAttributes::default());
4378 assert!(vitals.health > 0.0);
4379 }
4380
4381 #[test]
4382 fn stored_vitals_roundtrip_preserves_partial_pools() {
4383 let attrs = PrimaryAttributes::default();
4384 let mut live = PlayerVitals::from_attributes(attrs);
4385 live.health = 25.0;
4386 live.hunger = 77.0;
4387 live.deaths = 2;
4388 let stored = StoredVitalsState::from_live(&live);
4389 let restored = stored.apply_to(attrs);
4390 assert!(
4391 (restored.health - 25.0).abs() < 0.01,
4392 "partial HP below cap stays absolute"
4393 );
4394 assert_eq!(restored.hunger, 77.0);
4395 assert_eq!(restored.deaths, 2);
4396 }
4397
4398 #[test]
4399 fn skill_tiers_start_at_zero() {
4400 let skill = SkillProgress::default();
4401 assert_eq!(skill.level, 0);
4402 assert_eq!(skill.display_tier(), 0);
4403 let trained = SkillProgress {
4404 level: 250,
4405 last_trained_tick: 1,
4406 };
4407 assert_eq!(trained.display_tier(), 2);
4408 }
4409
4410 #[test]
4411 fn quest_server_messages_roundtrip_json() {
4412 use crate::codec::{Codec, PostcardCodec};
4413
4414 let offer = ServerMessage::QuestOffer(QuestOffer {
4415 quest_id: "ada_goblin_hunt".into(),
4416 title: "Goblin Trouble".into(),
4417 description: "Help Ada".into(),
4418 step_count: 3,
4419 });
4420 let notice = ServerMessage::QuestAccepted(QuestNotice {
4421 quest_id: "ada_goblin_hunt".into(),
4422 title: "Goblin Trouble".into(),
4423 message: "Quest accepted".into(),
4424 });
4425 for msg in [offer, notice] {
4426 let bytes = PostcardCodec.encode(&msg).unwrap();
4427 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
4428 assert_eq!(decoded, msg);
4429 }
4430 }
4431
4432 #[test]
4433 fn hotbar_consumable_binding_roundtrips() {
4434 let binding = hotbar_consumable_binding("vegetable_soup");
4435 assert_eq!(binding, "item:vegetable_soup");
4436 assert!(hotbar_binding_is_consumable(&binding));
4437 assert_eq!(
4438 hotbar_consumable_template(&binding),
4439 Some("vegetable_soup")
4440 );
4441 assert!(!hotbar_binding_is_consumable("fireball"));
4442 assert_eq!(hotbar_consumable_template("fireball"), None);
4443 }
4444}