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 ListOnMarket {
2228 template: String,
2229 #[serde(default = "default_true_view")]
2230 list_all: bool,
2231 #[serde(default)]
2232 hall_id: Option<String>,
2233 },
2234 WithdrawFrom {
2235 container_id: String,
2236 items: Vec<WorkerWithdrawItemView>,
2237 },
2238 CraftAt {
2239 device: String,
2240 blueprint: String,
2241 #[serde(default)]
2242 qty: Option<u32>,
2243 },
2244 CultivatePlot {
2245 plot_id: uuid::Uuid,
2246 },
2247 PlantPlot {
2248 plot_id: uuid::Uuid,
2249 seed_template: String,
2250 },
2251 HarvestPlot {
2252 plot_id: uuid::Uuid,
2253 },
2254 RestIfNeeded,
2255 Wait {
2256 #[serde(default)]
2257 wait_ticks: u64,
2258 },
2259}
2260
2261#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2263#[serde(rename_all = "snake_case")]
2264pub enum LedgerCategory {
2265 Workers,
2266 Hire,
2267 Train,
2268 ShopBuy,
2269 Taxes,
2270 WorkerSales,
2271 TraderSales,
2272 BankDeposit,
2273 BankWithdraw,
2274 BankTransferOut,
2275 BankTransferIn,
2276 BankTransferFee,
2277 StorageShipFee,
2278 PropertyBuy,
2280 PropertySell,
2282 TaxShare,
2284 MarketBuy,
2286 MarketSell,
2288 Other,
2289}
2290
2291impl LedgerCategory {
2292 pub fn as_str(self) -> &'static str {
2293 match self {
2294 Self::Workers => "workers",
2295 Self::Hire => "hire",
2296 Self::Train => "train",
2297 Self::ShopBuy => "shop_buy",
2298 Self::Taxes => "taxes",
2299 Self::WorkerSales => "worker_sales",
2300 Self::TraderSales => "trader_sales",
2301 Self::BankDeposit => "bank_deposit",
2302 Self::BankWithdraw => "bank_withdraw",
2303 Self::BankTransferOut => "bank_transfer_out",
2304 Self::BankTransferIn => "bank_transfer_in",
2305 Self::BankTransferFee => "bank_transfer_fee",
2306 Self::StorageShipFee => "storage_ship_fee",
2307 Self::PropertyBuy => "property_buy",
2308 Self::PropertySell => "property_sell",
2309 Self::TaxShare => "tax_share",
2310 Self::MarketBuy => "market_buy",
2311 Self::MarketSell => "market_sell",
2312 Self::Other => "other",
2313 }
2314 }
2315}
2316
2317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2318pub struct LedgerEntryView {
2319 pub id: uuid::Uuid,
2320 pub game_day: u64,
2321 pub signed_copper: i64,
2322 pub category: LedgerCategory,
2323 #[serde(default)]
2324 pub label: String,
2325}
2326
2327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2328pub struct LedgerPeriodTotals {
2329 #[serde(default)]
2331 pub expenses: std::collections::HashMap<String, u64>,
2332 #[serde(default)]
2334 pub income: std::collections::HashMap<String, u64>,
2335 pub expense_copper: u64,
2336 pub income_copper: u64,
2337 pub cash_flow_copper: i64,
2339}
2340
2341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2342pub struct PlayerLedgerView {
2343 pub current_game_day: u64,
2344 #[serde(default)]
2345 pub period_day: LedgerPeriodTotals,
2346 #[serde(default)]
2347 pub period_week: LedgerPeriodTotals,
2348 #[serde(default)]
2349 pub period_month: LedgerPeriodTotals,
2350 #[serde(default)]
2351 pub period_lifetime: LedgerPeriodTotals,
2352 #[serde(default)]
2353 pub recent: Vec<LedgerEntryView>,
2354 #[serde(default)]
2356 pub wealth_on_person_copper: u64,
2357 #[serde(default)]
2359 pub wealth_in_storage_copper: u64,
2360 #[serde(default)]
2362 pub wealth_in_bank_copper: u64,
2363 #[serde(default)]
2365 pub wealth_total_copper: u64,
2366 #[serde(default)]
2368 pub wealth_in_property_copper: u64,
2369 #[serde(default)]
2371 pub wealth_net_worth_copper: u64,
2372 #[serde(default)]
2374 pub property_assets: Vec<PropertyAssetView>,
2375 #[serde(default)]
2377 pub property_market_nearby: Vec<PropertyMarketCompView>,
2378 #[serde(default)]
2380 pub live_expense_per_interval_copper: u64,
2381 #[serde(default)]
2383 pub live_income_route_est_per_loop_copper: u64,
2384 #[serde(default)]
2386 pub live_income_avg_per_interval_copper: u64,
2387 #[serde(default)]
2389 pub live_income_avg_window_intervals: u32,
2390 #[serde(default)]
2392 pub live_net_avg_per_interval_copper: i64,
2393}
2394
2395#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2397pub struct PropertyAssetView {
2398 pub plot_id: Uuid,
2399 pub label: String,
2401 pub zone_id: String,
2402 #[serde(default)]
2403 pub zone_label: Option<String>,
2404 pub area_m2: f32,
2405 pub purchase_basis_copper: u64,
2407 pub upkeep_copper_per_day: u64,
2408}
2409
2410#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2412pub struct PropertyMarketCompView {
2413 pub day: u64,
2414 pub zone_id: String,
2415 #[serde(default)]
2416 pub zone_label: Option<String>,
2417 pub area_m2: f32,
2418 pub price_copper: u64,
2419 pub price_per_m2_copper: u64,
2421 pub kind: String,
2423 pub distance_m: f32,
2425}
2426
2427#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2429#[serde(rename_all = "snake_case")]
2430pub enum AnalyticsMetric {
2431 NpcKill,
2432 WildlifeKill,
2433 Harvest,
2434 QuestComplete,
2435 QuestAccept,
2436 QuestAbandon,
2437 PlayerDeath,
2438 Craft,
2439 WorkerHire,
2440 WorkerDismiss,
2441 WorkerTeach,
2442 NpcTalk,
2443 ShopBuy,
2444 ShopSell,
2445 PlaceContainer,
2446 PickupContainer,
2447 PickupDrop,
2448 ConsumableUse,
2449 AbilityUse,
2450 DistanceWalkedM,
2451 DoorUse,
2452 BuildingEnter,
2453}
2454
2455impl AnalyticsMetric {
2456 pub fn as_str(self) -> &'static str {
2457 match self {
2458 Self::NpcKill => "npc_kill",
2459 Self::WildlifeKill => "wildlife_kill",
2460 Self::Harvest => "harvest",
2461 Self::QuestComplete => "quest_complete",
2462 Self::QuestAccept => "quest_accept",
2463 Self::QuestAbandon => "quest_abandon",
2464 Self::PlayerDeath => "player_death",
2465 Self::Craft => "craft",
2466 Self::WorkerHire => "worker_hire",
2467 Self::WorkerDismiss => "worker_dismiss",
2468 Self::WorkerTeach => "worker_teach",
2469 Self::NpcTalk => "npc_talk",
2470 Self::ShopBuy => "shop_buy",
2471 Self::ShopSell => "shop_sell",
2472 Self::PlaceContainer => "place_container",
2473 Self::PickupContainer => "pickup_container",
2474 Self::PickupDrop => "pickup_drop",
2475 Self::ConsumableUse => "consumable_use",
2476 Self::AbilityUse => "ability_use",
2477 Self::DistanceWalkedM => "distance_walked_m",
2478 Self::DoorUse => "door_use",
2479 Self::BuildingEnter => "building_enter",
2480 }
2481 }
2482
2483 pub fn from_str_key(s: &str) -> Option<Self> {
2484 Some(match s {
2485 "npc_kill" => Self::NpcKill,
2486 "wildlife_kill" => Self::WildlifeKill,
2487 "harvest" => Self::Harvest,
2488 "quest_complete" => Self::QuestComplete,
2489 "quest_accept" => Self::QuestAccept,
2490 "quest_abandon" => Self::QuestAbandon,
2491 "player_death" => Self::PlayerDeath,
2492 "craft" => Self::Craft,
2493 "worker_hire" => Self::WorkerHire,
2494 "worker_dismiss" => Self::WorkerDismiss,
2495 "worker_teach" => Self::WorkerTeach,
2496 "npc_talk" => Self::NpcTalk,
2497 "shop_buy" => Self::ShopBuy,
2498 "shop_sell" => Self::ShopSell,
2499 "place_container" => Self::PlaceContainer,
2500 "pickup_container" => Self::PickupContainer,
2501 "pickup_drop" => Self::PickupDrop,
2502 "consumable_use" => Self::ConsumableUse,
2503 "ability_use" => Self::AbilityUse,
2504 "distance_walked_m" => Self::DistanceWalkedM,
2505 "door_use" => Self::DoorUse,
2506 "building_enter" => Self::BuildingEnter,
2507 _ => return None,
2508 })
2509 }
2510}
2511
2512#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2513pub struct CareerMetricRow {
2514 pub subject_id: String,
2515 pub amount: u64,
2516}
2517
2518#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2520pub struct PlayerCareerView {
2521 pub current_game_day: u64,
2522 #[serde(default)]
2523 pub kills: Vec<CareerMetricRow>,
2524 #[serde(default)]
2525 pub harvests: Vec<CareerMetricRow>,
2526 pub quests_completed: u64,
2527 #[serde(default)]
2528 pub crafts: Vec<CareerMetricRow>,
2529 pub deaths: u64,
2530 pub npc_talks: u64,
2531 pub shop_buys: u64,
2532 pub shop_sells: u64,
2533 pub distance_m: u64,
2534 #[serde(default)]
2535 pub other: Vec<CareerMetricRow>,
2536}
2537
2538#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2543pub struct WorkerEquipmentView {
2544 #[serde(default)]
2545 pub mainhand: Option<ItemStack>,
2546 #[serde(default)]
2547 pub offhand: Option<ItemStack>,
2548 #[serde(default)]
2549 pub worn: Vec<(BodySlot, ItemStack)>,
2550}
2551
2552#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2554pub struct HiredWorkerView {
2555 pub instance_id: String,
2556 pub entity_id: EntityId,
2557 pub def_id: String,
2558 pub label: String,
2560 pub x: f32,
2561 pub y: f32,
2562 pub z: f32,
2563 pub mode: WorkerModeView,
2564 pub state: WorkerStateView,
2565 #[serde(default)]
2566 pub step_label: String,
2567 pub vitals: WorkerVitalsSummary,
2568 #[serde(default)]
2569 pub carry_pct: f32,
2570 #[serde(default)]
2571 pub last_error: Option<String>,
2572 pub wage_copper_per_interval: u32,
2573 #[serde(default)]
2575 pub effective_wage_copper: u32,
2576 #[serde(default)]
2578 pub wage_meters_walked: f32,
2579 #[serde(default)]
2581 pub lodging_container_id: Option<String>,
2582 #[serde(default)]
2584 pub route: Option<WorkerRouteView>,
2585 #[serde(default)]
2588 pub route_stop_index: Option<u32>,
2589 #[serde(default)]
2591 pub known_blueprint_ids: Vec<String>,
2592 #[serde(default = "default_worker_view_level")]
2594 pub level: u32,
2595 #[serde(default)]
2597 pub worker_xp: f64,
2598 #[serde(default)]
2600 pub inventory: Vec<ItemStack>,
2601 #[serde(default)]
2603 pub equipment: WorkerEquipmentView,
2604 #[serde(default)]
2607 pub issue_hint: Option<String>,
2608}
2609
2610fn default_worker_view_level() -> u32 {
2611 1
2612}
2613
2614#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2616pub struct TickDelta {
2617 pub tick: Tick,
2618 pub entities: Vec<EntityState>,
2619 #[serde(default)]
2620 pub resource_nodes: Vec<ResourceNodeView>,
2621 #[serde(default)]
2622 pub buildings: Vec<BuildingView>,
2623 #[serde(default)]
2624 pub doors: Vec<DoorView>,
2625 #[serde(default)]
2626 pub npcs: Vec<NpcView>,
2627 #[serde(default)]
2629 pub inventory: Vec<ItemStack>,
2630 #[serde(default)]
2631 pub blueprints: Vec<BlueprintView>,
2632 #[serde(default)]
2634 pub building_materials: Vec<BuildingMaterialView>,
2635 #[serde(default)]
2636 pub world_clock: WorldClock,
2637 #[serde(default)]
2638 pub ground_drops: Vec<GroundDropView>,
2639 #[serde(default)]
2640 pub placed_containers: Vec<PlacedContainerView>,
2641 #[serde(default)]
2642 pub combat: Option<CombatHud>,
2643 #[serde(default)]
2644 pub interior_map: Option<InteriorMapView>,
2645 #[serde(default)]
2646 pub quest_log: Vec<QuestLogEntry>,
2647 #[serde(default)]
2648 pub hired_workers: Vec<HiredWorkerView>,
2649 #[serde(default)]
2650 pub interactables: Vec<InteractableView>,
2651 #[serde(default)]
2652 pub ledger: Option<PlayerLedgerView>,
2653 #[serde(default)]
2654 pub career: Option<PlayerCareerView>,
2655 #[serde(default)]
2657 pub combat_fx: Vec<CombatFx>,
2658 #[serde(default)]
2660 pub ground_hazards: Vec<GroundHazardView>,
2661 #[serde(default)]
2663 pub property_plots: Vec<PropertyPlotView>,
2664 #[serde(default)]
2666 pub terrain_overlays: Vec<TerrainZoneView>,
2667}
2668#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2669pub struct GroundDropView {
2670 pub id: String,
2671 pub template_id: String,
2672 pub quantity: u32,
2673 pub x: f32,
2674 pub y: f32,
2675 pub z: f32,
2676 #[serde(default)]
2678 pub tile_id: Option<String>,
2679 #[serde(default)]
2681 pub display_name: Option<String>,
2682 #[serde(default)]
2684 pub yaw: f32,
2685 #[serde(default)]
2687 pub pitch: f32,
2688 #[serde(default)]
2690 pub roll: f32,
2691 #[serde(default = "default_draw_scale")]
2693 pub draw_scale: f32,
2694 #[serde(default)]
2696 pub item_instance_id: Option<Uuid>,
2697 #[serde(default)]
2699 pub props: std::collections::BTreeMap<String, String>,
2700 #[serde(default)]
2702 pub status_bindings: Vec<ItemStatusBinding>,
2703}
2704
2705fn default_draw_scale() -> f32 {
2706 1.0
2707}
2708
2709#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2711pub struct Snapshot {
2712 pub tick: Tick,
2713 pub chunk_rev: u64,
2714 #[serde(default)]
2716 pub content_rev: u64,
2717 #[serde(default)]
2719 pub publish_rev: u64,
2720 pub entities: Vec<EntityState>,
2721 #[serde(default)]
2722 pub resource_nodes: Vec<ResourceNodeView>,
2723 #[serde(default)]
2725 pub world_x0: f32,
2726 #[serde(default)]
2727 pub world_y0: f32,
2728 #[serde(default)]
2730 pub world_width_m: f32,
2731 #[serde(default)]
2732 pub world_height_m: f32,
2733 #[serde(default)]
2734 pub buildings: Vec<BuildingView>,
2735 #[serde(default)]
2736 pub doors: Vec<DoorView>,
2737 #[serde(default)]
2738 pub npcs: Vec<NpcView>,
2739 #[serde(default)]
2740 pub inventory: Vec<ItemStack>,
2741 #[serde(default)]
2742 pub blueprints: Vec<BlueprintView>,
2743 #[serde(default)]
2745 pub building_materials: Vec<BuildingMaterialView>,
2746 #[serde(default)]
2747 pub world_clock: WorldClock,
2748 #[serde(default)]
2749 pub terrain_zones: Vec<TerrainZoneView>,
2750 #[serde(default)]
2751 pub z_platforms: Vec<ZPlatformView>,
2752 #[serde(default)]
2753 pub z_transitions: Vec<ZTransitionView>,
2754 #[serde(default)]
2755 pub ground_drops: Vec<GroundDropView>,
2756 #[serde(default)]
2757 pub placed_containers: Vec<PlacedContainerView>,
2758 #[serde(default)]
2759 pub combat: Option<CombatHud>,
2760 #[serde(default)]
2761 pub interior_map: Option<InteriorMapView>,
2762 #[serde(default)]
2763 pub quest_log: Vec<QuestLogEntry>,
2764 #[serde(default)]
2765 pub hired_workers: Vec<HiredWorkerView>,
2766 #[serde(default)]
2767 pub interactables: Vec<InteractableView>,
2768 #[serde(default)]
2769 pub ledger: Option<PlayerLedgerView>,
2770 #[serde(default)]
2771 pub career: Option<PlayerCareerView>,
2772 #[serde(default)]
2774 pub combat_fx: Vec<CombatFx>,
2775 #[serde(default)]
2777 pub ground_hazards: Vec<GroundHazardView>,
2778 #[serde(default)]
2780 pub property_zones: Vec<PropertyZoneView>,
2781 #[serde(default)]
2783 pub tax_zones: Vec<TaxZoneView>,
2784 #[serde(default)]
2786 pub boundary_zones: Vec<BoundaryZoneView>,
2787 #[serde(default)]
2789 pub encounter_zones: Vec<EncounterZoneView>,
2790 #[serde(default)]
2792 pub growth_zones: Vec<GrowthZoneView>,
2793 #[serde(default)]
2795 pub biome_zones: Vec<BiomeZoneView>,
2796 #[serde(default)]
2798 pub terrain_kind_nav: Vec<TerrainKindNavView>,
2799 #[serde(default)]
2801 pub property_plots: Vec<PropertyPlotView>,
2802 #[serde(default)]
2804 pub property_plot_settings: Option<PropertyPlotSettingsView>,
2805 #[serde(default)]
2808 pub item_catalog: Vec<ItemCatalogEntryView>,
2809}
2810
2811#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2813pub struct ItemCatalogEntryView {
2814 pub template_id: String,
2815 #[serde(default)]
2816 pub display_name: String,
2817 #[serde(default)]
2818 pub category: String,
2819 #[serde(default)]
2821 pub seed_for: Option<String>,
2822}
2823
2824impl ItemCatalogEntryView {
2825 pub fn is_harvest_node(&self) -> bool {
2826 self.category == "harvest_node"
2827 }
2828
2829 pub fn is_depositable_stack(&self) -> bool {
2831 !self.is_harvest_node()
2832 }
2833
2834 pub fn is_farm_seed(&self) -> bool {
2835 self.seed_for
2836 .as_deref()
2837 .is_some_and(|s| !s.trim().is_empty())
2838 || self.category == "seed"
2839 }
2840}
2841
2842#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2844pub struct ResourceNodeView {
2845 pub id: String,
2846 pub label: String,
2847 pub x: f32,
2848 pub y: f32,
2849 pub z: f32,
2850 pub item_template: String,
2851 #[serde(default = "default_node_state")]
2852 pub state: ResourceNodeState,
2853 #[serde(default = "default_blocking_view")]
2855 pub blocking: bool,
2856 #[serde(default = "default_blocking_radius_view")]
2858 pub blocking_radius_m: f32,
2859 #[serde(default)]
2861 pub harvest_off: bool,
2862 #[serde(default)]
2864 pub tile_id: Option<String>,
2865 #[serde(default)]
2867 pub yaw: f32,
2868 #[serde(default)]
2870 pub pitch: f32,
2871 #[serde(default)]
2873 pub roll: f32,
2874 #[serde(default = "default_draw_scale")]
2876 pub draw_scale: f32,
2877 #[serde(default)]
2879 pub sprite_mode: Option<String>,
2880 #[serde(default)]
2882 pub presentation_state: Option<String>,
2883 #[serde(default)]
2886 pub growth_progress: Option<f32>,
2887 #[serde(default)]
2889 pub channel_start_tick: Option<Tick>,
2890 #[serde(default)]
2891 pub channel_end_tick: Option<Tick>,
2892 #[serde(default)]
2894 pub harvest_drop_templates: Vec<String>,
2895}
2896
2897fn default_blocking_radius_view() -> f32 {
2898 0.8
2899}
2900
2901fn default_blocking_view() -> bool {
2902 true
2903}
2904
2905#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2906#[serde(rename_all = "snake_case")]
2907pub enum ResourceNodeState {
2908 Available,
2909 Harvesting,
2910 Cooldown,
2911}
2912fn default_node_state() -> ResourceNodeState {
2913 ResourceNodeState::Available
2914}
2915
2916#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2918#[serde(rename_all = "snake_case")]
2919pub enum ItemSpawnStateView {
2920 Spawned,
2921 PickedUp { respawn_at_tick: u64 },
2922 Consumed,
2923}
2924
2925#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2927pub struct ItemSpawnView {
2928 pub id: String,
2929 pub label: String,
2930 pub item_template: String,
2931 pub quantity: u32,
2932 pub x: f32,
2933 pub y: f32,
2934 pub z: f32,
2935 pub respawn_ticks: u32,
2936 #[serde(default)]
2937 pub building_id: Option<String>,
2938 pub state: ItemSpawnStateView,
2939 #[serde(default)]
2941 pub once_per_character: bool,
2942 #[serde(default)]
2944 pub collected_count: u32,
2945}
2946
2947#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2948#[serde(rename_all = "snake_case")]
2949pub enum ItemStatusBindingMode {
2950 OnHit,
2951 WhileEquipped,
2952}
2953
2954impl Default for ItemStatusBindingMode {
2955 fn default() -> Self {
2956 Self::OnHit
2957 }
2958}
2959
2960#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2962pub struct ItemStatusBinding {
2963 pub effect_id: String,
2964 #[serde(default)]
2965 pub mode: ItemStatusBindingMode,
2966 #[serde(default)]
2968 pub source: String,
2969 #[serde(default)]
2970 pub applied_at_tick: u64,
2971 #[serde(default)]
2974 pub expires_at_tick: Option<u64>,
2975}
2976
2977#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2978pub struct ItemStack {
2979 pub template_id: String,
2980 pub quantity: u32,
2981 #[serde(default)]
2983 pub item_instance_id: Option<Uuid>,
2984 #[serde(default)]
2986 pub props: BTreeMap<String, String>,
2987 #[serde(default)]
2989 pub status_bindings: Vec<ItemStatusBinding>,
2990 #[serde(default)]
2992 pub contents: Vec<ItemStack>,
2993 #[serde(default)]
2995 pub display_name: Option<String>,
2996 #[serde(default)]
2998 pub category: Option<String>,
2999 #[serde(default)]
3001 pub base_mass: Option<f32>,
3002 #[serde(default)]
3004 pub base_volume: Option<f32>,
3005 #[serde(default)]
3007 pub capacity_volume: Option<f32>,
3008 #[serde(default)]
3010 pub stackable: Option<bool>,
3011 #[serde(default)]
3013 pub world_placeable: Option<bool>,
3014 #[serde(default)]
3016 pub worker_lodging_capacity: Option<u32>,
3017 #[serde(default)]
3019 pub equip_slot: Option<BodySlot>,
3020 #[serde(default)]
3022 pub armor_physical: Option<f32>,
3023 #[serde(default)]
3025 pub resists: Vec<(String, f32)>,
3026 #[serde(default)]
3028 pub hand_slots: Option<u8>,
3029 #[serde(default)]
3031 pub listable: Option<bool>,
3032 #[serde(default)]
3034 pub base_value_copper: Option<u32>,
3035}
3036
3037impl ItemStack {
3038 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
3039 Self {
3040 template_id: template_id.into(),
3041 quantity,
3042 ..Default::default()
3043 }
3044 }
3045}
3046
3047impl Default for ItemStack {
3048 fn default() -> Self {
3049 Self {
3050 template_id: String::new(),
3051 quantity: 0,
3052 item_instance_id: None,
3053 props: BTreeMap::new(),
3054 status_bindings: Vec::new(),
3055 contents: Vec::new(),
3056 display_name: None,
3057 category: None,
3058 base_mass: None,
3059 base_volume: None,
3060 capacity_volume: None,
3061 stackable: None,
3062 world_placeable: None,
3063 worker_lodging_capacity: None,
3064 equip_slot: None,
3065 armor_physical: None,
3066 resists: Vec::new(),
3067 hand_slots: None,
3068 listable: None,
3069 base_value_copper: None,
3070 }
3071 }
3072}
3073
3074#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3076#[serde(rename_all = "snake_case")]
3077pub enum EncumbranceState {
3078 #[default]
3079 Light,
3080 Heavy,
3081 Orange,
3082 Over,
3083}
3084
3085impl EncumbranceState {
3086 pub fn label(self) -> &'static str {
3088 match self {
3089 Self::Light => "Light",
3090 Self::Heavy => "Heavy",
3091 Self::Orange => "Overloaded",
3092 Self::Over => "Over",
3093 }
3094 }
3095
3096 pub fn allows_sprint(self) -> bool {
3098 !matches!(self, Self::Over)
3099 }
3100}
3101
3102#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
3106#[serde(rename_all = "snake_case")]
3107pub enum BodySlot {
3108 Head,
3109 #[serde(alias = "body")]
3111 Chest,
3112 #[serde(alias = "arms")]
3114 Forearms,
3115 Legs,
3116 Feet,
3117 Cloak,
3118 Back,
3119 Waist,
3120 Earrings,
3121 Necklace,
3122 Eyeglasses,
3123 #[serde(rename = "ring_left_1", alias = "ring_left1")]
3125 RingLeft1,
3126 #[serde(rename = "ring_left_2", alias = "ring_left2")]
3127 RingLeft2,
3128 #[serde(rename = "ring_right_1", alias = "ring_right1")]
3129 RingRight1,
3130 #[serde(rename = "ring_right_2", alias = "ring_right2")]
3131 RingRight2,
3132}
3133
3134impl BodySlot {
3135 pub const ALL: [BodySlot; 15] = [
3137 BodySlot::Head,
3138 BodySlot::Chest,
3139 BodySlot::Forearms,
3140 BodySlot::Legs,
3141 BodySlot::Feet,
3142 BodySlot::Cloak,
3143 BodySlot::Back,
3144 BodySlot::Waist,
3145 BodySlot::Earrings,
3146 BodySlot::Necklace,
3147 BodySlot::Eyeglasses,
3148 BodySlot::RingLeft1,
3149 BodySlot::RingLeft2,
3150 BodySlot::RingRight1,
3151 BodySlot::RingRight2,
3152 ];
3153
3154 pub fn as_str(self) -> &'static str {
3155 match self {
3156 BodySlot::Head => "head",
3157 BodySlot::Chest => "chest",
3158 BodySlot::Forearms => "forearms",
3159 BodySlot::Legs => "legs",
3160 BodySlot::Feet => "feet",
3161 BodySlot::Cloak => "cloak",
3162 BodySlot::Back => "back",
3163 BodySlot::Waist => "waist",
3164 BodySlot::Earrings => "earrings",
3165 BodySlot::Necklace => "necklace",
3166 BodySlot::Eyeglasses => "eyeglasses",
3167 BodySlot::RingLeft1 => "ring_left_1",
3168 BodySlot::RingLeft2 => "ring_left_2",
3169 BodySlot::RingRight1 => "ring_right_1",
3170 BodySlot::RingRight2 => "ring_right_2",
3171 }
3172 }
3173}
3174
3175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3177#[serde(rename_all = "snake_case")]
3178pub enum InventoryLocation {
3179 Root,
3181 Worn { slot: BodySlot },
3183 Placed { container_id: String },
3185 Keychain,
3187 WhisperPouch,
3189}
3190
3191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3193pub struct PlacedContainerView {
3194 pub id: String,
3195 pub template_id: String,
3196 pub display_name: String,
3197 pub x: f32,
3198 pub y: f32,
3199 pub z: f32,
3200 pub locked: bool,
3201 #[serde(default)]
3203 pub accessible: bool,
3204 #[serde(default)]
3205 pub owner_character_id: Option<Uuid>,
3206 #[serde(default)]
3208 pub contents: Vec<ItemStack>,
3209 #[serde(default)]
3211 pub lock_id: Option<String>,
3212 #[serde(default)]
3214 pub capacity_volume: Option<f32>,
3215 #[serde(default)]
3217 pub item_instance_id: Option<Uuid>,
3218 #[serde(default)]
3220 pub tile_id: Option<String>,
3221 #[serde(default)]
3223 pub worker_lodging_capacity: Option<u32>,
3224 #[serde(default)]
3226 pub blocking: bool,
3227 #[serde(default)]
3229 pub blocking_radius_m: f32,
3230 #[serde(default)]
3233 pub building_id: Option<String>,
3234}
3235
3236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3237pub struct BlueprintIngredientView {
3238 pub template_id: String,
3239 pub quantity: u32,
3240 pub consumed: bool,
3242 #[serde(default)]
3244 pub display_name: String,
3245}
3246
3247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3248pub struct ToolRequirementView {
3249 pub item: String,
3250 pub consumed: bool,
3252 #[serde(default)]
3254 pub display_name: String,
3255}
3256
3257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3258pub struct SkillRequirementView {
3259 pub skill: String,
3260 pub level: u32,
3261}
3262
3263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3264pub struct BlueprintView {
3265 pub id: String,
3266 pub label: String,
3267 pub output: String,
3268 pub output_qty: u32,
3269 pub craft_ticks: u32,
3270 pub inputs: Vec<BlueprintIngredientView>,
3271 #[serde(default)]
3273 pub station: Option<String>,
3274 #[serde(default)]
3275 pub category: Option<String>,
3276 #[serde(default)]
3277 pub required_tools: Vec<ToolRequirementView>,
3278 #[serde(default)]
3279 pub skill: Option<SkillRequirementView>,
3280 #[serde(default)]
3281 pub failure_chance: f32,
3282 #[serde(default)]
3284 pub worker_train_copper: u64,
3285 #[serde(default)]
3287 pub output_display_name: String,
3288 #[serde(default)]
3290 pub craft_tier: u32,
3291}
3292
3293#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3295pub struct TerrainKindNavView {
3296 pub kind: TerrainKindView,
3297 #[serde(default = "default_move_speed_mult_one")]
3298 pub move_speed_mult: f32,
3299 #[serde(default)]
3300 pub impassable: bool,
3301}
3302
3303fn default_move_speed_mult_one() -> f32 {
3304 1.0
3305}
3306
3307#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3309#[serde(rename_all = "snake_case")]
3310pub enum TerrainKindView {
3311 #[default]
3312 Grass,
3313 Dirt,
3314 Tilled,
3315 Desert,
3316 Hill,
3317 Bog,
3318 Beach,
3319 ShallowWater,
3320 DeepWater,
3321 Trail,
3322 Road,
3323 Rock,
3324}
3325
3326#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3327pub struct TerrainZoneView {
3328 pub id: String,
3329 pub x0: f32,
3330 pub y0: f32,
3331 pub x1: f32,
3332 pub y1: f32,
3333 #[serde(default)]
3334 pub kind: TerrainKindView,
3335 #[serde(default)]
3337 pub elevation: f32,
3338 #[serde(default)]
3341 pub glyph: Option<String>,
3342 #[serde(default)]
3344 pub color: Option<String>,
3345 #[serde(default)]
3347 pub tile_id: Option<String>,
3348 #[serde(default)]
3350 pub z_order: i32,
3351 #[serde(default)]
3353 pub channel_start_tick: Option<Tick>,
3354 #[serde(default)]
3355 pub channel_end_tick: Option<Tick>,
3356}
3357
3358#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3360pub struct ZoneRectView {
3361 pub x0: f32,
3362 pub y0: f32,
3363 pub x1: f32,
3364 pub y1: f32,
3365}
3366
3367#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3369pub struct PropertyZoneView {
3370 pub id: String,
3371 #[serde(default)]
3373 pub label: Option<String>,
3374 pub rects: Vec<ZoneRectView>,
3375 #[serde(default)]
3376 pub z_order: i32,
3377 pub crown_price_copper: u64,
3378 pub upkeep_copper_per_day: u64,
3379 #[serde(default)]
3380 pub max_area_m2: Option<f32>,
3381 #[serde(default)]
3382 pub owner_tax_discount_bps: u32,
3383}
3384
3385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3387pub struct TaxZoneView {
3388 pub id: String,
3389 #[serde(default)]
3390 pub label: Option<String>,
3391 pub rects: Vec<ZoneRectView>,
3392 #[serde(default)]
3393 pub z_order: i32,
3394 pub rate_bps: u32,
3395 #[serde(default)]
3396 pub flat_copper: u64,
3397 #[serde(default)]
3399 pub market_sales_tax_bps: u32,
3400 #[serde(default)]
3402 pub market_sales_flat_copper: u32,
3403}
3404
3405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3407pub struct BoundaryZoneView {
3408 pub id: String,
3409 #[serde(default)]
3410 pub label: Option<String>,
3411 pub rects: Vec<ZoneRectView>,
3412 #[serde(default)]
3413 pub z_order: i32,
3414 #[serde(default, skip_serializing_if = "Option::is_none")]
3415 pub jurisdiction_id: Option<String>,
3416 #[serde(default = "default_true")]
3417 pub worker_logistics: bool,
3418 #[serde(default)]
3419 pub security_tier: String,
3420 #[serde(default)]
3421 pub pvp_mode: String,
3422 #[serde(default = "default_true")]
3423 pub crime_enabled: bool,
3424 #[serde(default)]
3425 pub guard_response: bool,
3426 #[serde(default)]
3428 pub pass_through_props: bool,
3429 #[serde(default)]
3431 pub presence_mode: String,
3432}
3433
3434#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3436pub struct EncounterZoneView {
3437 pub id: String,
3438 #[serde(default)]
3439 pub label: Option<String>,
3440 pub rects: Vec<ZoneRectView>,
3441 #[serde(default)]
3442 pub z_order: i32,
3443}
3444
3445fn default_true() -> bool {
3446 true
3447}
3448
3449#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3451pub struct GrowthZoneView {
3452 pub id: String,
3453 #[serde(default)]
3454 pub label: Option<String>,
3455 pub rects: Vec<ZoneRectView>,
3456 #[serde(default)]
3457 pub z_order: i32,
3458 #[serde(default = "default_one_f32")]
3459 pub fertility: f32,
3460}
3461
3462fn default_one_f32() -> f32 {
3463 1.0
3464}
3465
3466#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3468pub struct BiomeZoneView {
3469 pub id: String,
3470 #[serde(default)]
3471 pub label: Option<String>,
3472 pub rects: Vec<ZoneRectView>,
3473 #[serde(default)]
3474 pub z_order: i32,
3475 pub biome_id: String,
3476}
3477
3478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3480pub struct FarmGrantView {
3481 pub character_id: Uuid,
3482 #[serde(default)]
3484 pub character_label: String,
3485 pub tax_discount_bps: u32,
3486}
3487
3488#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3490pub struct PropertyPlotView {
3491 pub plot_id: Uuid,
3492 pub property_zone_id: String,
3493 #[serde(default)]
3494 pub zone_label: Option<String>,
3495 pub deed_instance_id: Uuid,
3496 pub x0: f32,
3497 pub y0: f32,
3498 pub x1: f32,
3499 pub y1: f32,
3500 pub upkeep_copper_per_day: u64,
3501 pub arrears_days: u32,
3502 #[serde(default)]
3504 pub is_mine: bool,
3505 #[serde(default)]
3507 pub may_farm: bool,
3508 #[serde(default)]
3510 pub purchase_basis_copper: u64,
3511 #[serde(default)]
3512 pub farm_public: bool,
3513 #[serde(default)]
3514 pub public_tax_discount_bps: u32,
3515 #[serde(default)]
3516 pub farm_allow: Vec<FarmGrantView>,
3517 #[serde(default)]
3519 pub owner_character_id: Option<Uuid>,
3520 #[serde(default)]
3521 pub owner_label: Option<String>,
3522 #[serde(default)]
3524 pub building_id: Option<String>,
3525 #[serde(default)]
3527 pub plot_code: String,
3528 #[serde(default)]
3530 pub label: String,
3531}
3532
3533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3535pub struct PropertyPlotSettingsView {
3536 pub min_plot_area_m2: f32,
3537 pub tax_premium_weight: f32,
3538 pub sellback_bps: u32,
3539}
3540
3541#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3543pub struct ZPlatformView {
3544 pub id: String,
3545 pub z: f32,
3546 pub x0: f32,
3547 pub y0: f32,
3548 pub x1: f32,
3549 pub y1: f32,
3550}
3551
3552#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3554pub struct ZTransitionView {
3555 pub id: String,
3556 pub z_from: f32,
3557 pub z_to: f32,
3558 pub x0: f32,
3559 pub y0: f32,
3560 pub x1: f32,
3561 pub y1: f32,
3562}
3563
3564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3565pub struct BuildingView {
3566 pub id: String,
3567 pub label: String,
3568 pub x: f32,
3569 pub y: f32,
3570 pub width_m: f32,
3571 pub depth_m: f32,
3572 #[serde(default)]
3573 pub interior_blueprint: Option<String>,
3574 #[serde(default)]
3575 pub tags: Vec<String>,
3576 #[serde(default)]
3578 pub market_boundary_zone_ids: Vec<String>,
3579 #[serde(default)]
3581 pub market_max_volume: Option<f32>,
3582 #[serde(default)]
3585 pub wall_set: Option<String>,
3586 #[serde(default)]
3588 pub roof_set: Option<String>,
3589}
3590
3591pub const DEFAULT_BUILDING_ART_SET: &str = "classic_stone";
3594
3595impl BuildingView {
3596 pub fn effective_wall_set(&self) -> &str {
3597 self.wall_set
3598 .as_deref()
3599 .filter(|s| !s.is_empty())
3600 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3601 }
3602
3603 pub fn effective_roof_set(&self) -> &str {
3604 self.roof_set
3605 .as_deref()
3606 .filter(|s| !s.is_empty())
3607 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3608 }
3609}
3610
3611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3612pub struct DoorView {
3613 pub id: String,
3614 pub building_id: String,
3615 pub x: f32,
3616 pub y: f32,
3617 #[serde(default)]
3618 pub open: bool,
3619 #[serde(default)]
3620 pub portal: Option<String>,
3621 #[serde(default)]
3624 pub locked: bool,
3625 #[serde(default = "default_door_accessible")]
3627 pub accessible: bool,
3628 #[serde(default)]
3629 pub lock_id: Option<Uuid>,
3630}
3631
3632fn default_door_accessible() -> bool {
3633 true
3634}
3635
3636#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3638pub struct InteriorRoomEdit {
3639 pub id: String,
3640 pub label: String,
3641 pub x0: f32,
3642 pub y0: f32,
3643 pub x1: f32,
3644 pub y1: f32,
3645}
3646
3647#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3648pub struct InteriorRoomDoorEdit {
3649 pub id: String,
3650 pub room_a: String,
3651 pub room_b: String,
3652 pub x: f32,
3653 pub y: f32,
3654}
3655
3656#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3657pub struct InteriorRoomView {
3658 pub id: String,
3659 pub label: String,
3660 pub floor: i32,
3661 pub x0: f32,
3662 pub y0: f32,
3663 pub x1: f32,
3664 pub y1: f32,
3665 #[serde(default)]
3666 pub floor_color: Option<String>,
3667 #[serde(default)]
3668 pub floor_glyph: Option<String>,
3669}
3670
3671#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3672pub struct InteriorDoorView {
3673 pub id: String,
3674 pub room_a: String,
3675 pub room_b: String,
3676 pub x: f32,
3677 pub y: f32,
3678 pub kind: String,
3679 #[serde(default)]
3680 pub x_a: Option<f32>,
3681 #[serde(default)]
3682 pub y_a: Option<f32>,
3683 #[serde(default)]
3684 pub x_b: Option<f32>,
3685 #[serde(default)]
3686 pub y_b: Option<f32>,
3687}
3688
3689#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3690pub struct InteriorMapView {
3691 pub building_id: String,
3692 pub blueprint_id: String,
3693 pub background_color: String,
3694 #[serde(default)]
3695 pub default_floor_color: Option<String>,
3696 #[serde(default = "default_floor_height_view")]
3697 pub floor_height_m: f32,
3698 #[serde(default)]
3700 pub z_platforms: Vec<ZPlatformView>,
3701 #[serde(default)]
3702 pub z_transitions: Vec<ZTransitionView>,
3703 pub rooms: Vec<InteriorRoomView>,
3704 #[serde(default)]
3705 pub room_doors: Vec<InteriorDoorView>,
3706}
3707
3708fn default_floor_height_view() -> f32 {
3709 3.0
3710}
3711
3712#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3713pub struct NpcView {
3714 pub id: String,
3715 pub label: String,
3716 pub role: String,
3717 pub x: f32,
3718 pub y: f32,
3719 #[serde(default)]
3721 pub building_id: Option<String>,
3722 #[serde(default)]
3724 pub entity_id: Option<EntityId>,
3725 #[serde(default)]
3726 pub life_state: Option<LifeState>,
3727 #[serde(default)]
3728 pub hp_pct: Option<f32>,
3729 #[serde(default)]
3731 pub can_trade: bool,
3732 #[serde(default)]
3734 pub buy_templates: Vec<String>,
3735 #[serde(default)]
3737 pub tile_id: Option<String>,
3738 #[serde(default)]
3740 pub behavior_state: Option<String>,
3741 #[serde(default)]
3743 pub presentation_state: Option<String>,
3744 #[serde(default)]
3746 pub sprite_mode: Option<String>,
3747 #[serde(default)]
3749 pub paperdoll_ref: Option<String>,
3750 #[serde(default = "default_draw_scale")]
3752 pub draw_scale: f32,
3753 #[serde(default)]
3755 pub yaw: Option<f32>,
3756 #[serde(default)]
3758 pub perception_fov_deg: Option<f32>,
3759 #[serde(default)]
3761 pub perception_sight_m: Option<f32>,
3762 #[serde(default)]
3764 pub perception_hear_m: Option<f32>,
3765 #[serde(default)]
3767 pub quest_verbs: Vec<NpcQuestVerb>,
3768}
3769
3770#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3772pub struct NpcQuestVerb {
3773 pub quest_id: String,
3774 pub label: String,
3776 pub kind: String,
3777}
3778
3779impl NpcQuestVerb {
3780 pub const KIND_OFFER: &'static str = "offer";
3781 pub const KIND_TALK: &'static str = "talk";
3782 pub const KIND_GIVE: &'static str = "give";
3783}
3784
3785#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3786pub struct UseResult {
3787 pub template_id: String,
3788 pub hunger_restored: f32,
3789 pub thirst_restored: f32,
3790 #[serde(default)]
3791 pub health_restored: f32,
3792 #[serde(default)]
3793 pub mana_restored: f32,
3794 #[serde(default)]
3795 pub cleared_dot_ids: Vec<String>,
3796}
3797
3798#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3799pub struct CraftResult {
3800 pub blueprint_id: String,
3801 pub outputs: Vec<ItemStack>,
3802 pub consumed: Vec<ItemStack>,
3803 #[serde(default = "default_one")]
3805 pub batch_index: u32,
3806 #[serde(default = "default_one")]
3808 pub batch_total: u32,
3809}
3810
3811fn default_one() -> u32 {
3812 1
3813}
3814
3815#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3816pub struct DeathNotice {
3817 pub entity_id: EntityId,
3818 pub respawn_x: f32,
3819 pub respawn_y: f32,
3820 pub message: String,
3821}
3822
3823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3824pub struct InteractionNotice {
3825 pub target_id: String,
3826 pub message: String,
3827 #[serde(default)]
3828 pub coins_delta: i32,
3829 #[serde(default)]
3830 pub inventory_delta: Vec<ItemStack>,
3831}
3832
3833#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3834#[serde(rename_all = "snake_case")]
3835pub enum NpcTalkTrustFlag {
3836 Stranger,
3837 Acquainted,
3838 Trusted,
3839}
3840
3841#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3842#[serde(rename_all = "snake_case")]
3843pub enum NpcTalkDepth {
3844 #[default]
3845 Full,
3846 Brief,
3847 Unavailable,
3848}
3849
3850#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3851pub struct NpcTalkOpened {
3852 pub npc_id: String,
3853 pub npc_label: String,
3854 pub greeting: String,
3855 pub trust_flag: NpcTalkTrustFlag,
3856 #[serde(default)]
3857 pub talk_depth: NpcTalkDepth,
3858 #[serde(default = "default_true")]
3859 pub trade_allowed: bool,
3860 #[serde(default)]
3862 pub suggested_topics: Vec<String>,
3863}
3864
3865#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3866pub struct NpcTalkPending {
3867 pub npc_id: String,
3868}
3869
3870#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3871pub struct NpcTalkReply {
3872 pub npc_id: String,
3873 pub line: String,
3874 pub trust_flag: NpcTalkTrustFlag,
3875 #[serde(default)]
3876 pub wind_down: bool,
3877 #[serde(default)]
3878 pub trade_disabled: bool,
3879}
3880
3881#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3882pub struct NpcTalkClosed {
3883 pub npc_id: String,
3884}
3885
3886#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3887pub struct NpcTalkError {
3888 pub npc_id: String,
3889 pub reason: String,
3890}
3891
3892#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3893#[serde(rename_all = "snake_case")]
3894pub enum QuestStatusView {
3895 Available,
3896 Active,
3897 Completed,
3898}
3899
3900#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3901pub struct QuestObjectiveProgress {
3902 pub label: String,
3903 pub current: u32,
3904 pub required: u32,
3905 pub done: bool,
3906 #[serde(default)]
3910 pub kind: String,
3911 #[serde(default)]
3912 pub npc_ref: Option<String>,
3913 #[serde(default)]
3914 pub item_template: Option<String>,
3915 #[serde(default)]
3916 pub blueprint_id: Option<String>,
3917 #[serde(default)]
3918 pub building_id: Option<String>,
3919}
3920
3921#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3923pub struct QuestRewardItemView {
3924 pub template_id: String,
3925 #[serde(default)]
3927 pub display_name: String,
3928 pub quantity: u32,
3929}
3930
3931#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3933pub struct QuestRewardView {
3934 #[serde(default)]
3935 pub coins: u32,
3936 #[serde(default)]
3937 pub items: Vec<QuestRewardItemView>,
3938}
3939
3940impl QuestRewardView {
3941 pub fn is_empty(&self) -> bool {
3942 self.coins == 0 && self.items.is_empty()
3943 }
3944}
3945
3946#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3947#[serde(rename_all = "snake_case")]
3948pub enum QuestStepStatusView {
3949 #[default]
3950 Pending,
3951 Current,
3952 Done,
3953}
3954
3955#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3957pub struct QuestStepView {
3958 pub id: String,
3959 pub title: String,
3960 #[serde(default)]
3961 pub status: QuestStepStatusView,
3962 #[serde(default)]
3963 pub reward: QuestRewardView,
3964}
3965
3966#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3967pub struct QuestLogEntry {
3968 pub quest_id: String,
3969 pub title: String,
3970 pub description: String,
3971 pub status: QuestStatusView,
3972 #[serde(default)]
3973 pub current_step_id: Option<String>,
3974 #[serde(default)]
3975 pub current_step_title: String,
3976 #[serde(default)]
3977 pub current_step_index: u32,
3978 #[serde(default)]
3979 pub objectives: Vec<QuestObjectiveProgress>,
3980 #[serde(default)]
3982 pub current_step_reward: QuestRewardView,
3983 #[serde(default)]
3985 pub completion_reward: QuestRewardView,
3986 #[serde(default)]
3988 pub steps: Vec<QuestStepView>,
3989 #[serde(default)]
3990 pub is_tracked: bool,
3991 #[serde(default)]
3992 pub can_withdraw: bool,
3993}
3994
3995#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3996pub struct InteractableView {
3997 pub id: String,
3998 pub kind: String,
3999 pub label: String,
4000 pub x: f32,
4001 pub y: f32,
4002 pub z: f32,
4003 #[serde(default)]
4004 pub board_id: Option<String>,
4005}
4006
4007#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4008pub struct QuestOffer {
4009 pub quest_id: String,
4010 pub title: String,
4011 pub description: String,
4012 #[serde(default)]
4013 pub step_count: u32,
4014}
4015
4016#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4017pub struct QuestCatalogEntry {
4018 pub quest_id: String,
4019 pub title: String,
4020 pub description: String,
4021 pub step_count: u32,
4022 #[serde(default)]
4023 pub board_ids: Vec<String>,
4024}
4025
4026#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4027pub struct QuestCatalogUpdated {
4028 pub revision: u64,
4029 pub game_day: String,
4030 #[serde(default)]
4031 pub accepted: Vec<QuestCatalogEntry>,
4032 #[serde(default)]
4033 pub retired: Vec<String>,
4034}
4035
4036#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4037pub struct QuestNotice {
4038 pub quest_id: String,
4039 pub title: String,
4040 pub message: String,
4041}
4042
4043#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4044#[serde(rename_all = "snake_case")]
4045pub enum ShopOfferKind {
4046 Item,
4047 Blueprint,
4048}
4049
4050#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4051pub struct ShopOffer {
4052 pub offer_id: String,
4053 pub kind: ShopOfferKind,
4054 pub label: String,
4055 #[serde(default)]
4056 pub template_id: Option<String>,
4057 #[serde(default)]
4058 pub blueprint_id: Option<String>,
4059 pub price_copper: u32,
4060 #[serde(default)]
4061 pub affordable: bool,
4062 #[serde(default)]
4063 pub already_owned: bool,
4064}
4065
4066#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4067pub struct ShopBuyLine {
4068 pub template_id: String,
4069 pub label: String,
4070 pub quantity: u32,
4071 pub price_copper: u32,
4072}
4073
4074#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4076pub struct BankPanel {
4077 pub npc_id: String,
4078 pub npc_label: String,
4079 pub bank_balance_copper: u64,
4080 pub on_person_copper: u64,
4081 #[serde(default)]
4083 pub pending_outgoing_copper: u64,
4084 #[serde(default)]
4085 pub transfer_fee_bps: u32,
4086 #[serde(default)]
4087 pub transfer_clear_ticks: u64,
4088}
4089
4090#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4092pub struct StoragePanel {
4093 pub npc_id: String,
4094 pub npc_label: String,
4095 pub building_id: String,
4096 pub building_label: String,
4097 pub used_volume: f32,
4098 pub max_volume: f32,
4099 #[serde(default)]
4100 pub contents: Vec<ItemStack>,
4101 #[serde(default)]
4103 pub ship_destinations: Vec<StorageShipDest>,
4104}
4105
4106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4107pub struct StorageShipDest {
4108 pub building_id: String,
4109 pub label: String,
4110 pub distance_m: f32,
4111 pub fee_copper: u64,
4112 pub travel_ticks: u64,
4113}
4114
4115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4118pub enum GoodsLocation {
4119 Person,
4121 TownStorage { building_id: String },
4124}
4125
4126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4129pub struct MarketListingView {
4130 pub listing_id: Uuid,
4131 pub seller_character_id: Uuid,
4132 pub seller_label: String,
4134 pub hall_building_id: String,
4135 pub hall_label: String,
4136 pub template_id: String,
4137 pub display_name: String,
4138 #[serde(default)]
4140 pub category: String,
4141 pub quantity: u32,
4142 pub unit_price_copper: u64,
4143 pub line_total_copper: u64,
4145 #[serde(default)]
4147 pub npc_price: bool,
4148 #[serde(default)]
4151 pub npc_dump_unit_copper: Option<u32>,
4152 pub mine: bool,
4154}
4155
4156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4158pub struct MarketListVault {
4159 pub building_id: String,
4160 pub building_label: String,
4162 #[serde(default)]
4163 pub contents: Vec<ItemStack>,
4164}
4165
4166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4169pub struct MarketPanel {
4170 pub npc_id: String,
4171 pub npc_label: String,
4172 pub building_id: String,
4173 pub building_label: String,
4174 pub used_volume: f32,
4176 pub max_volume: f32,
4177 #[serde(default)]
4180 pub listings: Vec<MarketListingView>,
4181 #[serde(default)]
4183 pub tax_bps: u32,
4184 #[serde(default)]
4185 pub tax_flat_copper: u32,
4186 #[serde(default)]
4188 pub list_vaults: Vec<MarketListVault>,
4189}
4190
4191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4192pub struct ShopCatalog {
4193 pub npc_id: String,
4194 pub npc_label: String,
4195 #[serde(default)]
4196 pub sells: Vec<ShopOffer>,
4197 #[serde(default)]
4198 pub buys: Vec<ShopBuyLine>,
4199}
4200
4201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4202pub struct HarvestResult {
4203 pub node_id: String,
4204 pub quantity: u32,
4206 pub item_template: String,
4207 #[serde(default)]
4210 pub item_instance_id: Option<Uuid>,
4211}
4212
4213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4215pub struct Envelope<T> {
4216 pub protocol_version: u16,
4217 pub payload: T,
4218}
4219
4220impl<T> Envelope<T> {
4221 pub fn new(payload: T) -> Self {
4222 Self {
4223 protocol_version: crate::PROTOCOL_VERSION,
4224 payload,
4225 }
4226 }
4227}
4228
4229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4231pub struct Hello {
4232 pub client_name: String,
4233 pub protocol_version: u16,
4234 #[serde(default)]
4235 pub auth: AuthCredential,
4236 #[serde(default)]
4238 pub character_id: Option<Uuid>,
4239}
4240
4241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4244#[serde(rename_all = "snake_case")]
4245pub enum AuthCredential {
4246 DevLocal,
4247 Session { token: String },
4248 ApiToken { token: String, character_id: Uuid },
4249}
4250
4251impl Default for AuthCredential {
4252 fn default() -> Self {
4253 Self::DevLocal
4254 }
4255}
4256
4257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4258pub struct Welcome {
4259 pub session_id: SessionId,
4260 pub entity_id: EntityId,
4261 pub snapshot: Snapshot,
4262}
4263
4264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4265pub enum ServerMessage {
4266 Welcome(Welcome),
4267 ContentUpdated(Snapshot),
4269 Tick(TickDelta),
4270 IntentAck {
4271 entity_id: EntityId,
4272 seq: Seq,
4273 tick: Tick,
4274 },
4275 Chat(ChatMessage),
4276 HarvestResult(HarvestResult),
4277 UseResult(UseResult),
4278 CraftResult(CraftResult),
4279 Death(DeathNotice),
4280 Interaction(InteractionNotice),
4281 ShopOpened(ShopCatalog),
4282 NpcTalkOpened(NpcTalkOpened),
4283 NpcTalkPending(NpcTalkPending),
4284 NpcTalkReply(NpcTalkReply),
4285 NpcTalkClosed(NpcTalkClosed),
4286 NpcTalkError(NpcTalkError),
4287 QuestOffer(QuestOffer),
4288 QuestAccepted(QuestNotice),
4289 QuestWithdrawn(QuestNotice),
4290 QuestStepCompleted(QuestNotice),
4291 QuestCompleted(QuestNotice),
4292 QuestCatalogUpdated(QuestCatalogUpdated),
4293 BankOpened(BankPanel),
4295 StorageOpened(StoragePanel),
4297 MarketOpened(MarketPanel),
4299 TradeOpened(TradePanel),
4301 TradeClosed {
4303 reason: String,
4304 },
4305 ConnectRejected {
4308 reason: String,
4309 },
4310}
4311
4312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4314pub struct TradePanel {
4315 pub peer_entity_id: EntityId,
4316 pub peer_name: String,
4317 pub my_presented: Vec<ItemStack>,
4318 pub their_presented: Vec<ItemStack>,
4319 pub i_ready: bool,
4320 pub they_ready: bool,
4321 pub my_mass_after: f32,
4323 pub my_mass_max: f32,
4324 pub my_encumbrance_after: EncumbranceState,
4325 pub overburden_warning: bool,
4327}
4328
4329#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4330pub enum ClientMessage {
4331 Hello(Hello),
4332 Intent(Intent),
4333 Disconnect,
4334}
4335
4336#[cfg(test)]
4337mod tests {
4338 use super::*;
4339
4340 #[test]
4341 fn pristine_vitals_state_yields_full_pools() {
4342 let attrs = PrimaryAttributes::default();
4343 let vitals = StoredVitalsState::default().apply_to(attrs);
4344 assert!(vitals.health > 0.0);
4345 assert_eq!(vitals.health, vitals.health_max);
4346 assert!((vitals.mana_max - 61.0).abs() < 0.01);
4347 }
4348
4349 #[test]
4350 fn humanize_snake_id_title_cases_parts() {
4351 assert_eq!(humanize_snake_id("heal_touch"), "Heal Touch");
4352 assert_eq!(humanize_snake_id("fireball"), "Fireball");
4353 assert_eq!(humanize_snake_id("cone_frost"), "Cone Frost");
4354 }
4355
4356 #[test]
4357 fn saved_vitals_scale_when_pool_max_increases() {
4358 let mut attrs = PrimaryAttributes::default();
4359 attrs.intelligence = 140;
4360 attrs.wisdom = 140;
4361 let saved = StoredVitalsState {
4362 health: 100.0,
4363 mana: 14.0,
4364 stamina: 100.0,
4365 ..StoredVitalsState::default()
4366 };
4367 let vitals = saved.apply_to(attrs);
4368 assert!(vitals.mana_max > 55.0);
4369 assert!(
4370 (vitals.mana - vitals.mana_max).abs() < 0.01,
4371 "full legacy mana bar migrates to full new bar"
4372 );
4373 }
4374
4375 #[test]
4376 fn empty_vitals_state_is_pristine() {
4377 let pristine = StoredVitalsState {
4378 health: 0.0,
4379 mana: 0.0,
4380 stamina: 0.0,
4381 hunger: 0.0,
4382 thirst: 0.0,
4383 coins: 0,
4384 deaths: 0,
4385 life_state: LifeState::Alive,
4386 };
4387 assert!(pristine.is_pristine());
4388 let vitals = pristine.apply_to(PrimaryAttributes::default());
4389 assert!(vitals.health > 0.0);
4390 }
4391
4392 #[test]
4393 fn stored_vitals_roundtrip_preserves_partial_pools() {
4394 let attrs = PrimaryAttributes::default();
4395 let mut live = PlayerVitals::from_attributes(attrs);
4396 live.health = 25.0;
4397 live.hunger = 77.0;
4398 live.deaths = 2;
4399 let stored = StoredVitalsState::from_live(&live);
4400 let restored = stored.apply_to(attrs);
4401 assert!(
4402 (restored.health - 25.0).abs() < 0.01,
4403 "partial HP below cap stays absolute"
4404 );
4405 assert_eq!(restored.hunger, 77.0);
4406 assert_eq!(restored.deaths, 2);
4407 }
4408
4409 #[test]
4410 fn skill_tiers_start_at_zero() {
4411 let skill = SkillProgress::default();
4412 assert_eq!(skill.level, 0);
4413 assert_eq!(skill.display_tier(), 0);
4414 let trained = SkillProgress {
4415 level: 250,
4416 last_trained_tick: 1,
4417 };
4418 assert_eq!(trained.display_tier(), 2);
4419 }
4420
4421 #[test]
4422 fn quest_server_messages_roundtrip_json() {
4423 use crate::codec::{Codec, PostcardCodec};
4424
4425 let offer = ServerMessage::QuestOffer(QuestOffer {
4426 quest_id: "ada_goblin_hunt".into(),
4427 title: "Goblin Trouble".into(),
4428 description: "Help Ada".into(),
4429 step_count: 3,
4430 });
4431 let notice = ServerMessage::QuestAccepted(QuestNotice {
4432 quest_id: "ada_goblin_hunt".into(),
4433 title: "Goblin Trouble".into(),
4434 message: "Quest accepted".into(),
4435 });
4436 for msg in [offer, notice] {
4437 let bytes = PostcardCodec.encode(&msg).unwrap();
4438 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
4439 assert_eq!(decoded, msg);
4440 }
4441 }
4442
4443 #[test]
4444 fn hotbar_consumable_binding_roundtrips() {
4445 let binding = hotbar_consumable_binding("vegetable_soup");
4446 assert_eq!(binding, "item:vegetable_soup");
4447 assert!(hotbar_binding_is_consumable(&binding));
4448 assert_eq!(
4449 hotbar_consumable_template(&binding),
4450 Some("vegetable_soup")
4451 );
4452 assert!(!hotbar_binding_is_consumable("fireball"));
4453 assert_eq!(hotbar_consumable_template("fireball"), None);
4454 }
4455}