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 whisper_pouch: Vec<ItemStack>,
576 #[serde(default)]
578 pub known_abilities: Vec<KnownAbility>,
579 #[serde(default)]
581 pub hotbar: Vec<Option<String>>,
582 #[serde(default)]
584 pub abilities_schema_version: u32,
585 #[serde(default)]
587 pub bank_balance_copper: u64,
588}
589
590fn default_auto_attack() -> bool {
591 true
592}
593
594impl Default for StoredCombatProfile {
595 fn default() -> Self {
596 Self {
597 combat_target_instance_id: None,
598 in_combat: false,
599 last_combat_tick: 0,
600 last_attack_tick: 0,
601 cooldowns_until_tick: BTreeMap::new(),
602 auto_attack_enabled: true,
603 mainhand_template_id: None,
604 mainhand_instance_id: None,
605 offhand_template_id: None,
606 offhand_instance_id: None,
607 worn: Vec::new(),
608 rotation_presets: Vec::new(),
609 target_slots: Vec::new(),
610 known_blueprint_ids: Vec::new(),
611 keychain: Vec::new(),
612 whisper_pouch: Vec::new(),
613 known_abilities: Vec::new(),
614 hotbar: Vec::new(),
615 abilities_schema_version: 0,
616 bank_balance_copper: 0,
617 }
618 }
619}
620
621#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
622pub struct EntityState {
623 pub id: EntityId,
624 pub transform: Transform,
625 #[serde(default)]
627 pub label: String,
628 #[serde(default)]
629 pub vitals: Option<PlayerVitals>,
630 #[serde(default)]
632 pub attributes: Option<PrimaryAttributes>,
633 #[serde(default)]
634 pub skills: Option<PlayerSkills>,
635 #[serde(default)]
637 pub inside_building: Option<String>,
638 #[serde(default)]
640 pub tile_id: Option<String>,
641 #[serde(default)]
643 pub paperdoll_ref: Option<String>,
644 #[serde(default)]
646 pub presentation_state: Option<String>,
647 #[serde(default)]
649 pub sprite_mode: Option<String>,
650 #[serde(default)]
652 pub progression_xp: Option<ProgressionXp>,
653}
654
655#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
657#[serde(rename_all = "snake_case")]
658pub enum ChatChannel {
659 Nearby,
661 Direct,
663 Whisper,
665 WhisperStone,
667}
668
669#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
671#[serde(rename_all = "snake_case")]
672pub enum ChatClarity {
673 #[default]
674 Clear,
675 Partial,
676 Heavy,
677}
678
679#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
680pub struct ChatMessage {
681 pub channel: ChatChannel,
682 pub from_entity: EntityId,
683 pub from_name: String,
684 pub text: String,
686 pub tick: Tick,
687 #[serde(default)]
689 pub to_entity: Option<EntityId>,
690 #[serde(default)]
691 pub clarity: ChatClarity,
692}
693
694#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
696pub enum Intent {
697 Move {
698 entity_id: EntityId,
699 forward: f32,
700 strafe: f32,
701 #[serde(default)]
703 vertical: f32,
704 #[serde(default)]
706 sprint: bool,
707 seq: Seq,
708 },
709 Stop {
710 entity_id: EntityId,
711 seq: Seq,
712 },
713 Harvest {
714 entity_id: EntityId,
715 node_id: String,
716 seq: Seq,
717 },
718 Use {
719 entity_id: EntityId,
720 template_id: String,
721 seq: Seq,
722 },
723 UseGrant {
725 entity_id: EntityId,
726 grant_instance_id: Uuid,
727 target_instance_id: Uuid,
728 seq: Seq,
729 },
730 Say {
731 entity_id: EntityId,
732 channel: ChatChannel,
733 text: String,
734 #[serde(default)]
736 to_entity: Option<EntityId>,
737 seq: Seq,
738 },
739 Craft {
741 entity_id: EntityId,
742 blueprint_id: String,
743 #[serde(default)]
745 count: Option<u32>,
746 seq: Seq,
747 },
748 Interact {
750 entity_id: EntityId,
751 target_id: String,
752 seq: Seq,
753 },
754 ShopBuy {
756 entity_id: EntityId,
757 npc_id: String,
758 offer_id: String,
759 #[serde(default = "default_one")]
760 quantity: u32,
761 seq: Seq,
762 },
763 ShopSell {
765 entity_id: EntityId,
766 npc_id: String,
767 template_id: String,
768 #[serde(default = "default_one")]
769 quantity: u32,
770 seq: Seq,
771 },
772 ShopClose {
774 entity_id: EntityId,
775 npc_id: String,
776 seq: Seq,
777 },
778 TestDamage {
780 entity_id: EntityId,
781 amount: f32,
782 seq: Seq,
783 },
784 SetTarget {
786 entity_id: EntityId,
787 target_id: EntityId,
788 seq: Seq,
789 },
790 SetTargetSlot {
792 entity_id: EntityId,
793 slot_index: u8,
794 target_id: EntityId,
795 seq: Seq,
796 },
797 ClearTarget {
798 entity_id: EntityId,
799 seq: Seq,
800 },
801 ClearTargetSlot {
802 entity_id: EntityId,
803 slot_index: u8,
804 seq: Seq,
805 },
806 SetAutoAttack {
808 entity_id: EntityId,
809 slot_index: u8,
810 enabled: bool,
811 seq: Seq,
812 },
813 Attack {
815 entity_id: EntityId,
816 #[serde(default)]
817 target_id: Option<EntityId>,
818 #[serde(default)]
819 weapon_slot: Option<u32>,
820 seq: Seq,
821 },
822 Pickup {
824 entity_id: EntityId,
825 #[serde(default)]
826 drop_id: Option<String>,
827 seq: Seq,
828 },
829 Cast {
831 entity_id: EntityId,
832 ability_id: String,
833 target_id: EntityId,
834 seq: Seq,
835 },
836 BindActionSlot {
838 entity_id: EntityId,
839 slot_index: u8,
840 ability_id: String,
841 #[serde(default = "default_auto_attack")]
842 auto_enabled: bool,
843 seq: Seq,
844 },
845 UseActionSlot {
847 entity_id: EntityId,
848 slot_index: u8,
849 seq: Seq,
850 },
851 Dodge {
853 entity_id: EntityId,
854 seq: Seq,
855 },
856 Lunge {
858 entity_id: EntityId,
859 #[serde(default)]
861 forward: f32,
862 #[serde(default)]
864 strafe: f32,
865 seq: Seq,
866 },
867 DirectionalJump {
869 entity_id: EntityId,
870 #[serde(default)]
872 forward: f32,
873 #[serde(default)]
875 strafe: f32,
876 seq: Seq,
877 },
878 Block {
880 entity_id: EntityId,
881 #[serde(default = "default_block_enabled")]
882 enabled: bool,
883 seq: Seq,
884 },
885 EquipMainhand {
889 entity_id: EntityId,
890 #[serde(default)]
891 template_id: Option<String>,
892 #[serde(default)]
893 instance_id: Option<Uuid>,
894 seq: Seq,
895 },
896 EquipOffhand {
898 entity_id: EntityId,
899 #[serde(default)]
900 template_id: Option<String>,
901 #[serde(default)]
902 instance_id: Option<Uuid>,
903 seq: Seq,
904 },
905 EquipWorn {
908 entity_id: EntityId,
909 slot: BodySlot,
910 #[serde(default)]
911 instance_id: Option<Uuid>,
912 seq: Seq,
913 },
914 MoveItem {
916 entity_id: EntityId,
917 item_instance_id: Uuid,
918 from: InventoryLocation,
919 to: InventoryLocation,
920 #[serde(default)]
922 to_parent_instance_id: Option<Uuid>,
923 #[serde(default)]
925 quantity: Option<u32>,
926 seq: Seq,
927 },
928 PlaceContainer {
930 entity_id: EntityId,
931 item_instance_id: Uuid,
932 seq: Seq,
933 },
934 PickupContainer {
936 entity_id: EntityId,
937 container_id: String,
938 seq: Seq,
939 },
940 SetContainerLocked {
942 entity_id: EntityId,
943 location: InventoryLocation,
945 locked: bool,
946 seq: Seq,
947 },
948 DropItem {
950 entity_id: EntityId,
951 item_instance_id: Uuid,
952 from: InventoryLocation,
953 seq: Seq,
954 },
955 DestroyItem {
957 entity_id: EntityId,
958 item_instance_id: Uuid,
959 from: InventoryLocation,
960 #[serde(default)]
962 quantity: Option<u32>,
963 seq: Seq,
964 },
965 RenameContainer {
967 entity_id: EntityId,
968 item_instance_id: Uuid,
969 location: InventoryLocation,
970 name: String,
971 seq: Seq,
972 },
973 UpsertRotationPreset {
975 entity_id: EntityId,
976 preset: RotationPreset,
977 seq: Seq,
978 },
979 DeleteRotationPreset {
981 entity_id: EntityId,
982 preset_id: String,
983 seq: Seq,
984 },
985 AssignSlotPreset {
987 entity_id: EntityId,
988 slot_index: u8,
989 preset_id: String,
990 seq: Seq,
991 },
992 SetHotbarSlot {
995 entity_id: EntityId,
996 slot: u8,
998 #[serde(default)]
1000 ability_id: Option<String>,
1001 seq: Seq,
1002 },
1003 AdvanceRotation {
1005 entity_id: EntityId,
1006 slot_index: u8,
1007 seq: Seq,
1008 },
1009 NpcTalkOpen {
1011 entity_id: EntityId,
1012 npc_id: String,
1013 seq: Seq,
1014 },
1015 NpcTalkSay {
1017 entity_id: EntityId,
1018 npc_id: String,
1019 message: String,
1020 seq: Seq,
1021 },
1022 NpcTalkClose {
1024 entity_id: EntityId,
1025 npc_id: String,
1026 seq: Seq,
1027 },
1028 AcceptQuest {
1030 entity_id: EntityId,
1031 quest_id: String,
1032 seq: Seq,
1033 },
1034 WithdrawQuest {
1036 entity_id: EntityId,
1037 quest_id: String,
1038 seq: Seq,
1039 },
1040 TrackQuest {
1042 entity_id: EntityId,
1043 quest_id: String,
1044 seq: Seq,
1045 },
1046 QuestGiveItem {
1048 entity_id: EntityId,
1049 npc_id: String,
1050 template_id: String,
1051 #[serde(default = "default_one")]
1052 quantity: u32,
1053 seq: Seq,
1054 },
1055 HireWorker {
1057 entity_id: EntityId,
1058 def_id: String,
1059 wage_copper_per_interval: u32,
1060 #[serde(default)]
1061 lodging_container_id: Option<String>,
1062 #[serde(default)]
1063 job_yaml: Option<String>,
1064 seq: Seq,
1065 },
1066 DismissWorker {
1068 entity_id: EntityId,
1069 worker_instance_id: String,
1070 seq: Seq,
1071 },
1072 SetWorkerJob {
1074 entity_id: EntityId,
1075 worker_instance_id: String,
1076 job_yaml: String,
1077 seq: Seq,
1078 },
1079 AssignWorkerLodging {
1081 entity_id: EntityId,
1082 worker_instance_id: String,
1083 lodging_container_id: String,
1084 seq: Seq,
1085 },
1086 SetWorkerMode {
1088 entity_id: EntityId,
1089 worker_instance_id: String,
1090 mode: String,
1091 seq: Seq,
1092 },
1093 GiveWorkerItem {
1096 entity_id: EntityId,
1097 worker_instance_id: String,
1098 item_instance_id: uuid::Uuid,
1099 #[serde(default)]
1100 quantity: Option<u32>,
1101 seq: Seq,
1102 },
1103 TakeWorkerItem {
1105 entity_id: EntityId,
1106 worker_instance_id: String,
1107 item_instance_id: uuid::Uuid,
1108 #[serde(default)]
1109 quantity: Option<u32>,
1110 seq: Seq,
1111 },
1112 RenameHiredWorker {
1114 entity_id: EntityId,
1115 worker_instance_id: String,
1116 name: String,
1117 seq: Seq,
1118 },
1119 TeachWorkerBlueprint {
1121 entity_id: EntityId,
1122 worker_instance_id: String,
1123 blueprint_id: String,
1124 seq: Seq,
1125 },
1126 BuyProperty {
1128 entity_id: EntityId,
1129 zone_id: String,
1130 seq: Seq,
1131 },
1132 SellProperty {
1134 entity_id: EntityId,
1135 zone_id: String,
1136 seq: Seq,
1137 },
1138 BankDeposit {
1140 entity_id: EntityId,
1141 npc_id: String,
1142 #[serde(default)]
1144 amount_copper: u64,
1145 seq: Seq,
1146 },
1147 BankWithdraw {
1149 entity_id: EntityId,
1150 npc_id: String,
1151 #[serde(default)]
1153 amount_copper: u64,
1154 seq: Seq,
1155 },
1156 BankClose {
1158 entity_id: EntityId,
1159 npc_id: String,
1160 seq: Seq,
1161 },
1162 BankTransfer {
1164 entity_id: EntityId,
1165 npc_id: String,
1166 #[serde(default)]
1168 to_character_id: Option<Uuid>,
1169 #[serde(default)]
1171 to_name: String,
1172 amount_copper: u64,
1174 seq: Seq,
1175 },
1176 StorageStore {
1178 entity_id: EntityId,
1179 npc_id: String,
1180 item_instance_id: Uuid,
1181 #[serde(default)]
1182 quantity: Option<u32>,
1183 seq: Seq,
1184 },
1185 StorageTake {
1187 entity_id: EntityId,
1188 npc_id: String,
1189 item_instance_id: Uuid,
1190 #[serde(default)]
1191 quantity: Option<u32>,
1192 seq: Seq,
1193 },
1194 StorageShip {
1196 entity_id: EntityId,
1197 npc_id: String,
1198 dest_building_id: String,
1199 item_instance_id: Uuid,
1200 #[serde(default)]
1201 quantity: Option<u32>,
1202 seq: Seq,
1203 },
1204 StorageClose {
1206 entity_id: EntityId,
1207 npc_id: String,
1208 seq: Seq,
1209 },
1210 TradeRequest {
1212 entity_id: EntityId,
1213 peer_entity_id: EntityId,
1214 seq: Seq,
1215 },
1216 TradeRespond {
1218 entity_id: EntityId,
1219 peer_entity_id: EntityId,
1220 accept: bool,
1221 seq: Seq,
1222 },
1223 TradePresent {
1225 entity_id: EntityId,
1226 item_instance_id: Uuid,
1227 #[serde(default)]
1228 quantity: Option<u32>,
1229 seq: Seq,
1230 },
1231 TradeUnpresent {
1233 entity_id: EntityId,
1234 item_instance_id: Uuid,
1235 seq: Seq,
1236 },
1237 TradeSetReady {
1239 entity_id: EntityId,
1240 ready: bool,
1241 seq: Seq,
1242 },
1243 TradeCancel {
1245 entity_id: EntityId,
1246 seq: Seq,
1247 },
1248 DestroyWhisperStone {
1250 entity_id: EntityId,
1251 item_instance_id: Uuid,
1252 seq: Seq,
1253 },
1254 StowWhisperStone {
1256 entity_id: EntityId,
1257 item_instance_id: Uuid,
1258 seq: Seq,
1259 },
1260}
1261
1262fn default_block_enabled() -> bool {
1263 true
1264}
1265
1266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1268pub struct StatusEffectHud {
1269 pub effect_id: String,
1270 pub label: String,
1271 #[serde(default)]
1272 pub polarity: String,
1273 #[serde(default)]
1274 pub icon_tile_id: Option<String>,
1275 #[serde(default)]
1277 pub remaining_sec: Option<f32>,
1278}
1279
1280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1282pub struct CombatTargetHud {
1283 pub entity_id: EntityId,
1284 #[serde(default)]
1285 pub label: String,
1286 #[serde(default)]
1287 pub level: u32,
1288 pub health: f32,
1289 pub health_max: f32,
1290 #[serde(default)]
1291 pub life_state: LifeState,
1292 #[serde(default)]
1293 pub distance_m: f32,
1294 #[serde(default)]
1295 pub statuses: Vec<StatusEffectHud>,
1296}
1297
1298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1300pub struct CastProgressHud {
1301 #[serde(default)]
1302 pub ability_id: String,
1303 #[serde(default)]
1304 pub ability_label: String,
1305 #[serde(default)]
1306 pub ticks_remaining: u64,
1307 #[serde(default)]
1308 pub ticks_total: u64,
1309}
1310
1311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1313pub struct AbilityCooldownHud {
1314 #[serde(default)]
1315 pub ability_id: String,
1316 #[serde(default)]
1317 pub label: String,
1318 #[serde(default)]
1319 pub cd_ticks: u64,
1320 #[serde(default)]
1321 pub cd_total_ticks: u64,
1322}
1323
1324#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1326pub struct CombatSlotHud {
1327 pub slot_index: u8,
1328 #[serde(default)]
1329 pub target_entity_id: Option<EntityId>,
1330 #[serde(default)]
1331 pub target_label: Option<String>,
1332 #[serde(default)]
1333 pub target: Option<CombatTargetHud>,
1334 #[serde(default)]
1335 pub preset_id: Option<String>,
1336 #[serde(default)]
1337 pub preset_label: Option<String>,
1338 #[serde(default)]
1339 pub rotation: Vec<String>,
1340 #[serde(default)]
1341 pub rotation_index: u32,
1342 #[serde(default)]
1343 pub next_ability_id: Option<String>,
1344 #[serde(default)]
1345 pub auto_enabled: bool,
1346}
1347
1348#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1350pub struct DefensePieceHud {
1351 pub slot: BodySlot,
1352 pub label: String,
1353 pub template_id: String,
1354 #[serde(default)]
1355 pub armor_physical: f32,
1356 #[serde(default)]
1357 pub resists: Vec<(String, f32)>,
1358}
1359
1360impl Default for DefensePieceHud {
1361 fn default() -> Self {
1362 Self {
1363 slot: BodySlot::Head,
1364 label: String::new(),
1365 template_id: String::new(),
1366 armor_physical: 0.0,
1367 resists: Vec::new(),
1368 }
1369 }
1370}
1371
1372#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1374pub struct DefenseHud {
1375 pub armor_physical: f32,
1376 pub vitality_contribution: f32,
1377 pub total_mitigation_rating: f32,
1378 pub estimated_physical_dr: f32,
1380 #[serde(default)]
1381 pub resists: Vec<(String, f32)>,
1382 #[serde(default)]
1383 pub pieces: Vec<DefensePieceHud>,
1384}
1385
1386#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1388pub struct CombatHud {
1389 pub in_combat: bool,
1390 pub auto_attack: bool,
1392 pub has_los: bool,
1393 pub attack_cd_ticks: u64,
1394 #[serde(default)]
1395 pub ability_id: String,
1396 #[serde(default)]
1397 pub target_entity_id: Option<EntityId>,
1398 #[serde(default)]
1399 pub target_label: Option<String>,
1400 #[serde(default)]
1401 pub max_target_slots: u8,
1402 #[serde(default)]
1403 pub slots: Vec<CombatSlotHud>,
1404 #[serde(default)]
1405 pub rotation_presets: Vec<RotationPreset>,
1406 #[serde(default)]
1407 pub gcd_ticks: u64,
1408 #[serde(default)]
1409 pub mainhand_template_id: Option<String>,
1410 #[serde(default)]
1411 pub mainhand_label: Option<String>,
1412 #[serde(default)]
1413 pub offhand_template_id: Option<String>,
1414 #[serde(default)]
1415 pub offhand_label: Option<String>,
1416 #[serde(default)]
1418 pub mainhand_hand_slots: u8,
1419 #[serde(default)]
1421 pub worn: Vec<(BodySlot, ItemStack)>,
1422 #[serde(default)]
1424 pub defense: Option<DefenseHud>,
1425 #[serde(default)]
1426 pub carry_mass: f32,
1427 #[serde(default)]
1428 pub carry_mass_max: f32,
1429 #[serde(default)]
1430 pub encumbrance: EncumbranceState,
1431 #[serde(default)]
1433 pub keychain: Vec<ItemStack>,
1434 #[serde(default)]
1436 pub whisper_pouch: Vec<ItemStack>,
1437 #[serde(default)]
1438 pub target: Option<CombatTargetHud>,
1439 #[serde(default)]
1440 pub cast: Option<CastProgressHud>,
1441 #[serde(default)]
1442 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1443 #[serde(default)]
1444 pub blocking_active: bool,
1445 #[serde(default)]
1447 pub progression_xp: Option<ProgressionXp>,
1448 #[serde(default)]
1449 pub progression_baseline: u16,
1450 #[serde(default)]
1451 pub progression_xp_base: f64,
1452 #[serde(default)]
1453 pub progression_xp_growth: f64,
1454 #[serde(default)]
1455 pub attributes: Option<PrimaryAttributes>,
1456 #[serde(default)]
1457 pub skills: Option<PlayerSkills>,
1458 #[serde(default)]
1460 pub statuses: Vec<StatusEffectHud>,
1461 #[serde(default)]
1463 pub known_abilities: Vec<String>,
1464 #[serde(default)]
1467 pub hotbar: Vec<Option<String>>,
1468 #[serde(default)]
1470 pub max_abilities_per_rotation: u8,
1471}
1472
1473pub const HOTBAR_ITEM_PREFIX: &str = "item:";
1475
1476pub fn hotbar_consumable_binding(template_id: &str) -> String {
1478 format!("{HOTBAR_ITEM_PREFIX}{}", template_id.trim())
1479}
1480
1481pub fn hotbar_consumable_template(binding: &str) -> Option<&str> {
1483 binding
1484 .strip_prefix(HOTBAR_ITEM_PREFIX)
1485 .map(str::trim)
1486 .filter(|id| !id.is_empty())
1487}
1488
1489pub fn hotbar_binding_is_consumable(binding: &str) -> bool {
1491 hotbar_consumable_template(binding).is_some()
1492}
1493
1494#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1496#[serde(rename_all = "snake_case")]
1497pub enum CombatFxKind {
1498 MeleeArc,
1499 Cone,
1500 Sphere,
1501 Beam,
1502 HitMarker,
1503}
1504
1505#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1507#[serde(rename_all = "snake_case")]
1508pub enum CombatFxHitOutcome {
1509 #[default]
1510 Hit,
1511 Blocked,
1512 Miss,
1513 Glance,
1514}
1515
1516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1518pub struct CombatFxHit {
1519 pub entity_id: EntityId,
1520 pub x: f32,
1521 pub y: f32,
1522 pub z: f32,
1523 #[serde(default)]
1524 pub outcome: CombatFxHitOutcome,
1525}
1526
1527#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1529pub struct CombatFx {
1530 pub id: u64,
1531 pub kind: CombatFxKind,
1532 pub ability_id: String,
1533 pub caster_id: EntityId,
1534 pub origin_x: f32,
1535 pub origin_y: f32,
1536 pub origin_z: f32,
1537 #[serde(default)]
1538 pub end_x: Option<f32>,
1539 #[serde(default)]
1540 pub end_y: Option<f32>,
1541 #[serde(default)]
1542 pub end_z: Option<f32>,
1543 #[serde(default)]
1544 pub yaw: Option<f32>,
1545 #[serde(default)]
1546 pub reach_m: Option<f32>,
1547 #[serde(default)]
1548 pub arc_deg: Option<f32>,
1549 #[serde(default)]
1550 pub radius_m: Option<f32>,
1551 #[serde(default)]
1552 pub hits: Vec<CombatFxHit>,
1553 pub until_tick: u64,
1555 #[serde(default)]
1556 pub damage_type: String,
1557}
1558
1559#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1561#[serde(rename_all = "snake_case")]
1562pub enum WorkerModeView {
1563 Companion,
1564 JobLoop,
1565 Idle,
1568}
1569
1570#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1572#[serde(rename_all = "snake_case")]
1573pub enum WorkerStateView {
1574 Idle,
1575 Traveling,
1576 Working,
1577 Resting,
1578 Waiting,
1579 Strike,
1580 Dismissed,
1581}
1582
1583#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
1585pub struct WorkerVitalsSummary {
1586 pub health_pct: f32,
1587 pub stamina_pct: f32,
1588}
1589
1590#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1592#[serde(rename_all = "snake_case")]
1593pub enum WorkerRouteKindView {
1594 #[default]
1595 HarvestLoop,
1596 Ordered,
1597}
1598
1599#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1601pub struct WorkerRouteView {
1602 #[serde(default)]
1603 pub kind: WorkerRouteKindView,
1604 #[serde(default)]
1605 pub lodging_container_id: Option<String>,
1606 #[serde(default)]
1608 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
1609 #[serde(default)]
1611 pub harvest_nodes: Vec<String>,
1612 #[serde(default = "default_route_carry_ratio")]
1613 pub carry_return_ratio: f32,
1614 #[serde(default)]
1616 pub stops: Vec<WorkerRouteStopView>,
1617}
1618
1619fn default_route_carry_ratio() -> f32 {
1620 0.90
1621}
1622
1623fn default_true_view() -> bool {
1624 true
1625}
1626
1627#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1629pub struct WorkerWithdrawItemView {
1630 pub template: String,
1631 #[serde(default)]
1632 pub qty: u32,
1633 #[serde(default)]
1635 pub all: bool,
1636}
1637
1638#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1639pub struct WorkerRouteWaypointView {
1640 pub x: f32,
1641 pub y: f32,
1642 pub z: f32,
1643}
1644
1645#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1654#[serde(rename_all = "snake_case")]
1655pub enum WorkerRouteStopView {
1656 Waypoint {
1657 x: f32,
1658 y: f32,
1659 #[serde(default)]
1660 z: f32,
1661 },
1662 HarvestNode {
1663 node_id: String,
1664 },
1665 DepositAt {
1666 container_id: String,
1667 #[serde(default)]
1668 filter: Option<Vec<String>>,
1669 },
1670 TradeWith {
1671 #[serde(default)]
1672 npc_id: Option<String>,
1673 template: String,
1674 #[serde(default = "default_true_view")]
1675 sell_all: bool,
1676 },
1677 WithdrawFrom {
1678 container_id: String,
1679 items: Vec<WorkerWithdrawItemView>,
1680 },
1681 CraftAt {
1682 device: String,
1683 blueprint: String,
1684 #[serde(default)]
1685 qty: Option<u32>,
1686 },
1687 RestIfNeeded,
1688 Wait {
1689 #[serde(default)]
1690 wait_ticks: u64,
1691 },
1692}
1693
1694#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1696#[serde(rename_all = "snake_case")]
1697pub enum LedgerCategory {
1698 Workers,
1699 Hire,
1700 Train,
1701 ShopBuy,
1702 Taxes,
1703 WorkerSales,
1704 TraderSales,
1705 BankDeposit,
1706 BankWithdraw,
1707 BankTransferOut,
1708 BankTransferIn,
1709 BankTransferFee,
1710 StorageShipFee,
1711 Other,
1712}
1713
1714impl LedgerCategory {
1715 pub fn as_str(self) -> &'static str {
1716 match self {
1717 Self::Workers => "workers",
1718 Self::Hire => "hire",
1719 Self::Train => "train",
1720 Self::ShopBuy => "shop_buy",
1721 Self::Taxes => "taxes",
1722 Self::WorkerSales => "worker_sales",
1723 Self::TraderSales => "trader_sales",
1724 Self::BankDeposit => "bank_deposit",
1725 Self::BankWithdraw => "bank_withdraw",
1726 Self::BankTransferOut => "bank_transfer_out",
1727 Self::BankTransferIn => "bank_transfer_in",
1728 Self::BankTransferFee => "bank_transfer_fee",
1729 Self::StorageShipFee => "storage_ship_fee",
1730 Self::Other => "other",
1731 }
1732 }
1733}
1734
1735#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1736pub struct LedgerEntryView {
1737 pub id: uuid::Uuid,
1738 pub game_day: u64,
1739 pub signed_copper: i64,
1740 pub category: LedgerCategory,
1741 #[serde(default)]
1742 pub label: String,
1743}
1744
1745#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1746pub struct LedgerPeriodTotals {
1747 #[serde(default)]
1749 pub expenses: std::collections::HashMap<String, u64>,
1750 #[serde(default)]
1752 pub income: std::collections::HashMap<String, u64>,
1753 pub expense_copper: u64,
1754 pub income_copper: u64,
1755 pub cash_flow_copper: i64,
1757}
1758
1759#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1760pub struct PlayerLedgerView {
1761 pub current_game_day: u64,
1762 #[serde(default)]
1763 pub period_day: LedgerPeriodTotals,
1764 #[serde(default)]
1765 pub period_week: LedgerPeriodTotals,
1766 #[serde(default)]
1767 pub period_month: LedgerPeriodTotals,
1768 #[serde(default)]
1769 pub period_lifetime: LedgerPeriodTotals,
1770 #[serde(default)]
1771 pub recent: Vec<LedgerEntryView>,
1772 #[serde(default)]
1774 pub wealth_on_person_copper: u64,
1775 #[serde(default)]
1777 pub wealth_in_storage_copper: u64,
1778 #[serde(default)]
1780 pub wealth_in_bank_copper: u64,
1781 #[serde(default)]
1783 pub wealth_total_copper: u64,
1784 #[serde(default)]
1786 pub live_expense_per_interval_copper: u64,
1787 #[serde(default)]
1789 pub live_income_route_est_per_loop_copper: u64,
1790 #[serde(default)]
1792 pub live_income_avg_per_interval_copper: u64,
1793 #[serde(default)]
1795 pub live_income_avg_window_intervals: u32,
1796 #[serde(default)]
1798 pub live_net_avg_per_interval_copper: i64,
1799}
1800
1801#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1803#[serde(rename_all = "snake_case")]
1804pub enum AnalyticsMetric {
1805 NpcKill,
1806 WildlifeKill,
1807 Harvest,
1808 QuestComplete,
1809 QuestAccept,
1810 QuestAbandon,
1811 PlayerDeath,
1812 Craft,
1813 WorkerHire,
1814 WorkerDismiss,
1815 WorkerTeach,
1816 NpcTalk,
1817 ShopBuy,
1818 ShopSell,
1819 PlaceContainer,
1820 PickupContainer,
1821 PickupDrop,
1822 ConsumableUse,
1823 AbilityUse,
1824 DistanceWalkedM,
1825 DoorUse,
1826 BuildingEnter,
1827}
1828
1829impl AnalyticsMetric {
1830 pub fn as_str(self) -> &'static str {
1831 match self {
1832 Self::NpcKill => "npc_kill",
1833 Self::WildlifeKill => "wildlife_kill",
1834 Self::Harvest => "harvest",
1835 Self::QuestComplete => "quest_complete",
1836 Self::QuestAccept => "quest_accept",
1837 Self::QuestAbandon => "quest_abandon",
1838 Self::PlayerDeath => "player_death",
1839 Self::Craft => "craft",
1840 Self::WorkerHire => "worker_hire",
1841 Self::WorkerDismiss => "worker_dismiss",
1842 Self::WorkerTeach => "worker_teach",
1843 Self::NpcTalk => "npc_talk",
1844 Self::ShopBuy => "shop_buy",
1845 Self::ShopSell => "shop_sell",
1846 Self::PlaceContainer => "place_container",
1847 Self::PickupContainer => "pickup_container",
1848 Self::PickupDrop => "pickup_drop",
1849 Self::ConsumableUse => "consumable_use",
1850 Self::AbilityUse => "ability_use",
1851 Self::DistanceWalkedM => "distance_walked_m",
1852 Self::DoorUse => "door_use",
1853 Self::BuildingEnter => "building_enter",
1854 }
1855 }
1856
1857 pub fn from_str_key(s: &str) -> Option<Self> {
1858 Some(match s {
1859 "npc_kill" => Self::NpcKill,
1860 "wildlife_kill" => Self::WildlifeKill,
1861 "harvest" => Self::Harvest,
1862 "quest_complete" => Self::QuestComplete,
1863 "quest_accept" => Self::QuestAccept,
1864 "quest_abandon" => Self::QuestAbandon,
1865 "player_death" => Self::PlayerDeath,
1866 "craft" => Self::Craft,
1867 "worker_hire" => Self::WorkerHire,
1868 "worker_dismiss" => Self::WorkerDismiss,
1869 "worker_teach" => Self::WorkerTeach,
1870 "npc_talk" => Self::NpcTalk,
1871 "shop_buy" => Self::ShopBuy,
1872 "shop_sell" => Self::ShopSell,
1873 "place_container" => Self::PlaceContainer,
1874 "pickup_container" => Self::PickupContainer,
1875 "pickup_drop" => Self::PickupDrop,
1876 "consumable_use" => Self::ConsumableUse,
1877 "ability_use" => Self::AbilityUse,
1878 "distance_walked_m" => Self::DistanceWalkedM,
1879 "door_use" => Self::DoorUse,
1880 "building_enter" => Self::BuildingEnter,
1881 _ => return None,
1882 })
1883 }
1884}
1885
1886#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1887pub struct CareerMetricRow {
1888 pub subject_id: String,
1889 pub amount: u64,
1890}
1891
1892#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1894pub struct PlayerCareerView {
1895 pub current_game_day: u64,
1896 #[serde(default)]
1897 pub kills: Vec<CareerMetricRow>,
1898 #[serde(default)]
1899 pub harvests: Vec<CareerMetricRow>,
1900 pub quests_completed: u64,
1901 #[serde(default)]
1902 pub crafts: Vec<CareerMetricRow>,
1903 pub deaths: u64,
1904 pub npc_talks: u64,
1905 pub shop_buys: u64,
1906 pub shop_sells: u64,
1907 pub distance_m: u64,
1908 #[serde(default)]
1909 pub other: Vec<CareerMetricRow>,
1910}
1911
1912#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1914pub struct HiredWorkerView {
1915 pub instance_id: String,
1916 pub entity_id: EntityId,
1917 pub def_id: String,
1918 pub label: String,
1920 pub x: f32,
1921 pub y: f32,
1922 pub z: f32,
1923 pub mode: WorkerModeView,
1924 pub state: WorkerStateView,
1925 #[serde(default)]
1926 pub step_label: String,
1927 pub vitals: WorkerVitalsSummary,
1928 #[serde(default)]
1929 pub carry_pct: f32,
1930 #[serde(default)]
1931 pub last_error: Option<String>,
1932 pub wage_copper_per_interval: u32,
1933 #[serde(default)]
1935 pub effective_wage_copper: u32,
1936 #[serde(default)]
1938 pub wage_meters_walked: f32,
1939 #[serde(default)]
1941 pub lodging_container_id: Option<String>,
1942 #[serde(default)]
1944 pub route: Option<WorkerRouteView>,
1945 #[serde(default)]
1947 pub known_blueprint_ids: Vec<String>,
1948 #[serde(default = "default_worker_view_level")]
1950 pub level: u32,
1951 #[serde(default)]
1953 pub worker_xp: f64,
1954 #[serde(default)]
1956 pub inventory: Vec<ItemStack>,
1957}
1958
1959fn default_worker_view_level() -> u32 {
1960 1
1961}
1962
1963#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1965pub struct TickDelta {
1966 pub tick: Tick,
1967 pub entities: Vec<EntityState>,
1968 #[serde(default)]
1969 pub resource_nodes: Vec<ResourceNodeView>,
1970 #[serde(default)]
1971 pub buildings: Vec<BuildingView>,
1972 #[serde(default)]
1973 pub doors: Vec<DoorView>,
1974 #[serde(default)]
1975 pub npcs: Vec<NpcView>,
1976 #[serde(default)]
1978 pub inventory: Vec<ItemStack>,
1979 #[serde(default)]
1980 pub blueprints: Vec<BlueprintView>,
1981 #[serde(default)]
1982 pub world_clock: WorldClock,
1983 #[serde(default)]
1984 pub ground_drops: Vec<GroundDropView>,
1985 #[serde(default)]
1986 pub placed_containers: Vec<PlacedContainerView>,
1987 #[serde(default)]
1988 pub combat: Option<CombatHud>,
1989 #[serde(default)]
1990 pub interior_map: Option<InteriorMapView>,
1991 #[serde(default)]
1992 pub quest_log: Vec<QuestLogEntry>,
1993 #[serde(default)]
1994 pub hired_workers: Vec<HiredWorkerView>,
1995 #[serde(default)]
1996 pub interactables: Vec<InteractableView>,
1997 #[serde(default)]
1998 pub ledger: Option<PlayerLedgerView>,
1999 #[serde(default)]
2000 pub career: Option<PlayerCareerView>,
2001 #[serde(default)]
2003 pub combat_fx: Vec<CombatFx>,
2004}
2005#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2006pub struct GroundDropView {
2007 pub id: String,
2008 pub template_id: String,
2009 pub quantity: u32,
2010 pub x: f32,
2011 pub y: f32,
2012 pub z: f32,
2013 #[serde(default)]
2015 pub tile_id: Option<String>,
2016 #[serde(default)]
2018 pub paperdoll_ref: Option<String>,
2019 #[serde(default)]
2021 pub yaw: f32,
2022 #[serde(default)]
2024 pub pitch: f32,
2025 #[serde(default)]
2027 pub roll: f32,
2028 #[serde(default = "default_draw_scale")]
2030 pub draw_scale: f32,
2031}
2032
2033fn default_draw_scale() -> f32 {
2034 1.0
2035}
2036
2037#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2039pub struct Snapshot {
2040 pub tick: Tick,
2041 pub chunk_rev: u64,
2042 #[serde(default)]
2044 pub content_rev: u64,
2045 #[serde(default)]
2047 pub publish_rev: u64,
2048 pub entities: Vec<EntityState>,
2049 #[serde(default)]
2050 pub resource_nodes: Vec<ResourceNodeView>,
2051 #[serde(default)]
2053 pub world_width_m: f32,
2054 #[serde(default)]
2055 pub world_height_m: f32,
2056 #[serde(default)]
2057 pub buildings: Vec<BuildingView>,
2058 #[serde(default)]
2059 pub doors: Vec<DoorView>,
2060 #[serde(default)]
2061 pub npcs: Vec<NpcView>,
2062 #[serde(default)]
2063 pub inventory: Vec<ItemStack>,
2064 #[serde(default)]
2065 pub blueprints: Vec<BlueprintView>,
2066 #[serde(default)]
2067 pub world_clock: WorldClock,
2068 #[serde(default)]
2069 pub terrain_zones: Vec<TerrainZoneView>,
2070 #[serde(default)]
2071 pub z_platforms: Vec<ZPlatformView>,
2072 #[serde(default)]
2073 pub z_transitions: Vec<ZTransitionView>,
2074 #[serde(default)]
2075 pub ground_drops: Vec<GroundDropView>,
2076 #[serde(default)]
2077 pub placed_containers: Vec<PlacedContainerView>,
2078 #[serde(default)]
2079 pub combat: Option<CombatHud>,
2080 #[serde(default)]
2081 pub interior_map: Option<InteriorMapView>,
2082 #[serde(default)]
2083 pub quest_log: Vec<QuestLogEntry>,
2084 #[serde(default)]
2085 pub hired_workers: Vec<HiredWorkerView>,
2086 #[serde(default)]
2087 pub interactables: Vec<InteractableView>,
2088 #[serde(default)]
2089 pub ledger: Option<PlayerLedgerView>,
2090 #[serde(default)]
2091 pub career: Option<PlayerCareerView>,
2092 #[serde(default)]
2094 pub combat_fx: Vec<CombatFx>,
2095}
2096
2097#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2099pub struct ResourceNodeView {
2100 pub id: String,
2101 pub label: String,
2102 pub x: f32,
2103 pub y: f32,
2104 pub z: f32,
2105 pub item_template: String,
2106 #[serde(default = "default_node_state")]
2107 pub state: ResourceNodeState,
2108 #[serde(default = "default_blocking_view")]
2110 pub blocking: bool,
2111 #[serde(default = "default_blocking_radius_view")]
2113 pub blocking_radius_m: f32,
2114 #[serde(default)]
2116 pub tile_id: Option<String>,
2117 #[serde(default)]
2119 pub paperdoll_ref: Option<String>,
2120 #[serde(default)]
2122 pub yaw: f32,
2123 #[serde(default)]
2125 pub pitch: f32,
2126 #[serde(default)]
2128 pub roll: f32,
2129 #[serde(default = "default_draw_scale")]
2131 pub draw_scale: f32,
2132 #[serde(default)]
2134 pub sprite_mode: Option<String>,
2135 #[serde(default)]
2137 pub presentation_state: Option<String>,
2138}
2139
2140fn default_blocking_radius_view() -> f32 {
2141 0.8
2142}
2143
2144fn default_blocking_view() -> bool {
2145 true
2146}
2147
2148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2149#[serde(rename_all = "snake_case")]
2150pub enum ResourceNodeState {
2151 Available,
2152 Harvesting,
2153 Cooldown,
2154}
2155
2156fn default_node_state() -> ResourceNodeState {
2157 ResourceNodeState::Available
2158}
2159
2160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2161#[serde(rename_all = "snake_case")]
2162pub enum ItemStatusBindingMode {
2163 OnHit,
2164 WhileEquipped,
2165}
2166
2167impl Default for ItemStatusBindingMode {
2168 fn default() -> Self {
2169 Self::OnHit
2170 }
2171}
2172
2173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2175pub struct ItemStatusBinding {
2176 pub effect_id: String,
2177 #[serde(default)]
2178 pub mode: ItemStatusBindingMode,
2179 #[serde(default)]
2181 pub source: String,
2182 #[serde(default)]
2183 pub applied_at_tick: u64,
2184 #[serde(default, skip_serializing_if = "Option::is_none")]
2186 pub expires_at_tick: Option<u64>,
2187}
2188
2189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2190pub struct ItemStack {
2191 pub template_id: String,
2192 pub quantity: u32,
2193 #[serde(default)]
2195 pub item_instance_id: Option<Uuid>,
2196 #[serde(default)]
2198 pub props: BTreeMap<String, String>,
2199 #[serde(default)]
2201 pub status_bindings: Vec<ItemStatusBinding>,
2202 #[serde(default)]
2204 pub contents: Vec<ItemStack>,
2205 #[serde(default)]
2207 pub display_name: Option<String>,
2208 #[serde(default)]
2210 pub category: Option<String>,
2211 #[serde(default)]
2213 pub base_mass: Option<f32>,
2214 #[serde(default)]
2216 pub base_volume: Option<f32>,
2217 #[serde(default)]
2219 pub capacity_volume: Option<f32>,
2220 #[serde(default)]
2222 pub stackable: Option<bool>,
2223 #[serde(default)]
2225 pub world_placeable: Option<bool>,
2226 #[serde(default)]
2228 pub worker_lodging_capacity: Option<u32>,
2229 #[serde(default)]
2231 pub equip_slot: Option<BodySlot>,
2232 #[serde(default)]
2234 pub armor_physical: Option<f32>,
2235 #[serde(default)]
2237 pub resists: Vec<(String, f32)>,
2238 #[serde(default)]
2240 pub hand_slots: Option<u8>,
2241}
2242
2243impl ItemStack {
2244 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
2245 Self {
2246 template_id: template_id.into(),
2247 quantity,
2248 ..Default::default()
2249 }
2250 }
2251}
2252
2253impl Default for ItemStack {
2254 fn default() -> Self {
2255 Self {
2256 template_id: String::new(),
2257 quantity: 0,
2258 item_instance_id: None,
2259 props: BTreeMap::new(),
2260 status_bindings: Vec::new(),
2261 contents: Vec::new(),
2262 display_name: None,
2263 category: None,
2264 base_mass: None,
2265 base_volume: None,
2266 capacity_volume: None,
2267 stackable: None,
2268 world_placeable: None,
2269 worker_lodging_capacity: None,
2270 equip_slot: None,
2271 armor_physical: None,
2272 resists: Vec::new(),
2273 hand_slots: None,
2274 }
2275 }
2276}
2277
2278#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2280#[serde(rename_all = "snake_case")]
2281pub enum EncumbranceState {
2282 #[default]
2283 Light,
2284 Heavy,
2285 Over,
2286}
2287
2288#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
2292#[serde(rename_all = "snake_case")]
2293pub enum BodySlot {
2294 Head,
2295 #[serde(alias = "body")]
2297 Chest,
2298 #[serde(alias = "arms")]
2300 Forearms,
2301 Legs,
2302 Feet,
2303 Cloak,
2304 Back,
2305 Waist,
2306 Earrings,
2307 Necklace,
2308 Eyeglasses,
2309 #[serde(rename = "ring_left_1", alias = "ring_left1")]
2311 RingLeft1,
2312 #[serde(rename = "ring_left_2", alias = "ring_left2")]
2313 RingLeft2,
2314 #[serde(rename = "ring_right_1", alias = "ring_right1")]
2315 RingRight1,
2316 #[serde(rename = "ring_right_2", alias = "ring_right2")]
2317 RingRight2,
2318}
2319
2320impl BodySlot {
2321 pub const ALL: [BodySlot; 15] = [
2323 BodySlot::Head,
2324 BodySlot::Chest,
2325 BodySlot::Forearms,
2326 BodySlot::Legs,
2327 BodySlot::Feet,
2328 BodySlot::Cloak,
2329 BodySlot::Back,
2330 BodySlot::Waist,
2331 BodySlot::Earrings,
2332 BodySlot::Necklace,
2333 BodySlot::Eyeglasses,
2334 BodySlot::RingLeft1,
2335 BodySlot::RingLeft2,
2336 BodySlot::RingRight1,
2337 BodySlot::RingRight2,
2338 ];
2339
2340 pub fn as_str(self) -> &'static str {
2341 match self {
2342 BodySlot::Head => "head",
2343 BodySlot::Chest => "chest",
2344 BodySlot::Forearms => "forearms",
2345 BodySlot::Legs => "legs",
2346 BodySlot::Feet => "feet",
2347 BodySlot::Cloak => "cloak",
2348 BodySlot::Back => "back",
2349 BodySlot::Waist => "waist",
2350 BodySlot::Earrings => "earrings",
2351 BodySlot::Necklace => "necklace",
2352 BodySlot::Eyeglasses => "eyeglasses",
2353 BodySlot::RingLeft1 => "ring_left_1",
2354 BodySlot::RingLeft2 => "ring_left_2",
2355 BodySlot::RingRight1 => "ring_right_1",
2356 BodySlot::RingRight2 => "ring_right_2",
2357 }
2358 }
2359}
2360
2361#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2363#[serde(rename_all = "snake_case")]
2364pub enum InventoryLocation {
2365 Root,
2367 Worn { slot: BodySlot },
2369 Placed { container_id: String },
2371 Keychain,
2373 WhisperPouch,
2375}
2376
2377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2379pub struct PlacedContainerView {
2380 pub id: String,
2381 pub template_id: String,
2382 pub display_name: String,
2383 pub x: f32,
2384 pub y: f32,
2385 pub z: f32,
2386 pub locked: bool,
2387 #[serde(default)]
2389 pub accessible: bool,
2390 #[serde(default)]
2391 pub owner_character_id: Option<Uuid>,
2392 #[serde(default)]
2394 pub contents: Vec<ItemStack>,
2395 #[serde(default)]
2397 pub lock_id: Option<String>,
2398 #[serde(default)]
2400 pub capacity_volume: Option<f32>,
2401 #[serde(default)]
2403 pub item_instance_id: Option<Uuid>,
2404 #[serde(default)]
2406 pub tile_id: Option<String>,
2407 #[serde(default)]
2409 pub paperdoll_ref: Option<String>,
2410 #[serde(default)]
2412 pub worker_lodging_capacity: Option<u32>,
2413}
2414
2415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2416pub struct BlueprintIngredientView {
2417 pub template_id: String,
2418 pub quantity: u32,
2419 pub consumed: bool,
2421}
2422
2423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2424pub struct ToolRequirementView {
2425 pub item: String,
2426 pub consumed: bool,
2428}
2429
2430#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2431pub struct SkillRequirementView {
2432 pub skill: String,
2433 pub level: u32,
2434}
2435
2436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2437pub struct BlueprintView {
2438 pub id: String,
2439 pub label: String,
2440 pub output: String,
2441 pub output_qty: u32,
2442 pub craft_ticks: u32,
2443 pub inputs: Vec<BlueprintIngredientView>,
2444 #[serde(default)]
2446 pub station: Option<String>,
2447 #[serde(default)]
2448 pub category: Option<String>,
2449 #[serde(default)]
2450 pub required_tools: Vec<ToolRequirementView>,
2451 #[serde(default)]
2452 pub skill: Option<SkillRequirementView>,
2453 #[serde(default)]
2454 pub failure_chance: f32,
2455 #[serde(default)]
2457 pub worker_train_copper: u64,
2458}
2459
2460#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2462#[serde(rename_all = "snake_case")]
2463pub enum TerrainKindView {
2464 #[default]
2465 Grass,
2466 Dirt,
2467 Tilled,
2468 Desert,
2469 Hill,
2470 Bog,
2471 ShallowWater,
2472 DeepWater,
2473 Trail,
2474 Road,
2475 Rock,
2476}
2477
2478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2479pub struct TerrainZoneView {
2480 pub id: String,
2481 pub x0: f32,
2482 pub y0: f32,
2483 pub x1: f32,
2484 pub y1: f32,
2485 #[serde(default)]
2486 pub kind: TerrainKindView,
2487 #[serde(default)]
2489 pub elevation: f32,
2490 #[serde(default)]
2493 pub glyph: Option<String>,
2494 #[serde(default)]
2496 pub color: Option<String>,
2497 #[serde(default)]
2499 pub tile_id: Option<String>,
2500 #[serde(default)]
2502 pub z_order: i32,
2503}
2504
2505#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2507pub struct ZPlatformView {
2508 pub id: String,
2509 pub z: f32,
2510 pub x0: f32,
2511 pub y0: f32,
2512 pub x1: f32,
2513 pub y1: f32,
2514}
2515
2516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2518pub struct ZTransitionView {
2519 pub id: String,
2520 pub z_from: f32,
2521 pub z_to: f32,
2522 pub x0: f32,
2523 pub y0: f32,
2524 pub x1: f32,
2525 pub y1: f32,
2526}
2527
2528#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2529pub struct BuildingView {
2530 pub id: String,
2531 pub label: String,
2532 pub x: f32,
2533 pub y: f32,
2534 pub width_m: f32,
2535 pub depth_m: f32,
2536 #[serde(default)]
2537 pub interior_blueprint: Option<String>,
2538 #[serde(default)]
2539 pub tags: Vec<String>,
2540}
2541
2542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2543pub struct DoorView {
2544 pub id: String,
2545 pub building_id: String,
2546 pub x: f32,
2547 pub y: f32,
2548 #[serde(default)]
2549 pub open: bool,
2550 #[serde(default)]
2551 pub portal: Option<String>,
2552}
2553
2554#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2555pub struct InteriorRoomView {
2556 pub id: String,
2557 pub label: String,
2558 pub floor: i32,
2559 pub x0: f32,
2560 pub y0: f32,
2561 pub x1: f32,
2562 pub y1: f32,
2563 #[serde(default)]
2564 pub floor_color: Option<String>,
2565 #[serde(default)]
2566 pub floor_glyph: Option<String>,
2567}
2568
2569#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2570pub struct InteriorDoorView {
2571 pub id: String,
2572 pub room_a: String,
2573 pub room_b: String,
2574 pub x: f32,
2575 pub y: f32,
2576 pub kind: String,
2577}
2578
2579#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2580pub struct InteriorMapView {
2581 pub building_id: String,
2582 pub blueprint_id: String,
2583 pub background_color: String,
2584 #[serde(default)]
2585 pub default_floor_color: Option<String>,
2586 #[serde(default = "default_floor_height_view")]
2587 pub floor_height_m: f32,
2588 pub rooms: Vec<InteriorRoomView>,
2589 #[serde(default)]
2590 pub room_doors: Vec<InteriorDoorView>,
2591}
2592
2593fn default_floor_height_view() -> f32 {
2594 3.0
2595}
2596
2597#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2598pub struct NpcView {
2599 pub id: String,
2600 pub label: String,
2601 pub role: String,
2602 pub x: f32,
2603 pub y: f32,
2604 #[serde(default)]
2606 pub building_id: Option<String>,
2607 #[serde(default)]
2609 pub entity_id: Option<EntityId>,
2610 #[serde(default)]
2611 pub life_state: Option<LifeState>,
2612 #[serde(default)]
2613 pub hp_pct: Option<f32>,
2614 #[serde(default)]
2616 pub can_trade: bool,
2617 #[serde(default)]
2619 pub tile_id: Option<String>,
2620 #[serde(default)]
2622 pub behavior_state: Option<String>,
2623 #[serde(default)]
2625 pub presentation_state: Option<String>,
2626 #[serde(default)]
2628 pub sprite_mode: Option<String>,
2629 #[serde(default)]
2631 pub paperdoll_ref: Option<String>,
2632}
2633
2634#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2635pub struct UseResult {
2636 pub template_id: String,
2637 pub hunger_restored: f32,
2638 pub thirst_restored: f32,
2639 #[serde(default)]
2640 pub health_restored: f32,
2641 #[serde(default)]
2642 pub mana_restored: f32,
2643 #[serde(default)]
2644 pub cleared_dot_ids: Vec<String>,
2645}
2646
2647#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2648pub struct CraftResult {
2649 pub blueprint_id: String,
2650 pub outputs: Vec<ItemStack>,
2651 pub consumed: Vec<ItemStack>,
2652 #[serde(default = "default_one")]
2654 pub batch_index: u32,
2655 #[serde(default = "default_one")]
2657 pub batch_total: u32,
2658}
2659
2660fn default_one() -> u32 {
2661 1
2662}
2663
2664#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2665pub struct DeathNotice {
2666 pub entity_id: EntityId,
2667 pub respawn_x: f32,
2668 pub respawn_y: f32,
2669 pub message: String,
2670}
2671
2672#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2673pub struct InteractionNotice {
2674 pub target_id: String,
2675 pub message: String,
2676 #[serde(default)]
2677 pub coins_delta: i32,
2678 #[serde(default)]
2679 pub inventory_delta: Vec<ItemStack>,
2680}
2681
2682#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2683#[serde(rename_all = "snake_case")]
2684pub enum NpcTalkTrustFlag {
2685 Stranger,
2686 Acquainted,
2687 Trusted,
2688}
2689
2690#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
2691#[serde(rename_all = "snake_case")]
2692pub enum NpcTalkDepth {
2693 #[default]
2694 Full,
2695 Brief,
2696 Unavailable,
2697}
2698
2699#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2700pub struct NpcTalkOpened {
2701 pub npc_id: String,
2702 pub npc_label: String,
2703 pub greeting: String,
2704 pub trust_flag: NpcTalkTrustFlag,
2705 #[serde(default)]
2706 pub talk_depth: NpcTalkDepth,
2707 #[serde(default = "default_true")]
2708 pub trade_allowed: bool,
2709}
2710
2711fn default_true() -> bool {
2712 true
2713}
2714
2715#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2716pub struct NpcTalkPending {
2717 pub npc_id: String,
2718}
2719
2720#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2721pub struct NpcTalkReply {
2722 pub npc_id: String,
2723 pub line: String,
2724 pub trust_flag: NpcTalkTrustFlag,
2725 #[serde(default)]
2726 pub wind_down: bool,
2727 #[serde(default)]
2728 pub trade_disabled: bool,
2729}
2730
2731#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2732pub struct NpcTalkClosed {
2733 pub npc_id: String,
2734}
2735
2736#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2737pub struct NpcTalkError {
2738 pub npc_id: String,
2739 pub reason: String,
2740}
2741
2742#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2743#[serde(rename_all = "snake_case")]
2744pub enum QuestStatusView {
2745 Available,
2746 Active,
2747 Completed,
2748}
2749
2750#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2751pub struct QuestObjectiveProgress {
2752 pub label: String,
2753 pub current: u32,
2754 pub required: u32,
2755 pub done: bool,
2756}
2757
2758#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2759pub struct QuestLogEntry {
2760 pub quest_id: String,
2761 pub title: String,
2762 pub description: String,
2763 pub status: QuestStatusView,
2764 #[serde(default)]
2765 pub current_step_id: Option<String>,
2766 #[serde(default)]
2767 pub current_step_title: String,
2768 #[serde(default)]
2769 pub objectives: Vec<QuestObjectiveProgress>,
2770 #[serde(default)]
2771 pub is_tracked: bool,
2772 #[serde(default)]
2773 pub can_withdraw: bool,
2774}
2775
2776#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2777pub struct InteractableView {
2778 pub id: String,
2779 pub kind: String,
2780 pub label: String,
2781 pub x: f32,
2782 pub y: f32,
2783 pub z: f32,
2784 #[serde(default)]
2785 pub board_id: Option<String>,
2786}
2787
2788#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2789pub struct QuestOffer {
2790 pub quest_id: String,
2791 pub title: String,
2792 pub description: String,
2793 #[serde(default)]
2794 pub step_count: u32,
2795}
2796
2797#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2798pub struct QuestNotice {
2799 pub quest_id: String,
2800 pub title: String,
2801 pub message: String,
2802}
2803
2804#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2805#[serde(rename_all = "snake_case")]
2806pub enum ShopOfferKind {
2807 Item,
2808 Blueprint,
2809}
2810
2811#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2812pub struct ShopOffer {
2813 pub offer_id: String,
2814 pub kind: ShopOfferKind,
2815 pub label: String,
2816 #[serde(default)]
2817 pub template_id: Option<String>,
2818 #[serde(default)]
2819 pub blueprint_id: Option<String>,
2820 pub price_copper: u32,
2821 #[serde(default)]
2822 pub affordable: bool,
2823 #[serde(default)]
2824 pub already_owned: bool,
2825}
2826
2827#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2828pub struct ShopBuyLine {
2829 pub template_id: String,
2830 pub label: String,
2831 pub quantity: u32,
2832 pub price_copper: u32,
2833}
2834
2835#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2837pub struct BankPanel {
2838 pub npc_id: String,
2839 pub npc_label: String,
2840 pub bank_balance_copper: u64,
2841 pub on_person_copper: u64,
2842 #[serde(default)]
2844 pub pending_outgoing_copper: u64,
2845 #[serde(default)]
2846 pub transfer_fee_bps: u32,
2847 #[serde(default)]
2848 pub transfer_clear_ticks: u64,
2849}
2850
2851#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2853pub struct StoragePanel {
2854 pub npc_id: String,
2855 pub npc_label: String,
2856 pub building_id: String,
2857 pub building_label: String,
2858 pub used_volume: f32,
2859 pub max_volume: f32,
2860 #[serde(default)]
2861 pub contents: Vec<ItemStack>,
2862 #[serde(default)]
2864 pub ship_destinations: Vec<StorageShipDest>,
2865}
2866
2867#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2868pub struct StorageShipDest {
2869 pub building_id: String,
2870 pub label: String,
2871 pub distance_m: f32,
2872 pub fee_copper: u64,
2873 pub travel_ticks: u64,
2874}
2875
2876#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2877pub struct ShopCatalog {
2878 pub npc_id: String,
2879 pub npc_label: String,
2880 #[serde(default)]
2881 pub sells: Vec<ShopOffer>,
2882 #[serde(default)]
2883 pub buys: Vec<ShopBuyLine>,
2884}
2885
2886#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2887pub struct HarvestResult {
2888 pub node_id: String,
2889 pub quantity: u32,
2891 pub item_template: String,
2892 #[serde(default)]
2895 pub item_instance_id: Option<Uuid>,
2896}
2897
2898#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2900pub struct Envelope<T> {
2901 pub protocol_version: u16,
2902 pub payload: T,
2903}
2904
2905impl<T> Envelope<T> {
2906 pub fn new(payload: T) -> Self {
2907 Self {
2908 protocol_version: crate::PROTOCOL_VERSION,
2909 payload,
2910 }
2911 }
2912}
2913
2914#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2916pub struct Hello {
2917 pub client_name: String,
2918 pub protocol_version: u16,
2919 #[serde(default)]
2920 pub auth: AuthCredential,
2921 #[serde(default)]
2923 pub character_id: Option<Uuid>,
2924}
2925
2926#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2929#[serde(rename_all = "snake_case")]
2930pub enum AuthCredential {
2931 DevLocal,
2932 Session { token: String },
2933 ApiToken { token: String, character_id: Uuid },
2934}
2935
2936impl Default for AuthCredential {
2937 fn default() -> Self {
2938 Self::DevLocal
2939 }
2940}
2941
2942#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2943pub struct Welcome {
2944 pub session_id: SessionId,
2945 pub entity_id: EntityId,
2946 pub snapshot: Snapshot,
2947}
2948
2949#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2950pub enum ServerMessage {
2951 Welcome(Welcome),
2952 ContentUpdated(Snapshot),
2954 Tick(TickDelta),
2955 IntentAck {
2956 entity_id: EntityId,
2957 seq: Seq,
2958 tick: Tick,
2959 },
2960 Chat(ChatMessage),
2961 HarvestResult(HarvestResult),
2962 UseResult(UseResult),
2963 CraftResult(CraftResult),
2964 Death(DeathNotice),
2965 Interaction(InteractionNotice),
2966 ShopOpened(ShopCatalog),
2967 NpcTalkOpened(NpcTalkOpened),
2968 NpcTalkPending(NpcTalkPending),
2969 NpcTalkReply(NpcTalkReply),
2970 NpcTalkClosed(NpcTalkClosed),
2971 NpcTalkError(NpcTalkError),
2972 QuestOffer(QuestOffer),
2973 QuestAccepted(QuestNotice),
2974 QuestWithdrawn(QuestNotice),
2975 QuestStepCompleted(QuestNotice),
2976 QuestCompleted(QuestNotice),
2977 BankOpened(BankPanel),
2979 StorageOpened(StoragePanel),
2981 TradeOpened(TradePanel),
2983 TradeClosed {
2985 reason: String,
2986 },
2987}
2988
2989#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2991pub struct TradePanel {
2992 pub peer_entity_id: EntityId,
2993 pub peer_name: String,
2994 pub my_presented: Vec<ItemStack>,
2995 pub their_presented: Vec<ItemStack>,
2996 pub i_ready: bool,
2997 pub they_ready: bool,
2998 pub my_mass_after: f32,
3000 pub my_mass_max: f32,
3001 pub my_encumbrance_after: EncumbranceState,
3002 pub overburden_warning: bool,
3004}
3005
3006#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3007pub enum ClientMessage {
3008 Hello(Hello),
3009 Intent(Intent),
3010 Disconnect,
3011}
3012
3013#[cfg(test)]
3014mod tests {
3015 use super::*;
3016
3017 #[test]
3018 fn pristine_vitals_state_yields_full_pools() {
3019 let attrs = PrimaryAttributes::default();
3020 let vitals = StoredVitalsState::default().apply_to(attrs);
3021 assert!(vitals.health > 0.0);
3022 assert_eq!(vitals.health, vitals.health_max);
3023 assert!((vitals.mana_max - 61.0).abs() < 0.01);
3024 }
3025
3026 #[test]
3027 fn saved_vitals_scale_when_pool_max_increases() {
3028 let mut attrs = PrimaryAttributes::default();
3029 attrs.intelligence = 140;
3030 attrs.wisdom = 140;
3031 let saved = StoredVitalsState {
3032 health: 100.0,
3033 mana: 14.0,
3034 stamina: 100.0,
3035 ..StoredVitalsState::default()
3036 };
3037 let vitals = saved.apply_to(attrs);
3038 assert!(vitals.mana_max > 55.0);
3039 assert!(
3040 (vitals.mana - vitals.mana_max).abs() < 0.01,
3041 "full legacy mana bar migrates to full new bar"
3042 );
3043 }
3044
3045 #[test]
3046 fn empty_vitals_state_is_pristine() {
3047 let pristine = StoredVitalsState {
3048 health: 0.0,
3049 mana: 0.0,
3050 stamina: 0.0,
3051 hunger: 0.0,
3052 thirst: 0.0,
3053 coins: 0,
3054 deaths: 0,
3055 life_state: LifeState::Alive,
3056 };
3057 assert!(pristine.is_pristine());
3058 let vitals = pristine.apply_to(PrimaryAttributes::default());
3059 assert!(vitals.health > 0.0);
3060 }
3061
3062 #[test]
3063 fn stored_vitals_roundtrip_preserves_partial_pools() {
3064 let attrs = PrimaryAttributes::default();
3065 let mut live = PlayerVitals::from_attributes(attrs);
3066 live.health = 25.0;
3067 live.hunger = 77.0;
3068 live.deaths = 2;
3069 let stored = StoredVitalsState::from_live(&live);
3070 let restored = stored.apply_to(attrs);
3071 assert!(
3072 (restored.health - 25.0).abs() < 0.01,
3073 "partial HP below cap stays absolute"
3074 );
3075 assert_eq!(restored.hunger, 77.0);
3076 assert_eq!(restored.deaths, 2);
3077 }
3078
3079 #[test]
3080 fn skill_tiers_start_at_zero() {
3081 let skill = SkillProgress::default();
3082 assert_eq!(skill.level, 0);
3083 assert_eq!(skill.display_tier(), 0);
3084 let trained = SkillProgress {
3085 level: 250,
3086 last_trained_tick: 1,
3087 };
3088 assert_eq!(trained.display_tier(), 2);
3089 }
3090
3091 #[test]
3092 fn quest_server_messages_roundtrip_json() {
3093 use crate::codec::{Codec, PostcardCodec};
3094
3095 let offer = ServerMessage::QuestOffer(QuestOffer {
3096 quest_id: "ada_goblin_hunt".into(),
3097 title: "Goblin Trouble".into(),
3098 description: "Help Ada".into(),
3099 step_count: 3,
3100 });
3101 let notice = ServerMessage::QuestAccepted(QuestNotice {
3102 quest_id: "ada_goblin_hunt".into(),
3103 title: "Goblin Trouble".into(),
3104 message: "Quest accepted".into(),
3105 });
3106 for msg in [offer, notice] {
3107 let bytes = PostcardCodec.encode(&msg).unwrap();
3108 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
3109 assert_eq!(decoded, msg);
3110 }
3111 }
3112
3113 #[test]
3114 fn hotbar_consumable_binding_roundtrips() {
3115 let binding = hotbar_consumable_binding("bottle_of_water");
3116 assert_eq!(binding, "item:bottle_of_water");
3117 assert!(hotbar_binding_is_consumable(&binding));
3118 assert_eq!(
3119 hotbar_consumable_template(&binding),
3120 Some("bottle_of_water")
3121 );
3122 assert!(!hotbar_binding_is_consumable("fireball"));
3123 assert_eq!(hotbar_consumable_template("fireball"), None);
3124 }
3125}