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
461#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
463pub struct KnownAbility {
464 pub ability_id: String,
465 #[serde(default = "default_known_permanent")]
467 pub permanent: bool,
468 #[serde(default)]
470 pub expires_at_tick: Option<u64>,
471}
472
473fn default_known_permanent() -> bool {
474 true
475}
476
477impl KnownAbility {
478 pub fn permanent(ability_id: impl Into<String>) -> Self {
479 Self {
480 ability_id: ability_id.into(),
481 permanent: true,
482 expires_at_tick: None,
483 }
484 }
485
486 pub fn is_active(&self, tick: u64) -> bool {
487 if self.permanent {
488 return true;
489 }
490 match self.expires_at_tick {
491 Some(exp) => tick < exp,
492 None => false,
493 }
494 }
495}
496
497#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
499pub struct RotationPreset {
500 pub id: String,
501 pub label: String,
502 #[serde(default)]
503 pub abilities: Vec<String>,
504}
505
506impl RotationPreset {
507 pub fn melee_default(ability_id: impl Into<String>) -> Self {
508 let id = ability_id.into();
509 Self {
510 id: "melee".into(),
511 label: "Weapon".into(),
513 abilities: vec![id],
514 }
515 }
516
517 pub fn is_weapon_preset(&self) -> bool {
518 self.id == "melee"
519 }
520}
521
522#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
524pub struct StoredTargetSlot {
525 pub instance_id: Option<String>,
526 #[serde(default)]
527 pub preset_id: Option<String>,
528 #[serde(default)]
529 pub rotation_index: u32,
530 #[serde(default)]
531 pub auto_enabled: bool,
532}
533
534#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
535#[serde(default)]
536pub struct StoredCombatProfile {
537 pub combat_target_instance_id: Option<String>,
539 pub in_combat: bool,
540 pub last_combat_tick: u64,
541 pub last_attack_tick: u64,
542 pub cooldowns_until_tick: BTreeMap<String, u64>,
543 #[serde(default = "default_auto_attack")]
544 pub auto_attack_enabled: bool,
545 #[serde(default)]
547 pub mainhand_template_id: Option<String>,
548 #[serde(default)]
550 pub mainhand_instance_id: Option<Uuid>,
551 #[serde(default)]
553 pub offhand_template_id: Option<String>,
554 #[serde(default)]
556 pub offhand_instance_id: Option<Uuid>,
557 #[serde(default)]
560 pub worn: Vec<(BodySlot, ItemStack)>,
561 #[serde(default)]
563 pub rotation_presets: Vec<RotationPreset>,
564 #[serde(default)]
566 pub target_slots: Vec<StoredTargetSlot>,
567 #[serde(default)]
569 pub known_blueprint_ids: Vec<String>,
570 #[serde(default)]
572 pub keychain: Vec<ItemStack>,
573 #[serde(default)]
575 pub known_abilities: Vec<KnownAbility>,
576 #[serde(default)]
578 pub hotbar: Vec<Option<String>>,
579 #[serde(default)]
581 pub abilities_schema_version: u32,
582 #[serde(default)]
584 pub bank_balance_copper: u64,
585}
586
587fn default_auto_attack() -> bool {
588 true
589}
590
591impl Default for StoredCombatProfile {
592 fn default() -> Self {
593 Self {
594 combat_target_instance_id: None,
595 in_combat: false,
596 last_combat_tick: 0,
597 last_attack_tick: 0,
598 cooldowns_until_tick: BTreeMap::new(),
599 auto_attack_enabled: true,
600 mainhand_template_id: None,
601 mainhand_instance_id: None,
602 offhand_template_id: None,
603 offhand_instance_id: None,
604 worn: Vec::new(),
605 rotation_presets: Vec::new(),
606 target_slots: Vec::new(),
607 known_blueprint_ids: Vec::new(),
608 keychain: Vec::new(),
609 known_abilities: Vec::new(),
610 hotbar: Vec::new(),
611 abilities_schema_version: 0,
612 bank_balance_copper: 0,
613 }
614 }
615}
616
617#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
618pub struct EntityState {
619 pub id: EntityId,
620 pub transform: Transform,
621 #[serde(default)]
623 pub label: String,
624 #[serde(default)]
625 pub vitals: Option<PlayerVitals>,
626 #[serde(default)]
628 pub attributes: Option<PrimaryAttributes>,
629 #[serde(default)]
630 pub skills: Option<PlayerSkills>,
631 #[serde(default)]
633 pub inside_building: Option<String>,
634 #[serde(default)]
636 pub tile_id: Option<String>,
637 #[serde(default)]
639 pub paperdoll_ref: Option<String>,
640 #[serde(default)]
642 pub presentation_state: Option<String>,
643 #[serde(default)]
645 pub sprite_mode: Option<String>,
646 #[serde(default)]
648 pub progression_xp: Option<ProgressionXp>,
649}
650
651#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
653#[serde(rename_all = "snake_case")]
654pub enum ChatChannel {
655 Nearby,
656 WhisperStone,
657}
658
659#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
660pub struct ChatMessage {
661 pub channel: ChatChannel,
662 pub from_entity: EntityId,
663 pub from_name: String,
664 pub text: String,
665 pub tick: Tick,
666}
667
668#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
670pub enum Intent {
671 Move {
672 entity_id: EntityId,
673 forward: f32,
674 strafe: f32,
675 #[serde(default)]
677 vertical: f32,
678 #[serde(default)]
680 sprint: bool,
681 seq: Seq,
682 },
683 Stop {
684 entity_id: EntityId,
685 seq: Seq,
686 },
687 Harvest {
688 entity_id: EntityId,
689 node_id: String,
690 seq: Seq,
691 },
692 Use {
693 entity_id: EntityId,
694 template_id: String,
695 seq: Seq,
696 },
697 UseGrant {
699 entity_id: EntityId,
700 grant_instance_id: Uuid,
701 target_instance_id: Uuid,
702 seq: Seq,
703 },
704 Say {
705 entity_id: EntityId,
706 channel: ChatChannel,
707 text: String,
708 seq: Seq,
709 },
710 Craft {
712 entity_id: EntityId,
713 blueprint_id: String,
714 #[serde(default)]
716 count: Option<u32>,
717 seq: Seq,
718 },
719 Interact {
721 entity_id: EntityId,
722 target_id: String,
723 seq: Seq,
724 },
725 ShopBuy {
727 entity_id: EntityId,
728 npc_id: String,
729 offer_id: String,
730 #[serde(default = "default_one")]
731 quantity: u32,
732 seq: Seq,
733 },
734 ShopSell {
736 entity_id: EntityId,
737 npc_id: String,
738 template_id: String,
739 #[serde(default = "default_one")]
740 quantity: u32,
741 seq: Seq,
742 },
743 ShopClose {
745 entity_id: EntityId,
746 npc_id: String,
747 seq: Seq,
748 },
749 TestDamage {
751 entity_id: EntityId,
752 amount: f32,
753 seq: Seq,
754 },
755 SetTarget {
757 entity_id: EntityId,
758 target_id: EntityId,
759 seq: Seq,
760 },
761 SetTargetSlot {
763 entity_id: EntityId,
764 slot_index: u8,
765 target_id: EntityId,
766 seq: Seq,
767 },
768 ClearTarget {
769 entity_id: EntityId,
770 seq: Seq,
771 },
772 ClearTargetSlot {
773 entity_id: EntityId,
774 slot_index: u8,
775 seq: Seq,
776 },
777 SetAutoAttack {
779 entity_id: EntityId,
780 slot_index: u8,
781 enabled: bool,
782 seq: Seq,
783 },
784 Attack {
786 entity_id: EntityId,
787 #[serde(default)]
788 target_id: Option<EntityId>,
789 #[serde(default)]
790 weapon_slot: Option<u32>,
791 seq: Seq,
792 },
793 Pickup {
795 entity_id: EntityId,
796 #[serde(default)]
797 drop_id: Option<String>,
798 seq: Seq,
799 },
800 Cast {
802 entity_id: EntityId,
803 ability_id: String,
804 target_id: EntityId,
805 seq: Seq,
806 },
807 BindActionSlot {
809 entity_id: EntityId,
810 slot_index: u8,
811 ability_id: String,
812 #[serde(default = "default_auto_attack")]
813 auto_enabled: bool,
814 seq: Seq,
815 },
816 UseActionSlot {
818 entity_id: EntityId,
819 slot_index: u8,
820 seq: Seq,
821 },
822 Dodge {
824 entity_id: EntityId,
825 seq: Seq,
826 },
827 Lunge {
829 entity_id: EntityId,
830 #[serde(default)]
832 forward: f32,
833 #[serde(default)]
835 strafe: f32,
836 seq: Seq,
837 },
838 DirectionalJump {
840 entity_id: EntityId,
841 #[serde(default)]
843 forward: f32,
844 #[serde(default)]
846 strafe: f32,
847 seq: Seq,
848 },
849 Block {
851 entity_id: EntityId,
852 #[serde(default = "default_block_enabled")]
853 enabled: bool,
854 seq: Seq,
855 },
856 EquipMainhand {
860 entity_id: EntityId,
861 #[serde(default)]
862 template_id: Option<String>,
863 #[serde(default)]
864 instance_id: Option<Uuid>,
865 seq: Seq,
866 },
867 EquipOffhand {
869 entity_id: EntityId,
870 #[serde(default)]
871 template_id: Option<String>,
872 #[serde(default)]
873 instance_id: Option<Uuid>,
874 seq: Seq,
875 },
876 EquipWorn {
879 entity_id: EntityId,
880 slot: BodySlot,
881 #[serde(default)]
882 instance_id: Option<Uuid>,
883 seq: Seq,
884 },
885 MoveItem {
887 entity_id: EntityId,
888 item_instance_id: Uuid,
889 from: InventoryLocation,
890 to: InventoryLocation,
891 #[serde(default)]
893 to_parent_instance_id: Option<Uuid>,
894 #[serde(default)]
896 quantity: Option<u32>,
897 seq: Seq,
898 },
899 PlaceContainer {
901 entity_id: EntityId,
902 item_instance_id: Uuid,
903 seq: Seq,
904 },
905 PickupContainer {
907 entity_id: EntityId,
908 container_id: String,
909 seq: Seq,
910 },
911 SetContainerLocked {
913 entity_id: EntityId,
914 location: InventoryLocation,
916 locked: bool,
917 seq: Seq,
918 },
919 DropItem {
921 entity_id: EntityId,
922 item_instance_id: Uuid,
923 from: InventoryLocation,
924 seq: Seq,
925 },
926 DestroyItem {
928 entity_id: EntityId,
929 item_instance_id: Uuid,
930 from: InventoryLocation,
931 #[serde(default)]
933 quantity: Option<u32>,
934 seq: Seq,
935 },
936 RenameContainer {
938 entity_id: EntityId,
939 item_instance_id: Uuid,
940 location: InventoryLocation,
941 name: String,
942 seq: Seq,
943 },
944 UpsertRotationPreset {
946 entity_id: EntityId,
947 preset: RotationPreset,
948 seq: Seq,
949 },
950 DeleteRotationPreset {
952 entity_id: EntityId,
953 preset_id: String,
954 seq: Seq,
955 },
956 AssignSlotPreset {
958 entity_id: EntityId,
959 slot_index: u8,
960 preset_id: String,
961 seq: Seq,
962 },
963 SetHotbarSlot {
966 entity_id: EntityId,
967 slot: u8,
969 #[serde(default)]
971 ability_id: Option<String>,
972 seq: Seq,
973 },
974 AdvanceRotation {
976 entity_id: EntityId,
977 slot_index: u8,
978 seq: Seq,
979 },
980 NpcTalkOpen {
982 entity_id: EntityId,
983 npc_id: String,
984 seq: Seq,
985 },
986 NpcTalkSay {
988 entity_id: EntityId,
989 npc_id: String,
990 message: String,
991 seq: Seq,
992 },
993 NpcTalkClose {
995 entity_id: EntityId,
996 npc_id: String,
997 seq: Seq,
998 },
999 AcceptQuest {
1001 entity_id: EntityId,
1002 quest_id: String,
1003 seq: Seq,
1004 },
1005 WithdrawQuest {
1007 entity_id: EntityId,
1008 quest_id: String,
1009 seq: Seq,
1010 },
1011 TrackQuest {
1013 entity_id: EntityId,
1014 quest_id: String,
1015 seq: Seq,
1016 },
1017 QuestGiveItem {
1019 entity_id: EntityId,
1020 npc_id: String,
1021 template_id: String,
1022 #[serde(default = "default_one")]
1023 quantity: u32,
1024 seq: Seq,
1025 },
1026 HireWorker {
1028 entity_id: EntityId,
1029 def_id: String,
1030 wage_copper_per_interval: u32,
1031 #[serde(default)]
1032 lodging_container_id: Option<String>,
1033 #[serde(default)]
1034 job_yaml: Option<String>,
1035 seq: Seq,
1036 },
1037 DismissWorker {
1039 entity_id: EntityId,
1040 worker_instance_id: String,
1041 seq: Seq,
1042 },
1043 SetWorkerJob {
1045 entity_id: EntityId,
1046 worker_instance_id: String,
1047 job_yaml: String,
1048 seq: Seq,
1049 },
1050 AssignWorkerLodging {
1052 entity_id: EntityId,
1053 worker_instance_id: String,
1054 lodging_container_id: String,
1055 seq: Seq,
1056 },
1057 SetWorkerMode {
1059 entity_id: EntityId,
1060 worker_instance_id: String,
1061 mode: String,
1062 seq: Seq,
1063 },
1064 GiveWorkerItem {
1067 entity_id: EntityId,
1068 worker_instance_id: String,
1069 item_instance_id: uuid::Uuid,
1070 #[serde(default)]
1071 quantity: Option<u32>,
1072 seq: Seq,
1073 },
1074 TakeWorkerItem {
1076 entity_id: EntityId,
1077 worker_instance_id: String,
1078 item_instance_id: uuid::Uuid,
1079 #[serde(default)]
1080 quantity: Option<u32>,
1081 seq: Seq,
1082 },
1083 RenameHiredWorker {
1085 entity_id: EntityId,
1086 worker_instance_id: String,
1087 name: String,
1088 seq: Seq,
1089 },
1090 TeachWorkerBlueprint {
1092 entity_id: EntityId,
1093 worker_instance_id: String,
1094 blueprint_id: String,
1095 seq: Seq,
1096 },
1097 BuyProperty {
1099 entity_id: EntityId,
1100 zone_id: String,
1101 seq: Seq,
1102 },
1103 SellProperty {
1105 entity_id: EntityId,
1106 zone_id: String,
1107 seq: Seq,
1108 },
1109 BankDeposit {
1111 entity_id: EntityId,
1112 npc_id: String,
1113 #[serde(default)]
1115 amount_copper: u64,
1116 seq: Seq,
1117 },
1118 BankWithdraw {
1120 entity_id: EntityId,
1121 npc_id: String,
1122 #[serde(default)]
1124 amount_copper: u64,
1125 seq: Seq,
1126 },
1127 BankClose {
1129 entity_id: EntityId,
1130 npc_id: String,
1131 seq: Seq,
1132 },
1133 BankTransfer {
1135 entity_id: EntityId,
1136 npc_id: String,
1137 #[serde(default)]
1139 to_character_id: Option<Uuid>,
1140 #[serde(default)]
1142 to_name: String,
1143 amount_copper: u64,
1145 seq: Seq,
1146 },
1147 StorageStore {
1149 entity_id: EntityId,
1150 npc_id: String,
1151 item_instance_id: Uuid,
1152 #[serde(default)]
1153 quantity: Option<u32>,
1154 seq: Seq,
1155 },
1156 StorageTake {
1158 entity_id: EntityId,
1159 npc_id: String,
1160 item_instance_id: Uuid,
1161 #[serde(default)]
1162 quantity: Option<u32>,
1163 seq: Seq,
1164 },
1165 StorageShip {
1167 entity_id: EntityId,
1168 npc_id: String,
1169 dest_building_id: String,
1170 item_instance_id: Uuid,
1171 #[serde(default)]
1172 quantity: Option<u32>,
1173 seq: Seq,
1174 },
1175 StorageClose {
1177 entity_id: EntityId,
1178 npc_id: String,
1179 seq: Seq,
1180 },
1181}
1182
1183fn default_block_enabled() -> bool {
1184 true
1185}
1186
1187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1189pub struct StatusEffectHud {
1190 pub effect_id: String,
1191 pub label: String,
1192 #[serde(default)]
1193 pub polarity: String,
1194 #[serde(default)]
1195 pub icon_tile_id: Option<String>,
1196 #[serde(default)]
1198 pub remaining_sec: Option<f32>,
1199}
1200
1201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1203pub struct CombatTargetHud {
1204 pub entity_id: EntityId,
1205 #[serde(default)]
1206 pub label: String,
1207 #[serde(default)]
1208 pub level: u32,
1209 pub health: f32,
1210 pub health_max: f32,
1211 #[serde(default)]
1212 pub life_state: LifeState,
1213 #[serde(default)]
1214 pub distance_m: f32,
1215 #[serde(default)]
1216 pub statuses: Vec<StatusEffectHud>,
1217}
1218
1219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1221pub struct CastProgressHud {
1222 #[serde(default)]
1223 pub ability_id: String,
1224 #[serde(default)]
1225 pub ability_label: String,
1226 #[serde(default)]
1227 pub ticks_remaining: u64,
1228 #[serde(default)]
1229 pub ticks_total: u64,
1230}
1231
1232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1234pub struct AbilityCooldownHud {
1235 #[serde(default)]
1236 pub ability_id: String,
1237 #[serde(default)]
1238 pub label: String,
1239 #[serde(default)]
1240 pub cd_ticks: u64,
1241 #[serde(default)]
1242 pub cd_total_ticks: u64,
1243}
1244
1245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1247pub struct CombatSlotHud {
1248 pub slot_index: u8,
1249 #[serde(default)]
1250 pub target_entity_id: Option<EntityId>,
1251 #[serde(default)]
1252 pub target_label: Option<String>,
1253 #[serde(default)]
1254 pub target: Option<CombatTargetHud>,
1255 #[serde(default)]
1256 pub preset_id: Option<String>,
1257 #[serde(default)]
1258 pub preset_label: Option<String>,
1259 #[serde(default)]
1260 pub rotation: Vec<String>,
1261 #[serde(default)]
1262 pub rotation_index: u32,
1263 #[serde(default)]
1264 pub next_ability_id: Option<String>,
1265 #[serde(default)]
1266 pub auto_enabled: bool,
1267}
1268
1269#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1271pub struct DefensePieceHud {
1272 pub slot: BodySlot,
1273 pub label: String,
1274 pub template_id: String,
1275 #[serde(default)]
1276 pub armor_physical: f32,
1277 #[serde(default)]
1278 pub resists: Vec<(String, f32)>,
1279}
1280
1281impl Default for DefensePieceHud {
1282 fn default() -> Self {
1283 Self {
1284 slot: BodySlot::Head,
1285 label: String::new(),
1286 template_id: String::new(),
1287 armor_physical: 0.0,
1288 resists: Vec::new(),
1289 }
1290 }
1291}
1292
1293#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1295pub struct DefenseHud {
1296 pub armor_physical: f32,
1297 pub vitality_contribution: f32,
1298 pub total_mitigation_rating: f32,
1299 pub estimated_physical_dr: f32,
1301 #[serde(default)]
1302 pub resists: Vec<(String, f32)>,
1303 #[serde(default)]
1304 pub pieces: Vec<DefensePieceHud>,
1305}
1306
1307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1309pub struct CombatHud {
1310 pub in_combat: bool,
1311 pub auto_attack: bool,
1313 pub has_los: bool,
1314 pub attack_cd_ticks: u64,
1315 #[serde(default)]
1316 pub ability_id: String,
1317 #[serde(default)]
1318 pub target_entity_id: Option<EntityId>,
1319 #[serde(default)]
1320 pub target_label: Option<String>,
1321 #[serde(default)]
1322 pub max_target_slots: u8,
1323 #[serde(default)]
1324 pub slots: Vec<CombatSlotHud>,
1325 #[serde(default)]
1326 pub rotation_presets: Vec<RotationPreset>,
1327 #[serde(default)]
1328 pub gcd_ticks: u64,
1329 #[serde(default)]
1330 pub mainhand_template_id: Option<String>,
1331 #[serde(default)]
1332 pub mainhand_label: Option<String>,
1333 #[serde(default)]
1334 pub offhand_template_id: Option<String>,
1335 #[serde(default)]
1336 pub offhand_label: Option<String>,
1337 #[serde(default)]
1339 pub mainhand_hand_slots: u8,
1340 #[serde(default)]
1342 pub worn: Vec<(BodySlot, ItemStack)>,
1343 #[serde(default)]
1345 pub defense: Option<DefenseHud>,
1346 #[serde(default)]
1347 pub carry_mass: f32,
1348 #[serde(default)]
1349 pub carry_mass_max: f32,
1350 #[serde(default)]
1351 pub encumbrance: EncumbranceState,
1352 #[serde(default)]
1354 pub keychain: Vec<ItemStack>,
1355 #[serde(default)]
1356 pub target: Option<CombatTargetHud>,
1357 #[serde(default)]
1358 pub cast: Option<CastProgressHud>,
1359 #[serde(default)]
1360 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1361 #[serde(default)]
1362 pub blocking_active: bool,
1363 #[serde(default)]
1365 pub progression_xp: Option<ProgressionXp>,
1366 #[serde(default)]
1367 pub progression_baseline: u16,
1368 #[serde(default)]
1369 pub progression_xp_base: f64,
1370 #[serde(default)]
1371 pub progression_xp_growth: f64,
1372 #[serde(default)]
1373 pub attributes: Option<PrimaryAttributes>,
1374 #[serde(default)]
1375 pub skills: Option<PlayerSkills>,
1376 #[serde(default)]
1378 pub statuses: Vec<StatusEffectHud>,
1379 #[serde(default)]
1381 pub known_abilities: Vec<String>,
1382 #[serde(default)]
1385 pub hotbar: Vec<Option<String>>,
1386 #[serde(default)]
1388 pub max_abilities_per_rotation: u8,
1389}
1390
1391pub const HOTBAR_ITEM_PREFIX: &str = "item:";
1393
1394pub fn hotbar_consumable_binding(template_id: &str) -> String {
1396 format!("{HOTBAR_ITEM_PREFIX}{}", template_id.trim())
1397}
1398
1399pub fn hotbar_consumable_template(binding: &str) -> Option<&str> {
1401 binding
1402 .strip_prefix(HOTBAR_ITEM_PREFIX)
1403 .map(str::trim)
1404 .filter(|id| !id.is_empty())
1405}
1406
1407pub fn hotbar_binding_is_consumable(binding: &str) -> bool {
1409 hotbar_consumable_template(binding).is_some()
1410}
1411
1412#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1414#[serde(rename_all = "snake_case")]
1415pub enum CombatFxKind {
1416 MeleeArc,
1417 Cone,
1418 Sphere,
1419 Beam,
1420 HitMarker,
1421}
1422
1423#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1425#[serde(rename_all = "snake_case")]
1426pub enum CombatFxHitOutcome {
1427 #[default]
1428 Hit,
1429 Blocked,
1430 Miss,
1431 Glance,
1432}
1433
1434#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1436pub struct CombatFxHit {
1437 pub entity_id: EntityId,
1438 pub x: f32,
1439 pub y: f32,
1440 pub z: f32,
1441 #[serde(default)]
1442 pub outcome: CombatFxHitOutcome,
1443}
1444
1445#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1447pub struct CombatFx {
1448 pub id: u64,
1449 pub kind: CombatFxKind,
1450 pub ability_id: String,
1451 pub caster_id: EntityId,
1452 pub origin_x: f32,
1453 pub origin_y: f32,
1454 pub origin_z: f32,
1455 #[serde(default)]
1456 pub end_x: Option<f32>,
1457 #[serde(default)]
1458 pub end_y: Option<f32>,
1459 #[serde(default)]
1460 pub end_z: Option<f32>,
1461 #[serde(default)]
1462 pub yaw: Option<f32>,
1463 #[serde(default)]
1464 pub reach_m: Option<f32>,
1465 #[serde(default)]
1466 pub arc_deg: Option<f32>,
1467 #[serde(default)]
1468 pub radius_m: Option<f32>,
1469 #[serde(default)]
1470 pub hits: Vec<CombatFxHit>,
1471 pub until_tick: u64,
1473 #[serde(default)]
1474 pub damage_type: String,
1475}
1476
1477#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1479#[serde(rename_all = "snake_case")]
1480pub enum WorkerModeView {
1481 Companion,
1482 JobLoop,
1483 Idle,
1486}
1487
1488#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1490#[serde(rename_all = "snake_case")]
1491pub enum WorkerStateView {
1492 Idle,
1493 Traveling,
1494 Working,
1495 Resting,
1496 Waiting,
1497 Strike,
1498 Dismissed,
1499}
1500
1501#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
1503pub struct WorkerVitalsSummary {
1504 pub health_pct: f32,
1505 pub stamina_pct: f32,
1506}
1507
1508#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1510#[serde(rename_all = "snake_case")]
1511pub enum WorkerRouteKindView {
1512 #[default]
1513 HarvestLoop,
1514 Ordered,
1515}
1516
1517#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1519pub struct WorkerRouteView {
1520 #[serde(default)]
1521 pub kind: WorkerRouteKindView,
1522 #[serde(default)]
1523 pub lodging_container_id: Option<String>,
1524 #[serde(default)]
1526 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
1527 #[serde(default)]
1529 pub harvest_nodes: Vec<String>,
1530 #[serde(default = "default_route_carry_ratio")]
1531 pub carry_return_ratio: f32,
1532 #[serde(default)]
1534 pub stops: Vec<WorkerRouteStopView>,
1535}
1536
1537fn default_route_carry_ratio() -> f32 {
1538 0.90
1539}
1540
1541fn default_true_view() -> bool {
1542 true
1543}
1544
1545#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1547pub struct WorkerWithdrawItemView {
1548 pub template: String,
1549 #[serde(default)]
1550 pub qty: u32,
1551 #[serde(default)]
1553 pub all: bool,
1554}
1555
1556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1557pub struct WorkerRouteWaypointView {
1558 pub x: f32,
1559 pub y: f32,
1560 pub z: f32,
1561}
1562
1563#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1572#[serde(rename_all = "snake_case")]
1573pub enum WorkerRouteStopView {
1574 Waypoint {
1575 x: f32,
1576 y: f32,
1577 #[serde(default)]
1578 z: f32,
1579 },
1580 HarvestNode {
1581 node_id: String,
1582 },
1583 DepositAt {
1584 container_id: String,
1585 #[serde(default)]
1586 filter: Option<Vec<String>>,
1587 },
1588 TradeWith {
1589 #[serde(default)]
1590 npc_id: Option<String>,
1591 template: String,
1592 #[serde(default = "default_true_view")]
1593 sell_all: bool,
1594 },
1595 WithdrawFrom {
1596 container_id: String,
1597 items: Vec<WorkerWithdrawItemView>,
1598 },
1599 CraftAt {
1600 device: String,
1601 blueprint: String,
1602 #[serde(default)]
1603 qty: Option<u32>,
1604 },
1605 RestIfNeeded,
1606 Wait {
1607 #[serde(default)]
1608 wait_ticks: u64,
1609 },
1610}
1611
1612#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1614#[serde(rename_all = "snake_case")]
1615pub enum LedgerCategory {
1616 Workers,
1617 Hire,
1618 Train,
1619 ShopBuy,
1620 Taxes,
1621 WorkerSales,
1622 TraderSales,
1623 BankDeposit,
1624 BankWithdraw,
1625 BankTransferOut,
1626 BankTransferIn,
1627 BankTransferFee,
1628 StorageShipFee,
1629 Other,
1630}
1631
1632impl LedgerCategory {
1633 pub fn as_str(self) -> &'static str {
1634 match self {
1635 Self::Workers => "workers",
1636 Self::Hire => "hire",
1637 Self::Train => "train",
1638 Self::ShopBuy => "shop_buy",
1639 Self::Taxes => "taxes",
1640 Self::WorkerSales => "worker_sales",
1641 Self::TraderSales => "trader_sales",
1642 Self::BankDeposit => "bank_deposit",
1643 Self::BankWithdraw => "bank_withdraw",
1644 Self::BankTransferOut => "bank_transfer_out",
1645 Self::BankTransferIn => "bank_transfer_in",
1646 Self::BankTransferFee => "bank_transfer_fee",
1647 Self::StorageShipFee => "storage_ship_fee",
1648 Self::Other => "other",
1649 }
1650 }
1651}
1652
1653#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1654pub struct LedgerEntryView {
1655 pub id: uuid::Uuid,
1656 pub game_day: u64,
1657 pub signed_copper: i64,
1658 pub category: LedgerCategory,
1659 #[serde(default)]
1660 pub label: String,
1661}
1662
1663#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1664pub struct LedgerPeriodTotals {
1665 #[serde(default)]
1667 pub expenses: std::collections::HashMap<String, u64>,
1668 #[serde(default)]
1670 pub income: std::collections::HashMap<String, u64>,
1671 pub expense_copper: u64,
1672 pub income_copper: u64,
1673 pub cash_flow_copper: i64,
1675}
1676
1677#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1678pub struct PlayerLedgerView {
1679 pub current_game_day: u64,
1680 #[serde(default)]
1681 pub period_day: LedgerPeriodTotals,
1682 #[serde(default)]
1683 pub period_week: LedgerPeriodTotals,
1684 #[serde(default)]
1685 pub period_month: LedgerPeriodTotals,
1686 #[serde(default)]
1687 pub period_lifetime: LedgerPeriodTotals,
1688 #[serde(default)]
1689 pub recent: Vec<LedgerEntryView>,
1690 #[serde(default)]
1692 pub wealth_on_person_copper: u64,
1693 #[serde(default)]
1695 pub wealth_in_storage_copper: u64,
1696 #[serde(default)]
1698 pub wealth_in_bank_copper: u64,
1699 #[serde(default)]
1701 pub wealth_total_copper: u64,
1702 #[serde(default)]
1704 pub live_expense_per_interval_copper: u64,
1705 #[serde(default)]
1707 pub live_income_route_est_per_loop_copper: u64,
1708 #[serde(default)]
1710 pub live_income_avg_per_interval_copper: u64,
1711 #[serde(default)]
1713 pub live_income_avg_window_intervals: u32,
1714 #[serde(default)]
1716 pub live_net_avg_per_interval_copper: i64,
1717}
1718
1719#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1721#[serde(rename_all = "snake_case")]
1722pub enum AnalyticsMetric {
1723 NpcKill,
1724 WildlifeKill,
1725 Harvest,
1726 QuestComplete,
1727 QuestAccept,
1728 QuestAbandon,
1729 PlayerDeath,
1730 Craft,
1731 WorkerHire,
1732 WorkerDismiss,
1733 WorkerTeach,
1734 NpcTalk,
1735 ShopBuy,
1736 ShopSell,
1737 PlaceContainer,
1738 PickupContainer,
1739 PickupDrop,
1740 ConsumableUse,
1741 AbilityUse,
1742 DistanceWalkedM,
1743 DoorUse,
1744 BuildingEnter,
1745}
1746
1747impl AnalyticsMetric {
1748 pub fn as_str(self) -> &'static str {
1749 match self {
1750 Self::NpcKill => "npc_kill",
1751 Self::WildlifeKill => "wildlife_kill",
1752 Self::Harvest => "harvest",
1753 Self::QuestComplete => "quest_complete",
1754 Self::QuestAccept => "quest_accept",
1755 Self::QuestAbandon => "quest_abandon",
1756 Self::PlayerDeath => "player_death",
1757 Self::Craft => "craft",
1758 Self::WorkerHire => "worker_hire",
1759 Self::WorkerDismiss => "worker_dismiss",
1760 Self::WorkerTeach => "worker_teach",
1761 Self::NpcTalk => "npc_talk",
1762 Self::ShopBuy => "shop_buy",
1763 Self::ShopSell => "shop_sell",
1764 Self::PlaceContainer => "place_container",
1765 Self::PickupContainer => "pickup_container",
1766 Self::PickupDrop => "pickup_drop",
1767 Self::ConsumableUse => "consumable_use",
1768 Self::AbilityUse => "ability_use",
1769 Self::DistanceWalkedM => "distance_walked_m",
1770 Self::DoorUse => "door_use",
1771 Self::BuildingEnter => "building_enter",
1772 }
1773 }
1774
1775 pub fn from_str_key(s: &str) -> Option<Self> {
1776 Some(match s {
1777 "npc_kill" => Self::NpcKill,
1778 "wildlife_kill" => Self::WildlifeKill,
1779 "harvest" => Self::Harvest,
1780 "quest_complete" => Self::QuestComplete,
1781 "quest_accept" => Self::QuestAccept,
1782 "quest_abandon" => Self::QuestAbandon,
1783 "player_death" => Self::PlayerDeath,
1784 "craft" => Self::Craft,
1785 "worker_hire" => Self::WorkerHire,
1786 "worker_dismiss" => Self::WorkerDismiss,
1787 "worker_teach" => Self::WorkerTeach,
1788 "npc_talk" => Self::NpcTalk,
1789 "shop_buy" => Self::ShopBuy,
1790 "shop_sell" => Self::ShopSell,
1791 "place_container" => Self::PlaceContainer,
1792 "pickup_container" => Self::PickupContainer,
1793 "pickup_drop" => Self::PickupDrop,
1794 "consumable_use" => Self::ConsumableUse,
1795 "ability_use" => Self::AbilityUse,
1796 "distance_walked_m" => Self::DistanceWalkedM,
1797 "door_use" => Self::DoorUse,
1798 "building_enter" => Self::BuildingEnter,
1799 _ => return None,
1800 })
1801 }
1802}
1803
1804#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1805pub struct CareerMetricRow {
1806 pub subject_id: String,
1807 pub amount: u64,
1808}
1809
1810#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1812pub struct PlayerCareerView {
1813 pub current_game_day: u64,
1814 #[serde(default)]
1815 pub kills: Vec<CareerMetricRow>,
1816 #[serde(default)]
1817 pub harvests: Vec<CareerMetricRow>,
1818 pub quests_completed: u64,
1819 #[serde(default)]
1820 pub crafts: Vec<CareerMetricRow>,
1821 pub deaths: u64,
1822 pub npc_talks: u64,
1823 pub shop_buys: u64,
1824 pub shop_sells: u64,
1825 pub distance_m: u64,
1826 #[serde(default)]
1827 pub other: Vec<CareerMetricRow>,
1828}
1829
1830#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1832pub struct HiredWorkerView {
1833 pub instance_id: String,
1834 pub entity_id: EntityId,
1835 pub def_id: String,
1836 pub label: String,
1838 pub x: f32,
1839 pub y: f32,
1840 pub z: f32,
1841 pub mode: WorkerModeView,
1842 pub state: WorkerStateView,
1843 #[serde(default)]
1844 pub step_label: String,
1845 pub vitals: WorkerVitalsSummary,
1846 #[serde(default)]
1847 pub carry_pct: f32,
1848 #[serde(default)]
1849 pub last_error: Option<String>,
1850 pub wage_copper_per_interval: u32,
1851 #[serde(default)]
1853 pub effective_wage_copper: u32,
1854 #[serde(default)]
1856 pub wage_meters_walked: f32,
1857 #[serde(default)]
1859 pub lodging_container_id: Option<String>,
1860 #[serde(default)]
1862 pub route: Option<WorkerRouteView>,
1863 #[serde(default)]
1865 pub known_blueprint_ids: Vec<String>,
1866 #[serde(default = "default_worker_view_level")]
1868 pub level: u32,
1869 #[serde(default)]
1871 pub worker_xp: f64,
1872 #[serde(default)]
1874 pub inventory: Vec<ItemStack>,
1875}
1876
1877fn default_worker_view_level() -> u32 {
1878 1
1879}
1880
1881#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1883pub struct TickDelta {
1884 pub tick: Tick,
1885 pub entities: Vec<EntityState>,
1886 #[serde(default)]
1887 pub resource_nodes: Vec<ResourceNodeView>,
1888 #[serde(default)]
1889 pub buildings: Vec<BuildingView>,
1890 #[serde(default)]
1891 pub doors: Vec<DoorView>,
1892 #[serde(default)]
1893 pub npcs: Vec<NpcView>,
1894 #[serde(default)]
1896 pub inventory: Vec<ItemStack>,
1897 #[serde(default)]
1898 pub blueprints: Vec<BlueprintView>,
1899 #[serde(default)]
1900 pub world_clock: WorldClock,
1901 #[serde(default)]
1902 pub ground_drops: Vec<GroundDropView>,
1903 #[serde(default)]
1904 pub placed_containers: Vec<PlacedContainerView>,
1905 #[serde(default)]
1906 pub combat: Option<CombatHud>,
1907 #[serde(default)]
1908 pub interior_map: Option<InteriorMapView>,
1909 #[serde(default)]
1910 pub quest_log: Vec<QuestLogEntry>,
1911 #[serde(default)]
1912 pub hired_workers: Vec<HiredWorkerView>,
1913 #[serde(default)]
1914 pub interactables: Vec<InteractableView>,
1915 #[serde(default)]
1916 pub ledger: Option<PlayerLedgerView>,
1917 #[serde(default)]
1918 pub career: Option<PlayerCareerView>,
1919 #[serde(default)]
1921 pub combat_fx: Vec<CombatFx>,
1922}
1923#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1924pub struct GroundDropView {
1925 pub id: String,
1926 pub template_id: String,
1927 pub quantity: u32,
1928 pub x: f32,
1929 pub y: f32,
1930 pub z: f32,
1931 #[serde(default)]
1933 pub tile_id: Option<String>,
1934 #[serde(default)]
1936 pub paperdoll_ref: Option<String>,
1937 #[serde(default)]
1939 pub yaw: f32,
1940 #[serde(default)]
1942 pub pitch: f32,
1943 #[serde(default)]
1945 pub roll: f32,
1946 #[serde(default = "default_draw_scale")]
1948 pub draw_scale: f32,
1949}
1950
1951fn default_draw_scale() -> f32 {
1952 1.0
1953}
1954
1955#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1957pub struct Snapshot {
1958 pub tick: Tick,
1959 pub chunk_rev: u64,
1960 #[serde(default)]
1962 pub content_rev: u64,
1963 #[serde(default)]
1965 pub publish_rev: u64,
1966 pub entities: Vec<EntityState>,
1967 #[serde(default)]
1968 pub resource_nodes: Vec<ResourceNodeView>,
1969 #[serde(default)]
1971 pub world_width_m: f32,
1972 #[serde(default)]
1973 pub world_height_m: f32,
1974 #[serde(default)]
1975 pub buildings: Vec<BuildingView>,
1976 #[serde(default)]
1977 pub doors: Vec<DoorView>,
1978 #[serde(default)]
1979 pub npcs: Vec<NpcView>,
1980 #[serde(default)]
1981 pub inventory: Vec<ItemStack>,
1982 #[serde(default)]
1983 pub blueprints: Vec<BlueprintView>,
1984 #[serde(default)]
1985 pub world_clock: WorldClock,
1986 #[serde(default)]
1987 pub terrain_zones: Vec<TerrainZoneView>,
1988 #[serde(default)]
1989 pub z_platforms: Vec<ZPlatformView>,
1990 #[serde(default)]
1991 pub z_transitions: Vec<ZTransitionView>,
1992 #[serde(default)]
1993 pub ground_drops: Vec<GroundDropView>,
1994 #[serde(default)]
1995 pub placed_containers: Vec<PlacedContainerView>,
1996 #[serde(default)]
1997 pub combat: Option<CombatHud>,
1998 #[serde(default)]
1999 pub interior_map: Option<InteriorMapView>,
2000 #[serde(default)]
2001 pub quest_log: Vec<QuestLogEntry>,
2002 #[serde(default)]
2003 pub hired_workers: Vec<HiredWorkerView>,
2004 #[serde(default)]
2005 pub interactables: Vec<InteractableView>,
2006 #[serde(default)]
2007 pub ledger: Option<PlayerLedgerView>,
2008 #[serde(default)]
2009 pub career: Option<PlayerCareerView>,
2010 #[serde(default)]
2012 pub combat_fx: Vec<CombatFx>,
2013}
2014
2015#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2017pub struct ResourceNodeView {
2018 pub id: String,
2019 pub label: String,
2020 pub x: f32,
2021 pub y: f32,
2022 pub z: f32,
2023 pub item_template: String,
2024 #[serde(default = "default_node_state")]
2025 pub state: ResourceNodeState,
2026 #[serde(default = "default_blocking_view")]
2028 pub blocking: bool,
2029 #[serde(default = "default_blocking_radius_view")]
2031 pub blocking_radius_m: f32,
2032 #[serde(default)]
2034 pub tile_id: Option<String>,
2035 #[serde(default)]
2037 pub paperdoll_ref: Option<String>,
2038 #[serde(default)]
2040 pub yaw: f32,
2041 #[serde(default)]
2043 pub pitch: f32,
2044 #[serde(default)]
2046 pub roll: f32,
2047 #[serde(default = "default_draw_scale")]
2049 pub draw_scale: f32,
2050 #[serde(default)]
2052 pub sprite_mode: Option<String>,
2053 #[serde(default)]
2055 pub presentation_state: Option<String>,
2056}
2057
2058fn default_blocking_radius_view() -> f32 {
2059 0.8
2060}
2061
2062fn default_blocking_view() -> bool {
2063 true
2064}
2065
2066#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2067#[serde(rename_all = "snake_case")]
2068pub enum ResourceNodeState {
2069 Available,
2070 Harvesting,
2071 Cooldown,
2072}
2073
2074fn default_node_state() -> ResourceNodeState {
2075 ResourceNodeState::Available
2076}
2077
2078#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2079#[serde(rename_all = "snake_case")]
2080pub enum ItemStatusBindingMode {
2081 OnHit,
2082 WhileEquipped,
2083}
2084
2085impl Default for ItemStatusBindingMode {
2086 fn default() -> Self {
2087 Self::OnHit
2088 }
2089}
2090
2091#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2093pub struct ItemStatusBinding {
2094 pub effect_id: String,
2095 #[serde(default)]
2096 pub mode: ItemStatusBindingMode,
2097 #[serde(default)]
2099 pub source: String,
2100 #[serde(default)]
2101 pub applied_at_tick: u64,
2102 #[serde(default, skip_serializing_if = "Option::is_none")]
2104 pub expires_at_tick: Option<u64>,
2105}
2106
2107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2108pub struct ItemStack {
2109 pub template_id: String,
2110 pub quantity: u32,
2111 #[serde(default)]
2113 pub item_instance_id: Option<Uuid>,
2114 #[serde(default)]
2116 pub props: BTreeMap<String, String>,
2117 #[serde(default)]
2119 pub status_bindings: Vec<ItemStatusBinding>,
2120 #[serde(default)]
2122 pub contents: Vec<ItemStack>,
2123 #[serde(default)]
2125 pub display_name: Option<String>,
2126 #[serde(default)]
2128 pub category: Option<String>,
2129 #[serde(default)]
2131 pub base_mass: Option<f32>,
2132 #[serde(default)]
2134 pub base_volume: Option<f32>,
2135 #[serde(default)]
2137 pub capacity_volume: Option<f32>,
2138 #[serde(default)]
2140 pub stackable: Option<bool>,
2141 #[serde(default)]
2143 pub world_placeable: Option<bool>,
2144 #[serde(default)]
2146 pub worker_lodging_capacity: Option<u32>,
2147 #[serde(default)]
2149 pub equip_slot: Option<BodySlot>,
2150 #[serde(default)]
2152 pub armor_physical: Option<f32>,
2153 #[serde(default)]
2155 pub resists: Vec<(String, f32)>,
2156 #[serde(default)]
2158 pub hand_slots: Option<u8>,
2159}
2160
2161impl ItemStack {
2162 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
2163 Self {
2164 template_id: template_id.into(),
2165 quantity,
2166 ..Default::default()
2167 }
2168 }
2169}
2170
2171impl Default for ItemStack {
2172 fn default() -> Self {
2173 Self {
2174 template_id: String::new(),
2175 quantity: 0,
2176 item_instance_id: None,
2177 props: BTreeMap::new(),
2178 status_bindings: Vec::new(),
2179 contents: Vec::new(),
2180 display_name: None,
2181 category: None,
2182 base_mass: None,
2183 base_volume: None,
2184 capacity_volume: None,
2185 stackable: None,
2186 world_placeable: None,
2187 worker_lodging_capacity: None,
2188 equip_slot: None,
2189 armor_physical: None,
2190 resists: Vec::new(),
2191 hand_slots: None,
2192 }
2193 }
2194}
2195
2196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2198#[serde(rename_all = "snake_case")]
2199pub enum EncumbranceState {
2200 #[default]
2201 Light,
2202 Heavy,
2203 Over,
2204}
2205
2206#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
2210#[serde(rename_all = "snake_case")]
2211pub enum BodySlot {
2212 Head,
2213 #[serde(alias = "body")]
2215 Chest,
2216 #[serde(alias = "arms")]
2218 Forearms,
2219 Legs,
2220 Feet,
2221 Cloak,
2222 Back,
2223 Waist,
2224 Earrings,
2225 Necklace,
2226 Eyeglasses,
2227 #[serde(rename = "ring_left_1", alias = "ring_left1")]
2229 RingLeft1,
2230 #[serde(rename = "ring_left_2", alias = "ring_left2")]
2231 RingLeft2,
2232 #[serde(rename = "ring_right_1", alias = "ring_right1")]
2233 RingRight1,
2234 #[serde(rename = "ring_right_2", alias = "ring_right2")]
2235 RingRight2,
2236}
2237
2238impl BodySlot {
2239 pub const ALL: [BodySlot; 15] = [
2241 BodySlot::Head,
2242 BodySlot::Chest,
2243 BodySlot::Forearms,
2244 BodySlot::Legs,
2245 BodySlot::Feet,
2246 BodySlot::Cloak,
2247 BodySlot::Back,
2248 BodySlot::Waist,
2249 BodySlot::Earrings,
2250 BodySlot::Necklace,
2251 BodySlot::Eyeglasses,
2252 BodySlot::RingLeft1,
2253 BodySlot::RingLeft2,
2254 BodySlot::RingRight1,
2255 BodySlot::RingRight2,
2256 ];
2257
2258 pub fn as_str(self) -> &'static str {
2259 match self {
2260 BodySlot::Head => "head",
2261 BodySlot::Chest => "chest",
2262 BodySlot::Forearms => "forearms",
2263 BodySlot::Legs => "legs",
2264 BodySlot::Feet => "feet",
2265 BodySlot::Cloak => "cloak",
2266 BodySlot::Back => "back",
2267 BodySlot::Waist => "waist",
2268 BodySlot::Earrings => "earrings",
2269 BodySlot::Necklace => "necklace",
2270 BodySlot::Eyeglasses => "eyeglasses",
2271 BodySlot::RingLeft1 => "ring_left_1",
2272 BodySlot::RingLeft2 => "ring_left_2",
2273 BodySlot::RingRight1 => "ring_right_1",
2274 BodySlot::RingRight2 => "ring_right_2",
2275 }
2276 }
2277}
2278
2279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2281#[serde(rename_all = "snake_case")]
2282pub enum InventoryLocation {
2283 Root,
2285 Worn { slot: BodySlot },
2287 Placed { container_id: String },
2289 Keychain,
2291}
2292
2293#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2295pub struct PlacedContainerView {
2296 pub id: String,
2297 pub template_id: String,
2298 pub display_name: String,
2299 pub x: f32,
2300 pub y: f32,
2301 pub z: f32,
2302 pub locked: bool,
2303 #[serde(default)]
2305 pub accessible: bool,
2306 #[serde(default)]
2307 pub owner_character_id: Option<Uuid>,
2308 #[serde(default)]
2310 pub contents: Vec<ItemStack>,
2311 #[serde(default)]
2313 pub lock_id: Option<String>,
2314 #[serde(default)]
2316 pub capacity_volume: Option<f32>,
2317 #[serde(default)]
2319 pub item_instance_id: Option<Uuid>,
2320 #[serde(default)]
2322 pub tile_id: Option<String>,
2323 #[serde(default)]
2325 pub paperdoll_ref: Option<String>,
2326 #[serde(default)]
2328 pub worker_lodging_capacity: Option<u32>,
2329}
2330
2331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2332pub struct BlueprintIngredientView {
2333 pub template_id: String,
2334 pub quantity: u32,
2335 pub consumed: bool,
2337}
2338
2339#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2340pub struct ToolRequirementView {
2341 pub item: String,
2342 pub consumed: bool,
2344}
2345
2346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2347pub struct SkillRequirementView {
2348 pub skill: String,
2349 pub level: u32,
2350}
2351
2352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2353pub struct BlueprintView {
2354 pub id: String,
2355 pub label: String,
2356 pub output: String,
2357 pub output_qty: u32,
2358 pub craft_ticks: u32,
2359 pub inputs: Vec<BlueprintIngredientView>,
2360 #[serde(default)]
2362 pub station: Option<String>,
2363 #[serde(default)]
2364 pub category: Option<String>,
2365 #[serde(default)]
2366 pub required_tools: Vec<ToolRequirementView>,
2367 #[serde(default)]
2368 pub skill: Option<SkillRequirementView>,
2369 #[serde(default)]
2370 pub failure_chance: f32,
2371 #[serde(default)]
2373 pub worker_train_copper: u64,
2374}
2375
2376#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2378#[serde(rename_all = "snake_case")]
2379pub enum TerrainKindView {
2380 #[default]
2381 Grass,
2382 Dirt,
2383 Tilled,
2384 Desert,
2385 Hill,
2386 Bog,
2387 ShallowWater,
2388 DeepWater,
2389 Trail,
2390 Road,
2391 Rock,
2392}
2393
2394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2395pub struct TerrainZoneView {
2396 pub id: String,
2397 pub x0: f32,
2398 pub y0: f32,
2399 pub x1: f32,
2400 pub y1: f32,
2401 #[serde(default)]
2402 pub kind: TerrainKindView,
2403 #[serde(default)]
2405 pub elevation: f32,
2406 #[serde(default)]
2409 pub glyph: Option<String>,
2410 #[serde(default)]
2412 pub color: Option<String>,
2413 #[serde(default)]
2415 pub tile_id: Option<String>,
2416 #[serde(default)]
2418 pub z_order: i32,
2419}
2420
2421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2423pub struct ZPlatformView {
2424 pub id: String,
2425 pub z: f32,
2426 pub x0: f32,
2427 pub y0: f32,
2428 pub x1: f32,
2429 pub y1: f32,
2430}
2431
2432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2434pub struct ZTransitionView {
2435 pub id: String,
2436 pub z_from: f32,
2437 pub z_to: f32,
2438 pub x0: f32,
2439 pub y0: f32,
2440 pub x1: f32,
2441 pub y1: f32,
2442}
2443
2444#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2445pub struct BuildingView {
2446 pub id: String,
2447 pub label: String,
2448 pub x: f32,
2449 pub y: f32,
2450 pub width_m: f32,
2451 pub depth_m: f32,
2452 #[serde(default)]
2453 pub interior_blueprint: Option<String>,
2454 #[serde(default)]
2455 pub tags: Vec<String>,
2456}
2457
2458#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2459pub struct DoorView {
2460 pub id: String,
2461 pub building_id: String,
2462 pub x: f32,
2463 pub y: f32,
2464 #[serde(default)]
2465 pub open: bool,
2466 #[serde(default)]
2467 pub portal: Option<String>,
2468}
2469
2470#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2471pub struct InteriorRoomView {
2472 pub id: String,
2473 pub label: String,
2474 pub floor: i32,
2475 pub x0: f32,
2476 pub y0: f32,
2477 pub x1: f32,
2478 pub y1: f32,
2479 #[serde(default)]
2480 pub floor_color: Option<String>,
2481 #[serde(default)]
2482 pub floor_glyph: Option<String>,
2483}
2484
2485#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2486pub struct InteriorDoorView {
2487 pub id: String,
2488 pub room_a: String,
2489 pub room_b: String,
2490 pub x: f32,
2491 pub y: f32,
2492 pub kind: String,
2493}
2494
2495#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2496pub struct InteriorMapView {
2497 pub building_id: String,
2498 pub blueprint_id: String,
2499 pub background_color: String,
2500 #[serde(default)]
2501 pub default_floor_color: Option<String>,
2502 #[serde(default = "default_floor_height_view")]
2503 pub floor_height_m: f32,
2504 pub rooms: Vec<InteriorRoomView>,
2505 #[serde(default)]
2506 pub room_doors: Vec<InteriorDoorView>,
2507}
2508
2509fn default_floor_height_view() -> f32 {
2510 3.0
2511}
2512
2513#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2514pub struct NpcView {
2515 pub id: String,
2516 pub label: String,
2517 pub role: String,
2518 pub x: f32,
2519 pub y: f32,
2520 #[serde(default)]
2522 pub building_id: Option<String>,
2523 #[serde(default)]
2525 pub entity_id: Option<EntityId>,
2526 #[serde(default)]
2527 pub life_state: Option<LifeState>,
2528 #[serde(default)]
2529 pub hp_pct: Option<f32>,
2530 #[serde(default)]
2532 pub can_trade: bool,
2533 #[serde(default)]
2535 pub tile_id: Option<String>,
2536 #[serde(default)]
2538 pub behavior_state: Option<String>,
2539 #[serde(default)]
2541 pub presentation_state: Option<String>,
2542 #[serde(default)]
2544 pub sprite_mode: Option<String>,
2545 #[serde(default)]
2547 pub paperdoll_ref: Option<String>,
2548}
2549
2550#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2551pub struct UseResult {
2552 pub template_id: String,
2553 pub hunger_restored: f32,
2554 pub thirst_restored: f32,
2555 #[serde(default)]
2556 pub health_restored: f32,
2557 #[serde(default)]
2558 pub mana_restored: f32,
2559 #[serde(default)]
2560 pub cleared_dot_ids: Vec<String>,
2561}
2562
2563#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2564pub struct CraftResult {
2565 pub blueprint_id: String,
2566 pub outputs: Vec<ItemStack>,
2567 pub consumed: Vec<ItemStack>,
2568 #[serde(default = "default_one")]
2570 pub batch_index: u32,
2571 #[serde(default = "default_one")]
2573 pub batch_total: u32,
2574}
2575
2576fn default_one() -> u32 {
2577 1
2578}
2579
2580#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2581pub struct DeathNotice {
2582 pub entity_id: EntityId,
2583 pub respawn_x: f32,
2584 pub respawn_y: f32,
2585 pub message: String,
2586}
2587
2588#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2589pub struct InteractionNotice {
2590 pub target_id: String,
2591 pub message: String,
2592 #[serde(default)]
2593 pub coins_delta: i32,
2594 #[serde(default)]
2595 pub inventory_delta: Vec<ItemStack>,
2596}
2597
2598#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2599#[serde(rename_all = "snake_case")]
2600pub enum NpcTalkTrustFlag {
2601 Stranger,
2602 Acquainted,
2603 Trusted,
2604}
2605
2606#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
2607#[serde(rename_all = "snake_case")]
2608pub enum NpcTalkDepth {
2609 #[default]
2610 Full,
2611 Brief,
2612 Unavailable,
2613}
2614
2615#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2616pub struct NpcTalkOpened {
2617 pub npc_id: String,
2618 pub npc_label: String,
2619 pub greeting: String,
2620 pub trust_flag: NpcTalkTrustFlag,
2621 #[serde(default)]
2622 pub talk_depth: NpcTalkDepth,
2623 #[serde(default = "default_true")]
2624 pub trade_allowed: bool,
2625}
2626
2627fn default_true() -> bool {
2628 true
2629}
2630
2631#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2632pub struct NpcTalkPending {
2633 pub npc_id: String,
2634}
2635
2636#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2637pub struct NpcTalkReply {
2638 pub npc_id: String,
2639 pub line: String,
2640 pub trust_flag: NpcTalkTrustFlag,
2641 #[serde(default)]
2642 pub wind_down: bool,
2643 #[serde(default)]
2644 pub trade_disabled: bool,
2645}
2646
2647#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2648pub struct NpcTalkClosed {
2649 pub npc_id: String,
2650}
2651
2652#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2653pub struct NpcTalkError {
2654 pub npc_id: String,
2655 pub reason: String,
2656}
2657
2658#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2659#[serde(rename_all = "snake_case")]
2660pub enum QuestStatusView {
2661 Available,
2662 Active,
2663 Completed,
2664}
2665
2666#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2667pub struct QuestObjectiveProgress {
2668 pub label: String,
2669 pub current: u32,
2670 pub required: u32,
2671 pub done: bool,
2672}
2673
2674#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2675pub struct QuestLogEntry {
2676 pub quest_id: String,
2677 pub title: String,
2678 pub description: String,
2679 pub status: QuestStatusView,
2680 #[serde(default)]
2681 pub current_step_id: Option<String>,
2682 #[serde(default)]
2683 pub current_step_title: String,
2684 #[serde(default)]
2685 pub objectives: Vec<QuestObjectiveProgress>,
2686 #[serde(default)]
2687 pub is_tracked: bool,
2688 #[serde(default)]
2689 pub can_withdraw: bool,
2690}
2691
2692#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2693pub struct InteractableView {
2694 pub id: String,
2695 pub kind: String,
2696 pub label: String,
2697 pub x: f32,
2698 pub y: f32,
2699 pub z: f32,
2700 #[serde(default)]
2701 pub board_id: Option<String>,
2702}
2703
2704#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2705pub struct QuestOffer {
2706 pub quest_id: String,
2707 pub title: String,
2708 pub description: String,
2709 #[serde(default)]
2710 pub step_count: u32,
2711}
2712
2713#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2714pub struct QuestNotice {
2715 pub quest_id: String,
2716 pub title: String,
2717 pub message: String,
2718}
2719
2720#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2721#[serde(rename_all = "snake_case")]
2722pub enum ShopOfferKind {
2723 Item,
2724 Blueprint,
2725}
2726
2727#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2728pub struct ShopOffer {
2729 pub offer_id: String,
2730 pub kind: ShopOfferKind,
2731 pub label: String,
2732 #[serde(default)]
2733 pub template_id: Option<String>,
2734 #[serde(default)]
2735 pub blueprint_id: Option<String>,
2736 pub price_copper: u32,
2737 #[serde(default)]
2738 pub affordable: bool,
2739 #[serde(default)]
2740 pub already_owned: bool,
2741}
2742
2743#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2744pub struct ShopBuyLine {
2745 pub template_id: String,
2746 pub label: String,
2747 pub quantity: u32,
2748 pub price_copper: u32,
2749}
2750
2751#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2753pub struct BankPanel {
2754 pub npc_id: String,
2755 pub npc_label: String,
2756 pub bank_balance_copper: u64,
2757 pub on_person_copper: u64,
2758 #[serde(default)]
2760 pub pending_outgoing_copper: u64,
2761 #[serde(default)]
2762 pub transfer_fee_bps: u32,
2763 #[serde(default)]
2764 pub transfer_clear_ticks: u64,
2765}
2766
2767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2769pub struct StoragePanel {
2770 pub npc_id: String,
2771 pub npc_label: String,
2772 pub building_id: String,
2773 pub building_label: String,
2774 pub used_volume: f32,
2775 pub max_volume: f32,
2776 #[serde(default)]
2777 pub contents: Vec<ItemStack>,
2778 #[serde(default)]
2780 pub ship_destinations: Vec<StorageShipDest>,
2781}
2782
2783#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2784pub struct StorageShipDest {
2785 pub building_id: String,
2786 pub label: String,
2787 pub distance_m: f32,
2788 pub fee_copper: u64,
2789 pub travel_ticks: u64,
2790}
2791
2792#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2793pub struct ShopCatalog {
2794 pub npc_id: String,
2795 pub npc_label: String,
2796 #[serde(default)]
2797 pub sells: Vec<ShopOffer>,
2798 #[serde(default)]
2799 pub buys: Vec<ShopBuyLine>,
2800}
2801
2802#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2803pub struct HarvestResult {
2804 pub node_id: String,
2805 pub quantity: u32,
2807 pub item_template: String,
2808 #[serde(default)]
2811 pub item_instance_id: Option<Uuid>,
2812}
2813
2814#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2816pub struct Envelope<T> {
2817 pub protocol_version: u16,
2818 pub payload: T,
2819}
2820
2821impl<T> Envelope<T> {
2822 pub fn new(payload: T) -> Self {
2823 Self {
2824 protocol_version: crate::PROTOCOL_VERSION,
2825 payload,
2826 }
2827 }
2828}
2829
2830#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2832pub struct Hello {
2833 pub client_name: String,
2834 pub protocol_version: u16,
2835 #[serde(default)]
2836 pub auth: AuthCredential,
2837 #[serde(default)]
2839 pub character_id: Option<Uuid>,
2840}
2841
2842#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2845#[serde(rename_all = "snake_case")]
2846pub enum AuthCredential {
2847 DevLocal,
2848 Session { token: String },
2849 ApiToken { token: String, character_id: Uuid },
2850}
2851
2852impl Default for AuthCredential {
2853 fn default() -> Self {
2854 Self::DevLocal
2855 }
2856}
2857
2858#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2859pub struct Welcome {
2860 pub session_id: SessionId,
2861 pub entity_id: EntityId,
2862 pub snapshot: Snapshot,
2863}
2864
2865#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2866pub enum ServerMessage {
2867 Welcome(Welcome),
2868 ContentUpdated(Snapshot),
2870 Tick(TickDelta),
2871 IntentAck {
2872 entity_id: EntityId,
2873 seq: Seq,
2874 tick: Tick,
2875 },
2876 Chat(ChatMessage),
2877 HarvestResult(HarvestResult),
2878 UseResult(UseResult),
2879 CraftResult(CraftResult),
2880 Death(DeathNotice),
2881 Interaction(InteractionNotice),
2882 ShopOpened(ShopCatalog),
2883 NpcTalkOpened(NpcTalkOpened),
2884 NpcTalkPending(NpcTalkPending),
2885 NpcTalkReply(NpcTalkReply),
2886 NpcTalkClosed(NpcTalkClosed),
2887 NpcTalkError(NpcTalkError),
2888 QuestOffer(QuestOffer),
2889 QuestAccepted(QuestNotice),
2890 QuestWithdrawn(QuestNotice),
2891 QuestStepCompleted(QuestNotice),
2892 QuestCompleted(QuestNotice),
2893 BankOpened(BankPanel),
2895 StorageOpened(StoragePanel),
2897}
2898
2899#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2900pub enum ClientMessage {
2901 Hello(Hello),
2902 Intent(Intent),
2903 Disconnect,
2904}
2905
2906#[cfg(test)]
2907mod tests {
2908 use super::*;
2909
2910 #[test]
2911 fn pristine_vitals_state_yields_full_pools() {
2912 let attrs = PrimaryAttributes::default();
2913 let vitals = StoredVitalsState::default().apply_to(attrs);
2914 assert!(vitals.health > 0.0);
2915 assert_eq!(vitals.health, vitals.health_max);
2916 assert!((vitals.mana_max - 61.0).abs() < 0.01);
2917 }
2918
2919 #[test]
2920 fn saved_vitals_scale_when_pool_max_increases() {
2921 let mut attrs = PrimaryAttributes::default();
2922 attrs.intelligence = 140;
2923 attrs.wisdom = 140;
2924 let saved = StoredVitalsState {
2925 health: 100.0,
2926 mana: 14.0,
2927 stamina: 100.0,
2928 ..StoredVitalsState::default()
2929 };
2930 let vitals = saved.apply_to(attrs);
2931 assert!(vitals.mana_max > 55.0);
2932 assert!(
2933 (vitals.mana - vitals.mana_max).abs() < 0.01,
2934 "full legacy mana bar migrates to full new bar"
2935 );
2936 }
2937
2938 #[test]
2939 fn empty_vitals_state_is_pristine() {
2940 let pristine = StoredVitalsState {
2941 health: 0.0,
2942 mana: 0.0,
2943 stamina: 0.0,
2944 hunger: 0.0,
2945 thirst: 0.0,
2946 coins: 0,
2947 deaths: 0,
2948 life_state: LifeState::Alive,
2949 };
2950 assert!(pristine.is_pristine());
2951 let vitals = pristine.apply_to(PrimaryAttributes::default());
2952 assert!(vitals.health > 0.0);
2953 }
2954
2955 #[test]
2956 fn stored_vitals_roundtrip_preserves_partial_pools() {
2957 let attrs = PrimaryAttributes::default();
2958 let mut live = PlayerVitals::from_attributes(attrs);
2959 live.health = 25.0;
2960 live.hunger = 77.0;
2961 live.deaths = 2;
2962 let stored = StoredVitalsState::from_live(&live);
2963 let restored = stored.apply_to(attrs);
2964 assert!(
2965 (restored.health - 25.0).abs() < 0.01,
2966 "partial HP below cap stays absolute"
2967 );
2968 assert_eq!(restored.hunger, 77.0);
2969 assert_eq!(restored.deaths, 2);
2970 }
2971
2972 #[test]
2973 fn skill_tiers_start_at_zero() {
2974 let skill = SkillProgress::default();
2975 assert_eq!(skill.level, 0);
2976 assert_eq!(skill.display_tier(), 0);
2977 let trained = SkillProgress {
2978 level: 250,
2979 last_trained_tick: 1,
2980 };
2981 assert_eq!(trained.display_tier(), 2);
2982 }
2983
2984 #[test]
2985 fn quest_server_messages_roundtrip_json() {
2986 use crate::codec::{Codec, PostcardCodec};
2987
2988 let offer = ServerMessage::QuestOffer(QuestOffer {
2989 quest_id: "ada_goblin_hunt".into(),
2990 title: "Goblin Trouble".into(),
2991 description: "Help Ada".into(),
2992 step_count: 3,
2993 });
2994 let notice = ServerMessage::QuestAccepted(QuestNotice {
2995 quest_id: "ada_goblin_hunt".into(),
2996 title: "Goblin Trouble".into(),
2997 message: "Quest accepted".into(),
2998 });
2999 for msg in [offer, notice] {
3000 let bytes = PostcardCodec.encode(&msg).unwrap();
3001 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
3002 assert_eq!(decoded, msg);
3003 }
3004 }
3005
3006 #[test]
3007 fn hotbar_consumable_binding_roundtrips() {
3008 let binding = hotbar_consumable_binding("bottle_of_water");
3009 assert_eq!(binding, "item:bottle_of_water");
3010 assert!(hotbar_binding_is_consumable(&binding));
3011 assert_eq!(
3012 hotbar_consumable_template(&binding),
3013 Some("bottle_of_water")
3014 );
3015 assert!(!hotbar_binding_is_consumable("fireball"));
3016 assert_eq!(hotbar_consumable_template("fireball"), None);
3017 }
3018}