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
21impl WorldCoord {
22 pub fn surface(x: f32, y: f32) -> Self {
23 Self {
24 x,
25 y,
26 z: 0.0,
27 w: 0,
28 t: 0,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
34pub struct Velocity2D {
35 pub vx: f32,
36 pub vy: f32,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
40pub struct Transform {
41 pub position: WorldCoord,
42 pub yaw: f32,
43 pub velocity: Velocity2D,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum LifeState {
49 Alive,
50 Dead,
51}
52
53impl Default for LifeState {
54 fn default() -> Self {
55 Self::Alive
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum TimeOfDayPhase {
63 Night,
64 Dawn,
65 Morning,
66 Midday,
67 Afternoon,
68 Evening,
69}
70
71impl TimeOfDayPhase {
72 pub fn label(self) -> &'static str {
73 match self {
74 Self::Night => "Night",
75 Self::Dawn => "Dawn",
76 Self::Morning => "Morning",
77 Self::Midday => "Midday",
78 Self::Afternoon => "Afternoon",
79 Self::Evening => "Evening",
80 }
81 }
82
83 pub fn from_name(name: &str) -> Self {
84 match name.to_ascii_lowercase().as_str() {
85 "dawn" => Self::Dawn,
86 "morning" => Self::Morning,
87 "midday" | "mid_day" | "noon" => Self::Midday,
88 "afternoon" => Self::Afternoon,
89 "evening" | "dusk" => Self::Evening,
90 _ => Self::Night,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
97pub struct WorldClock {
98 pub day: u64,
100 pub hour: u8,
101 pub minute: u8,
102 pub phase: TimeOfDayPhase,
103}
104
105impl Default for WorldClock {
106 fn default() -> Self {
107 Self {
108 day: 0,
109 hour: 8,
110 minute: 0,
111 phase: TimeOfDayPhase::Morning,
112 }
113 }
114}
115
116impl WorldClock {
117 pub fn display_time(self) -> String {
118 format!("{:02}:{:02}", self.hour, self.minute)
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub struct PrimaryAttributes {
125 pub strength: u16,
126 pub dexterity: u16,
127 pub intelligence: u16,
128 pub stamina: u16,
129 pub vitality: u16,
130 pub wisdom: u16,
131 pub charisma: u16,
132}
133
134impl Default for PrimaryAttributes {
135 fn default() -> Self {
136 Self {
137 strength: 150,
138 dexterity: 150,
139 intelligence: 150,
140 stamina: 150,
141 vitality: 150,
142 wisdom: 150,
143 charisma: 150,
144 }
145 }
146}
147
148impl PrimaryAttributes {
149 pub fn display(value: u16) -> u16 {
151 (value / 10).clamp(1, 100)
152 }
153
154 pub fn derived_preview(&self) -> DerivedPreview {
156 let str_d = Self::display(self.strength) as f32;
157 let dex_d = Self::display(self.dexterity) as f32;
158 let int_d = Self::display(self.intelligence) as f32;
159 let wis_d = Self::display(self.wisdom) as f32;
160 DerivedPreview {
161 attack_power: str_d * 1.2 + dex_d * 0.3,
162 spell_power: int_d * 1.1 + wis_d * 0.4,
163 evasion: dex_d * 0.8 + wis_d * 0.2,
164 carry_mass_max: str_d * 2.5,
165 sight_range_m: 12.0 + wis_d * 0.15 + dex_d * 0.05,
166 fov_deg: 120.0 + wis_d * 0.2,
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq)]
173pub struct DerivedPreview {
174 pub attack_power: f32,
175 pub spell_power: f32,
176 pub evasion: f32,
177 pub carry_mass_max: f32,
178 pub sight_range_m: f32,
179 pub fov_deg: f32,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184pub struct SkillProgress {
185 pub level: u16,
186 #[serde(default)]
187 pub last_trained_tick: u64,
188}
189
190impl Default for SkillProgress {
191 fn default() -> Self {
192 Self {
193 level: 0,
194 last_trained_tick: 0,
195 }
196 }
197}
198
199impl SkillProgress {
200 pub fn display_tier(&self) -> u16 {
202 (self.level / 100).min(10)
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
208#[serde(default)]
209pub struct ProgressionXp {
210 pub strength: f64,
211 pub dexterity: f64,
212 pub intelligence: f64,
213 pub stamina: f64,
214 pub vitality: f64,
215 pub wisdom: f64,
216 pub charisma: f64,
217 pub logging: f64,
218 pub mining: f64,
219 pub evocation: f64,
220 pub restoration: f64,
221 pub swords: f64,
222 pub archery: f64,
223 pub crafting: f64,
224 pub alchemy: f64,
225 pub cartography: f64,
226}
227
228impl ProgressionXp {
229 pub fn bootstrap_new(baseline_display: u16, xp_base: f64, xp_growth: f64) -> Self {
231 let bootstrap = |display: f64| {
232 if display <= 1.0 {
233 0.0
234 } else {
235 xp_base * xp_growth.powf(display - 1.0)
236 }
237 };
238 let b = baseline_display as f64;
239 let primary = bootstrap(b);
240 Self {
241 strength: primary,
242 dexterity: primary,
243 intelligence: primary,
244 stamina: primary,
245 vitality: primary,
246 wisdom: primary,
247 charisma: primary,
248 ..Self::default()
249 }
250 }
251
252 pub fn is_empty(&self) -> bool {
253 self.strength == 0.0
254 && self.dexterity == 0.0
255 && self.intelligence == 0.0
256 && self.stamina == 0.0
257 && self.vitality == 0.0
258 && self.wisdom == 0.0
259 && self.charisma == 0.0
260 && self.logging == 0.0
261 && self.mining == 0.0
262 && self.evocation == 0.0
263 && self.restoration == 0.0
264 && self.swords == 0.0
265 && self.archery == 0.0
266 && self.crafting == 0.0
267 && self.alchemy == 0.0
268 && self.cartography == 0.0
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274#[serde(default)]
275pub struct PlayerSkills {
276 pub logging: SkillProgress,
277 pub mining: SkillProgress,
278 pub evocation: SkillProgress,
279 #[serde(default)]
280 pub restoration: SkillProgress,
281 pub swords: SkillProgress,
282 #[serde(default)]
283 pub archery: SkillProgress,
284 pub crafting: SkillProgress,
285 #[serde(default)]
286 pub alchemy: SkillProgress,
287 pub cartography: SkillProgress,
288}
289
290impl Default for PlayerSkills {
291 fn default() -> Self {
292 Self {
293 logging: SkillProgress::default(),
294 mining: SkillProgress::default(),
295 evocation: SkillProgress::default(),
296 restoration: SkillProgress::default(),
297 swords: SkillProgress::default(),
298 archery: SkillProgress::default(),
299 crafting: SkillProgress::default(),
300 alchemy: SkillProgress::default(),
301 cartography: SkillProgress::default(),
302 }
303 }
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
308pub struct PlayerVitals {
309 pub health: f32,
310 pub health_max: f32,
311 pub mana: f32,
312 pub mana_max: f32,
313 pub stamina: f32,
314 pub stamina_max: f32,
315 #[serde(default = "default_survival_pool_max")]
316 pub hunger: f32,
317 #[serde(default = "default_survival_pool_max")]
318 pub hunger_max: f32,
319 #[serde(default = "default_survival_pool_max")]
320 pub thirst: f32,
321 #[serde(default = "default_survival_pool_max")]
322 pub thirst_max: f32,
323 #[serde(default)]
324 pub coins: u32,
325 #[serde(default)]
326 pub deaths: u32,
327 #[serde(default)]
328 pub life_state: LifeState,
329}
330
331fn default_survival_pool_max() -> f32 {
332 100.0
333}
334
335impl Default for PlayerVitals {
336 fn default() -> Self {
337 Self::from_attributes(PrimaryAttributes::default())
338 }
339}
340
341impl PlayerVitals {
342 pub fn from_attributes(attrs: PrimaryAttributes) -> Self {
349 let vit_d = PrimaryAttributes::display(attrs.vitality) as f32;
350 let int_d = PrimaryAttributes::display(attrs.intelligence) as f32;
351 let wis_d = PrimaryAttributes::display(attrs.wisdom) as f32;
352 let sta_d = PrimaryAttributes::display(attrs.stamina) as f32;
353
354 let health_max = 50.0 + vit_d * 2.0;
355 let stamina_max = 30.0 + sta_d * 1.4;
356 let mana_max = 25.0 + int_d * 1.1 + wis_d * 1.3;
357 let hunger_max = 100.0;
358 let thirst_max = 100.0;
359 Self {
360 health: health_max,
361 health_max,
362 mana: mana_max,
363 mana_max,
364 stamina: stamina_max,
365 stamina_max,
366 hunger: hunger_max,
367 hunger_max,
368 thirst: thirst_max,
369 thirst_max,
370 coins: 0,
371 deaths: 0,
372 life_state: LifeState::Alive,
373 }
374 }
375
376 pub fn legacy_maxima(attrs: PrimaryAttributes) -> (f32, f32, f32) {
378 (
379 attrs.vitality as f32 / 5.0,
380 attrs.stamina as f32 / 5.0,
381 (attrs.intelligence as f32 + attrs.wisdom as f32) / 20.0,
382 )
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
388#[serde(default)]
389pub struct StoredVitalsState {
390 pub health: f32,
391 pub mana: f32,
392 pub stamina: f32,
393 pub hunger: f32,
394 pub thirst: f32,
395 pub coins: u32,
396 pub deaths: u32,
397 pub life_state: LifeState,
398}
399
400impl StoredVitalsState {
401 pub fn from_live(v: &PlayerVitals) -> Self {
402 Self {
403 health: v.health,
404 mana: v.mana,
405 stamina: v.stamina,
406 hunger: v.hunger,
407 thirst: v.thirst,
408 coins: v.coins,
409 deaths: v.deaths,
410 life_state: v.life_state,
411 }
412 }
413
414 pub fn is_pristine(&self) -> bool {
416 self.health == 0.0
417 && self.mana == 0.0
418 && self.stamina == 0.0
419 && self.hunger == 0.0
420 && self.thirst == 0.0
421 && self.coins == 0
422 && self.deaths == 0
423 && self.life_state == LifeState::Alive
424 }
425
426 pub fn apply_to(self, attrs: PrimaryAttributes) -> PlayerVitals {
427 if self.is_pristine() {
428 return PlayerVitals::from_attributes(attrs);
429 }
430 let fresh = PlayerVitals::from_attributes(attrs);
431 let (legacy_hp, legacy_sta, legacy_mana) = PlayerVitals::legacy_maxima(attrs);
432
433 let scale = |current: f32, legacy_max: f32, new_max: f32| {
434 if legacy_max > 0.0 && new_max > legacy_max * 1.05 && current >= legacy_max * 0.95 {
435 let ratio = (current / legacy_max).clamp(0.0, 1.0);
436 (new_max * ratio).min(new_max)
437 } else {
438 current.min(new_max)
439 }
440 };
441
442 let mut v = fresh;
443 v.health = scale(self.health, legacy_hp, fresh.health_max);
444 v.mana = scale(self.mana, legacy_mana, fresh.mana_max);
445 v.stamina = scale(self.stamina, legacy_sta, fresh.stamina_max);
446 v.hunger = self.hunger.min(v.hunger_max);
447 v.thirst = self.thirst.min(v.thirst_max);
448 v.coins = self.coins;
449 v.deaths = self.deaths;
450 v.life_state = self.life_state;
451 v
452 }
453}
454
455impl Default for StoredVitalsState {
456 fn default() -> Self {
457 Self::from_live(&PlayerVitals::default())
458 }
459}
460
461pub fn humanize_snake_id(id: &str) -> String {
465 id.split('_')
466 .filter(|part| !part.is_empty())
467 .map(|part| {
468 let mut chars = part.chars();
469 match chars.next() {
470 None => String::new(),
471 Some(first) => first.to_uppercase().chain(chars).collect(),
472 }
473 })
474 .collect::<Vec<_>>()
475 .join(" ")
476}
477
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
480pub struct KnownAbility {
481 pub ability_id: String,
482 #[serde(default = "default_known_permanent")]
484 pub permanent: bool,
485 #[serde(default)]
487 pub expires_at_tick: Option<u64>,
488}
489
490fn default_known_permanent() -> bool {
491 true
492}
493
494impl KnownAbility {
495 pub fn permanent(ability_id: impl Into<String>) -> Self {
496 Self {
497 ability_id: ability_id.into(),
498 permanent: true,
499 expires_at_tick: None,
500 }
501 }
502
503 pub fn is_active(&self, tick: u64) -> bool {
504 if self.permanent {
505 return true;
506 }
507 match self.expires_at_tick {
508 Some(exp) => tick < exp,
509 None => false,
510 }
511 }
512}
513
514#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516pub struct RotationPreset {
517 pub id: String,
518 pub label: String,
519 #[serde(default)]
520 pub abilities: Vec<String>,
521}
522
523impl RotationPreset {
524 pub fn melee_default(ability_id: impl Into<String>) -> Self {
525 let id = ability_id.into();
526 Self {
527 id: "melee".into(),
528 label: "Weapon".into(),
530 abilities: vec![id],
531 }
532 }
533
534 pub fn is_weapon_preset(&self) -> bool {
535 self.id == "melee"
536 }
537}
538
539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
541pub struct StoredTargetSlot {
542 pub instance_id: Option<String>,
543 #[serde(default)]
544 pub preset_id: Option<String>,
545 #[serde(default)]
546 pub rotation_index: u32,
547 #[serde(default)]
548 pub auto_enabled: bool,
549}
550
551#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
552#[serde(default)]
553pub struct StoredCombatProfile {
554 pub combat_target_instance_id: Option<String>,
556 pub in_combat: bool,
557 pub last_combat_tick: u64,
558 pub last_attack_tick: u64,
559 pub cooldowns_until_tick: BTreeMap<String, u64>,
560 #[serde(default = "default_auto_attack")]
561 pub auto_attack_enabled: bool,
562 #[serde(default)]
564 pub mainhand_template_id: Option<String>,
565 #[serde(default)]
567 pub mainhand_instance_id: Option<Uuid>,
568 #[serde(default)]
570 pub offhand_template_id: Option<String>,
571 #[serde(default)]
573 pub offhand_instance_id: Option<Uuid>,
574 #[serde(default)]
577 pub worn: Vec<(BodySlot, ItemStack)>,
578 #[serde(default)]
580 pub rotation_presets: Vec<RotationPreset>,
581 #[serde(default)]
583 pub target_slots: Vec<StoredTargetSlot>,
584 #[serde(default)]
586 pub known_blueprint_ids: Vec<String>,
587 #[serde(default)]
589 pub keychain: Vec<ItemStack>,
590 #[serde(default)]
592 pub whisper_pouch: Vec<ItemStack>,
593 #[serde(default)]
595 pub known_abilities: Vec<KnownAbility>,
596 #[serde(default)]
598 pub hotbar: Vec<Option<String>>,
599 #[serde(default)]
601 pub abilities_schema_version: u32,
602 #[serde(default)]
604 pub bank_balance_copper: u64,
605}
606
607fn default_auto_attack() -> bool {
608 true
609}
610
611impl Default for StoredCombatProfile {
612 fn default() -> Self {
613 Self {
614 combat_target_instance_id: None,
615 in_combat: false,
616 last_combat_tick: 0,
617 last_attack_tick: 0,
618 cooldowns_until_tick: BTreeMap::new(),
619 auto_attack_enabled: true,
620 mainhand_template_id: None,
621 mainhand_instance_id: None,
622 offhand_template_id: None,
623 offhand_instance_id: None,
624 worn: Vec::new(),
625 rotation_presets: Vec::new(),
626 target_slots: Vec::new(),
627 known_blueprint_ids: Vec::new(),
628 keychain: Vec::new(),
629 whisper_pouch: Vec::new(),
630 known_abilities: Vec::new(),
631 hotbar: Vec::new(),
632 abilities_schema_version: 0,
633 bank_balance_copper: 0,
634 }
635 }
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
640#[serde(rename_all = "snake_case")]
641pub enum CombatCueKind {
642 Dodge,
643 Block,
644 AttackTelegraph,
645}
646
647#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
648pub struct CombatCueView {
649 pub kind: CombatCueKind,
650 pub until_tick: Tick,
652 #[serde(default)]
654 pub start_tick: Tick,
655 #[serde(default)]
657 pub ability_id: Option<String>,
658}
659
660#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
661pub struct EntityState {
662 pub id: EntityId,
663 pub transform: Transform,
664 #[serde(default)]
666 pub label: String,
667 #[serde(default)]
668 pub vitals: Option<PlayerVitals>,
669 #[serde(default)]
671 pub attributes: Option<PrimaryAttributes>,
672 #[serde(default)]
673 pub skills: Option<PlayerSkills>,
674 #[serde(default)]
676 pub inside_building: Option<String>,
677 #[serde(default)]
679 pub tile_id: Option<String>,
680 #[serde(default)]
682 pub paperdoll_ref: Option<String>,
683 #[serde(default)]
685 pub presentation_state: Option<String>,
686 #[serde(default)]
688 pub sprite_mode: Option<String>,
689 #[serde(default)]
691 pub progression_xp: Option<ProgressionXp>,
692 #[serde(default)]
694 pub combat_cues: Vec<CombatCueView>,
695}
696
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(rename_all = "snake_case")]
700pub enum ChatChannel {
701 Nearby,
703 Direct,
705 Whisper,
707 WhisperStone,
709}
710
711#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
713#[serde(rename_all = "snake_case")]
714pub enum ChatClarity {
715 #[default]
716 Clear,
717 Partial,
718 Heavy,
719}
720
721#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
722pub struct ChatMessage {
723 pub channel: ChatChannel,
724 pub from_entity: EntityId,
725 pub from_name: String,
726 pub text: String,
728 pub tick: Tick,
729 #[serde(default)]
731 pub to_entity: Option<EntityId>,
732 #[serde(default)]
733 pub clarity: ChatClarity,
734}
735
736#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
738pub enum Intent {
739 Move {
740 entity_id: EntityId,
741 forward: f32,
742 strafe: f32,
743 #[serde(default)]
745 vertical: f32,
746 #[serde(default)]
748 sprint: bool,
749 seq: Seq,
750 },
751 Stop {
752 entity_id: EntityId,
753 seq: Seq,
754 },
755 Harvest {
756 entity_id: EntityId,
757 node_id: String,
758 seq: Seq,
759 },
760 Use {
761 entity_id: EntityId,
762 template_id: String,
763 seq: Seq,
764 },
765 UseGrant {
767 entity_id: EntityId,
768 grant_instance_id: Uuid,
769 target_instance_id: Uuid,
770 seq: Seq,
771 },
772 Say {
773 entity_id: EntityId,
774 channel: ChatChannel,
775 text: String,
776 #[serde(default)]
778 to_entity: Option<EntityId>,
779 seq: Seq,
780 },
781 Craft {
783 entity_id: EntityId,
784 blueprint_id: String,
785 #[serde(default)]
787 count: Option<u32>,
788 seq: Seq,
789 },
790 Interact {
792 entity_id: EntityId,
793 target_id: String,
794 seq: Seq,
795 },
796 ShopBuy {
798 entity_id: EntityId,
799 npc_id: String,
800 offer_id: String,
801 #[serde(default = "default_one")]
802 quantity: u32,
803 seq: Seq,
804 },
805 ShopSell {
807 entity_id: EntityId,
808 npc_id: String,
809 template_id: String,
810 #[serde(default = "default_one")]
811 quantity: u32,
812 seq: Seq,
813 },
814 ShopClose {
816 entity_id: EntityId,
817 npc_id: String,
818 seq: Seq,
819 },
820 TestDamage {
822 entity_id: EntityId,
823 amount: f32,
824 seq: Seq,
825 },
826 SetTarget {
828 entity_id: EntityId,
829 target_id: EntityId,
830 seq: Seq,
831 },
832 SetTargetSlot {
834 entity_id: EntityId,
835 slot_index: u8,
836 target_id: EntityId,
837 seq: Seq,
838 },
839 ClearTarget {
840 entity_id: EntityId,
841 seq: Seq,
842 },
843 ClearTargetSlot {
844 entity_id: EntityId,
845 slot_index: u8,
846 seq: Seq,
847 },
848 SetAutoAttack {
850 entity_id: EntityId,
851 slot_index: u8,
852 enabled: bool,
853 seq: Seq,
854 },
855 Attack {
857 entity_id: EntityId,
858 #[serde(default)]
859 target_id: Option<EntityId>,
860 #[serde(default)]
861 weapon_slot: Option<u32>,
862 seq: Seq,
863 },
864 Pickup {
866 entity_id: EntityId,
867 #[serde(default)]
868 drop_id: Option<String>,
869 seq: Seq,
870 },
871 Cast {
873 entity_id: EntityId,
874 ability_id: String,
875 target_id: EntityId,
876 seq: Seq,
877 },
878 BindActionSlot {
880 entity_id: EntityId,
881 slot_index: u8,
882 ability_id: String,
883 #[serde(default = "default_auto_attack")]
884 auto_enabled: bool,
885 seq: Seq,
886 },
887 UseActionSlot {
889 entity_id: EntityId,
890 slot_index: u8,
891 seq: Seq,
892 },
893 Dodge {
895 entity_id: EntityId,
896 seq: Seq,
897 },
898 Lunge {
900 entity_id: EntityId,
901 #[serde(default)]
903 forward: f32,
904 #[serde(default)]
906 strafe: f32,
907 seq: Seq,
908 },
909 DirectionalJump {
911 entity_id: EntityId,
912 #[serde(default)]
914 forward: f32,
915 #[serde(default)]
917 strafe: f32,
918 seq: Seq,
919 },
920 Block {
922 entity_id: EntityId,
923 #[serde(default = "default_block_enabled")]
924 enabled: bool,
925 seq: Seq,
926 },
927 EquipMainhand {
931 entity_id: EntityId,
932 #[serde(default)]
933 template_id: Option<String>,
934 #[serde(default)]
935 instance_id: Option<Uuid>,
936 seq: Seq,
937 },
938 EquipOffhand {
940 entity_id: EntityId,
941 #[serde(default)]
942 template_id: Option<String>,
943 #[serde(default)]
944 instance_id: Option<Uuid>,
945 seq: Seq,
946 },
947 EquipWorn {
950 entity_id: EntityId,
951 slot: BodySlot,
952 #[serde(default)]
953 instance_id: Option<Uuid>,
954 seq: Seq,
955 },
956 MoveItem {
958 entity_id: EntityId,
959 item_instance_id: Uuid,
960 from: InventoryLocation,
961 to: InventoryLocation,
962 #[serde(default)]
964 to_parent_instance_id: Option<Uuid>,
965 #[serde(default)]
967 quantity: Option<u32>,
968 seq: Seq,
969 },
970 PlaceContainer {
972 entity_id: EntityId,
973 item_instance_id: Uuid,
974 seq: Seq,
975 },
976 PickupContainer {
978 entity_id: EntityId,
979 container_id: String,
980 seq: Seq,
981 },
982 MovePlacedContainer {
984 entity_id: EntityId,
985 container_id: String,
986 x: f32,
987 y: f32,
988 seq: Seq,
989 },
990 SetContainerLocked {
992 entity_id: EntityId,
993 location: InventoryLocation,
995 locked: bool,
996 seq: Seq,
997 },
998 DropItem {
1000 entity_id: EntityId,
1001 item_instance_id: Uuid,
1002 from: InventoryLocation,
1003 seq: Seq,
1004 },
1005 DestroyItem {
1007 entity_id: EntityId,
1008 item_instance_id: Uuid,
1009 from: InventoryLocation,
1010 #[serde(default)]
1012 quantity: Option<u32>,
1013 seq: Seq,
1014 },
1015 RenameContainer {
1017 entity_id: EntityId,
1018 item_instance_id: Uuid,
1019 location: InventoryLocation,
1020 name: String,
1021 seq: Seq,
1022 },
1023 UpsertRotationPreset {
1025 entity_id: EntityId,
1026 preset: RotationPreset,
1027 seq: Seq,
1028 },
1029 DeleteRotationPreset {
1031 entity_id: EntityId,
1032 preset_id: String,
1033 seq: Seq,
1034 },
1035 AssignSlotPreset {
1037 entity_id: EntityId,
1038 slot_index: u8,
1039 preset_id: String,
1040 seq: Seq,
1041 },
1042 SetHotbarSlot {
1045 entity_id: EntityId,
1046 slot: u8,
1048 #[serde(default)]
1050 ability_id: Option<String>,
1051 seq: Seq,
1052 },
1053 AdvanceRotation {
1055 entity_id: EntityId,
1056 slot_index: u8,
1057 seq: Seq,
1058 },
1059 NpcTalkOpen {
1061 entity_id: EntityId,
1062 npc_id: String,
1063 seq: Seq,
1064 },
1065 NpcTalkSay {
1067 entity_id: EntityId,
1068 npc_id: String,
1069 message: String,
1070 seq: Seq,
1071 },
1072 NpcTalkClose {
1074 entity_id: EntityId,
1075 npc_id: String,
1076 seq: Seq,
1077 },
1078 AcceptQuest {
1080 entity_id: EntityId,
1081 quest_id: String,
1082 seq: Seq,
1083 },
1084 WithdrawQuest {
1086 entity_id: EntityId,
1087 quest_id: String,
1088 seq: Seq,
1089 },
1090 TrackQuest {
1092 entity_id: EntityId,
1093 quest_id: String,
1094 seq: Seq,
1095 },
1096 QuestGiveItem {
1098 entity_id: EntityId,
1099 npc_id: String,
1100 template_id: String,
1101 #[serde(default = "default_one")]
1102 quantity: u32,
1103 seq: Seq,
1104 },
1105 HireWorker {
1107 entity_id: EntityId,
1108 def_id: String,
1109 wage_copper_per_interval: u32,
1110 #[serde(default)]
1111 lodging_container_id: Option<String>,
1112 #[serde(default)]
1113 job_yaml: Option<String>,
1114 seq: Seq,
1115 },
1116 DismissWorker {
1118 entity_id: EntityId,
1119 worker_instance_id: String,
1120 seq: Seq,
1121 },
1122 SetWorkerJob {
1124 entity_id: EntityId,
1125 worker_instance_id: String,
1126 job_yaml: String,
1127 seq: Seq,
1128 },
1129 AssignWorkerLodging {
1131 entity_id: EntityId,
1132 worker_instance_id: String,
1133 lodging_container_id: String,
1134 seq: Seq,
1135 },
1136 SetWorkerMode {
1138 entity_id: EntityId,
1139 worker_instance_id: String,
1140 mode: String,
1141 seq: Seq,
1142 },
1143 GiveWorkerItem {
1146 entity_id: EntityId,
1147 worker_instance_id: String,
1148 item_instance_id: uuid::Uuid,
1149 #[serde(default)]
1150 quantity: Option<u32>,
1151 seq: Seq,
1152 },
1153 TakeWorkerItem {
1155 entity_id: EntityId,
1156 worker_instance_id: String,
1157 item_instance_id: uuid::Uuid,
1158 #[serde(default)]
1159 quantity: Option<u32>,
1160 seq: Seq,
1161 },
1162 RenameHiredWorker {
1164 entity_id: EntityId,
1165 worker_instance_id: String,
1166 name: String,
1167 seq: Seq,
1168 },
1169 TeachWorkerBlueprint {
1171 entity_id: EntityId,
1172 worker_instance_id: String,
1173 blueprint_id: String,
1174 seq: Seq,
1175 },
1176 AttendHiredWorker {
1178 entity_id: EntityId,
1179 worker_instance_id: String,
1180 attending: bool,
1181 seq: Seq,
1182 },
1183 BuyPlot {
1185 entity_id: EntityId,
1186 zone_id: String,
1187 x0: f32,
1188 y0: f32,
1189 x1: f32,
1190 y1: f32,
1191 seq: Seq,
1192 },
1193 BuyPlotAllFree {
1195 entity_id: EntityId,
1196 zone_id: String,
1197 seq: Seq,
1198 },
1199 SellPlotToCrown {
1201 entity_id: EntityId,
1202 plot_id: Uuid,
1203 seq: Seq,
1204 },
1205 Cultivate {
1207 entity_id: EntityId,
1208 x: f32,
1210 y: f32,
1211 seq: Seq,
1212 },
1213 PlantSeeds {
1215 entity_id: EntityId,
1216 seed_template_id: String,
1217 quantity: u32,
1218 seq: Seq,
1219 },
1220 SetPlotFarmPublic {
1222 entity_id: EntityId,
1223 plot_id: Uuid,
1224 public: bool,
1225 #[serde(default)]
1226 public_tax_discount_bps: u32,
1227 seq: Seq,
1228 },
1229 PlotFarmAllowUpsert {
1231 entity_id: EntityId,
1232 plot_id: Uuid,
1233 #[serde(default)]
1235 character_id: Option<Uuid>,
1236 #[serde(default)]
1238 character_name: String,
1239 #[serde(default)]
1240 tax_discount_bps: u32,
1241 seq: Seq,
1242 },
1243 PlotFarmAllowRemove {
1245 entity_id: EntityId,
1246 plot_id: Uuid,
1247 character_id: Uuid,
1248 seq: Seq,
1249 },
1250 BankDeposit {
1252 entity_id: EntityId,
1253 npc_id: String,
1254 #[serde(default)]
1256 amount_copper: u64,
1257 seq: Seq,
1258 },
1259 BankWithdraw {
1261 entity_id: EntityId,
1262 npc_id: String,
1263 #[serde(default)]
1265 amount_copper: u64,
1266 seq: Seq,
1267 },
1268 BankClose {
1270 entity_id: EntityId,
1271 npc_id: String,
1272 seq: Seq,
1273 },
1274 BankTransfer {
1276 entity_id: EntityId,
1277 npc_id: String,
1278 #[serde(default)]
1280 to_character_id: Option<Uuid>,
1281 #[serde(default)]
1283 to_name: String,
1284 amount_copper: u64,
1286 seq: Seq,
1287 },
1288 StorageStore {
1290 entity_id: EntityId,
1291 npc_id: String,
1292 item_instance_id: Uuid,
1293 #[serde(default)]
1294 quantity: Option<u32>,
1295 seq: Seq,
1296 },
1297 StorageTake {
1299 entity_id: EntityId,
1300 npc_id: String,
1301 item_instance_id: Uuid,
1302 #[serde(default)]
1303 quantity: Option<u32>,
1304 seq: Seq,
1305 },
1306 StorageShip {
1308 entity_id: EntityId,
1309 npc_id: String,
1310 dest_building_id: String,
1311 item_instance_id: Uuid,
1312 #[serde(default)]
1313 quantity: Option<u32>,
1314 seq: Seq,
1315 },
1316 StorageClose {
1318 entity_id: EntityId,
1319 npc_id: String,
1320 seq: Seq,
1321 },
1322 MarketList {
1325 entity_id: EntityId,
1326 npc_id: String,
1327 source: GoodsLocation,
1328 item_instance_id: Uuid,
1329 #[serde(default)]
1330 quantity: Option<u32>,
1331 unit_price_copper: u64,
1332 seq: Seq,
1333 },
1334 MarketReprice {
1336 entity_id: EntityId,
1337 npc_id: String,
1338 listing_id: Uuid,
1339 unit_price_copper: u64,
1340 seq: Seq,
1341 },
1342 MarketDelist {
1344 entity_id: EntityId,
1345 npc_id: String,
1346 listing_id: Uuid,
1347 dest: GoodsLocation,
1348 seq: Seq,
1349 },
1350 MarketBuy {
1352 entity_id: EntityId,
1353 npc_id: String,
1354 listing_id: Uuid,
1355 #[serde(default = "default_one")]
1356 quantity: u32,
1357 dest: GoodsLocation,
1358 seq: Seq,
1359 },
1360 MarketClose {
1362 entity_id: EntityId,
1363 npc_id: String,
1364 seq: Seq,
1365 },
1366 TradeRequest {
1368 entity_id: EntityId,
1369 peer_entity_id: EntityId,
1370 seq: Seq,
1371 },
1372 TradeRespond {
1374 entity_id: EntityId,
1375 peer_entity_id: EntityId,
1376 accept: bool,
1377 seq: Seq,
1378 },
1379 TradePresent {
1381 entity_id: EntityId,
1382 item_instance_id: Uuid,
1383 #[serde(default)]
1384 quantity: Option<u32>,
1385 seq: Seq,
1386 },
1387 TradeUnpresent {
1389 entity_id: EntityId,
1390 item_instance_id: Uuid,
1391 seq: Seq,
1392 },
1393 TradeSetReady {
1395 entity_id: EntityId,
1396 ready: bool,
1397 seq: Seq,
1398 },
1399 TradeCancel {
1401 entity_id: EntityId,
1402 seq: Seq,
1403 },
1404 DestroyWhisperStone {
1406 entity_id: EntityId,
1407 item_instance_id: Uuid,
1408 seq: Seq,
1409 },
1410 StowWhisperStone {
1412 entity_id: EntityId,
1413 item_instance_id: Uuid,
1414 seq: Seq,
1415 },
1416}
1417
1418fn default_block_enabled() -> bool {
1419 true
1420}
1421
1422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1424pub struct StatusEffectHud {
1425 pub effect_id: String,
1426 pub label: String,
1427 #[serde(default)]
1428 pub polarity: String,
1429 #[serde(default)]
1430 pub icon_tile_id: Option<String>,
1431 #[serde(default)]
1433 pub remaining_sec: Option<f32>,
1434}
1435
1436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1438pub struct CombatTargetHud {
1439 pub entity_id: EntityId,
1440 #[serde(default)]
1441 pub label: String,
1442 #[serde(default)]
1443 pub level: u32,
1444 pub health: f32,
1445 pub health_max: f32,
1446 #[serde(default)]
1447 pub life_state: LifeState,
1448 #[serde(default)]
1449 pub distance_m: f32,
1450 #[serde(default)]
1451 pub statuses: Vec<StatusEffectHud>,
1452}
1453
1454#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1456#[serde(rename_all = "snake_case")]
1457pub enum TimedChannelKind {
1458 #[default]
1459 Cultivate,
1460 Plant,
1461 Harvest,
1462}
1463
1464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1466pub struct TimedChannelHud {
1467 #[serde(default)]
1468 pub label: String,
1469 #[serde(default)]
1470 pub channel: TimedChannelKind,
1471 #[serde(default)]
1472 pub cell_x: i32,
1473 #[serde(default)]
1474 pub cell_y: i32,
1475 #[serde(default)]
1476 pub ticks_remaining: u64,
1477 #[serde(default)]
1478 pub ticks_total: u64,
1479}
1480
1481#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1483pub struct CastProgressHud {
1484 #[serde(default)]
1485 pub ability_id: String,
1486 #[serde(default)]
1487 pub ability_label: String,
1488 #[serde(default)]
1489 pub ticks_remaining: u64,
1490 #[serde(default)]
1491 pub ticks_total: u64,
1492}
1493
1494#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1496pub struct AbilityCooldownHud {
1497 #[serde(default)]
1498 pub ability_id: String,
1499 #[serde(default)]
1500 pub label: String,
1501 #[serde(default)]
1502 pub cd_ticks: u64,
1503 #[serde(default)]
1504 pub cd_total_ticks: u64,
1505}
1506
1507#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1509pub struct CombatSlotHud {
1510 pub slot_index: u8,
1511 #[serde(default)]
1512 pub target_entity_id: Option<EntityId>,
1513 #[serde(default)]
1514 pub target_label: Option<String>,
1515 #[serde(default)]
1516 pub target: Option<CombatTargetHud>,
1517 #[serde(default)]
1518 pub preset_id: Option<String>,
1519 #[serde(default)]
1520 pub preset_label: Option<String>,
1521 #[serde(default)]
1522 pub rotation: Vec<String>,
1523 #[serde(default)]
1524 pub rotation_index: u32,
1525 #[serde(default)]
1526 pub next_ability_id: Option<String>,
1527 #[serde(default)]
1528 pub auto_enabled: bool,
1529}
1530
1531#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1533pub struct DefensePieceHud {
1534 pub slot: BodySlot,
1535 pub label: String,
1536 pub template_id: String,
1537 #[serde(default)]
1538 pub armor_physical: f32,
1539 #[serde(default)]
1540 pub resists: Vec<(String, f32)>,
1541}
1542
1543impl Default for DefensePieceHud {
1544 fn default() -> Self {
1545 Self {
1546 slot: BodySlot::Head,
1547 label: String::new(),
1548 template_id: String::new(),
1549 armor_physical: 0.0,
1550 resists: Vec::new(),
1551 }
1552 }
1553}
1554
1555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1557pub struct DefenseHud {
1558 pub armor_physical: f32,
1559 pub vitality_contribution: f32,
1560 pub total_mitigation_rating: f32,
1561 pub estimated_physical_dr: f32,
1563 #[serde(default)]
1564 pub resists: Vec<(String, f32)>,
1565 #[serde(default)]
1566 pub pieces: Vec<DefensePieceHud>,
1567}
1568
1569#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1571pub struct CombatHud {
1572 pub in_combat: bool,
1573 pub auto_attack: bool,
1575 pub has_los: bool,
1576 pub attack_cd_ticks: u64,
1577 #[serde(default)]
1578 pub ability_id: String,
1579 #[serde(default)]
1580 pub target_entity_id: Option<EntityId>,
1581 #[serde(default)]
1582 pub target_label: Option<String>,
1583 #[serde(default)]
1584 pub max_target_slots: u8,
1585 #[serde(default)]
1586 pub slots: Vec<CombatSlotHud>,
1587 #[serde(default)]
1588 pub rotation_presets: Vec<RotationPreset>,
1589 #[serde(default)]
1590 pub gcd_ticks: u64,
1591 #[serde(default)]
1592 pub mainhand_template_id: Option<String>,
1593 #[serde(default)]
1594 pub mainhand_label: Option<String>,
1595 #[serde(default)]
1596 pub offhand_template_id: Option<String>,
1597 #[serde(default)]
1598 pub offhand_label: Option<String>,
1599 #[serde(default)]
1601 pub mainhand_hand_slots: u8,
1602 #[serde(default)]
1604 pub worn: Vec<(BodySlot, ItemStack)>,
1605 #[serde(default)]
1607 pub defense: Option<DefenseHud>,
1608 #[serde(default)]
1609 pub carry_mass: f32,
1610 #[serde(default)]
1611 pub carry_mass_max: f32,
1612 #[serde(default)]
1613 pub encumbrance: EncumbranceState,
1614 #[serde(default)]
1616 pub keychain: Vec<ItemStack>,
1617 #[serde(default)]
1619 pub whisper_pouch: Vec<ItemStack>,
1620 #[serde(default)]
1621 pub target: Option<CombatTargetHud>,
1622 #[serde(default)]
1623 pub cast: Option<CastProgressHud>,
1624 #[serde(default)]
1626 pub timed_channel: Option<TimedChannelHud>,
1627 #[serde(default)]
1628 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1629 #[serde(default)]
1630 pub blocking_active: bool,
1631 #[serde(default)]
1633 pub progression_xp: Option<ProgressionXp>,
1634 #[serde(default)]
1635 pub progression_baseline: u16,
1636 #[serde(default)]
1637 pub progression_xp_base: f64,
1638 #[serde(default)]
1639 pub progression_xp_growth: f64,
1640 #[serde(default)]
1641 pub attributes: Option<PrimaryAttributes>,
1642 #[serde(default)]
1643 pub skills: Option<PlayerSkills>,
1644 #[serde(default)]
1646 pub statuses: Vec<StatusEffectHud>,
1647 #[serde(default)]
1649 pub known_abilities: Vec<String>,
1650 #[serde(default)]
1653 pub hotbar: Vec<Option<String>>,
1654 #[serde(default)]
1656 pub max_abilities_per_rotation: u8,
1657}
1658
1659pub const HOTBAR_ITEM_PREFIX: &str = "item:";
1661
1662pub fn hotbar_consumable_binding(template_id: &str) -> String {
1664 format!("{HOTBAR_ITEM_PREFIX}{}", template_id.trim())
1665}
1666
1667pub fn hotbar_consumable_template(binding: &str) -> Option<&str> {
1669 binding
1670 .strip_prefix(HOTBAR_ITEM_PREFIX)
1671 .map(str::trim)
1672 .filter(|id| !id.is_empty())
1673}
1674
1675pub fn hotbar_binding_is_consumable(binding: &str) -> bool {
1677 hotbar_consumable_template(binding).is_some()
1678}
1679
1680#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1682#[serde(rename_all = "snake_case")]
1683pub enum CombatFxKind {
1684 MeleeArc,
1685 Cone,
1686 Sphere,
1687 Beam,
1688 HitMarker,
1689}
1690
1691#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1693#[serde(rename_all = "snake_case")]
1694pub enum CombatFxHitOutcome {
1695 #[default]
1696 Hit,
1697 Blocked,
1698 Miss,
1699 Glance,
1700}
1701
1702#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1704pub struct CombatFxHit {
1705 pub entity_id: EntityId,
1706 pub x: f32,
1707 pub y: f32,
1708 pub z: f32,
1709 #[serde(default)]
1710 pub outcome: CombatFxHitOutcome,
1711}
1712
1713#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1715pub struct CombatFx {
1716 pub id: u64,
1717 pub kind: CombatFxKind,
1718 pub ability_id: String,
1719 pub caster_id: EntityId,
1720 pub origin_x: f32,
1721 pub origin_y: f32,
1722 pub origin_z: f32,
1723 #[serde(default)]
1724 pub end_x: Option<f32>,
1725 #[serde(default)]
1726 pub end_y: Option<f32>,
1727 #[serde(default)]
1728 pub end_z: Option<f32>,
1729 #[serde(default)]
1730 pub yaw: Option<f32>,
1731 #[serde(default)]
1732 pub reach_m: Option<f32>,
1733 #[serde(default)]
1734 pub arc_deg: Option<f32>,
1735 #[serde(default)]
1736 pub radius_m: Option<f32>,
1737 #[serde(default)]
1738 pub hits: Vec<CombatFxHit>,
1739 pub until_tick: u64,
1741 #[serde(default)]
1742 pub damage_type: String,
1743}
1744
1745#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1747#[serde(rename_all = "snake_case")]
1748pub enum WorkerModeView {
1749 Companion,
1750 JobLoop,
1751 Idle,
1754}
1755
1756#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1758#[serde(rename_all = "snake_case")]
1759pub enum WorkerStateView {
1760 Idle,
1761 Traveling,
1762 Working,
1763 Resting,
1764 Waiting,
1765 Strike,
1766 Dismissed,
1767}
1768
1769#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
1771pub struct WorkerVitalsSummary {
1772 pub health_pct: f32,
1773 pub stamina_pct: f32,
1774}
1775
1776#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1778#[serde(rename_all = "snake_case")]
1779pub enum WorkerRouteKindView {
1780 #[default]
1781 HarvestLoop,
1782 Ordered,
1783}
1784
1785#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1787pub struct WorkerRouteView {
1788 #[serde(default)]
1789 pub kind: WorkerRouteKindView,
1790 #[serde(default)]
1791 pub lodging_container_id: Option<String>,
1792 #[serde(default)]
1794 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
1795 #[serde(default)]
1797 pub harvest_nodes: Vec<String>,
1798 #[serde(default = "default_route_carry_ratio")]
1799 pub carry_return_ratio: f32,
1800 #[serde(default)]
1802 pub stops: Vec<WorkerRouteStopView>,
1803}
1804
1805fn default_route_carry_ratio() -> f32 {
1806 0.90
1807}
1808
1809fn default_true_view() -> bool {
1810 true
1811}
1812
1813#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1815pub struct WorkerWithdrawItemView {
1816 pub template: String,
1817 #[serde(default)]
1819 pub qty: u32,
1820 #[serde(default)]
1822 pub all: bool,
1823}
1824
1825#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1826pub struct WorkerRouteWaypointView {
1827 pub x: f32,
1828 pub y: f32,
1829 pub z: f32,
1830}
1831
1832#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1841#[serde(rename_all = "snake_case")]
1842pub enum WorkerRouteStopView {
1843 Waypoint {
1844 x: f32,
1845 y: f32,
1846 #[serde(default)]
1847 z: f32,
1848 },
1849 HarvestNode {
1850 node_id: String,
1851 },
1852 DepositAt {
1853 container_id: String,
1854 #[serde(default)]
1855 filter: Option<Vec<String>>,
1856 },
1857 TradeWith {
1858 #[serde(default)]
1859 npc_id: Option<String>,
1860 template: String,
1861 #[serde(default = "default_true_view")]
1862 sell_all: bool,
1863 },
1864 WithdrawFrom {
1865 container_id: String,
1866 items: Vec<WorkerWithdrawItemView>,
1867 },
1868 CraftAt {
1869 device: String,
1870 blueprint: String,
1871 #[serde(default)]
1872 qty: Option<u32>,
1873 },
1874 CultivatePlot {
1875 plot_id: uuid::Uuid,
1876 },
1877 PlantPlot {
1878 plot_id: uuid::Uuid,
1879 seed_template: String,
1880 },
1881 HarvestPlot {
1882 plot_id: uuid::Uuid,
1883 },
1884 RestIfNeeded,
1885 Wait {
1886 #[serde(default)]
1887 wait_ticks: u64,
1888 },
1889}
1890
1891#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1893#[serde(rename_all = "snake_case")]
1894pub enum LedgerCategory {
1895 Workers,
1896 Hire,
1897 Train,
1898 ShopBuy,
1899 Taxes,
1900 WorkerSales,
1901 TraderSales,
1902 BankDeposit,
1903 BankWithdraw,
1904 BankTransferOut,
1905 BankTransferIn,
1906 BankTransferFee,
1907 StorageShipFee,
1908 PropertyBuy,
1910 PropertySell,
1912 TaxShare,
1914 MarketBuy,
1916 MarketSell,
1918 Other,
1919}
1920
1921impl LedgerCategory {
1922 pub fn as_str(self) -> &'static str {
1923 match self {
1924 Self::Workers => "workers",
1925 Self::Hire => "hire",
1926 Self::Train => "train",
1927 Self::ShopBuy => "shop_buy",
1928 Self::Taxes => "taxes",
1929 Self::WorkerSales => "worker_sales",
1930 Self::TraderSales => "trader_sales",
1931 Self::BankDeposit => "bank_deposit",
1932 Self::BankWithdraw => "bank_withdraw",
1933 Self::BankTransferOut => "bank_transfer_out",
1934 Self::BankTransferIn => "bank_transfer_in",
1935 Self::BankTransferFee => "bank_transfer_fee",
1936 Self::StorageShipFee => "storage_ship_fee",
1937 Self::PropertyBuy => "property_buy",
1938 Self::PropertySell => "property_sell",
1939 Self::TaxShare => "tax_share",
1940 Self::MarketBuy => "market_buy",
1941 Self::MarketSell => "market_sell",
1942 Self::Other => "other",
1943 }
1944 }
1945}
1946
1947#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1948pub struct LedgerEntryView {
1949 pub id: uuid::Uuid,
1950 pub game_day: u64,
1951 pub signed_copper: i64,
1952 pub category: LedgerCategory,
1953 #[serde(default)]
1954 pub label: String,
1955}
1956
1957#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1958pub struct LedgerPeriodTotals {
1959 #[serde(default)]
1961 pub expenses: std::collections::HashMap<String, u64>,
1962 #[serde(default)]
1964 pub income: std::collections::HashMap<String, u64>,
1965 pub expense_copper: u64,
1966 pub income_copper: u64,
1967 pub cash_flow_copper: i64,
1969}
1970
1971#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1972pub struct PlayerLedgerView {
1973 pub current_game_day: u64,
1974 #[serde(default)]
1975 pub period_day: LedgerPeriodTotals,
1976 #[serde(default)]
1977 pub period_week: LedgerPeriodTotals,
1978 #[serde(default)]
1979 pub period_month: LedgerPeriodTotals,
1980 #[serde(default)]
1981 pub period_lifetime: LedgerPeriodTotals,
1982 #[serde(default)]
1983 pub recent: Vec<LedgerEntryView>,
1984 #[serde(default)]
1986 pub wealth_on_person_copper: u64,
1987 #[serde(default)]
1989 pub wealth_in_storage_copper: u64,
1990 #[serde(default)]
1992 pub wealth_in_bank_copper: u64,
1993 #[serde(default)]
1995 pub wealth_total_copper: u64,
1996 #[serde(default)]
1998 pub wealth_in_property_copper: u64,
1999 #[serde(default)]
2001 pub wealth_net_worth_copper: u64,
2002 #[serde(default)]
2004 pub property_assets: Vec<PropertyAssetView>,
2005 #[serde(default)]
2007 pub property_market_nearby: Vec<PropertyMarketCompView>,
2008 #[serde(default)]
2010 pub live_expense_per_interval_copper: u64,
2011 #[serde(default)]
2013 pub live_income_route_est_per_loop_copper: u64,
2014 #[serde(default)]
2016 pub live_income_avg_per_interval_copper: u64,
2017 #[serde(default)]
2019 pub live_income_avg_window_intervals: u32,
2020 #[serde(default)]
2022 pub live_net_avg_per_interval_copper: i64,
2023}
2024
2025#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2027pub struct PropertyAssetView {
2028 pub plot_id: Uuid,
2029 pub label: String,
2031 pub zone_id: String,
2032 #[serde(default)]
2033 pub zone_label: Option<String>,
2034 pub area_m2: f32,
2035 pub purchase_basis_copper: u64,
2037 pub upkeep_copper_per_day: u64,
2038}
2039
2040#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2042pub struct PropertyMarketCompView {
2043 pub day: u64,
2044 pub zone_id: String,
2045 #[serde(default)]
2046 pub zone_label: Option<String>,
2047 pub area_m2: f32,
2048 pub price_copper: u64,
2049 pub price_per_m2_copper: u64,
2051 pub kind: String,
2053 pub distance_m: f32,
2055}
2056
2057#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2059#[serde(rename_all = "snake_case")]
2060pub enum AnalyticsMetric {
2061 NpcKill,
2062 WildlifeKill,
2063 Harvest,
2064 QuestComplete,
2065 QuestAccept,
2066 QuestAbandon,
2067 PlayerDeath,
2068 Craft,
2069 WorkerHire,
2070 WorkerDismiss,
2071 WorkerTeach,
2072 NpcTalk,
2073 ShopBuy,
2074 ShopSell,
2075 PlaceContainer,
2076 PickupContainer,
2077 PickupDrop,
2078 ConsumableUse,
2079 AbilityUse,
2080 DistanceWalkedM,
2081 DoorUse,
2082 BuildingEnter,
2083}
2084
2085impl AnalyticsMetric {
2086 pub fn as_str(self) -> &'static str {
2087 match self {
2088 Self::NpcKill => "npc_kill",
2089 Self::WildlifeKill => "wildlife_kill",
2090 Self::Harvest => "harvest",
2091 Self::QuestComplete => "quest_complete",
2092 Self::QuestAccept => "quest_accept",
2093 Self::QuestAbandon => "quest_abandon",
2094 Self::PlayerDeath => "player_death",
2095 Self::Craft => "craft",
2096 Self::WorkerHire => "worker_hire",
2097 Self::WorkerDismiss => "worker_dismiss",
2098 Self::WorkerTeach => "worker_teach",
2099 Self::NpcTalk => "npc_talk",
2100 Self::ShopBuy => "shop_buy",
2101 Self::ShopSell => "shop_sell",
2102 Self::PlaceContainer => "place_container",
2103 Self::PickupContainer => "pickup_container",
2104 Self::PickupDrop => "pickup_drop",
2105 Self::ConsumableUse => "consumable_use",
2106 Self::AbilityUse => "ability_use",
2107 Self::DistanceWalkedM => "distance_walked_m",
2108 Self::DoorUse => "door_use",
2109 Self::BuildingEnter => "building_enter",
2110 }
2111 }
2112
2113 pub fn from_str_key(s: &str) -> Option<Self> {
2114 Some(match s {
2115 "npc_kill" => Self::NpcKill,
2116 "wildlife_kill" => Self::WildlifeKill,
2117 "harvest" => Self::Harvest,
2118 "quest_complete" => Self::QuestComplete,
2119 "quest_accept" => Self::QuestAccept,
2120 "quest_abandon" => Self::QuestAbandon,
2121 "player_death" => Self::PlayerDeath,
2122 "craft" => Self::Craft,
2123 "worker_hire" => Self::WorkerHire,
2124 "worker_dismiss" => Self::WorkerDismiss,
2125 "worker_teach" => Self::WorkerTeach,
2126 "npc_talk" => Self::NpcTalk,
2127 "shop_buy" => Self::ShopBuy,
2128 "shop_sell" => Self::ShopSell,
2129 "place_container" => Self::PlaceContainer,
2130 "pickup_container" => Self::PickupContainer,
2131 "pickup_drop" => Self::PickupDrop,
2132 "consumable_use" => Self::ConsumableUse,
2133 "ability_use" => Self::AbilityUse,
2134 "distance_walked_m" => Self::DistanceWalkedM,
2135 "door_use" => Self::DoorUse,
2136 "building_enter" => Self::BuildingEnter,
2137 _ => return None,
2138 })
2139 }
2140}
2141
2142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2143pub struct CareerMetricRow {
2144 pub subject_id: String,
2145 pub amount: u64,
2146}
2147
2148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2150pub struct PlayerCareerView {
2151 pub current_game_day: u64,
2152 #[serde(default)]
2153 pub kills: Vec<CareerMetricRow>,
2154 #[serde(default)]
2155 pub harvests: Vec<CareerMetricRow>,
2156 pub quests_completed: u64,
2157 #[serde(default)]
2158 pub crafts: Vec<CareerMetricRow>,
2159 pub deaths: u64,
2160 pub npc_talks: u64,
2161 pub shop_buys: u64,
2162 pub shop_sells: u64,
2163 pub distance_m: u64,
2164 #[serde(default)]
2165 pub other: Vec<CareerMetricRow>,
2166}
2167
2168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2170pub struct HiredWorkerView {
2171 pub instance_id: String,
2172 pub entity_id: EntityId,
2173 pub def_id: String,
2174 pub label: String,
2176 pub x: f32,
2177 pub y: f32,
2178 pub z: f32,
2179 pub mode: WorkerModeView,
2180 pub state: WorkerStateView,
2181 #[serde(default)]
2182 pub step_label: String,
2183 pub vitals: WorkerVitalsSummary,
2184 #[serde(default)]
2185 pub carry_pct: f32,
2186 #[serde(default)]
2187 pub last_error: Option<String>,
2188 pub wage_copper_per_interval: u32,
2189 #[serde(default)]
2191 pub effective_wage_copper: u32,
2192 #[serde(default)]
2194 pub wage_meters_walked: f32,
2195 #[serde(default)]
2197 pub lodging_container_id: Option<String>,
2198 #[serde(default)]
2200 pub route: Option<WorkerRouteView>,
2201 #[serde(default)]
2204 pub route_stop_index: Option<u32>,
2205 #[serde(default)]
2207 pub known_blueprint_ids: Vec<String>,
2208 #[serde(default = "default_worker_view_level")]
2210 pub level: u32,
2211 #[serde(default)]
2213 pub worker_xp: f64,
2214 #[serde(default)]
2216 pub inventory: Vec<ItemStack>,
2217}
2218
2219fn default_worker_view_level() -> u32 {
2220 1
2221}
2222
2223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2225pub struct TickDelta {
2226 pub tick: Tick,
2227 pub entities: Vec<EntityState>,
2228 #[serde(default)]
2229 pub resource_nodes: Vec<ResourceNodeView>,
2230 #[serde(default)]
2231 pub buildings: Vec<BuildingView>,
2232 #[serde(default)]
2233 pub doors: Vec<DoorView>,
2234 #[serde(default)]
2235 pub npcs: Vec<NpcView>,
2236 #[serde(default)]
2238 pub inventory: Vec<ItemStack>,
2239 #[serde(default)]
2240 pub blueprints: Vec<BlueprintView>,
2241 #[serde(default)]
2242 pub world_clock: WorldClock,
2243 #[serde(default)]
2244 pub ground_drops: Vec<GroundDropView>,
2245 #[serde(default)]
2246 pub placed_containers: Vec<PlacedContainerView>,
2247 #[serde(default)]
2248 pub combat: Option<CombatHud>,
2249 #[serde(default)]
2250 pub interior_map: Option<InteriorMapView>,
2251 #[serde(default)]
2252 pub quest_log: Vec<QuestLogEntry>,
2253 #[serde(default)]
2254 pub hired_workers: Vec<HiredWorkerView>,
2255 #[serde(default)]
2256 pub interactables: Vec<InteractableView>,
2257 #[serde(default)]
2258 pub ledger: Option<PlayerLedgerView>,
2259 #[serde(default)]
2260 pub career: Option<PlayerCareerView>,
2261 #[serde(default)]
2263 pub combat_fx: Vec<CombatFx>,
2264 #[serde(default)]
2266 pub property_plots: Vec<PropertyPlotView>,
2267 #[serde(default)]
2269 pub terrain_overlays: Vec<TerrainZoneView>,
2270}
2271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2272pub struct GroundDropView {
2273 pub id: String,
2274 pub template_id: String,
2275 pub quantity: u32,
2276 pub x: f32,
2277 pub y: f32,
2278 pub z: f32,
2279 #[serde(default)]
2281 pub tile_id: Option<String>,
2282 #[serde(default)]
2284 pub display_name: Option<String>,
2285 #[serde(default)]
2287 pub yaw: f32,
2288 #[serde(default)]
2290 pub pitch: f32,
2291 #[serde(default)]
2293 pub roll: f32,
2294 #[serde(default = "default_draw_scale")]
2296 pub draw_scale: f32,
2297}
2298
2299fn default_draw_scale() -> f32 {
2300 1.0
2301}
2302
2303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2305pub struct Snapshot {
2306 pub tick: Tick,
2307 pub chunk_rev: u64,
2308 #[serde(default)]
2310 pub content_rev: u64,
2311 #[serde(default)]
2313 pub publish_rev: u64,
2314 pub entities: Vec<EntityState>,
2315 #[serde(default)]
2316 pub resource_nodes: Vec<ResourceNodeView>,
2317 #[serde(default)]
2319 pub world_x0: f32,
2320 #[serde(default)]
2321 pub world_y0: f32,
2322 #[serde(default)]
2324 pub world_width_m: f32,
2325 #[serde(default)]
2326 pub world_height_m: f32,
2327 #[serde(default)]
2328 pub buildings: Vec<BuildingView>,
2329 #[serde(default)]
2330 pub doors: Vec<DoorView>,
2331 #[serde(default)]
2332 pub npcs: Vec<NpcView>,
2333 #[serde(default)]
2334 pub inventory: Vec<ItemStack>,
2335 #[serde(default)]
2336 pub blueprints: Vec<BlueprintView>,
2337 #[serde(default)]
2338 pub world_clock: WorldClock,
2339 #[serde(default)]
2340 pub terrain_zones: Vec<TerrainZoneView>,
2341 #[serde(default)]
2342 pub z_platforms: Vec<ZPlatformView>,
2343 #[serde(default)]
2344 pub z_transitions: Vec<ZTransitionView>,
2345 #[serde(default)]
2346 pub ground_drops: Vec<GroundDropView>,
2347 #[serde(default)]
2348 pub placed_containers: Vec<PlacedContainerView>,
2349 #[serde(default)]
2350 pub combat: Option<CombatHud>,
2351 #[serde(default)]
2352 pub interior_map: Option<InteriorMapView>,
2353 #[serde(default)]
2354 pub quest_log: Vec<QuestLogEntry>,
2355 #[serde(default)]
2356 pub hired_workers: Vec<HiredWorkerView>,
2357 #[serde(default)]
2358 pub interactables: Vec<InteractableView>,
2359 #[serde(default)]
2360 pub ledger: Option<PlayerLedgerView>,
2361 #[serde(default)]
2362 pub career: Option<PlayerCareerView>,
2363 #[serde(default)]
2365 pub combat_fx: Vec<CombatFx>,
2366 #[serde(default)]
2368 pub property_zones: Vec<PropertyZoneView>,
2369 #[serde(default)]
2371 pub tax_zones: Vec<TaxZoneView>,
2372 #[serde(default)]
2374 pub boundary_zones: Vec<BoundaryZoneView>,
2375 #[serde(default)]
2377 pub growth_zones: Vec<GrowthZoneView>,
2378 #[serde(default)]
2380 pub biome_zones: Vec<BiomeZoneView>,
2381 #[serde(default)]
2383 pub property_plots: Vec<PropertyPlotView>,
2384 #[serde(default)]
2386 pub property_plot_settings: Option<PropertyPlotSettingsView>,
2387}
2388
2389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2391pub struct ResourceNodeView {
2392 pub id: String,
2393 pub label: String,
2394 pub x: f32,
2395 pub y: f32,
2396 pub z: f32,
2397 pub item_template: String,
2398 #[serde(default = "default_node_state")]
2399 pub state: ResourceNodeState,
2400 #[serde(default = "default_blocking_view")]
2402 pub blocking: bool,
2403 #[serde(default = "default_blocking_radius_view")]
2405 pub blocking_radius_m: f32,
2406 #[serde(default)]
2408 pub tile_id: Option<String>,
2409 #[serde(default)]
2411 pub yaw: f32,
2412 #[serde(default)]
2414 pub pitch: f32,
2415 #[serde(default)]
2417 pub roll: f32,
2418 #[serde(default = "default_draw_scale")]
2420 pub draw_scale: f32,
2421 #[serde(default)]
2423 pub sprite_mode: Option<String>,
2424 #[serde(default)]
2426 pub presentation_state: Option<String>,
2427 #[serde(default)]
2430 pub growth_progress: Option<f32>,
2431 #[serde(default)]
2433 pub channel_start_tick: Option<Tick>,
2434 #[serde(default)]
2435 pub channel_end_tick: Option<Tick>,
2436 #[serde(default)]
2438 pub harvest_drop_templates: Vec<String>,
2439}
2440
2441fn default_blocking_radius_view() -> f32 {
2442 0.8
2443}
2444
2445fn default_blocking_view() -> bool {
2446 true
2447}
2448
2449#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2450#[serde(rename_all = "snake_case")]
2451pub enum ResourceNodeState {
2452 Available,
2453 Harvesting,
2454 Cooldown,
2455}
2456fn default_node_state() -> ResourceNodeState {
2457 ResourceNodeState::Available
2458}
2459
2460#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2462#[serde(rename_all = "snake_case")]
2463pub enum ItemSpawnStateView {
2464 Spawned,
2465 PickedUp {
2466 respawn_at_tick: u64,
2467 },
2468 Consumed,
2469}
2470
2471#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2473pub struct ItemSpawnView {
2474 pub id: String,
2475 pub label: String,
2476 pub item_template: String,
2477 pub quantity: u32,
2478 pub x: f32,
2479 pub y: f32,
2480 pub z: f32,
2481 pub respawn_ticks: u32,
2482 #[serde(default)]
2483 pub building_id: Option<String>,
2484 pub state: ItemSpawnStateView,
2485}
2486
2487#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2488#[serde(rename_all = "snake_case")]
2489pub enum ItemStatusBindingMode {
2490 OnHit,
2491 WhileEquipped,
2492}
2493
2494impl Default for ItemStatusBindingMode {
2495 fn default() -> Self {
2496 Self::OnHit
2497 }
2498}
2499
2500#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2502pub struct ItemStatusBinding {
2503 pub effect_id: String,
2504 #[serde(default)]
2505 pub mode: ItemStatusBindingMode,
2506 #[serde(default)]
2508 pub source: String,
2509 #[serde(default)]
2510 pub applied_at_tick: u64,
2511 #[serde(default, skip_serializing_if = "Option::is_none")]
2513 pub expires_at_tick: Option<u64>,
2514}
2515
2516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2517pub struct ItemStack {
2518 pub template_id: String,
2519 pub quantity: u32,
2520 #[serde(default)]
2522 pub item_instance_id: Option<Uuid>,
2523 #[serde(default)]
2525 pub props: BTreeMap<String, String>,
2526 #[serde(default)]
2528 pub status_bindings: Vec<ItemStatusBinding>,
2529 #[serde(default)]
2531 pub contents: Vec<ItemStack>,
2532 #[serde(default)]
2534 pub display_name: Option<String>,
2535 #[serde(default)]
2537 pub category: Option<String>,
2538 #[serde(default)]
2540 pub base_mass: Option<f32>,
2541 #[serde(default)]
2543 pub base_volume: Option<f32>,
2544 #[serde(default)]
2546 pub capacity_volume: Option<f32>,
2547 #[serde(default)]
2549 pub stackable: Option<bool>,
2550 #[serde(default)]
2552 pub world_placeable: Option<bool>,
2553 #[serde(default)]
2555 pub worker_lodging_capacity: Option<u32>,
2556 #[serde(default)]
2558 pub equip_slot: Option<BodySlot>,
2559 #[serde(default)]
2561 pub armor_physical: Option<f32>,
2562 #[serde(default)]
2564 pub resists: Vec<(String, f32)>,
2565 #[serde(default)]
2567 pub hand_slots: Option<u8>,
2568 #[serde(default)]
2570 pub listable: Option<bool>,
2571}
2572
2573impl ItemStack {
2574 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
2575 Self {
2576 template_id: template_id.into(),
2577 quantity,
2578 ..Default::default()
2579 }
2580 }
2581}
2582
2583impl Default for ItemStack {
2584 fn default() -> Self {
2585 Self {
2586 template_id: String::new(),
2587 quantity: 0,
2588 item_instance_id: None,
2589 props: BTreeMap::new(),
2590 status_bindings: Vec::new(),
2591 contents: Vec::new(),
2592 display_name: None,
2593 category: None,
2594 base_mass: None,
2595 base_volume: None,
2596 capacity_volume: None,
2597 stackable: None,
2598 world_placeable: None,
2599 worker_lodging_capacity: None,
2600 equip_slot: None,
2601 armor_physical: None,
2602 resists: Vec::new(),
2603 hand_slots: None,
2604 listable: None,
2605 }
2606 }
2607}
2608
2609#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2611#[serde(rename_all = "snake_case")]
2612pub enum EncumbranceState {
2613 #[default]
2614 Light,
2615 Heavy,
2616 Over,
2617}
2618
2619#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
2623#[serde(rename_all = "snake_case")]
2624pub enum BodySlot {
2625 Head,
2626 #[serde(alias = "body")]
2628 Chest,
2629 #[serde(alias = "arms")]
2631 Forearms,
2632 Legs,
2633 Feet,
2634 Cloak,
2635 Back,
2636 Waist,
2637 Earrings,
2638 Necklace,
2639 Eyeglasses,
2640 #[serde(rename = "ring_left_1", alias = "ring_left1")]
2642 RingLeft1,
2643 #[serde(rename = "ring_left_2", alias = "ring_left2")]
2644 RingLeft2,
2645 #[serde(rename = "ring_right_1", alias = "ring_right1")]
2646 RingRight1,
2647 #[serde(rename = "ring_right_2", alias = "ring_right2")]
2648 RingRight2,
2649}
2650
2651impl BodySlot {
2652 pub const ALL: [BodySlot; 15] = [
2654 BodySlot::Head,
2655 BodySlot::Chest,
2656 BodySlot::Forearms,
2657 BodySlot::Legs,
2658 BodySlot::Feet,
2659 BodySlot::Cloak,
2660 BodySlot::Back,
2661 BodySlot::Waist,
2662 BodySlot::Earrings,
2663 BodySlot::Necklace,
2664 BodySlot::Eyeglasses,
2665 BodySlot::RingLeft1,
2666 BodySlot::RingLeft2,
2667 BodySlot::RingRight1,
2668 BodySlot::RingRight2,
2669 ];
2670
2671 pub fn as_str(self) -> &'static str {
2672 match self {
2673 BodySlot::Head => "head",
2674 BodySlot::Chest => "chest",
2675 BodySlot::Forearms => "forearms",
2676 BodySlot::Legs => "legs",
2677 BodySlot::Feet => "feet",
2678 BodySlot::Cloak => "cloak",
2679 BodySlot::Back => "back",
2680 BodySlot::Waist => "waist",
2681 BodySlot::Earrings => "earrings",
2682 BodySlot::Necklace => "necklace",
2683 BodySlot::Eyeglasses => "eyeglasses",
2684 BodySlot::RingLeft1 => "ring_left_1",
2685 BodySlot::RingLeft2 => "ring_left_2",
2686 BodySlot::RingRight1 => "ring_right_1",
2687 BodySlot::RingRight2 => "ring_right_2",
2688 }
2689 }
2690}
2691
2692#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2694#[serde(rename_all = "snake_case")]
2695pub enum InventoryLocation {
2696 Root,
2698 Worn { slot: BodySlot },
2700 Placed { container_id: String },
2702 Keychain,
2704 WhisperPouch,
2706}
2707
2708#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2710pub struct PlacedContainerView {
2711 pub id: String,
2712 pub template_id: String,
2713 pub display_name: String,
2714 pub x: f32,
2715 pub y: f32,
2716 pub z: f32,
2717 pub locked: bool,
2718 #[serde(default)]
2720 pub accessible: bool,
2721 #[serde(default)]
2722 pub owner_character_id: Option<Uuid>,
2723 #[serde(default)]
2725 pub contents: Vec<ItemStack>,
2726 #[serde(default)]
2728 pub lock_id: Option<String>,
2729 #[serde(default)]
2731 pub capacity_volume: Option<f32>,
2732 #[serde(default)]
2734 pub item_instance_id: Option<Uuid>,
2735 #[serde(default)]
2737 pub tile_id: Option<String>,
2738 #[serde(default)]
2740 pub worker_lodging_capacity: Option<u32>,
2741 #[serde(default)]
2743 pub blocking: bool,
2744 #[serde(default)]
2746 pub blocking_radius_m: f32,
2747}
2748
2749#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2750pub struct BlueprintIngredientView {
2751 pub template_id: String,
2752 pub quantity: u32,
2753 pub consumed: bool,
2755}
2756
2757#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2758pub struct ToolRequirementView {
2759 pub item: String,
2760 pub consumed: bool,
2762}
2763
2764#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2765pub struct SkillRequirementView {
2766 pub skill: String,
2767 pub level: u32,
2768}
2769
2770#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2771pub struct BlueprintView {
2772 pub id: String,
2773 pub label: String,
2774 pub output: String,
2775 pub output_qty: u32,
2776 pub craft_ticks: u32,
2777 pub inputs: Vec<BlueprintIngredientView>,
2778 #[serde(default)]
2780 pub station: Option<String>,
2781 #[serde(default)]
2782 pub category: Option<String>,
2783 #[serde(default)]
2784 pub required_tools: Vec<ToolRequirementView>,
2785 #[serde(default)]
2786 pub skill: Option<SkillRequirementView>,
2787 #[serde(default)]
2788 pub failure_chance: f32,
2789 #[serde(default)]
2791 pub worker_train_copper: u64,
2792}
2793
2794#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2796#[serde(rename_all = "snake_case")]
2797pub enum TerrainKindView {
2798 #[default]
2799 Grass,
2800 Dirt,
2801 Tilled,
2802 Desert,
2803 Hill,
2804 Bog,
2805 Beach,
2806 ShallowWater,
2807 DeepWater,
2808 Trail,
2809 Road,
2810 Rock,
2811}
2812
2813#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2814pub struct TerrainZoneView {
2815 pub id: String,
2816 pub x0: f32,
2817 pub y0: f32,
2818 pub x1: f32,
2819 pub y1: f32,
2820 #[serde(default)]
2821 pub kind: TerrainKindView,
2822 #[serde(default)]
2824 pub elevation: f32,
2825 #[serde(default)]
2828 pub glyph: Option<String>,
2829 #[serde(default)]
2831 pub color: Option<String>,
2832 #[serde(default)]
2834 pub tile_id: Option<String>,
2835 #[serde(default)]
2837 pub z_order: i32,
2838 #[serde(default)]
2840 pub channel_start_tick: Option<Tick>,
2841 #[serde(default)]
2842 pub channel_end_tick: Option<Tick>,
2843}
2844
2845#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2847pub struct ZoneRectView {
2848 pub x0: f32,
2849 pub y0: f32,
2850 pub x1: f32,
2851 pub y1: f32,
2852}
2853
2854#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2856pub struct PropertyZoneView {
2857 pub id: String,
2858 #[serde(default)]
2860 pub label: Option<String>,
2861 pub rects: Vec<ZoneRectView>,
2862 #[serde(default)]
2863 pub z_order: i32,
2864 pub crown_price_copper: u64,
2865 pub upkeep_copper_per_day: u64,
2866 #[serde(default)]
2867 pub max_area_m2: Option<f32>,
2868 #[serde(default)]
2869 pub owner_tax_discount_bps: u32,
2870}
2871
2872#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2874pub struct TaxZoneView {
2875 pub id: String,
2876 #[serde(default)]
2877 pub label: Option<String>,
2878 pub rects: Vec<ZoneRectView>,
2879 #[serde(default)]
2880 pub z_order: i32,
2881 pub rate_bps: u32,
2882 #[serde(default)]
2883 pub flat_copper: u64,
2884 #[serde(default)]
2886 pub market_sales_tax_bps: u32,
2887 #[serde(default)]
2889 pub market_sales_flat_copper: u32,
2890}
2891
2892#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2894pub struct BoundaryZoneView {
2895 pub id: String,
2896 #[serde(default)]
2897 pub label: Option<String>,
2898 pub rects: Vec<ZoneRectView>,
2899 #[serde(default)]
2900 pub z_order: i32,
2901 #[serde(default, skip_serializing_if = "Option::is_none")]
2902 pub jurisdiction_id: Option<String>,
2903 #[serde(default = "default_true")]
2904 pub worker_logistics: bool,
2905 #[serde(default)]
2906 pub security_tier: String,
2907 #[serde(default)]
2908 pub pvp_mode: String,
2909 #[serde(default = "default_true")]
2910 pub crime_enabled: bool,
2911 #[serde(default)]
2912 pub guard_response: bool,
2913}
2914
2915fn default_true() -> bool {
2916 true
2917}
2918
2919#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2921pub struct GrowthZoneView {
2922 pub id: String,
2923 #[serde(default)]
2924 pub label: Option<String>,
2925 pub rects: Vec<ZoneRectView>,
2926 #[serde(default)]
2927 pub z_order: i32,
2928 #[serde(default = "default_one_f32")]
2929 pub fertility: f32,
2930}
2931
2932fn default_one_f32() -> f32 {
2933 1.0
2934}
2935
2936#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2938pub struct BiomeZoneView {
2939 pub id: String,
2940 #[serde(default)]
2941 pub label: Option<String>,
2942 pub rects: Vec<ZoneRectView>,
2943 #[serde(default)]
2944 pub z_order: i32,
2945 pub biome_id: String,
2946}
2947
2948#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2950pub struct FarmGrantView {
2951 pub character_id: Uuid,
2952 #[serde(default)]
2954 pub character_label: String,
2955 pub tax_discount_bps: u32,
2956}
2957
2958#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2960pub struct PropertyPlotView {
2961 pub plot_id: Uuid,
2962 pub property_zone_id: String,
2963 #[serde(default)]
2964 pub zone_label: Option<String>,
2965 pub deed_instance_id: Uuid,
2966 pub x0: f32,
2967 pub y0: f32,
2968 pub x1: f32,
2969 pub y1: f32,
2970 pub upkeep_copper_per_day: u64,
2971 pub arrears_days: u32,
2972 #[serde(default)]
2974 pub is_mine: bool,
2975 #[serde(default)]
2977 pub may_farm: bool,
2978 #[serde(default)]
2980 pub purchase_basis_copper: u64,
2981 #[serde(default)]
2982 pub farm_public: bool,
2983 #[serde(default)]
2984 pub public_tax_discount_bps: u32,
2985 #[serde(default)]
2986 pub farm_allow: Vec<FarmGrantView>,
2987 #[serde(default)]
2989 pub owner_character_id: Option<Uuid>,
2990 #[serde(default)]
2991 pub owner_label: Option<String>,
2992}
2993
2994#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2996pub struct PropertyPlotSettingsView {
2997 pub min_plot_area_m2: f32,
2998 pub tax_premium_weight: f32,
2999 pub sellback_bps: u32,
3000}
3001
3002#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3004pub struct ZPlatformView {
3005 pub id: String,
3006 pub z: f32,
3007 pub x0: f32,
3008 pub y0: f32,
3009 pub x1: f32,
3010 pub y1: f32,
3011}
3012
3013#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3015pub struct ZTransitionView {
3016 pub id: String,
3017 pub z_from: f32,
3018 pub z_to: f32,
3019 pub x0: f32,
3020 pub y0: f32,
3021 pub x1: f32,
3022 pub y1: f32,
3023}
3024
3025#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3026pub struct BuildingView {
3027 pub id: String,
3028 pub label: String,
3029 pub x: f32,
3030 pub y: f32,
3031 pub width_m: f32,
3032 pub depth_m: f32,
3033 #[serde(default)]
3034 pub interior_blueprint: Option<String>,
3035 #[serde(default)]
3036 pub tags: Vec<String>,
3037 #[serde(default)]
3039 pub market_boundary_zone_ids: Vec<String>,
3040 #[serde(default)]
3042 pub market_max_volume: Option<f32>,
3043 #[serde(default)]
3046 pub wall_set: Option<String>,
3047 #[serde(default)]
3049 pub roof_set: Option<String>,
3050}
3051
3052pub const DEFAULT_BUILDING_ART_SET: &str = "classic_stone";
3055
3056impl BuildingView {
3057 pub fn effective_wall_set(&self) -> &str {
3058 self.wall_set
3059 .as_deref()
3060 .filter(|s| !s.is_empty())
3061 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3062 }
3063
3064 pub fn effective_roof_set(&self) -> &str {
3065 self.roof_set
3066 .as_deref()
3067 .filter(|s| !s.is_empty())
3068 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3069 }
3070}
3071
3072#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3073pub struct DoorView {
3074 pub id: String,
3075 pub building_id: String,
3076 pub x: f32,
3077 pub y: f32,
3078 #[serde(default)]
3079 pub open: bool,
3080 #[serde(default)]
3081 pub portal: Option<String>,
3082}
3083
3084#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3085pub struct InteriorRoomView {
3086 pub id: String,
3087 pub label: String,
3088 pub floor: i32,
3089 pub x0: f32,
3090 pub y0: f32,
3091 pub x1: f32,
3092 pub y1: f32,
3093 #[serde(default)]
3094 pub floor_color: Option<String>,
3095 #[serde(default)]
3096 pub floor_glyph: Option<String>,
3097}
3098
3099#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3100pub struct InteriorDoorView {
3101 pub id: String,
3102 pub room_a: String,
3103 pub room_b: String,
3104 pub x: f32,
3105 pub y: f32,
3106 pub kind: String,
3107 #[serde(default)]
3108 pub x_a: Option<f32>,
3109 #[serde(default)]
3110 pub y_a: Option<f32>,
3111 #[serde(default)]
3112 pub x_b: Option<f32>,
3113 #[serde(default)]
3114 pub y_b: Option<f32>,
3115}
3116
3117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3118pub struct InteriorMapView {
3119 pub building_id: String,
3120 pub blueprint_id: String,
3121 pub background_color: String,
3122 #[serde(default)]
3123 pub default_floor_color: Option<String>,
3124 #[serde(default = "default_floor_height_view")]
3125 pub floor_height_m: f32,
3126 #[serde(default)]
3128 pub z_platforms: Vec<ZPlatformView>,
3129 #[serde(default)]
3130 pub z_transitions: Vec<ZTransitionView>,
3131 pub rooms: Vec<InteriorRoomView>,
3132 #[serde(default)]
3133 pub room_doors: Vec<InteriorDoorView>,
3134}
3135
3136fn default_floor_height_view() -> f32 {
3137 3.0
3138}
3139
3140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3141pub struct NpcView {
3142 pub id: String,
3143 pub label: String,
3144 pub role: String,
3145 pub x: f32,
3146 pub y: f32,
3147 #[serde(default)]
3149 pub building_id: Option<String>,
3150 #[serde(default)]
3152 pub entity_id: Option<EntityId>,
3153 #[serde(default)]
3154 pub life_state: Option<LifeState>,
3155 #[serde(default)]
3156 pub hp_pct: Option<f32>,
3157 #[serde(default)]
3159 pub can_trade: bool,
3160 #[serde(default)]
3162 pub tile_id: Option<String>,
3163 #[serde(default)]
3165 pub behavior_state: Option<String>,
3166 #[serde(default)]
3168 pub presentation_state: Option<String>,
3169 #[serde(default)]
3171 pub sprite_mode: Option<String>,
3172 #[serde(default)]
3174 pub paperdoll_ref: Option<String>,
3175}
3176
3177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3178pub struct UseResult {
3179 pub template_id: String,
3180 pub hunger_restored: f32,
3181 pub thirst_restored: f32,
3182 #[serde(default)]
3183 pub health_restored: f32,
3184 #[serde(default)]
3185 pub mana_restored: f32,
3186 #[serde(default)]
3187 pub cleared_dot_ids: Vec<String>,
3188}
3189
3190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3191pub struct CraftResult {
3192 pub blueprint_id: String,
3193 pub outputs: Vec<ItemStack>,
3194 pub consumed: Vec<ItemStack>,
3195 #[serde(default = "default_one")]
3197 pub batch_index: u32,
3198 #[serde(default = "default_one")]
3200 pub batch_total: u32,
3201}
3202
3203fn default_one() -> u32 {
3204 1
3205}
3206
3207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3208pub struct DeathNotice {
3209 pub entity_id: EntityId,
3210 pub respawn_x: f32,
3211 pub respawn_y: f32,
3212 pub message: String,
3213}
3214
3215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3216pub struct InteractionNotice {
3217 pub target_id: String,
3218 pub message: String,
3219 #[serde(default)]
3220 pub coins_delta: i32,
3221 #[serde(default)]
3222 pub inventory_delta: Vec<ItemStack>,
3223}
3224
3225#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3226#[serde(rename_all = "snake_case")]
3227pub enum NpcTalkTrustFlag {
3228 Stranger,
3229 Acquainted,
3230 Trusted,
3231}
3232
3233#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3234#[serde(rename_all = "snake_case")]
3235pub enum NpcTalkDepth {
3236 #[default]
3237 Full,
3238 Brief,
3239 Unavailable,
3240}
3241
3242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3243pub struct NpcTalkOpened {
3244 pub npc_id: String,
3245 pub npc_label: String,
3246 pub greeting: String,
3247 pub trust_flag: NpcTalkTrustFlag,
3248 #[serde(default)]
3249 pub talk_depth: NpcTalkDepth,
3250 #[serde(default = "default_true")]
3251 pub trade_allowed: bool,
3252}
3253
3254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3255pub struct NpcTalkPending {
3256 pub npc_id: String,
3257}
3258
3259#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3260pub struct NpcTalkReply {
3261 pub npc_id: String,
3262 pub line: String,
3263 pub trust_flag: NpcTalkTrustFlag,
3264 #[serde(default)]
3265 pub wind_down: bool,
3266 #[serde(default)]
3267 pub trade_disabled: bool,
3268}
3269
3270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3271pub struct NpcTalkClosed {
3272 pub npc_id: String,
3273}
3274
3275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3276pub struct NpcTalkError {
3277 pub npc_id: String,
3278 pub reason: String,
3279}
3280
3281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3282#[serde(rename_all = "snake_case")]
3283pub enum QuestStatusView {
3284 Available,
3285 Active,
3286 Completed,
3287}
3288
3289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3290pub struct QuestObjectiveProgress {
3291 pub label: String,
3292 pub current: u32,
3293 pub required: u32,
3294 pub done: bool,
3295}
3296
3297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3298pub struct QuestLogEntry {
3299 pub quest_id: String,
3300 pub title: String,
3301 pub description: String,
3302 pub status: QuestStatusView,
3303 #[serde(default)]
3304 pub current_step_id: Option<String>,
3305 #[serde(default)]
3306 pub current_step_title: String,
3307 #[serde(default)]
3308 pub objectives: Vec<QuestObjectiveProgress>,
3309 #[serde(default)]
3310 pub is_tracked: bool,
3311 #[serde(default)]
3312 pub can_withdraw: bool,
3313}
3314
3315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3316pub struct InteractableView {
3317 pub id: String,
3318 pub kind: String,
3319 pub label: String,
3320 pub x: f32,
3321 pub y: f32,
3322 pub z: f32,
3323 #[serde(default)]
3324 pub board_id: Option<String>,
3325}
3326
3327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3328pub struct QuestOffer {
3329 pub quest_id: String,
3330 pub title: String,
3331 pub description: String,
3332 #[serde(default)]
3333 pub step_count: u32,
3334}
3335
3336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3337pub struct QuestNotice {
3338 pub quest_id: String,
3339 pub title: String,
3340 pub message: String,
3341}
3342
3343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3344#[serde(rename_all = "snake_case")]
3345pub enum ShopOfferKind {
3346 Item,
3347 Blueprint,
3348}
3349
3350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3351pub struct ShopOffer {
3352 pub offer_id: String,
3353 pub kind: ShopOfferKind,
3354 pub label: String,
3355 #[serde(default)]
3356 pub template_id: Option<String>,
3357 #[serde(default)]
3358 pub blueprint_id: Option<String>,
3359 pub price_copper: u32,
3360 #[serde(default)]
3361 pub affordable: bool,
3362 #[serde(default)]
3363 pub already_owned: bool,
3364}
3365
3366#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3367pub struct ShopBuyLine {
3368 pub template_id: String,
3369 pub label: String,
3370 pub quantity: u32,
3371 pub price_copper: u32,
3372}
3373
3374#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3376pub struct BankPanel {
3377 pub npc_id: String,
3378 pub npc_label: String,
3379 pub bank_balance_copper: u64,
3380 pub on_person_copper: u64,
3381 #[serde(default)]
3383 pub pending_outgoing_copper: u64,
3384 #[serde(default)]
3385 pub transfer_fee_bps: u32,
3386 #[serde(default)]
3387 pub transfer_clear_ticks: u64,
3388}
3389
3390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3392pub struct StoragePanel {
3393 pub npc_id: String,
3394 pub npc_label: String,
3395 pub building_id: String,
3396 pub building_label: String,
3397 pub used_volume: f32,
3398 pub max_volume: f32,
3399 #[serde(default)]
3400 pub contents: Vec<ItemStack>,
3401 #[serde(default)]
3403 pub ship_destinations: Vec<StorageShipDest>,
3404}
3405
3406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3407pub struct StorageShipDest {
3408 pub building_id: String,
3409 pub label: String,
3410 pub distance_m: f32,
3411 pub fee_copper: u64,
3412 pub travel_ticks: u64,
3413}
3414
3415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3418pub enum GoodsLocation {
3419 Person,
3421 TownStorage { building_id: String },
3424}
3425
3426#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3429pub struct MarketListingView {
3430 pub listing_id: Uuid,
3431 pub seller_character_id: Uuid,
3432 pub seller_label: String,
3434 pub hall_building_id: String,
3435 pub hall_label: String,
3436 pub template_id: String,
3437 pub display_name: String,
3438 #[serde(default)]
3440 pub category: String,
3441 pub quantity: u32,
3442 pub unit_price_copper: u64,
3443 pub line_total_copper: u64,
3445 pub mine: bool,
3447}
3448
3449#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3451pub struct MarketListVault {
3452 pub building_id: String,
3453 pub building_label: String,
3455 #[serde(default)]
3456 pub contents: Vec<ItemStack>,
3457}
3458
3459#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3462pub struct MarketPanel {
3463 pub npc_id: String,
3464 pub npc_label: String,
3465 pub building_id: String,
3466 pub building_label: String,
3467 pub used_volume: f32,
3469 pub max_volume: f32,
3470 #[serde(default)]
3473 pub listings: Vec<MarketListingView>,
3474 #[serde(default)]
3476 pub tax_bps: u32,
3477 #[serde(default)]
3478 pub tax_flat_copper: u32,
3479 #[serde(default)]
3481 pub list_vaults: Vec<MarketListVault>,
3482}
3483
3484#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3485pub struct ShopCatalog {
3486 pub npc_id: String,
3487 pub npc_label: String,
3488 #[serde(default)]
3489 pub sells: Vec<ShopOffer>,
3490 #[serde(default)]
3491 pub buys: Vec<ShopBuyLine>,
3492}
3493
3494#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3495pub struct HarvestResult {
3496 pub node_id: String,
3497 pub quantity: u32,
3499 pub item_template: String,
3500 #[serde(default)]
3503 pub item_instance_id: Option<Uuid>,
3504}
3505
3506#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3508pub struct Envelope<T> {
3509 pub protocol_version: u16,
3510 pub payload: T,
3511}
3512
3513impl<T> Envelope<T> {
3514 pub fn new(payload: T) -> Self {
3515 Self {
3516 protocol_version: crate::PROTOCOL_VERSION,
3517 payload,
3518 }
3519 }
3520}
3521
3522#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3524pub struct Hello {
3525 pub client_name: String,
3526 pub protocol_version: u16,
3527 #[serde(default)]
3528 pub auth: AuthCredential,
3529 #[serde(default)]
3531 pub character_id: Option<Uuid>,
3532}
3533
3534#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3537#[serde(rename_all = "snake_case")]
3538pub enum AuthCredential {
3539 DevLocal,
3540 Session { token: String },
3541 ApiToken { token: String, character_id: Uuid },
3542}
3543
3544impl Default for AuthCredential {
3545 fn default() -> Self {
3546 Self::DevLocal
3547 }
3548}
3549
3550#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3551pub struct Welcome {
3552 pub session_id: SessionId,
3553 pub entity_id: EntityId,
3554 pub snapshot: Snapshot,
3555}
3556
3557#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3558pub enum ServerMessage {
3559 Welcome(Welcome),
3560 ContentUpdated(Snapshot),
3562 Tick(TickDelta),
3563 IntentAck {
3564 entity_id: EntityId,
3565 seq: Seq,
3566 tick: Tick,
3567 },
3568 Chat(ChatMessage),
3569 HarvestResult(HarvestResult),
3570 UseResult(UseResult),
3571 CraftResult(CraftResult),
3572 Death(DeathNotice),
3573 Interaction(InteractionNotice),
3574 ShopOpened(ShopCatalog),
3575 NpcTalkOpened(NpcTalkOpened),
3576 NpcTalkPending(NpcTalkPending),
3577 NpcTalkReply(NpcTalkReply),
3578 NpcTalkClosed(NpcTalkClosed),
3579 NpcTalkError(NpcTalkError),
3580 QuestOffer(QuestOffer),
3581 QuestAccepted(QuestNotice),
3582 QuestWithdrawn(QuestNotice),
3583 QuestStepCompleted(QuestNotice),
3584 QuestCompleted(QuestNotice),
3585 BankOpened(BankPanel),
3587 StorageOpened(StoragePanel),
3589 MarketOpened(MarketPanel),
3591 TradeOpened(TradePanel),
3593 TradeClosed {
3595 reason: String,
3596 },
3597}
3598
3599#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3601pub struct TradePanel {
3602 pub peer_entity_id: EntityId,
3603 pub peer_name: String,
3604 pub my_presented: Vec<ItemStack>,
3605 pub their_presented: Vec<ItemStack>,
3606 pub i_ready: bool,
3607 pub they_ready: bool,
3608 pub my_mass_after: f32,
3610 pub my_mass_max: f32,
3611 pub my_encumbrance_after: EncumbranceState,
3612 pub overburden_warning: bool,
3614}
3615
3616#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3617pub enum ClientMessage {
3618 Hello(Hello),
3619 Intent(Intent),
3620 Disconnect,
3621}
3622
3623#[cfg(test)]
3624mod tests {
3625 use super::*;
3626
3627 #[test]
3628 fn pristine_vitals_state_yields_full_pools() {
3629 let attrs = PrimaryAttributes::default();
3630 let vitals = StoredVitalsState::default().apply_to(attrs);
3631 assert!(vitals.health > 0.0);
3632 assert_eq!(vitals.health, vitals.health_max);
3633 assert!((vitals.mana_max - 61.0).abs() < 0.01);
3634 }
3635
3636 #[test]
3637 fn humanize_snake_id_title_cases_parts() {
3638 assert_eq!(humanize_snake_id("heal_touch"), "Heal Touch");
3639 assert_eq!(humanize_snake_id("fireball"), "Fireball");
3640 assert_eq!(humanize_snake_id("cone_frost"), "Cone Frost");
3641 }
3642
3643 #[test]
3644 fn saved_vitals_scale_when_pool_max_increases() {
3645 let mut attrs = PrimaryAttributes::default();
3646 attrs.intelligence = 140;
3647 attrs.wisdom = 140;
3648 let saved = StoredVitalsState {
3649 health: 100.0,
3650 mana: 14.0,
3651 stamina: 100.0,
3652 ..StoredVitalsState::default()
3653 };
3654 let vitals = saved.apply_to(attrs);
3655 assert!(vitals.mana_max > 55.0);
3656 assert!(
3657 (vitals.mana - vitals.mana_max).abs() < 0.01,
3658 "full legacy mana bar migrates to full new bar"
3659 );
3660 }
3661
3662 #[test]
3663 fn empty_vitals_state_is_pristine() {
3664 let pristine = StoredVitalsState {
3665 health: 0.0,
3666 mana: 0.0,
3667 stamina: 0.0,
3668 hunger: 0.0,
3669 thirst: 0.0,
3670 coins: 0,
3671 deaths: 0,
3672 life_state: LifeState::Alive,
3673 };
3674 assert!(pristine.is_pristine());
3675 let vitals = pristine.apply_to(PrimaryAttributes::default());
3676 assert!(vitals.health > 0.0);
3677 }
3678
3679 #[test]
3680 fn stored_vitals_roundtrip_preserves_partial_pools() {
3681 let attrs = PrimaryAttributes::default();
3682 let mut live = PlayerVitals::from_attributes(attrs);
3683 live.health = 25.0;
3684 live.hunger = 77.0;
3685 live.deaths = 2;
3686 let stored = StoredVitalsState::from_live(&live);
3687 let restored = stored.apply_to(attrs);
3688 assert!(
3689 (restored.health - 25.0).abs() < 0.01,
3690 "partial HP below cap stays absolute"
3691 );
3692 assert_eq!(restored.hunger, 77.0);
3693 assert_eq!(restored.deaths, 2);
3694 }
3695
3696 #[test]
3697 fn skill_tiers_start_at_zero() {
3698 let skill = SkillProgress::default();
3699 assert_eq!(skill.level, 0);
3700 assert_eq!(skill.display_tier(), 0);
3701 let trained = SkillProgress {
3702 level: 250,
3703 last_trained_tick: 1,
3704 };
3705 assert_eq!(trained.display_tier(), 2);
3706 }
3707
3708 #[test]
3709 fn quest_server_messages_roundtrip_json() {
3710 use crate::codec::{Codec, PostcardCodec};
3711
3712 let offer = ServerMessage::QuestOffer(QuestOffer {
3713 quest_id: "ada_goblin_hunt".into(),
3714 title: "Goblin Trouble".into(),
3715 description: "Help Ada".into(),
3716 step_count: 3,
3717 });
3718 let notice = ServerMessage::QuestAccepted(QuestNotice {
3719 quest_id: "ada_goblin_hunt".into(),
3720 title: "Goblin Trouble".into(),
3721 message: "Quest accepted".into(),
3722 });
3723 for msg in [offer, notice] {
3724 let bytes = PostcardCodec.encode(&msg).unwrap();
3725 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
3726 assert_eq!(decoded, msg);
3727 }
3728 }
3729
3730 #[test]
3731 fn hotbar_consumable_binding_roundtrips() {
3732 let binding = hotbar_consumable_binding("bottle_of_water");
3733 assert_eq!(binding, "item:bottle_of_water");
3734 assert!(hotbar_binding_is_consumable(&binding));
3735 assert_eq!(
3736 hotbar_consumable_template(&binding),
3737 Some("bottle_of_water")
3738 );
3739 assert!(!hotbar_binding_is_consumable("fireball"));
3740 assert_eq!(hotbar_consumable_template("fireball"), None);
3741 }
3742}