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)]
1948 pub route_stop_index: Option<u32>,
1949 #[serde(default)]
1951 pub known_blueprint_ids: Vec<String>,
1952 #[serde(default = "default_worker_view_level")]
1954 pub level: u32,
1955 #[serde(default)]
1957 pub worker_xp: f64,
1958 #[serde(default)]
1960 pub inventory: Vec<ItemStack>,
1961}
1962
1963fn default_worker_view_level() -> u32 {
1964 1
1965}
1966
1967#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1969pub struct TickDelta {
1970 pub tick: Tick,
1971 pub entities: Vec<EntityState>,
1972 #[serde(default)]
1973 pub resource_nodes: Vec<ResourceNodeView>,
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)]
1982 pub inventory: Vec<ItemStack>,
1983 #[serde(default)]
1984 pub blueprints: Vec<BlueprintView>,
1985 #[serde(default)]
1986 pub world_clock: WorldClock,
1987 #[serde(default)]
1988 pub ground_drops: Vec<GroundDropView>,
1989 #[serde(default)]
1990 pub placed_containers: Vec<PlacedContainerView>,
1991 #[serde(default)]
1992 pub combat: Option<CombatHud>,
1993 #[serde(default)]
1994 pub interior_map: Option<InteriorMapView>,
1995 #[serde(default)]
1996 pub quest_log: Vec<QuestLogEntry>,
1997 #[serde(default)]
1998 pub hired_workers: Vec<HiredWorkerView>,
1999 #[serde(default)]
2000 pub interactables: Vec<InteractableView>,
2001 #[serde(default)]
2002 pub ledger: Option<PlayerLedgerView>,
2003 #[serde(default)]
2004 pub career: Option<PlayerCareerView>,
2005 #[serde(default)]
2007 pub combat_fx: Vec<CombatFx>,
2008}
2009#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2010pub struct GroundDropView {
2011 pub id: String,
2012 pub template_id: String,
2013 pub quantity: u32,
2014 pub x: f32,
2015 pub y: f32,
2016 pub z: f32,
2017 #[serde(default)]
2019 pub tile_id: Option<String>,
2020 #[serde(default)]
2022 pub paperdoll_ref: Option<String>,
2023 #[serde(default)]
2025 pub yaw: f32,
2026 #[serde(default)]
2028 pub pitch: f32,
2029 #[serde(default)]
2031 pub roll: f32,
2032 #[serde(default = "default_draw_scale")]
2034 pub draw_scale: f32,
2035}
2036
2037fn default_draw_scale() -> f32 {
2038 1.0
2039}
2040
2041#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2043pub struct Snapshot {
2044 pub tick: Tick,
2045 pub chunk_rev: u64,
2046 #[serde(default)]
2048 pub content_rev: u64,
2049 #[serde(default)]
2051 pub publish_rev: u64,
2052 pub entities: Vec<EntityState>,
2053 #[serde(default)]
2054 pub resource_nodes: Vec<ResourceNodeView>,
2055 #[serde(default)]
2057 pub world_width_m: f32,
2058 #[serde(default)]
2059 pub world_height_m: f32,
2060 #[serde(default)]
2061 pub buildings: Vec<BuildingView>,
2062 #[serde(default)]
2063 pub doors: Vec<DoorView>,
2064 #[serde(default)]
2065 pub npcs: Vec<NpcView>,
2066 #[serde(default)]
2067 pub inventory: Vec<ItemStack>,
2068 #[serde(default)]
2069 pub blueprints: Vec<BlueprintView>,
2070 #[serde(default)]
2071 pub world_clock: WorldClock,
2072 #[serde(default)]
2073 pub terrain_zones: Vec<TerrainZoneView>,
2074 #[serde(default)]
2075 pub z_platforms: Vec<ZPlatformView>,
2076 #[serde(default)]
2077 pub z_transitions: Vec<ZTransitionView>,
2078 #[serde(default)]
2079 pub ground_drops: Vec<GroundDropView>,
2080 #[serde(default)]
2081 pub placed_containers: Vec<PlacedContainerView>,
2082 #[serde(default)]
2083 pub combat: Option<CombatHud>,
2084 #[serde(default)]
2085 pub interior_map: Option<InteriorMapView>,
2086 #[serde(default)]
2087 pub quest_log: Vec<QuestLogEntry>,
2088 #[serde(default)]
2089 pub hired_workers: Vec<HiredWorkerView>,
2090 #[serde(default)]
2091 pub interactables: Vec<InteractableView>,
2092 #[serde(default)]
2093 pub ledger: Option<PlayerLedgerView>,
2094 #[serde(default)]
2095 pub career: Option<PlayerCareerView>,
2096 #[serde(default)]
2098 pub combat_fx: Vec<CombatFx>,
2099}
2100
2101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2103pub struct ResourceNodeView {
2104 pub id: String,
2105 pub label: String,
2106 pub x: f32,
2107 pub y: f32,
2108 pub z: f32,
2109 pub item_template: String,
2110 #[serde(default = "default_node_state")]
2111 pub state: ResourceNodeState,
2112 #[serde(default = "default_blocking_view")]
2114 pub blocking: bool,
2115 #[serde(default = "default_blocking_radius_view")]
2117 pub blocking_radius_m: f32,
2118 #[serde(default)]
2120 pub tile_id: Option<String>,
2121 #[serde(default)]
2123 pub paperdoll_ref: Option<String>,
2124 #[serde(default)]
2126 pub yaw: f32,
2127 #[serde(default)]
2129 pub pitch: f32,
2130 #[serde(default)]
2132 pub roll: f32,
2133 #[serde(default = "default_draw_scale")]
2135 pub draw_scale: f32,
2136 #[serde(default)]
2138 pub sprite_mode: Option<String>,
2139 #[serde(default)]
2141 pub presentation_state: Option<String>,
2142}
2143
2144fn default_blocking_radius_view() -> f32 {
2145 0.8
2146}
2147
2148fn default_blocking_view() -> bool {
2149 true
2150}
2151
2152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2153#[serde(rename_all = "snake_case")]
2154pub enum ResourceNodeState {
2155 Available,
2156 Harvesting,
2157 Cooldown,
2158}
2159
2160fn default_node_state() -> ResourceNodeState {
2161 ResourceNodeState::Available
2162}
2163
2164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2165#[serde(rename_all = "snake_case")]
2166pub enum ItemStatusBindingMode {
2167 OnHit,
2168 WhileEquipped,
2169}
2170
2171impl Default for ItemStatusBindingMode {
2172 fn default() -> Self {
2173 Self::OnHit
2174 }
2175}
2176
2177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2179pub struct ItemStatusBinding {
2180 pub effect_id: String,
2181 #[serde(default)]
2182 pub mode: ItemStatusBindingMode,
2183 #[serde(default)]
2185 pub source: String,
2186 #[serde(default)]
2187 pub applied_at_tick: u64,
2188 #[serde(default, skip_serializing_if = "Option::is_none")]
2190 pub expires_at_tick: Option<u64>,
2191}
2192
2193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2194pub struct ItemStack {
2195 pub template_id: String,
2196 pub quantity: u32,
2197 #[serde(default)]
2199 pub item_instance_id: Option<Uuid>,
2200 #[serde(default)]
2202 pub props: BTreeMap<String, String>,
2203 #[serde(default)]
2205 pub status_bindings: Vec<ItemStatusBinding>,
2206 #[serde(default)]
2208 pub contents: Vec<ItemStack>,
2209 #[serde(default)]
2211 pub display_name: Option<String>,
2212 #[serde(default)]
2214 pub category: Option<String>,
2215 #[serde(default)]
2217 pub base_mass: Option<f32>,
2218 #[serde(default)]
2220 pub base_volume: Option<f32>,
2221 #[serde(default)]
2223 pub capacity_volume: Option<f32>,
2224 #[serde(default)]
2226 pub stackable: Option<bool>,
2227 #[serde(default)]
2229 pub world_placeable: Option<bool>,
2230 #[serde(default)]
2232 pub worker_lodging_capacity: Option<u32>,
2233 #[serde(default)]
2235 pub equip_slot: Option<BodySlot>,
2236 #[serde(default)]
2238 pub armor_physical: Option<f32>,
2239 #[serde(default)]
2241 pub resists: Vec<(String, f32)>,
2242 #[serde(default)]
2244 pub hand_slots: Option<u8>,
2245}
2246
2247impl ItemStack {
2248 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
2249 Self {
2250 template_id: template_id.into(),
2251 quantity,
2252 ..Default::default()
2253 }
2254 }
2255}
2256
2257impl Default for ItemStack {
2258 fn default() -> Self {
2259 Self {
2260 template_id: String::new(),
2261 quantity: 0,
2262 item_instance_id: None,
2263 props: BTreeMap::new(),
2264 status_bindings: Vec::new(),
2265 contents: Vec::new(),
2266 display_name: None,
2267 category: None,
2268 base_mass: None,
2269 base_volume: None,
2270 capacity_volume: None,
2271 stackable: None,
2272 world_placeable: None,
2273 worker_lodging_capacity: None,
2274 equip_slot: None,
2275 armor_physical: None,
2276 resists: Vec::new(),
2277 hand_slots: None,
2278 }
2279 }
2280}
2281
2282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2284#[serde(rename_all = "snake_case")]
2285pub enum EncumbranceState {
2286 #[default]
2287 Light,
2288 Heavy,
2289 Over,
2290}
2291
2292#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
2296#[serde(rename_all = "snake_case")]
2297pub enum BodySlot {
2298 Head,
2299 #[serde(alias = "body")]
2301 Chest,
2302 #[serde(alias = "arms")]
2304 Forearms,
2305 Legs,
2306 Feet,
2307 Cloak,
2308 Back,
2309 Waist,
2310 Earrings,
2311 Necklace,
2312 Eyeglasses,
2313 #[serde(rename = "ring_left_1", alias = "ring_left1")]
2315 RingLeft1,
2316 #[serde(rename = "ring_left_2", alias = "ring_left2")]
2317 RingLeft2,
2318 #[serde(rename = "ring_right_1", alias = "ring_right1")]
2319 RingRight1,
2320 #[serde(rename = "ring_right_2", alias = "ring_right2")]
2321 RingRight2,
2322}
2323
2324impl BodySlot {
2325 pub const ALL: [BodySlot; 15] = [
2327 BodySlot::Head,
2328 BodySlot::Chest,
2329 BodySlot::Forearms,
2330 BodySlot::Legs,
2331 BodySlot::Feet,
2332 BodySlot::Cloak,
2333 BodySlot::Back,
2334 BodySlot::Waist,
2335 BodySlot::Earrings,
2336 BodySlot::Necklace,
2337 BodySlot::Eyeglasses,
2338 BodySlot::RingLeft1,
2339 BodySlot::RingLeft2,
2340 BodySlot::RingRight1,
2341 BodySlot::RingRight2,
2342 ];
2343
2344 pub fn as_str(self) -> &'static str {
2345 match self {
2346 BodySlot::Head => "head",
2347 BodySlot::Chest => "chest",
2348 BodySlot::Forearms => "forearms",
2349 BodySlot::Legs => "legs",
2350 BodySlot::Feet => "feet",
2351 BodySlot::Cloak => "cloak",
2352 BodySlot::Back => "back",
2353 BodySlot::Waist => "waist",
2354 BodySlot::Earrings => "earrings",
2355 BodySlot::Necklace => "necklace",
2356 BodySlot::Eyeglasses => "eyeglasses",
2357 BodySlot::RingLeft1 => "ring_left_1",
2358 BodySlot::RingLeft2 => "ring_left_2",
2359 BodySlot::RingRight1 => "ring_right_1",
2360 BodySlot::RingRight2 => "ring_right_2",
2361 }
2362 }
2363}
2364
2365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2367#[serde(rename_all = "snake_case")]
2368pub enum InventoryLocation {
2369 Root,
2371 Worn { slot: BodySlot },
2373 Placed { container_id: String },
2375 Keychain,
2377 WhisperPouch,
2379}
2380
2381#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2383pub struct PlacedContainerView {
2384 pub id: String,
2385 pub template_id: String,
2386 pub display_name: String,
2387 pub x: f32,
2388 pub y: f32,
2389 pub z: f32,
2390 pub locked: bool,
2391 #[serde(default)]
2393 pub accessible: bool,
2394 #[serde(default)]
2395 pub owner_character_id: Option<Uuid>,
2396 #[serde(default)]
2398 pub contents: Vec<ItemStack>,
2399 #[serde(default)]
2401 pub lock_id: Option<String>,
2402 #[serde(default)]
2404 pub capacity_volume: Option<f32>,
2405 #[serde(default)]
2407 pub item_instance_id: Option<Uuid>,
2408 #[serde(default)]
2410 pub tile_id: Option<String>,
2411 #[serde(default)]
2413 pub paperdoll_ref: Option<String>,
2414 #[serde(default)]
2416 pub worker_lodging_capacity: Option<u32>,
2417}
2418
2419#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2420pub struct BlueprintIngredientView {
2421 pub template_id: String,
2422 pub quantity: u32,
2423 pub consumed: bool,
2425}
2426
2427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2428pub struct ToolRequirementView {
2429 pub item: String,
2430 pub consumed: bool,
2432}
2433
2434#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2435pub struct SkillRequirementView {
2436 pub skill: String,
2437 pub level: u32,
2438}
2439
2440#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2441pub struct BlueprintView {
2442 pub id: String,
2443 pub label: String,
2444 pub output: String,
2445 pub output_qty: u32,
2446 pub craft_ticks: u32,
2447 pub inputs: Vec<BlueprintIngredientView>,
2448 #[serde(default)]
2450 pub station: Option<String>,
2451 #[serde(default)]
2452 pub category: Option<String>,
2453 #[serde(default)]
2454 pub required_tools: Vec<ToolRequirementView>,
2455 #[serde(default)]
2456 pub skill: Option<SkillRequirementView>,
2457 #[serde(default)]
2458 pub failure_chance: f32,
2459 #[serde(default)]
2461 pub worker_train_copper: u64,
2462}
2463
2464#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2466#[serde(rename_all = "snake_case")]
2467pub enum TerrainKindView {
2468 #[default]
2469 Grass,
2470 Dirt,
2471 Tilled,
2472 Desert,
2473 Hill,
2474 Bog,
2475 ShallowWater,
2476 DeepWater,
2477 Trail,
2478 Road,
2479 Rock,
2480}
2481
2482#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2483pub struct TerrainZoneView {
2484 pub id: String,
2485 pub x0: f32,
2486 pub y0: f32,
2487 pub x1: f32,
2488 pub y1: f32,
2489 #[serde(default)]
2490 pub kind: TerrainKindView,
2491 #[serde(default)]
2493 pub elevation: f32,
2494 #[serde(default)]
2497 pub glyph: Option<String>,
2498 #[serde(default)]
2500 pub color: Option<String>,
2501 #[serde(default)]
2503 pub tile_id: Option<String>,
2504 #[serde(default)]
2506 pub z_order: i32,
2507}
2508
2509#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2511pub struct ZPlatformView {
2512 pub id: String,
2513 pub z: f32,
2514 pub x0: f32,
2515 pub y0: f32,
2516 pub x1: f32,
2517 pub y1: f32,
2518}
2519
2520#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2522pub struct ZTransitionView {
2523 pub id: String,
2524 pub z_from: f32,
2525 pub z_to: f32,
2526 pub x0: f32,
2527 pub y0: f32,
2528 pub x1: f32,
2529 pub y1: f32,
2530}
2531
2532#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2533pub struct BuildingView {
2534 pub id: String,
2535 pub label: String,
2536 pub x: f32,
2537 pub y: f32,
2538 pub width_m: f32,
2539 pub depth_m: f32,
2540 #[serde(default)]
2541 pub interior_blueprint: Option<String>,
2542 #[serde(default)]
2543 pub tags: Vec<String>,
2544}
2545
2546#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2547pub struct DoorView {
2548 pub id: String,
2549 pub building_id: String,
2550 pub x: f32,
2551 pub y: f32,
2552 #[serde(default)]
2553 pub open: bool,
2554 #[serde(default)]
2555 pub portal: Option<String>,
2556}
2557
2558#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2559pub struct InteriorRoomView {
2560 pub id: String,
2561 pub label: String,
2562 pub floor: i32,
2563 pub x0: f32,
2564 pub y0: f32,
2565 pub x1: f32,
2566 pub y1: f32,
2567 #[serde(default)]
2568 pub floor_color: Option<String>,
2569 #[serde(default)]
2570 pub floor_glyph: Option<String>,
2571}
2572
2573#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2574pub struct InteriorDoorView {
2575 pub id: String,
2576 pub room_a: String,
2577 pub room_b: String,
2578 pub x: f32,
2579 pub y: f32,
2580 pub kind: String,
2581}
2582
2583#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2584pub struct InteriorMapView {
2585 pub building_id: String,
2586 pub blueprint_id: String,
2587 pub background_color: String,
2588 #[serde(default)]
2589 pub default_floor_color: Option<String>,
2590 #[serde(default = "default_floor_height_view")]
2591 pub floor_height_m: f32,
2592 pub rooms: Vec<InteriorRoomView>,
2593 #[serde(default)]
2594 pub room_doors: Vec<InteriorDoorView>,
2595}
2596
2597fn default_floor_height_view() -> f32 {
2598 3.0
2599}
2600
2601#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2602pub struct NpcView {
2603 pub id: String,
2604 pub label: String,
2605 pub role: String,
2606 pub x: f32,
2607 pub y: f32,
2608 #[serde(default)]
2610 pub building_id: Option<String>,
2611 #[serde(default)]
2613 pub entity_id: Option<EntityId>,
2614 #[serde(default)]
2615 pub life_state: Option<LifeState>,
2616 #[serde(default)]
2617 pub hp_pct: Option<f32>,
2618 #[serde(default)]
2620 pub can_trade: bool,
2621 #[serde(default)]
2623 pub tile_id: Option<String>,
2624 #[serde(default)]
2626 pub behavior_state: Option<String>,
2627 #[serde(default)]
2629 pub presentation_state: Option<String>,
2630 #[serde(default)]
2632 pub sprite_mode: Option<String>,
2633 #[serde(default)]
2635 pub paperdoll_ref: Option<String>,
2636}
2637
2638#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2639pub struct UseResult {
2640 pub template_id: String,
2641 pub hunger_restored: f32,
2642 pub thirst_restored: f32,
2643 #[serde(default)]
2644 pub health_restored: f32,
2645 #[serde(default)]
2646 pub mana_restored: f32,
2647 #[serde(default)]
2648 pub cleared_dot_ids: Vec<String>,
2649}
2650
2651#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2652pub struct CraftResult {
2653 pub blueprint_id: String,
2654 pub outputs: Vec<ItemStack>,
2655 pub consumed: Vec<ItemStack>,
2656 #[serde(default = "default_one")]
2658 pub batch_index: u32,
2659 #[serde(default = "default_one")]
2661 pub batch_total: u32,
2662}
2663
2664fn default_one() -> u32 {
2665 1
2666}
2667
2668#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2669pub struct DeathNotice {
2670 pub entity_id: EntityId,
2671 pub respawn_x: f32,
2672 pub respawn_y: f32,
2673 pub message: String,
2674}
2675
2676#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2677pub struct InteractionNotice {
2678 pub target_id: String,
2679 pub message: String,
2680 #[serde(default)]
2681 pub coins_delta: i32,
2682 #[serde(default)]
2683 pub inventory_delta: Vec<ItemStack>,
2684}
2685
2686#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2687#[serde(rename_all = "snake_case")]
2688pub enum NpcTalkTrustFlag {
2689 Stranger,
2690 Acquainted,
2691 Trusted,
2692}
2693
2694#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
2695#[serde(rename_all = "snake_case")]
2696pub enum NpcTalkDepth {
2697 #[default]
2698 Full,
2699 Brief,
2700 Unavailable,
2701}
2702
2703#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2704pub struct NpcTalkOpened {
2705 pub npc_id: String,
2706 pub npc_label: String,
2707 pub greeting: String,
2708 pub trust_flag: NpcTalkTrustFlag,
2709 #[serde(default)]
2710 pub talk_depth: NpcTalkDepth,
2711 #[serde(default = "default_true")]
2712 pub trade_allowed: bool,
2713}
2714
2715fn default_true() -> bool {
2716 true
2717}
2718
2719#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2720pub struct NpcTalkPending {
2721 pub npc_id: String,
2722}
2723
2724#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2725pub struct NpcTalkReply {
2726 pub npc_id: String,
2727 pub line: String,
2728 pub trust_flag: NpcTalkTrustFlag,
2729 #[serde(default)]
2730 pub wind_down: bool,
2731 #[serde(default)]
2732 pub trade_disabled: bool,
2733}
2734
2735#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2736pub struct NpcTalkClosed {
2737 pub npc_id: String,
2738}
2739
2740#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2741pub struct NpcTalkError {
2742 pub npc_id: String,
2743 pub reason: String,
2744}
2745
2746#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2747#[serde(rename_all = "snake_case")]
2748pub enum QuestStatusView {
2749 Available,
2750 Active,
2751 Completed,
2752}
2753
2754#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2755pub struct QuestObjectiveProgress {
2756 pub label: String,
2757 pub current: u32,
2758 pub required: u32,
2759 pub done: bool,
2760}
2761
2762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2763pub struct QuestLogEntry {
2764 pub quest_id: String,
2765 pub title: String,
2766 pub description: String,
2767 pub status: QuestStatusView,
2768 #[serde(default)]
2769 pub current_step_id: Option<String>,
2770 #[serde(default)]
2771 pub current_step_title: String,
2772 #[serde(default)]
2773 pub objectives: Vec<QuestObjectiveProgress>,
2774 #[serde(default)]
2775 pub is_tracked: bool,
2776 #[serde(default)]
2777 pub can_withdraw: bool,
2778}
2779
2780#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2781pub struct InteractableView {
2782 pub id: String,
2783 pub kind: String,
2784 pub label: String,
2785 pub x: f32,
2786 pub y: f32,
2787 pub z: f32,
2788 #[serde(default)]
2789 pub board_id: Option<String>,
2790}
2791
2792#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2793pub struct QuestOffer {
2794 pub quest_id: String,
2795 pub title: String,
2796 pub description: String,
2797 #[serde(default)]
2798 pub step_count: u32,
2799}
2800
2801#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2802pub struct QuestNotice {
2803 pub quest_id: String,
2804 pub title: String,
2805 pub message: String,
2806}
2807
2808#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2809#[serde(rename_all = "snake_case")]
2810pub enum ShopOfferKind {
2811 Item,
2812 Blueprint,
2813}
2814
2815#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2816pub struct ShopOffer {
2817 pub offer_id: String,
2818 pub kind: ShopOfferKind,
2819 pub label: String,
2820 #[serde(default)]
2821 pub template_id: Option<String>,
2822 #[serde(default)]
2823 pub blueprint_id: Option<String>,
2824 pub price_copper: u32,
2825 #[serde(default)]
2826 pub affordable: bool,
2827 #[serde(default)]
2828 pub already_owned: bool,
2829}
2830
2831#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2832pub struct ShopBuyLine {
2833 pub template_id: String,
2834 pub label: String,
2835 pub quantity: u32,
2836 pub price_copper: u32,
2837}
2838
2839#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2841pub struct BankPanel {
2842 pub npc_id: String,
2843 pub npc_label: String,
2844 pub bank_balance_copper: u64,
2845 pub on_person_copper: u64,
2846 #[serde(default)]
2848 pub pending_outgoing_copper: u64,
2849 #[serde(default)]
2850 pub transfer_fee_bps: u32,
2851 #[serde(default)]
2852 pub transfer_clear_ticks: u64,
2853}
2854
2855#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2857pub struct StoragePanel {
2858 pub npc_id: String,
2859 pub npc_label: String,
2860 pub building_id: String,
2861 pub building_label: String,
2862 pub used_volume: f32,
2863 pub max_volume: f32,
2864 #[serde(default)]
2865 pub contents: Vec<ItemStack>,
2866 #[serde(default)]
2868 pub ship_destinations: Vec<StorageShipDest>,
2869}
2870
2871#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2872pub struct StorageShipDest {
2873 pub building_id: String,
2874 pub label: String,
2875 pub distance_m: f32,
2876 pub fee_copper: u64,
2877 pub travel_ticks: u64,
2878}
2879
2880#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2881pub struct ShopCatalog {
2882 pub npc_id: String,
2883 pub npc_label: String,
2884 #[serde(default)]
2885 pub sells: Vec<ShopOffer>,
2886 #[serde(default)]
2887 pub buys: Vec<ShopBuyLine>,
2888}
2889
2890#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2891pub struct HarvestResult {
2892 pub node_id: String,
2893 pub quantity: u32,
2895 pub item_template: String,
2896 #[serde(default)]
2899 pub item_instance_id: Option<Uuid>,
2900}
2901
2902#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2904pub struct Envelope<T> {
2905 pub protocol_version: u16,
2906 pub payload: T,
2907}
2908
2909impl<T> Envelope<T> {
2910 pub fn new(payload: T) -> Self {
2911 Self {
2912 protocol_version: crate::PROTOCOL_VERSION,
2913 payload,
2914 }
2915 }
2916}
2917
2918#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2920pub struct Hello {
2921 pub client_name: String,
2922 pub protocol_version: u16,
2923 #[serde(default)]
2924 pub auth: AuthCredential,
2925 #[serde(default)]
2927 pub character_id: Option<Uuid>,
2928}
2929
2930#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2933#[serde(rename_all = "snake_case")]
2934pub enum AuthCredential {
2935 DevLocal,
2936 Session { token: String },
2937 ApiToken { token: String, character_id: Uuid },
2938}
2939
2940impl Default for AuthCredential {
2941 fn default() -> Self {
2942 Self::DevLocal
2943 }
2944}
2945
2946#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2947pub struct Welcome {
2948 pub session_id: SessionId,
2949 pub entity_id: EntityId,
2950 pub snapshot: Snapshot,
2951}
2952
2953#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2954pub enum ServerMessage {
2955 Welcome(Welcome),
2956 ContentUpdated(Snapshot),
2958 Tick(TickDelta),
2959 IntentAck {
2960 entity_id: EntityId,
2961 seq: Seq,
2962 tick: Tick,
2963 },
2964 Chat(ChatMessage),
2965 HarvestResult(HarvestResult),
2966 UseResult(UseResult),
2967 CraftResult(CraftResult),
2968 Death(DeathNotice),
2969 Interaction(InteractionNotice),
2970 ShopOpened(ShopCatalog),
2971 NpcTalkOpened(NpcTalkOpened),
2972 NpcTalkPending(NpcTalkPending),
2973 NpcTalkReply(NpcTalkReply),
2974 NpcTalkClosed(NpcTalkClosed),
2975 NpcTalkError(NpcTalkError),
2976 QuestOffer(QuestOffer),
2977 QuestAccepted(QuestNotice),
2978 QuestWithdrawn(QuestNotice),
2979 QuestStepCompleted(QuestNotice),
2980 QuestCompleted(QuestNotice),
2981 BankOpened(BankPanel),
2983 StorageOpened(StoragePanel),
2985 TradeOpened(TradePanel),
2987 TradeClosed {
2989 reason: String,
2990 },
2991}
2992
2993#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2995pub struct TradePanel {
2996 pub peer_entity_id: EntityId,
2997 pub peer_name: String,
2998 pub my_presented: Vec<ItemStack>,
2999 pub their_presented: Vec<ItemStack>,
3000 pub i_ready: bool,
3001 pub they_ready: bool,
3002 pub my_mass_after: f32,
3004 pub my_mass_max: f32,
3005 pub my_encumbrance_after: EncumbranceState,
3006 pub overburden_warning: bool,
3008}
3009
3010#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3011pub enum ClientMessage {
3012 Hello(Hello),
3013 Intent(Intent),
3014 Disconnect,
3015}
3016
3017#[cfg(test)]
3018mod tests {
3019 use super::*;
3020
3021 #[test]
3022 fn pristine_vitals_state_yields_full_pools() {
3023 let attrs = PrimaryAttributes::default();
3024 let vitals = StoredVitalsState::default().apply_to(attrs);
3025 assert!(vitals.health > 0.0);
3026 assert_eq!(vitals.health, vitals.health_max);
3027 assert!((vitals.mana_max - 61.0).abs() < 0.01);
3028 }
3029
3030 #[test]
3031 fn saved_vitals_scale_when_pool_max_increases() {
3032 let mut attrs = PrimaryAttributes::default();
3033 attrs.intelligence = 140;
3034 attrs.wisdom = 140;
3035 let saved = StoredVitalsState {
3036 health: 100.0,
3037 mana: 14.0,
3038 stamina: 100.0,
3039 ..StoredVitalsState::default()
3040 };
3041 let vitals = saved.apply_to(attrs);
3042 assert!(vitals.mana_max > 55.0);
3043 assert!(
3044 (vitals.mana - vitals.mana_max).abs() < 0.01,
3045 "full legacy mana bar migrates to full new bar"
3046 );
3047 }
3048
3049 #[test]
3050 fn empty_vitals_state_is_pristine() {
3051 let pristine = StoredVitalsState {
3052 health: 0.0,
3053 mana: 0.0,
3054 stamina: 0.0,
3055 hunger: 0.0,
3056 thirst: 0.0,
3057 coins: 0,
3058 deaths: 0,
3059 life_state: LifeState::Alive,
3060 };
3061 assert!(pristine.is_pristine());
3062 let vitals = pristine.apply_to(PrimaryAttributes::default());
3063 assert!(vitals.health > 0.0);
3064 }
3065
3066 #[test]
3067 fn stored_vitals_roundtrip_preserves_partial_pools() {
3068 let attrs = PrimaryAttributes::default();
3069 let mut live = PlayerVitals::from_attributes(attrs);
3070 live.health = 25.0;
3071 live.hunger = 77.0;
3072 live.deaths = 2;
3073 let stored = StoredVitalsState::from_live(&live);
3074 let restored = stored.apply_to(attrs);
3075 assert!(
3076 (restored.health - 25.0).abs() < 0.01,
3077 "partial HP below cap stays absolute"
3078 );
3079 assert_eq!(restored.hunger, 77.0);
3080 assert_eq!(restored.deaths, 2);
3081 }
3082
3083 #[test]
3084 fn skill_tiers_start_at_zero() {
3085 let skill = SkillProgress::default();
3086 assert_eq!(skill.level, 0);
3087 assert_eq!(skill.display_tier(), 0);
3088 let trained = SkillProgress {
3089 level: 250,
3090 last_trained_tick: 1,
3091 };
3092 assert_eq!(trained.display_tier(), 2);
3093 }
3094
3095 #[test]
3096 fn quest_server_messages_roundtrip_json() {
3097 use crate::codec::{Codec, PostcardCodec};
3098
3099 let offer = ServerMessage::QuestOffer(QuestOffer {
3100 quest_id: "ada_goblin_hunt".into(),
3101 title: "Goblin Trouble".into(),
3102 description: "Help Ada".into(),
3103 step_count: 3,
3104 });
3105 let notice = ServerMessage::QuestAccepted(QuestNotice {
3106 quest_id: "ada_goblin_hunt".into(),
3107 title: "Goblin Trouble".into(),
3108 message: "Quest accepted".into(),
3109 });
3110 for msg in [offer, notice] {
3111 let bytes = PostcardCodec.encode(&msg).unwrap();
3112 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
3113 assert_eq!(decoded, msg);
3114 }
3115 }
3116
3117 #[test]
3118 fn hotbar_consumable_binding_roundtrips() {
3119 let binding = hotbar_consumable_binding("bottle_of_water");
3120 assert_eq!(binding, "item:bottle_of_water");
3121 assert!(hotbar_binding_is_consumable(&binding));
3122 assert_eq!(
3123 hotbar_consumable_template(&binding),
3124 Some("bottle_of_water")
3125 );
3126 assert!(!hotbar_binding_is_consumable("fireball"));
3127 assert_eq!(hotbar_consumable_template("fireball"), None);
3128 }
3129}