1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6pub type EntityId = u64;
7pub type Tick = u64;
8pub type Seq = u32;
9pub type SessionId = u64;
10
11#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct WorldCoord {
14 pub x: f32,
15 pub y: f32,
16 pub z: f32,
17 pub w: u32,
18 pub t: u32,
19}
20
21impl WorldCoord {
22 pub fn surface(x: f32, y: f32) -> Self {
23 Self {
24 x,
25 y,
26 z: 0.0,
27 w: 0,
28 t: 0,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
34pub struct Velocity2D {
35 pub vx: f32,
36 pub vy: f32,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
40pub struct Transform {
41 pub position: WorldCoord,
42 pub yaw: f32,
43 pub velocity: Velocity2D,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum LifeState {
49 Alive,
50 Dead,
51}
52
53impl Default for LifeState {
54 fn default() -> Self {
55 Self::Alive
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum TimeOfDayPhase {
63 Night,
64 Dawn,
65 Morning,
66 Midday,
67 Afternoon,
68 Evening,
69}
70
71impl TimeOfDayPhase {
72 pub fn label(self) -> &'static str {
73 match self {
74 Self::Night => "Night",
75 Self::Dawn => "Dawn",
76 Self::Morning => "Morning",
77 Self::Midday => "Midday",
78 Self::Afternoon => "Afternoon",
79 Self::Evening => "Evening",
80 }
81 }
82
83 pub fn from_name(name: &str) -> Self {
84 match name.to_ascii_lowercase().as_str() {
85 "dawn" => Self::Dawn,
86 "morning" => Self::Morning,
87 "midday" | "mid_day" | "noon" => Self::Midday,
88 "afternoon" => Self::Afternoon,
89 "evening" | "dusk" => Self::Evening,
90 _ => Self::Night,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
97pub struct WorldClock {
98 pub day: u64,
100 pub hour: u8,
101 pub minute: u8,
102 pub phase: TimeOfDayPhase,
103}
104
105impl Default for WorldClock {
106 fn default() -> Self {
107 Self {
108 day: 0,
109 hour: 8,
110 minute: 0,
111 phase: TimeOfDayPhase::Morning,
112 }
113 }
114}
115
116impl WorldClock {
117 pub fn display_time(self) -> String {
118 format!("{:02}:{:02}", self.hour, self.minute)
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub struct PrimaryAttributes {
125 pub strength: u16,
126 pub dexterity: u16,
127 pub intelligence: u16,
128 pub stamina: u16,
129 pub vitality: u16,
130 pub wisdom: u16,
131 pub charisma: u16,
132}
133
134impl Default for PrimaryAttributes {
135 fn default() -> Self {
136 Self {
137 strength: 150,
138 dexterity: 150,
139 intelligence: 150,
140 stamina: 150,
141 vitality: 150,
142 wisdom: 150,
143 charisma: 150,
144 }
145 }
146}
147
148impl PrimaryAttributes {
149 pub fn display(value: u16) -> u16 {
151 (value / 10).clamp(1, 100)
152 }
153
154 pub fn derived_preview(&self) -> DerivedPreview {
156 let str_d = Self::display(self.strength) as f32;
157 let dex_d = Self::display(self.dexterity) as f32;
158 let int_d = Self::display(self.intelligence) as f32;
159 let wis_d = Self::display(self.wisdom) as f32;
160 DerivedPreview {
161 attack_power: str_d * 1.2 + dex_d * 0.3,
162 spell_power: int_d * 1.1 + wis_d * 0.4,
163 evasion: dex_d * 0.8 + wis_d * 0.2,
164 carry_mass_max: str_d * 2.5,
165 sight_range_m: 12.0 + wis_d * 0.15 + dex_d * 0.05,
166 fov_deg: 120.0 + wis_d * 0.2,
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq)]
173pub struct DerivedPreview {
174 pub attack_power: f32,
175 pub spell_power: f32,
176 pub evasion: f32,
177 pub carry_mass_max: f32,
178 pub sight_range_m: f32,
179 pub fov_deg: f32,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184pub struct SkillProgress {
185 pub level: u16,
186 #[serde(default)]
187 pub last_trained_tick: u64,
188}
189
190impl Default for SkillProgress {
191 fn default() -> Self {
192 Self {
193 level: 0,
194 last_trained_tick: 0,
195 }
196 }
197}
198
199impl SkillProgress {
200 pub fn display_tier(&self) -> u16 {
202 (self.level / 100).min(10)
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
208#[serde(default)]
209pub struct ProgressionXp {
210 pub strength: f64,
211 pub dexterity: f64,
212 pub intelligence: f64,
213 pub stamina: f64,
214 pub vitality: f64,
215 pub wisdom: f64,
216 pub charisma: f64,
217 pub logging: f64,
218 pub mining: f64,
219 pub evocation: f64,
220 pub restoration: f64,
221 pub swords: f64,
222 pub archery: f64,
223 pub crafting: f64,
224 pub alchemy: f64,
225 pub cartography: f64,
226}
227
228impl ProgressionXp {
229 pub fn bootstrap_new(baseline_display: u16, xp_base: f64, xp_growth: f64) -> Self {
231 let bootstrap = |display: f64| {
232 if display <= 1.0 {
233 0.0
234 } else {
235 xp_base * xp_growth.powf(display - 1.0)
236 }
237 };
238 let b = baseline_display as f64;
239 let primary = bootstrap(b);
240 Self {
241 strength: primary,
242 dexterity: primary,
243 intelligence: primary,
244 stamina: primary,
245 vitality: primary,
246 wisdom: primary,
247 charisma: primary,
248 ..Self::default()
249 }
250 }
251
252 pub fn is_empty(&self) -> bool {
253 self.strength == 0.0
254 && self.dexterity == 0.0
255 && self.intelligence == 0.0
256 && self.stamina == 0.0
257 && self.vitality == 0.0
258 && self.wisdom == 0.0
259 && self.charisma == 0.0
260 && self.logging == 0.0
261 && self.mining == 0.0
262 && self.evocation == 0.0
263 && self.restoration == 0.0
264 && self.swords == 0.0
265 && self.archery == 0.0
266 && self.crafting == 0.0
267 && self.alchemy == 0.0
268 && self.cartography == 0.0
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274#[serde(default)]
275pub struct PlayerSkills {
276 pub logging: SkillProgress,
277 pub mining: SkillProgress,
278 pub evocation: SkillProgress,
279 #[serde(default)]
280 pub restoration: SkillProgress,
281 pub swords: SkillProgress,
282 #[serde(default)]
283 pub archery: SkillProgress,
284 pub crafting: SkillProgress,
285 #[serde(default)]
286 pub alchemy: SkillProgress,
287 pub cartography: SkillProgress,
288}
289
290impl Default for PlayerSkills {
291 fn default() -> Self {
292 Self {
293 logging: SkillProgress::default(),
294 mining: SkillProgress::default(),
295 evocation: SkillProgress::default(),
296 restoration: SkillProgress::default(),
297 swords: SkillProgress::default(),
298 archery: SkillProgress::default(),
299 crafting: SkillProgress::default(),
300 alchemy: SkillProgress::default(),
301 cartography: SkillProgress::default(),
302 }
303 }
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
308pub struct PlayerVitals {
309 pub health: f32,
310 pub health_max: f32,
311 pub mana: f32,
312 pub mana_max: f32,
313 pub stamina: f32,
314 pub stamina_max: f32,
315 #[serde(default = "default_survival_pool_max")]
316 pub hunger: f32,
317 #[serde(default = "default_survival_pool_max")]
318 pub hunger_max: f32,
319 #[serde(default = "default_survival_pool_max")]
320 pub thirst: f32,
321 #[serde(default = "default_survival_pool_max")]
322 pub thirst_max: f32,
323 #[serde(default)]
324 pub coins: u32,
325 #[serde(default)]
326 pub deaths: u32,
327 #[serde(default)]
328 pub life_state: LifeState,
329}
330
331fn default_survival_pool_max() -> f32 {
332 100.0
333}
334
335impl Default for PlayerVitals {
336 fn default() -> Self {
337 Self::from_attributes(PrimaryAttributes::default())
338 }
339}
340
341impl PlayerVitals {
342 pub fn from_attributes(attrs: PrimaryAttributes) -> Self {
349 let vit_d = PrimaryAttributes::display(attrs.vitality) as f32;
350 let int_d = PrimaryAttributes::display(attrs.intelligence) as f32;
351 let wis_d = PrimaryAttributes::display(attrs.wisdom) as f32;
352 let sta_d = PrimaryAttributes::display(attrs.stamina) as f32;
353
354 let health_max = 50.0 + vit_d * 2.0;
355 let stamina_max = 30.0 + sta_d * 1.4;
356 let mana_max = 25.0 + int_d * 1.1 + wis_d * 1.3;
357 let hunger_max = 100.0;
358 let thirst_max = 100.0;
359 Self {
360 health: health_max,
361 health_max,
362 mana: mana_max,
363 mana_max,
364 stamina: stamina_max,
365 stamina_max,
366 hunger: hunger_max,
367 hunger_max,
368 thirst: thirst_max,
369 thirst_max,
370 coins: 0,
371 deaths: 0,
372 life_state: LifeState::Alive,
373 }
374 }
375
376 pub fn legacy_maxima(attrs: PrimaryAttributes) -> (f32, f32, f32) {
378 (
379 attrs.vitality as f32 / 5.0,
380 attrs.stamina as f32 / 5.0,
381 (attrs.intelligence as f32 + attrs.wisdom as f32) / 20.0,
382 )
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
388#[serde(default)]
389pub struct StoredVitalsState {
390 pub health: f32,
391 pub mana: f32,
392 pub stamina: f32,
393 pub hunger: f32,
394 pub thirst: f32,
395 pub coins: u32,
396 pub deaths: u32,
397 pub life_state: LifeState,
398}
399
400impl StoredVitalsState {
401 pub fn from_live(v: &PlayerVitals) -> Self {
402 Self {
403 health: v.health,
404 mana: v.mana,
405 stamina: v.stamina,
406 hunger: v.hunger,
407 thirst: v.thirst,
408 coins: v.coins,
409 deaths: v.deaths,
410 life_state: v.life_state,
411 }
412 }
413
414 pub fn is_pristine(&self) -> bool {
416 self.health == 0.0
417 && self.mana == 0.0
418 && self.stamina == 0.0
419 && self.hunger == 0.0
420 && self.thirst == 0.0
421 && self.coins == 0
422 && self.deaths == 0
423 && self.life_state == LifeState::Alive
424 }
425
426 pub fn apply_to(self, attrs: PrimaryAttributes) -> PlayerVitals {
427 if self.is_pristine() {
428 return PlayerVitals::from_attributes(attrs);
429 }
430 let fresh = PlayerVitals::from_attributes(attrs);
431 let (legacy_hp, legacy_sta, legacy_mana) = PlayerVitals::legacy_maxima(attrs);
432
433 let scale = |current: f32, legacy_max: f32, new_max: f32| {
434 if legacy_max > 0.0 && new_max > legacy_max * 1.05 && current >= legacy_max * 0.95 {
435 let ratio = (current / legacy_max).clamp(0.0, 1.0);
436 (new_max * ratio).min(new_max)
437 } else {
438 current.min(new_max)
439 }
440 };
441
442 let mut v = fresh;
443 v.health = scale(self.health, legacy_hp, fresh.health_max);
444 v.mana = scale(self.mana, legacy_mana, fresh.mana_max);
445 v.stamina = scale(self.stamina, legacy_sta, fresh.stamina_max);
446 v.hunger = self.hunger.min(v.hunger_max);
447 v.thirst = self.thirst.min(v.thirst_max);
448 v.coins = self.coins;
449 v.deaths = self.deaths;
450 v.life_state = self.life_state;
451 v
452 }
453}
454
455impl Default for StoredVitalsState {
456 fn default() -> Self {
457 Self::from_live(&PlayerVitals::default())
458 }
459}
460
461pub fn humanize_snake_id(id: &str) -> String {
465 id.split('_')
466 .filter(|part| !part.is_empty())
467 .map(|part| {
468 let mut chars = part.chars();
469 match chars.next() {
470 None => String::new(),
471 Some(first) => first.to_uppercase().chain(chars).collect(),
472 }
473 })
474 .collect::<Vec<_>>()
475 .join(" ")
476}
477
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
480pub struct KnownAbility {
481 pub ability_id: String,
482 #[serde(default = "default_known_permanent")]
484 pub permanent: bool,
485 #[serde(default)]
487 pub expires_at_tick: Option<u64>,
488}
489
490fn default_known_permanent() -> bool {
491 true
492}
493
494impl KnownAbility {
495 pub fn permanent(ability_id: impl Into<String>) -> Self {
496 Self {
497 ability_id: ability_id.into(),
498 permanent: true,
499 expires_at_tick: None,
500 }
501 }
502
503 pub fn is_active(&self, tick: u64) -> bool {
504 if self.permanent {
505 return true;
506 }
507 match self.expires_at_tick {
508 Some(exp) => tick < exp,
509 None => false,
510 }
511 }
512}
513
514#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516pub struct RotationPreset {
517 pub id: String,
518 pub label: String,
519 #[serde(default)]
520 pub abilities: Vec<String>,
521}
522
523impl RotationPreset {
524 pub fn melee_default(ability_id: impl Into<String>) -> Self {
525 let id = ability_id.into();
526 Self {
527 id: "melee".into(),
528 label: "Weapon".into(),
530 abilities: vec![id],
531 }
532 }
533
534 pub fn is_weapon_preset(&self) -> bool {
535 self.id == "melee"
536 }
537}
538
539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
541pub struct StoredTargetSlot {
542 pub instance_id: Option<String>,
543 #[serde(default)]
544 pub preset_id: Option<String>,
545 #[serde(default)]
546 pub rotation_index: u32,
547 #[serde(default)]
548 pub auto_enabled: bool,
549}
550
551#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
552#[serde(default)]
553pub struct StoredCombatProfile {
554 pub combat_target_instance_id: Option<String>,
556 pub in_combat: bool,
557 pub last_combat_tick: u64,
558 pub last_attack_tick: u64,
559 pub cooldowns_until_tick: BTreeMap<String, u64>,
560 #[serde(default = "default_auto_attack")]
561 pub auto_attack_enabled: bool,
562 #[serde(default)]
564 pub mainhand_template_id: Option<String>,
565 #[serde(default)]
567 pub mainhand_instance_id: Option<Uuid>,
568 #[serde(default)]
570 pub offhand_template_id: Option<String>,
571 #[serde(default)]
573 pub offhand_instance_id: Option<Uuid>,
574 #[serde(default)]
577 pub worn: Vec<(BodySlot, ItemStack)>,
578 #[serde(default)]
580 pub rotation_presets: Vec<RotationPreset>,
581 #[serde(default)]
583 pub target_slots: Vec<StoredTargetSlot>,
584 #[serde(default)]
586 pub known_blueprint_ids: Vec<String>,
587 #[serde(default)]
589 pub keychain: Vec<ItemStack>,
590 #[serde(default)]
592 pub whisper_pouch: Vec<ItemStack>,
593 #[serde(default)]
595 pub known_abilities: Vec<KnownAbility>,
596 #[serde(default)]
598 pub hotbar: Vec<Option<String>>,
599 #[serde(default)]
601 pub abilities_schema_version: u32,
602 #[serde(default)]
604 pub bank_balance_copper: u64,
605}
606
607fn default_auto_attack() -> bool {
608 true
609}
610
611impl Default for StoredCombatProfile {
612 fn default() -> Self {
613 Self {
614 combat_target_instance_id: None,
615 in_combat: false,
616 last_combat_tick: 0,
617 last_attack_tick: 0,
618 cooldowns_until_tick: BTreeMap::new(),
619 auto_attack_enabled: true,
620 mainhand_template_id: None,
621 mainhand_instance_id: None,
622 offhand_template_id: None,
623 offhand_instance_id: None,
624 worn: Vec::new(),
625 rotation_presets: Vec::new(),
626 target_slots: Vec::new(),
627 known_blueprint_ids: Vec::new(),
628 keychain: Vec::new(),
629 whisper_pouch: Vec::new(),
630 known_abilities: Vec::new(),
631 hotbar: Vec::new(),
632 abilities_schema_version: 0,
633 bank_balance_copper: 0,
634 }
635 }
636}
637
638#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
639pub struct EntityState {
640 pub id: EntityId,
641 pub transform: Transform,
642 #[serde(default)]
644 pub label: String,
645 #[serde(default)]
646 pub vitals: Option<PlayerVitals>,
647 #[serde(default)]
649 pub attributes: Option<PrimaryAttributes>,
650 #[serde(default)]
651 pub skills: Option<PlayerSkills>,
652 #[serde(default)]
654 pub inside_building: Option<String>,
655 #[serde(default)]
657 pub tile_id: Option<String>,
658 #[serde(default)]
660 pub paperdoll_ref: Option<String>,
661 #[serde(default)]
663 pub presentation_state: Option<String>,
664 #[serde(default)]
666 pub sprite_mode: Option<String>,
667 #[serde(default)]
669 pub progression_xp: Option<ProgressionXp>,
670}
671
672#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
674#[serde(rename_all = "snake_case")]
675pub enum ChatChannel {
676 Nearby,
678 Direct,
680 Whisper,
682 WhisperStone,
684}
685
686#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
688#[serde(rename_all = "snake_case")]
689pub enum ChatClarity {
690 #[default]
691 Clear,
692 Partial,
693 Heavy,
694}
695
696#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
697pub struct ChatMessage {
698 pub channel: ChatChannel,
699 pub from_entity: EntityId,
700 pub from_name: String,
701 pub text: String,
703 pub tick: Tick,
704 #[serde(default)]
706 pub to_entity: Option<EntityId>,
707 #[serde(default)]
708 pub clarity: ChatClarity,
709}
710
711#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
713pub enum Intent {
714 Move {
715 entity_id: EntityId,
716 forward: f32,
717 strafe: f32,
718 #[serde(default)]
720 vertical: f32,
721 #[serde(default)]
723 sprint: bool,
724 seq: Seq,
725 },
726 Stop {
727 entity_id: EntityId,
728 seq: Seq,
729 },
730 Harvest {
731 entity_id: EntityId,
732 node_id: String,
733 seq: Seq,
734 },
735 Use {
736 entity_id: EntityId,
737 template_id: String,
738 seq: Seq,
739 },
740 UseGrant {
742 entity_id: EntityId,
743 grant_instance_id: Uuid,
744 target_instance_id: Uuid,
745 seq: Seq,
746 },
747 Say {
748 entity_id: EntityId,
749 channel: ChatChannel,
750 text: String,
751 #[serde(default)]
753 to_entity: Option<EntityId>,
754 seq: Seq,
755 },
756 Craft {
758 entity_id: EntityId,
759 blueprint_id: String,
760 #[serde(default)]
762 count: Option<u32>,
763 seq: Seq,
764 },
765 Interact {
767 entity_id: EntityId,
768 target_id: String,
769 seq: Seq,
770 },
771 ShopBuy {
773 entity_id: EntityId,
774 npc_id: String,
775 offer_id: String,
776 #[serde(default = "default_one")]
777 quantity: u32,
778 seq: Seq,
779 },
780 ShopSell {
782 entity_id: EntityId,
783 npc_id: String,
784 template_id: String,
785 #[serde(default = "default_one")]
786 quantity: u32,
787 seq: Seq,
788 },
789 ShopClose {
791 entity_id: EntityId,
792 npc_id: String,
793 seq: Seq,
794 },
795 TestDamage {
797 entity_id: EntityId,
798 amount: f32,
799 seq: Seq,
800 },
801 SetTarget {
803 entity_id: EntityId,
804 target_id: EntityId,
805 seq: Seq,
806 },
807 SetTargetSlot {
809 entity_id: EntityId,
810 slot_index: u8,
811 target_id: EntityId,
812 seq: Seq,
813 },
814 ClearTarget {
815 entity_id: EntityId,
816 seq: Seq,
817 },
818 ClearTargetSlot {
819 entity_id: EntityId,
820 slot_index: u8,
821 seq: Seq,
822 },
823 SetAutoAttack {
825 entity_id: EntityId,
826 slot_index: u8,
827 enabled: bool,
828 seq: Seq,
829 },
830 Attack {
832 entity_id: EntityId,
833 #[serde(default)]
834 target_id: Option<EntityId>,
835 #[serde(default)]
836 weapon_slot: Option<u32>,
837 seq: Seq,
838 },
839 Pickup {
841 entity_id: EntityId,
842 #[serde(default)]
843 drop_id: Option<String>,
844 seq: Seq,
845 },
846 Cast {
848 entity_id: EntityId,
849 ability_id: String,
850 target_id: EntityId,
851 seq: Seq,
852 },
853 BindActionSlot {
855 entity_id: EntityId,
856 slot_index: u8,
857 ability_id: String,
858 #[serde(default = "default_auto_attack")]
859 auto_enabled: bool,
860 seq: Seq,
861 },
862 UseActionSlot {
864 entity_id: EntityId,
865 slot_index: u8,
866 seq: Seq,
867 },
868 Dodge {
870 entity_id: EntityId,
871 seq: Seq,
872 },
873 Lunge {
875 entity_id: EntityId,
876 #[serde(default)]
878 forward: f32,
879 #[serde(default)]
881 strafe: f32,
882 seq: Seq,
883 },
884 DirectionalJump {
886 entity_id: EntityId,
887 #[serde(default)]
889 forward: f32,
890 #[serde(default)]
892 strafe: f32,
893 seq: Seq,
894 },
895 Block {
897 entity_id: EntityId,
898 #[serde(default = "default_block_enabled")]
899 enabled: bool,
900 seq: Seq,
901 },
902 EquipMainhand {
906 entity_id: EntityId,
907 #[serde(default)]
908 template_id: Option<String>,
909 #[serde(default)]
910 instance_id: Option<Uuid>,
911 seq: Seq,
912 },
913 EquipOffhand {
915 entity_id: EntityId,
916 #[serde(default)]
917 template_id: Option<String>,
918 #[serde(default)]
919 instance_id: Option<Uuid>,
920 seq: Seq,
921 },
922 EquipWorn {
925 entity_id: EntityId,
926 slot: BodySlot,
927 #[serde(default)]
928 instance_id: Option<Uuid>,
929 seq: Seq,
930 },
931 MoveItem {
933 entity_id: EntityId,
934 item_instance_id: Uuid,
935 from: InventoryLocation,
936 to: InventoryLocation,
937 #[serde(default)]
939 to_parent_instance_id: Option<Uuid>,
940 #[serde(default)]
942 quantity: Option<u32>,
943 seq: Seq,
944 },
945 PlaceContainer {
947 entity_id: EntityId,
948 item_instance_id: Uuid,
949 seq: Seq,
950 },
951 PickupContainer {
953 entity_id: EntityId,
954 container_id: String,
955 seq: Seq,
956 },
957 SetContainerLocked {
959 entity_id: EntityId,
960 location: InventoryLocation,
962 locked: bool,
963 seq: Seq,
964 },
965 DropItem {
967 entity_id: EntityId,
968 item_instance_id: Uuid,
969 from: InventoryLocation,
970 seq: Seq,
971 },
972 DestroyItem {
974 entity_id: EntityId,
975 item_instance_id: Uuid,
976 from: InventoryLocation,
977 #[serde(default)]
979 quantity: Option<u32>,
980 seq: Seq,
981 },
982 RenameContainer {
984 entity_id: EntityId,
985 item_instance_id: Uuid,
986 location: InventoryLocation,
987 name: String,
988 seq: Seq,
989 },
990 UpsertRotationPreset {
992 entity_id: EntityId,
993 preset: RotationPreset,
994 seq: Seq,
995 },
996 DeleteRotationPreset {
998 entity_id: EntityId,
999 preset_id: String,
1000 seq: Seq,
1001 },
1002 AssignSlotPreset {
1004 entity_id: EntityId,
1005 slot_index: u8,
1006 preset_id: String,
1007 seq: Seq,
1008 },
1009 SetHotbarSlot {
1012 entity_id: EntityId,
1013 slot: u8,
1015 #[serde(default)]
1017 ability_id: Option<String>,
1018 seq: Seq,
1019 },
1020 AdvanceRotation {
1022 entity_id: EntityId,
1023 slot_index: u8,
1024 seq: Seq,
1025 },
1026 NpcTalkOpen {
1028 entity_id: EntityId,
1029 npc_id: String,
1030 seq: Seq,
1031 },
1032 NpcTalkSay {
1034 entity_id: EntityId,
1035 npc_id: String,
1036 message: String,
1037 seq: Seq,
1038 },
1039 NpcTalkClose {
1041 entity_id: EntityId,
1042 npc_id: String,
1043 seq: Seq,
1044 },
1045 AcceptQuest {
1047 entity_id: EntityId,
1048 quest_id: String,
1049 seq: Seq,
1050 },
1051 WithdrawQuest {
1053 entity_id: EntityId,
1054 quest_id: String,
1055 seq: Seq,
1056 },
1057 TrackQuest {
1059 entity_id: EntityId,
1060 quest_id: String,
1061 seq: Seq,
1062 },
1063 QuestGiveItem {
1065 entity_id: EntityId,
1066 npc_id: String,
1067 template_id: String,
1068 #[serde(default = "default_one")]
1069 quantity: u32,
1070 seq: Seq,
1071 },
1072 HireWorker {
1074 entity_id: EntityId,
1075 def_id: String,
1076 wage_copper_per_interval: u32,
1077 #[serde(default)]
1078 lodging_container_id: Option<String>,
1079 #[serde(default)]
1080 job_yaml: Option<String>,
1081 seq: Seq,
1082 },
1083 DismissWorker {
1085 entity_id: EntityId,
1086 worker_instance_id: String,
1087 seq: Seq,
1088 },
1089 SetWorkerJob {
1091 entity_id: EntityId,
1092 worker_instance_id: String,
1093 job_yaml: String,
1094 seq: Seq,
1095 },
1096 AssignWorkerLodging {
1098 entity_id: EntityId,
1099 worker_instance_id: String,
1100 lodging_container_id: String,
1101 seq: Seq,
1102 },
1103 SetWorkerMode {
1105 entity_id: EntityId,
1106 worker_instance_id: String,
1107 mode: String,
1108 seq: Seq,
1109 },
1110 GiveWorkerItem {
1113 entity_id: EntityId,
1114 worker_instance_id: String,
1115 item_instance_id: uuid::Uuid,
1116 #[serde(default)]
1117 quantity: Option<u32>,
1118 seq: Seq,
1119 },
1120 TakeWorkerItem {
1122 entity_id: EntityId,
1123 worker_instance_id: String,
1124 item_instance_id: uuid::Uuid,
1125 #[serde(default)]
1126 quantity: Option<u32>,
1127 seq: Seq,
1128 },
1129 RenameHiredWorker {
1131 entity_id: EntityId,
1132 worker_instance_id: String,
1133 name: String,
1134 seq: Seq,
1135 },
1136 TeachWorkerBlueprint {
1138 entity_id: EntityId,
1139 worker_instance_id: String,
1140 blueprint_id: String,
1141 seq: Seq,
1142 },
1143 BuyProperty {
1145 entity_id: EntityId,
1146 zone_id: String,
1147 seq: Seq,
1148 },
1149 SellProperty {
1151 entity_id: EntityId,
1152 zone_id: String,
1153 seq: Seq,
1154 },
1155 BankDeposit {
1157 entity_id: EntityId,
1158 npc_id: String,
1159 #[serde(default)]
1161 amount_copper: u64,
1162 seq: Seq,
1163 },
1164 BankWithdraw {
1166 entity_id: EntityId,
1167 npc_id: String,
1168 #[serde(default)]
1170 amount_copper: u64,
1171 seq: Seq,
1172 },
1173 BankClose {
1175 entity_id: EntityId,
1176 npc_id: String,
1177 seq: Seq,
1178 },
1179 BankTransfer {
1181 entity_id: EntityId,
1182 npc_id: String,
1183 #[serde(default)]
1185 to_character_id: Option<Uuid>,
1186 #[serde(default)]
1188 to_name: String,
1189 amount_copper: u64,
1191 seq: Seq,
1192 },
1193 StorageStore {
1195 entity_id: EntityId,
1196 npc_id: String,
1197 item_instance_id: Uuid,
1198 #[serde(default)]
1199 quantity: Option<u32>,
1200 seq: Seq,
1201 },
1202 StorageTake {
1204 entity_id: EntityId,
1205 npc_id: String,
1206 item_instance_id: Uuid,
1207 #[serde(default)]
1208 quantity: Option<u32>,
1209 seq: Seq,
1210 },
1211 StorageShip {
1213 entity_id: EntityId,
1214 npc_id: String,
1215 dest_building_id: String,
1216 item_instance_id: Uuid,
1217 #[serde(default)]
1218 quantity: Option<u32>,
1219 seq: Seq,
1220 },
1221 StorageClose {
1223 entity_id: EntityId,
1224 npc_id: String,
1225 seq: Seq,
1226 },
1227 TradeRequest {
1229 entity_id: EntityId,
1230 peer_entity_id: EntityId,
1231 seq: Seq,
1232 },
1233 TradeRespond {
1235 entity_id: EntityId,
1236 peer_entity_id: EntityId,
1237 accept: bool,
1238 seq: Seq,
1239 },
1240 TradePresent {
1242 entity_id: EntityId,
1243 item_instance_id: Uuid,
1244 #[serde(default)]
1245 quantity: Option<u32>,
1246 seq: Seq,
1247 },
1248 TradeUnpresent {
1250 entity_id: EntityId,
1251 item_instance_id: Uuid,
1252 seq: Seq,
1253 },
1254 TradeSetReady {
1256 entity_id: EntityId,
1257 ready: bool,
1258 seq: Seq,
1259 },
1260 TradeCancel {
1262 entity_id: EntityId,
1263 seq: Seq,
1264 },
1265 DestroyWhisperStone {
1267 entity_id: EntityId,
1268 item_instance_id: Uuid,
1269 seq: Seq,
1270 },
1271 StowWhisperStone {
1273 entity_id: EntityId,
1274 item_instance_id: Uuid,
1275 seq: Seq,
1276 },
1277}
1278
1279fn default_block_enabled() -> bool {
1280 true
1281}
1282
1283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1285pub struct StatusEffectHud {
1286 pub effect_id: String,
1287 pub label: String,
1288 #[serde(default)]
1289 pub polarity: String,
1290 #[serde(default)]
1291 pub icon_tile_id: Option<String>,
1292 #[serde(default)]
1294 pub remaining_sec: Option<f32>,
1295}
1296
1297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1299pub struct CombatTargetHud {
1300 pub entity_id: EntityId,
1301 #[serde(default)]
1302 pub label: String,
1303 #[serde(default)]
1304 pub level: u32,
1305 pub health: f32,
1306 pub health_max: f32,
1307 #[serde(default)]
1308 pub life_state: LifeState,
1309 #[serde(default)]
1310 pub distance_m: f32,
1311 #[serde(default)]
1312 pub statuses: Vec<StatusEffectHud>,
1313}
1314
1315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1317pub struct CastProgressHud {
1318 #[serde(default)]
1319 pub ability_id: String,
1320 #[serde(default)]
1321 pub ability_label: String,
1322 #[serde(default)]
1323 pub ticks_remaining: u64,
1324 #[serde(default)]
1325 pub ticks_total: u64,
1326}
1327
1328#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1330pub struct AbilityCooldownHud {
1331 #[serde(default)]
1332 pub ability_id: String,
1333 #[serde(default)]
1334 pub label: String,
1335 #[serde(default)]
1336 pub cd_ticks: u64,
1337 #[serde(default)]
1338 pub cd_total_ticks: u64,
1339}
1340
1341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1343pub struct CombatSlotHud {
1344 pub slot_index: u8,
1345 #[serde(default)]
1346 pub target_entity_id: Option<EntityId>,
1347 #[serde(default)]
1348 pub target_label: Option<String>,
1349 #[serde(default)]
1350 pub target: Option<CombatTargetHud>,
1351 #[serde(default)]
1352 pub preset_id: Option<String>,
1353 #[serde(default)]
1354 pub preset_label: Option<String>,
1355 #[serde(default)]
1356 pub rotation: Vec<String>,
1357 #[serde(default)]
1358 pub rotation_index: u32,
1359 #[serde(default)]
1360 pub next_ability_id: Option<String>,
1361 #[serde(default)]
1362 pub auto_enabled: bool,
1363}
1364
1365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1367pub struct DefensePieceHud {
1368 pub slot: BodySlot,
1369 pub label: String,
1370 pub template_id: String,
1371 #[serde(default)]
1372 pub armor_physical: f32,
1373 #[serde(default)]
1374 pub resists: Vec<(String, f32)>,
1375}
1376
1377impl Default for DefensePieceHud {
1378 fn default() -> Self {
1379 Self {
1380 slot: BodySlot::Head,
1381 label: String::new(),
1382 template_id: String::new(),
1383 armor_physical: 0.0,
1384 resists: Vec::new(),
1385 }
1386 }
1387}
1388
1389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1391pub struct DefenseHud {
1392 pub armor_physical: f32,
1393 pub vitality_contribution: f32,
1394 pub total_mitigation_rating: f32,
1395 pub estimated_physical_dr: f32,
1397 #[serde(default)]
1398 pub resists: Vec<(String, f32)>,
1399 #[serde(default)]
1400 pub pieces: Vec<DefensePieceHud>,
1401}
1402
1403#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1405pub struct CombatHud {
1406 pub in_combat: bool,
1407 pub auto_attack: bool,
1409 pub has_los: bool,
1410 pub attack_cd_ticks: u64,
1411 #[serde(default)]
1412 pub ability_id: String,
1413 #[serde(default)]
1414 pub target_entity_id: Option<EntityId>,
1415 #[serde(default)]
1416 pub target_label: Option<String>,
1417 #[serde(default)]
1418 pub max_target_slots: u8,
1419 #[serde(default)]
1420 pub slots: Vec<CombatSlotHud>,
1421 #[serde(default)]
1422 pub rotation_presets: Vec<RotationPreset>,
1423 #[serde(default)]
1424 pub gcd_ticks: u64,
1425 #[serde(default)]
1426 pub mainhand_template_id: Option<String>,
1427 #[serde(default)]
1428 pub mainhand_label: Option<String>,
1429 #[serde(default)]
1430 pub offhand_template_id: Option<String>,
1431 #[serde(default)]
1432 pub offhand_label: Option<String>,
1433 #[serde(default)]
1435 pub mainhand_hand_slots: u8,
1436 #[serde(default)]
1438 pub worn: Vec<(BodySlot, ItemStack)>,
1439 #[serde(default)]
1441 pub defense: Option<DefenseHud>,
1442 #[serde(default)]
1443 pub carry_mass: f32,
1444 #[serde(default)]
1445 pub carry_mass_max: f32,
1446 #[serde(default)]
1447 pub encumbrance: EncumbranceState,
1448 #[serde(default)]
1450 pub keychain: Vec<ItemStack>,
1451 #[serde(default)]
1453 pub whisper_pouch: Vec<ItemStack>,
1454 #[serde(default)]
1455 pub target: Option<CombatTargetHud>,
1456 #[serde(default)]
1457 pub cast: Option<CastProgressHud>,
1458 #[serde(default)]
1459 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1460 #[serde(default)]
1461 pub blocking_active: bool,
1462 #[serde(default)]
1464 pub progression_xp: Option<ProgressionXp>,
1465 #[serde(default)]
1466 pub progression_baseline: u16,
1467 #[serde(default)]
1468 pub progression_xp_base: f64,
1469 #[serde(default)]
1470 pub progression_xp_growth: f64,
1471 #[serde(default)]
1472 pub attributes: Option<PrimaryAttributes>,
1473 #[serde(default)]
1474 pub skills: Option<PlayerSkills>,
1475 #[serde(default)]
1477 pub statuses: Vec<StatusEffectHud>,
1478 #[serde(default)]
1480 pub known_abilities: Vec<String>,
1481 #[serde(default)]
1484 pub hotbar: Vec<Option<String>>,
1485 #[serde(default)]
1487 pub max_abilities_per_rotation: u8,
1488}
1489
1490pub const HOTBAR_ITEM_PREFIX: &str = "item:";
1492
1493pub fn hotbar_consumable_binding(template_id: &str) -> String {
1495 format!("{HOTBAR_ITEM_PREFIX}{}", template_id.trim())
1496}
1497
1498pub fn hotbar_consumable_template(binding: &str) -> Option<&str> {
1500 binding
1501 .strip_prefix(HOTBAR_ITEM_PREFIX)
1502 .map(str::trim)
1503 .filter(|id| !id.is_empty())
1504}
1505
1506pub fn hotbar_binding_is_consumable(binding: &str) -> bool {
1508 hotbar_consumable_template(binding).is_some()
1509}
1510
1511#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1513#[serde(rename_all = "snake_case")]
1514pub enum CombatFxKind {
1515 MeleeArc,
1516 Cone,
1517 Sphere,
1518 Beam,
1519 HitMarker,
1520}
1521
1522#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1524#[serde(rename_all = "snake_case")]
1525pub enum CombatFxHitOutcome {
1526 #[default]
1527 Hit,
1528 Blocked,
1529 Miss,
1530 Glance,
1531}
1532
1533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1535pub struct CombatFxHit {
1536 pub entity_id: EntityId,
1537 pub x: f32,
1538 pub y: f32,
1539 pub z: f32,
1540 #[serde(default)]
1541 pub outcome: CombatFxHitOutcome,
1542}
1543
1544#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1546pub struct CombatFx {
1547 pub id: u64,
1548 pub kind: CombatFxKind,
1549 pub ability_id: String,
1550 pub caster_id: EntityId,
1551 pub origin_x: f32,
1552 pub origin_y: f32,
1553 pub origin_z: f32,
1554 #[serde(default)]
1555 pub end_x: Option<f32>,
1556 #[serde(default)]
1557 pub end_y: Option<f32>,
1558 #[serde(default)]
1559 pub end_z: Option<f32>,
1560 #[serde(default)]
1561 pub yaw: Option<f32>,
1562 #[serde(default)]
1563 pub reach_m: Option<f32>,
1564 #[serde(default)]
1565 pub arc_deg: Option<f32>,
1566 #[serde(default)]
1567 pub radius_m: Option<f32>,
1568 #[serde(default)]
1569 pub hits: Vec<CombatFxHit>,
1570 pub until_tick: u64,
1572 #[serde(default)]
1573 pub damage_type: String,
1574}
1575
1576#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1578#[serde(rename_all = "snake_case")]
1579pub enum WorkerModeView {
1580 Companion,
1581 JobLoop,
1582 Idle,
1585}
1586
1587#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1589#[serde(rename_all = "snake_case")]
1590pub enum WorkerStateView {
1591 Idle,
1592 Traveling,
1593 Working,
1594 Resting,
1595 Waiting,
1596 Strike,
1597 Dismissed,
1598}
1599
1600#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
1602pub struct WorkerVitalsSummary {
1603 pub health_pct: f32,
1604 pub stamina_pct: f32,
1605}
1606
1607#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1609#[serde(rename_all = "snake_case")]
1610pub enum WorkerRouteKindView {
1611 #[default]
1612 HarvestLoop,
1613 Ordered,
1614}
1615
1616#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1618pub struct WorkerRouteView {
1619 #[serde(default)]
1620 pub kind: WorkerRouteKindView,
1621 #[serde(default)]
1622 pub lodging_container_id: Option<String>,
1623 #[serde(default)]
1625 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
1626 #[serde(default)]
1628 pub harvest_nodes: Vec<String>,
1629 #[serde(default = "default_route_carry_ratio")]
1630 pub carry_return_ratio: f32,
1631 #[serde(default)]
1633 pub stops: Vec<WorkerRouteStopView>,
1634}
1635
1636fn default_route_carry_ratio() -> f32 {
1637 0.90
1638}
1639
1640fn default_true_view() -> bool {
1641 true
1642}
1643
1644#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1646pub struct WorkerWithdrawItemView {
1647 pub template: String,
1648 #[serde(default)]
1649 pub qty: u32,
1650 #[serde(default)]
1652 pub all: bool,
1653}
1654
1655#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1656pub struct WorkerRouteWaypointView {
1657 pub x: f32,
1658 pub y: f32,
1659 pub z: f32,
1660}
1661
1662#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1671#[serde(rename_all = "snake_case")]
1672pub enum WorkerRouteStopView {
1673 Waypoint {
1674 x: f32,
1675 y: f32,
1676 #[serde(default)]
1677 z: f32,
1678 },
1679 HarvestNode {
1680 node_id: String,
1681 },
1682 DepositAt {
1683 container_id: String,
1684 #[serde(default)]
1685 filter: Option<Vec<String>>,
1686 },
1687 TradeWith {
1688 #[serde(default)]
1689 npc_id: Option<String>,
1690 template: String,
1691 #[serde(default = "default_true_view")]
1692 sell_all: bool,
1693 },
1694 WithdrawFrom {
1695 container_id: String,
1696 items: Vec<WorkerWithdrawItemView>,
1697 },
1698 CraftAt {
1699 device: String,
1700 blueprint: String,
1701 #[serde(default)]
1702 qty: Option<u32>,
1703 },
1704 RestIfNeeded,
1705 Wait {
1706 #[serde(default)]
1707 wait_ticks: u64,
1708 },
1709}
1710
1711#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1713#[serde(rename_all = "snake_case")]
1714pub enum LedgerCategory {
1715 Workers,
1716 Hire,
1717 Train,
1718 ShopBuy,
1719 Taxes,
1720 WorkerSales,
1721 TraderSales,
1722 BankDeposit,
1723 BankWithdraw,
1724 BankTransferOut,
1725 BankTransferIn,
1726 BankTransferFee,
1727 StorageShipFee,
1728 Other,
1729}
1730
1731impl LedgerCategory {
1732 pub fn as_str(self) -> &'static str {
1733 match self {
1734 Self::Workers => "workers",
1735 Self::Hire => "hire",
1736 Self::Train => "train",
1737 Self::ShopBuy => "shop_buy",
1738 Self::Taxes => "taxes",
1739 Self::WorkerSales => "worker_sales",
1740 Self::TraderSales => "trader_sales",
1741 Self::BankDeposit => "bank_deposit",
1742 Self::BankWithdraw => "bank_withdraw",
1743 Self::BankTransferOut => "bank_transfer_out",
1744 Self::BankTransferIn => "bank_transfer_in",
1745 Self::BankTransferFee => "bank_transfer_fee",
1746 Self::StorageShipFee => "storage_ship_fee",
1747 Self::Other => "other",
1748 }
1749 }
1750}
1751
1752#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1753pub struct LedgerEntryView {
1754 pub id: uuid::Uuid,
1755 pub game_day: u64,
1756 pub signed_copper: i64,
1757 pub category: LedgerCategory,
1758 #[serde(default)]
1759 pub label: String,
1760}
1761
1762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1763pub struct LedgerPeriodTotals {
1764 #[serde(default)]
1766 pub expenses: std::collections::HashMap<String, u64>,
1767 #[serde(default)]
1769 pub income: std::collections::HashMap<String, u64>,
1770 pub expense_copper: u64,
1771 pub income_copper: u64,
1772 pub cash_flow_copper: i64,
1774}
1775
1776#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1777pub struct PlayerLedgerView {
1778 pub current_game_day: u64,
1779 #[serde(default)]
1780 pub period_day: LedgerPeriodTotals,
1781 #[serde(default)]
1782 pub period_week: LedgerPeriodTotals,
1783 #[serde(default)]
1784 pub period_month: LedgerPeriodTotals,
1785 #[serde(default)]
1786 pub period_lifetime: LedgerPeriodTotals,
1787 #[serde(default)]
1788 pub recent: Vec<LedgerEntryView>,
1789 #[serde(default)]
1791 pub wealth_on_person_copper: u64,
1792 #[serde(default)]
1794 pub wealth_in_storage_copper: u64,
1795 #[serde(default)]
1797 pub wealth_in_bank_copper: u64,
1798 #[serde(default)]
1800 pub wealth_total_copper: u64,
1801 #[serde(default)]
1803 pub live_expense_per_interval_copper: u64,
1804 #[serde(default)]
1806 pub live_income_route_est_per_loop_copper: u64,
1807 #[serde(default)]
1809 pub live_income_avg_per_interval_copper: u64,
1810 #[serde(default)]
1812 pub live_income_avg_window_intervals: u32,
1813 #[serde(default)]
1815 pub live_net_avg_per_interval_copper: i64,
1816}
1817
1818#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1820#[serde(rename_all = "snake_case")]
1821pub enum AnalyticsMetric {
1822 NpcKill,
1823 WildlifeKill,
1824 Harvest,
1825 QuestComplete,
1826 QuestAccept,
1827 QuestAbandon,
1828 PlayerDeath,
1829 Craft,
1830 WorkerHire,
1831 WorkerDismiss,
1832 WorkerTeach,
1833 NpcTalk,
1834 ShopBuy,
1835 ShopSell,
1836 PlaceContainer,
1837 PickupContainer,
1838 PickupDrop,
1839 ConsumableUse,
1840 AbilityUse,
1841 DistanceWalkedM,
1842 DoorUse,
1843 BuildingEnter,
1844}
1845
1846impl AnalyticsMetric {
1847 pub fn as_str(self) -> &'static str {
1848 match self {
1849 Self::NpcKill => "npc_kill",
1850 Self::WildlifeKill => "wildlife_kill",
1851 Self::Harvest => "harvest",
1852 Self::QuestComplete => "quest_complete",
1853 Self::QuestAccept => "quest_accept",
1854 Self::QuestAbandon => "quest_abandon",
1855 Self::PlayerDeath => "player_death",
1856 Self::Craft => "craft",
1857 Self::WorkerHire => "worker_hire",
1858 Self::WorkerDismiss => "worker_dismiss",
1859 Self::WorkerTeach => "worker_teach",
1860 Self::NpcTalk => "npc_talk",
1861 Self::ShopBuy => "shop_buy",
1862 Self::ShopSell => "shop_sell",
1863 Self::PlaceContainer => "place_container",
1864 Self::PickupContainer => "pickup_container",
1865 Self::PickupDrop => "pickup_drop",
1866 Self::ConsumableUse => "consumable_use",
1867 Self::AbilityUse => "ability_use",
1868 Self::DistanceWalkedM => "distance_walked_m",
1869 Self::DoorUse => "door_use",
1870 Self::BuildingEnter => "building_enter",
1871 }
1872 }
1873
1874 pub fn from_str_key(s: &str) -> Option<Self> {
1875 Some(match s {
1876 "npc_kill" => Self::NpcKill,
1877 "wildlife_kill" => Self::WildlifeKill,
1878 "harvest" => Self::Harvest,
1879 "quest_complete" => Self::QuestComplete,
1880 "quest_accept" => Self::QuestAccept,
1881 "quest_abandon" => Self::QuestAbandon,
1882 "player_death" => Self::PlayerDeath,
1883 "craft" => Self::Craft,
1884 "worker_hire" => Self::WorkerHire,
1885 "worker_dismiss" => Self::WorkerDismiss,
1886 "worker_teach" => Self::WorkerTeach,
1887 "npc_talk" => Self::NpcTalk,
1888 "shop_buy" => Self::ShopBuy,
1889 "shop_sell" => Self::ShopSell,
1890 "place_container" => Self::PlaceContainer,
1891 "pickup_container" => Self::PickupContainer,
1892 "pickup_drop" => Self::PickupDrop,
1893 "consumable_use" => Self::ConsumableUse,
1894 "ability_use" => Self::AbilityUse,
1895 "distance_walked_m" => Self::DistanceWalkedM,
1896 "door_use" => Self::DoorUse,
1897 "building_enter" => Self::BuildingEnter,
1898 _ => return None,
1899 })
1900 }
1901}
1902
1903#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1904pub struct CareerMetricRow {
1905 pub subject_id: String,
1906 pub amount: u64,
1907}
1908
1909#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1911pub struct PlayerCareerView {
1912 pub current_game_day: u64,
1913 #[serde(default)]
1914 pub kills: Vec<CareerMetricRow>,
1915 #[serde(default)]
1916 pub harvests: Vec<CareerMetricRow>,
1917 pub quests_completed: u64,
1918 #[serde(default)]
1919 pub crafts: Vec<CareerMetricRow>,
1920 pub deaths: u64,
1921 pub npc_talks: u64,
1922 pub shop_buys: u64,
1923 pub shop_sells: u64,
1924 pub distance_m: u64,
1925 #[serde(default)]
1926 pub other: Vec<CareerMetricRow>,
1927}
1928
1929#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1931pub struct HiredWorkerView {
1932 pub instance_id: String,
1933 pub entity_id: EntityId,
1934 pub def_id: String,
1935 pub label: String,
1937 pub x: f32,
1938 pub y: f32,
1939 pub z: f32,
1940 pub mode: WorkerModeView,
1941 pub state: WorkerStateView,
1942 #[serde(default)]
1943 pub step_label: String,
1944 pub vitals: WorkerVitalsSummary,
1945 #[serde(default)]
1946 pub carry_pct: f32,
1947 #[serde(default)]
1948 pub last_error: Option<String>,
1949 pub wage_copper_per_interval: u32,
1950 #[serde(default)]
1952 pub effective_wage_copper: u32,
1953 #[serde(default)]
1955 pub wage_meters_walked: f32,
1956 #[serde(default)]
1958 pub lodging_container_id: Option<String>,
1959 #[serde(default)]
1961 pub route: Option<WorkerRouteView>,
1962 #[serde(default)]
1965 pub route_stop_index: Option<u32>,
1966 #[serde(default)]
1968 pub known_blueprint_ids: Vec<String>,
1969 #[serde(default = "default_worker_view_level")]
1971 pub level: u32,
1972 #[serde(default)]
1974 pub worker_xp: f64,
1975 #[serde(default)]
1977 pub inventory: Vec<ItemStack>,
1978}
1979
1980fn default_worker_view_level() -> u32 {
1981 1
1982}
1983
1984#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1986pub struct TickDelta {
1987 pub tick: Tick,
1988 pub entities: Vec<EntityState>,
1989 #[serde(default)]
1990 pub resource_nodes: Vec<ResourceNodeView>,
1991 #[serde(default)]
1992 pub buildings: Vec<BuildingView>,
1993 #[serde(default)]
1994 pub doors: Vec<DoorView>,
1995 #[serde(default)]
1996 pub npcs: Vec<NpcView>,
1997 #[serde(default)]
1999 pub inventory: Vec<ItemStack>,
2000 #[serde(default)]
2001 pub blueprints: Vec<BlueprintView>,
2002 #[serde(default)]
2003 pub world_clock: WorldClock,
2004 #[serde(default)]
2005 pub ground_drops: Vec<GroundDropView>,
2006 #[serde(default)]
2007 pub placed_containers: Vec<PlacedContainerView>,
2008 #[serde(default)]
2009 pub combat: Option<CombatHud>,
2010 #[serde(default)]
2011 pub interior_map: Option<InteriorMapView>,
2012 #[serde(default)]
2013 pub quest_log: Vec<QuestLogEntry>,
2014 #[serde(default)]
2015 pub hired_workers: Vec<HiredWorkerView>,
2016 #[serde(default)]
2017 pub interactables: Vec<InteractableView>,
2018 #[serde(default)]
2019 pub ledger: Option<PlayerLedgerView>,
2020 #[serde(default)]
2021 pub career: Option<PlayerCareerView>,
2022 #[serde(default)]
2024 pub combat_fx: Vec<CombatFx>,
2025}
2026#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2027pub struct GroundDropView {
2028 pub id: String,
2029 pub template_id: String,
2030 pub quantity: u32,
2031 pub x: f32,
2032 pub y: f32,
2033 pub z: f32,
2034 #[serde(default)]
2036 pub tile_id: Option<String>,
2037 #[serde(default)]
2039 pub paperdoll_ref: Option<String>,
2040 #[serde(default)]
2042 pub yaw: f32,
2043 #[serde(default)]
2045 pub pitch: f32,
2046 #[serde(default)]
2048 pub roll: f32,
2049 #[serde(default = "default_draw_scale")]
2051 pub draw_scale: f32,
2052}
2053
2054fn default_draw_scale() -> f32 {
2055 1.0
2056}
2057
2058#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2060pub struct Snapshot {
2061 pub tick: Tick,
2062 pub chunk_rev: u64,
2063 #[serde(default)]
2065 pub content_rev: u64,
2066 #[serde(default)]
2068 pub publish_rev: u64,
2069 pub entities: Vec<EntityState>,
2070 #[serde(default)]
2071 pub resource_nodes: Vec<ResourceNodeView>,
2072 #[serde(default)]
2074 pub world_width_m: f32,
2075 #[serde(default)]
2076 pub world_height_m: f32,
2077 #[serde(default)]
2078 pub buildings: Vec<BuildingView>,
2079 #[serde(default)]
2080 pub doors: Vec<DoorView>,
2081 #[serde(default)]
2082 pub npcs: Vec<NpcView>,
2083 #[serde(default)]
2084 pub inventory: Vec<ItemStack>,
2085 #[serde(default)]
2086 pub blueprints: Vec<BlueprintView>,
2087 #[serde(default)]
2088 pub world_clock: WorldClock,
2089 #[serde(default)]
2090 pub terrain_zones: Vec<TerrainZoneView>,
2091 #[serde(default)]
2092 pub z_platforms: Vec<ZPlatformView>,
2093 #[serde(default)]
2094 pub z_transitions: Vec<ZTransitionView>,
2095 #[serde(default)]
2096 pub ground_drops: Vec<GroundDropView>,
2097 #[serde(default)]
2098 pub placed_containers: Vec<PlacedContainerView>,
2099 #[serde(default)]
2100 pub combat: Option<CombatHud>,
2101 #[serde(default)]
2102 pub interior_map: Option<InteriorMapView>,
2103 #[serde(default)]
2104 pub quest_log: Vec<QuestLogEntry>,
2105 #[serde(default)]
2106 pub hired_workers: Vec<HiredWorkerView>,
2107 #[serde(default)]
2108 pub interactables: Vec<InteractableView>,
2109 #[serde(default)]
2110 pub ledger: Option<PlayerLedgerView>,
2111 #[serde(default)]
2112 pub career: Option<PlayerCareerView>,
2113 #[serde(default)]
2115 pub combat_fx: Vec<CombatFx>,
2116}
2117
2118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2120pub struct ResourceNodeView {
2121 pub id: String,
2122 pub label: String,
2123 pub x: f32,
2124 pub y: f32,
2125 pub z: f32,
2126 pub item_template: String,
2127 #[serde(default = "default_node_state")]
2128 pub state: ResourceNodeState,
2129 #[serde(default = "default_blocking_view")]
2131 pub blocking: bool,
2132 #[serde(default = "default_blocking_radius_view")]
2134 pub blocking_radius_m: f32,
2135 #[serde(default)]
2137 pub tile_id: Option<String>,
2138 #[serde(default)]
2140 pub paperdoll_ref: Option<String>,
2141 #[serde(default)]
2143 pub yaw: f32,
2144 #[serde(default)]
2146 pub pitch: f32,
2147 #[serde(default)]
2149 pub roll: f32,
2150 #[serde(default = "default_draw_scale")]
2152 pub draw_scale: f32,
2153 #[serde(default)]
2155 pub sprite_mode: Option<String>,
2156 #[serde(default)]
2158 pub presentation_state: Option<String>,
2159}
2160
2161fn default_blocking_radius_view() -> f32 {
2162 0.8
2163}
2164
2165fn default_blocking_view() -> bool {
2166 true
2167}
2168
2169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2170#[serde(rename_all = "snake_case")]
2171pub enum ResourceNodeState {
2172 Available,
2173 Harvesting,
2174 Cooldown,
2175}
2176
2177fn default_node_state() -> ResourceNodeState {
2178 ResourceNodeState::Available
2179}
2180
2181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2182#[serde(rename_all = "snake_case")]
2183pub enum ItemStatusBindingMode {
2184 OnHit,
2185 WhileEquipped,
2186}
2187
2188impl Default for ItemStatusBindingMode {
2189 fn default() -> Self {
2190 Self::OnHit
2191 }
2192}
2193
2194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2196pub struct ItemStatusBinding {
2197 pub effect_id: String,
2198 #[serde(default)]
2199 pub mode: ItemStatusBindingMode,
2200 #[serde(default)]
2202 pub source: String,
2203 #[serde(default)]
2204 pub applied_at_tick: u64,
2205 #[serde(default, skip_serializing_if = "Option::is_none")]
2207 pub expires_at_tick: Option<u64>,
2208}
2209
2210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2211pub struct ItemStack {
2212 pub template_id: String,
2213 pub quantity: u32,
2214 #[serde(default)]
2216 pub item_instance_id: Option<Uuid>,
2217 #[serde(default)]
2219 pub props: BTreeMap<String, String>,
2220 #[serde(default)]
2222 pub status_bindings: Vec<ItemStatusBinding>,
2223 #[serde(default)]
2225 pub contents: Vec<ItemStack>,
2226 #[serde(default)]
2228 pub display_name: Option<String>,
2229 #[serde(default)]
2231 pub category: Option<String>,
2232 #[serde(default)]
2234 pub base_mass: Option<f32>,
2235 #[serde(default)]
2237 pub base_volume: Option<f32>,
2238 #[serde(default)]
2240 pub capacity_volume: Option<f32>,
2241 #[serde(default)]
2243 pub stackable: Option<bool>,
2244 #[serde(default)]
2246 pub world_placeable: Option<bool>,
2247 #[serde(default)]
2249 pub worker_lodging_capacity: Option<u32>,
2250 #[serde(default)]
2252 pub equip_slot: Option<BodySlot>,
2253 #[serde(default)]
2255 pub armor_physical: Option<f32>,
2256 #[serde(default)]
2258 pub resists: Vec<(String, f32)>,
2259 #[serde(default)]
2261 pub hand_slots: Option<u8>,
2262}
2263
2264impl ItemStack {
2265 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
2266 Self {
2267 template_id: template_id.into(),
2268 quantity,
2269 ..Default::default()
2270 }
2271 }
2272}
2273
2274impl Default for ItemStack {
2275 fn default() -> Self {
2276 Self {
2277 template_id: String::new(),
2278 quantity: 0,
2279 item_instance_id: None,
2280 props: BTreeMap::new(),
2281 status_bindings: Vec::new(),
2282 contents: Vec::new(),
2283 display_name: None,
2284 category: None,
2285 base_mass: None,
2286 base_volume: None,
2287 capacity_volume: None,
2288 stackable: None,
2289 world_placeable: None,
2290 worker_lodging_capacity: None,
2291 equip_slot: None,
2292 armor_physical: None,
2293 resists: Vec::new(),
2294 hand_slots: None,
2295 }
2296 }
2297}
2298
2299#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2301#[serde(rename_all = "snake_case")]
2302pub enum EncumbranceState {
2303 #[default]
2304 Light,
2305 Heavy,
2306 Over,
2307}
2308
2309#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
2313#[serde(rename_all = "snake_case")]
2314pub enum BodySlot {
2315 Head,
2316 #[serde(alias = "body")]
2318 Chest,
2319 #[serde(alias = "arms")]
2321 Forearms,
2322 Legs,
2323 Feet,
2324 Cloak,
2325 Back,
2326 Waist,
2327 Earrings,
2328 Necklace,
2329 Eyeglasses,
2330 #[serde(rename = "ring_left_1", alias = "ring_left1")]
2332 RingLeft1,
2333 #[serde(rename = "ring_left_2", alias = "ring_left2")]
2334 RingLeft2,
2335 #[serde(rename = "ring_right_1", alias = "ring_right1")]
2336 RingRight1,
2337 #[serde(rename = "ring_right_2", alias = "ring_right2")]
2338 RingRight2,
2339}
2340
2341impl BodySlot {
2342 pub const ALL: [BodySlot; 15] = [
2344 BodySlot::Head,
2345 BodySlot::Chest,
2346 BodySlot::Forearms,
2347 BodySlot::Legs,
2348 BodySlot::Feet,
2349 BodySlot::Cloak,
2350 BodySlot::Back,
2351 BodySlot::Waist,
2352 BodySlot::Earrings,
2353 BodySlot::Necklace,
2354 BodySlot::Eyeglasses,
2355 BodySlot::RingLeft1,
2356 BodySlot::RingLeft2,
2357 BodySlot::RingRight1,
2358 BodySlot::RingRight2,
2359 ];
2360
2361 pub fn as_str(self) -> &'static str {
2362 match self {
2363 BodySlot::Head => "head",
2364 BodySlot::Chest => "chest",
2365 BodySlot::Forearms => "forearms",
2366 BodySlot::Legs => "legs",
2367 BodySlot::Feet => "feet",
2368 BodySlot::Cloak => "cloak",
2369 BodySlot::Back => "back",
2370 BodySlot::Waist => "waist",
2371 BodySlot::Earrings => "earrings",
2372 BodySlot::Necklace => "necklace",
2373 BodySlot::Eyeglasses => "eyeglasses",
2374 BodySlot::RingLeft1 => "ring_left_1",
2375 BodySlot::RingLeft2 => "ring_left_2",
2376 BodySlot::RingRight1 => "ring_right_1",
2377 BodySlot::RingRight2 => "ring_right_2",
2378 }
2379 }
2380}
2381
2382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2384#[serde(rename_all = "snake_case")]
2385pub enum InventoryLocation {
2386 Root,
2388 Worn { slot: BodySlot },
2390 Placed { container_id: String },
2392 Keychain,
2394 WhisperPouch,
2396}
2397
2398#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2400pub struct PlacedContainerView {
2401 pub id: String,
2402 pub template_id: String,
2403 pub display_name: String,
2404 pub x: f32,
2405 pub y: f32,
2406 pub z: f32,
2407 pub locked: bool,
2408 #[serde(default)]
2410 pub accessible: bool,
2411 #[serde(default)]
2412 pub owner_character_id: Option<Uuid>,
2413 #[serde(default)]
2415 pub contents: Vec<ItemStack>,
2416 #[serde(default)]
2418 pub lock_id: Option<String>,
2419 #[serde(default)]
2421 pub capacity_volume: Option<f32>,
2422 #[serde(default)]
2424 pub item_instance_id: Option<Uuid>,
2425 #[serde(default)]
2427 pub tile_id: Option<String>,
2428 #[serde(default)]
2430 pub paperdoll_ref: Option<String>,
2431 #[serde(default)]
2433 pub worker_lodging_capacity: Option<u32>,
2434}
2435
2436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2437pub struct BlueprintIngredientView {
2438 pub template_id: String,
2439 pub quantity: u32,
2440 pub consumed: bool,
2442}
2443
2444#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2445pub struct ToolRequirementView {
2446 pub item: String,
2447 pub consumed: bool,
2449}
2450
2451#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2452pub struct SkillRequirementView {
2453 pub skill: String,
2454 pub level: u32,
2455}
2456
2457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2458pub struct BlueprintView {
2459 pub id: String,
2460 pub label: String,
2461 pub output: String,
2462 pub output_qty: u32,
2463 pub craft_ticks: u32,
2464 pub inputs: Vec<BlueprintIngredientView>,
2465 #[serde(default)]
2467 pub station: Option<String>,
2468 #[serde(default)]
2469 pub category: Option<String>,
2470 #[serde(default)]
2471 pub required_tools: Vec<ToolRequirementView>,
2472 #[serde(default)]
2473 pub skill: Option<SkillRequirementView>,
2474 #[serde(default)]
2475 pub failure_chance: f32,
2476 #[serde(default)]
2478 pub worker_train_copper: u64,
2479}
2480
2481#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2483#[serde(rename_all = "snake_case")]
2484pub enum TerrainKindView {
2485 #[default]
2486 Grass,
2487 Dirt,
2488 Tilled,
2489 Desert,
2490 Hill,
2491 Bog,
2492 ShallowWater,
2493 DeepWater,
2494 Trail,
2495 Road,
2496 Rock,
2497}
2498
2499#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2500pub struct TerrainZoneView {
2501 pub id: String,
2502 pub x0: f32,
2503 pub y0: f32,
2504 pub x1: f32,
2505 pub y1: f32,
2506 #[serde(default)]
2507 pub kind: TerrainKindView,
2508 #[serde(default)]
2510 pub elevation: f32,
2511 #[serde(default)]
2514 pub glyph: Option<String>,
2515 #[serde(default)]
2517 pub color: Option<String>,
2518 #[serde(default)]
2520 pub tile_id: Option<String>,
2521 #[serde(default)]
2523 pub z_order: i32,
2524}
2525
2526#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2528pub struct ZPlatformView {
2529 pub id: String,
2530 pub z: f32,
2531 pub x0: f32,
2532 pub y0: f32,
2533 pub x1: f32,
2534 pub y1: f32,
2535}
2536
2537#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2539pub struct ZTransitionView {
2540 pub id: String,
2541 pub z_from: f32,
2542 pub z_to: f32,
2543 pub x0: f32,
2544 pub y0: f32,
2545 pub x1: f32,
2546 pub y1: f32,
2547}
2548
2549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2550pub struct BuildingView {
2551 pub id: String,
2552 pub label: String,
2553 pub x: f32,
2554 pub y: f32,
2555 pub width_m: f32,
2556 pub depth_m: f32,
2557 #[serde(default)]
2558 pub interior_blueprint: Option<String>,
2559 #[serde(default)]
2560 pub tags: Vec<String>,
2561}
2562
2563#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2564pub struct DoorView {
2565 pub id: String,
2566 pub building_id: String,
2567 pub x: f32,
2568 pub y: f32,
2569 #[serde(default)]
2570 pub open: bool,
2571 #[serde(default)]
2572 pub portal: Option<String>,
2573}
2574
2575#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2576pub struct InteriorRoomView {
2577 pub id: String,
2578 pub label: String,
2579 pub floor: i32,
2580 pub x0: f32,
2581 pub y0: f32,
2582 pub x1: f32,
2583 pub y1: f32,
2584 #[serde(default)]
2585 pub floor_color: Option<String>,
2586 #[serde(default)]
2587 pub floor_glyph: Option<String>,
2588}
2589
2590#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2591pub struct InteriorDoorView {
2592 pub id: String,
2593 pub room_a: String,
2594 pub room_b: String,
2595 pub x: f32,
2596 pub y: f32,
2597 pub kind: String,
2598}
2599
2600#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2601pub struct InteriorMapView {
2602 pub building_id: String,
2603 pub blueprint_id: String,
2604 pub background_color: String,
2605 #[serde(default)]
2606 pub default_floor_color: Option<String>,
2607 #[serde(default = "default_floor_height_view")]
2608 pub floor_height_m: f32,
2609 pub rooms: Vec<InteriorRoomView>,
2610 #[serde(default)]
2611 pub room_doors: Vec<InteriorDoorView>,
2612}
2613
2614fn default_floor_height_view() -> f32 {
2615 3.0
2616}
2617
2618#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2619pub struct NpcView {
2620 pub id: String,
2621 pub label: String,
2622 pub role: String,
2623 pub x: f32,
2624 pub y: f32,
2625 #[serde(default)]
2627 pub building_id: Option<String>,
2628 #[serde(default)]
2630 pub entity_id: Option<EntityId>,
2631 #[serde(default)]
2632 pub life_state: Option<LifeState>,
2633 #[serde(default)]
2634 pub hp_pct: Option<f32>,
2635 #[serde(default)]
2637 pub can_trade: bool,
2638 #[serde(default)]
2640 pub tile_id: Option<String>,
2641 #[serde(default)]
2643 pub behavior_state: Option<String>,
2644 #[serde(default)]
2646 pub presentation_state: Option<String>,
2647 #[serde(default)]
2649 pub sprite_mode: Option<String>,
2650 #[serde(default)]
2652 pub paperdoll_ref: Option<String>,
2653}
2654
2655#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2656pub struct UseResult {
2657 pub template_id: String,
2658 pub hunger_restored: f32,
2659 pub thirst_restored: f32,
2660 #[serde(default)]
2661 pub health_restored: f32,
2662 #[serde(default)]
2663 pub mana_restored: f32,
2664 #[serde(default)]
2665 pub cleared_dot_ids: Vec<String>,
2666}
2667
2668#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2669pub struct CraftResult {
2670 pub blueprint_id: String,
2671 pub outputs: Vec<ItemStack>,
2672 pub consumed: Vec<ItemStack>,
2673 #[serde(default = "default_one")]
2675 pub batch_index: u32,
2676 #[serde(default = "default_one")]
2678 pub batch_total: u32,
2679}
2680
2681fn default_one() -> u32 {
2682 1
2683}
2684
2685#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2686pub struct DeathNotice {
2687 pub entity_id: EntityId,
2688 pub respawn_x: f32,
2689 pub respawn_y: f32,
2690 pub message: String,
2691}
2692
2693#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2694pub struct InteractionNotice {
2695 pub target_id: String,
2696 pub message: String,
2697 #[serde(default)]
2698 pub coins_delta: i32,
2699 #[serde(default)]
2700 pub inventory_delta: Vec<ItemStack>,
2701}
2702
2703#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2704#[serde(rename_all = "snake_case")]
2705pub enum NpcTalkTrustFlag {
2706 Stranger,
2707 Acquainted,
2708 Trusted,
2709}
2710
2711#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
2712#[serde(rename_all = "snake_case")]
2713pub enum NpcTalkDepth {
2714 #[default]
2715 Full,
2716 Brief,
2717 Unavailable,
2718}
2719
2720#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2721pub struct NpcTalkOpened {
2722 pub npc_id: String,
2723 pub npc_label: String,
2724 pub greeting: String,
2725 pub trust_flag: NpcTalkTrustFlag,
2726 #[serde(default)]
2727 pub talk_depth: NpcTalkDepth,
2728 #[serde(default = "default_true")]
2729 pub trade_allowed: bool,
2730}
2731
2732fn default_true() -> bool {
2733 true
2734}
2735
2736#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2737pub struct NpcTalkPending {
2738 pub npc_id: String,
2739}
2740
2741#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2742pub struct NpcTalkReply {
2743 pub npc_id: String,
2744 pub line: String,
2745 pub trust_flag: NpcTalkTrustFlag,
2746 #[serde(default)]
2747 pub wind_down: bool,
2748 #[serde(default)]
2749 pub trade_disabled: bool,
2750}
2751
2752#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2753pub struct NpcTalkClosed {
2754 pub npc_id: String,
2755}
2756
2757#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2758pub struct NpcTalkError {
2759 pub npc_id: String,
2760 pub reason: String,
2761}
2762
2763#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2764#[serde(rename_all = "snake_case")]
2765pub enum QuestStatusView {
2766 Available,
2767 Active,
2768 Completed,
2769}
2770
2771#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2772pub struct QuestObjectiveProgress {
2773 pub label: String,
2774 pub current: u32,
2775 pub required: u32,
2776 pub done: bool,
2777}
2778
2779#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2780pub struct QuestLogEntry {
2781 pub quest_id: String,
2782 pub title: String,
2783 pub description: String,
2784 pub status: QuestStatusView,
2785 #[serde(default)]
2786 pub current_step_id: Option<String>,
2787 #[serde(default)]
2788 pub current_step_title: String,
2789 #[serde(default)]
2790 pub objectives: Vec<QuestObjectiveProgress>,
2791 #[serde(default)]
2792 pub is_tracked: bool,
2793 #[serde(default)]
2794 pub can_withdraw: bool,
2795}
2796
2797#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2798pub struct InteractableView {
2799 pub id: String,
2800 pub kind: String,
2801 pub label: String,
2802 pub x: f32,
2803 pub y: f32,
2804 pub z: f32,
2805 #[serde(default)]
2806 pub board_id: Option<String>,
2807}
2808
2809#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2810pub struct QuestOffer {
2811 pub quest_id: String,
2812 pub title: String,
2813 pub description: String,
2814 #[serde(default)]
2815 pub step_count: u32,
2816}
2817
2818#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2819pub struct QuestNotice {
2820 pub quest_id: String,
2821 pub title: String,
2822 pub message: String,
2823}
2824
2825#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2826#[serde(rename_all = "snake_case")]
2827pub enum ShopOfferKind {
2828 Item,
2829 Blueprint,
2830}
2831
2832#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2833pub struct ShopOffer {
2834 pub offer_id: String,
2835 pub kind: ShopOfferKind,
2836 pub label: String,
2837 #[serde(default)]
2838 pub template_id: Option<String>,
2839 #[serde(default)]
2840 pub blueprint_id: Option<String>,
2841 pub price_copper: u32,
2842 #[serde(default)]
2843 pub affordable: bool,
2844 #[serde(default)]
2845 pub already_owned: bool,
2846}
2847
2848#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2849pub struct ShopBuyLine {
2850 pub template_id: String,
2851 pub label: String,
2852 pub quantity: u32,
2853 pub price_copper: u32,
2854}
2855
2856#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2858pub struct BankPanel {
2859 pub npc_id: String,
2860 pub npc_label: String,
2861 pub bank_balance_copper: u64,
2862 pub on_person_copper: u64,
2863 #[serde(default)]
2865 pub pending_outgoing_copper: u64,
2866 #[serde(default)]
2867 pub transfer_fee_bps: u32,
2868 #[serde(default)]
2869 pub transfer_clear_ticks: u64,
2870}
2871
2872#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2874pub struct StoragePanel {
2875 pub npc_id: String,
2876 pub npc_label: String,
2877 pub building_id: String,
2878 pub building_label: String,
2879 pub used_volume: f32,
2880 pub max_volume: f32,
2881 #[serde(default)]
2882 pub contents: Vec<ItemStack>,
2883 #[serde(default)]
2885 pub ship_destinations: Vec<StorageShipDest>,
2886}
2887
2888#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2889pub struct StorageShipDest {
2890 pub building_id: String,
2891 pub label: String,
2892 pub distance_m: f32,
2893 pub fee_copper: u64,
2894 pub travel_ticks: u64,
2895}
2896
2897#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2898pub struct ShopCatalog {
2899 pub npc_id: String,
2900 pub npc_label: String,
2901 #[serde(default)]
2902 pub sells: Vec<ShopOffer>,
2903 #[serde(default)]
2904 pub buys: Vec<ShopBuyLine>,
2905}
2906
2907#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2908pub struct HarvestResult {
2909 pub node_id: String,
2910 pub quantity: u32,
2912 pub item_template: String,
2913 #[serde(default)]
2916 pub item_instance_id: Option<Uuid>,
2917}
2918
2919#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2921pub struct Envelope<T> {
2922 pub protocol_version: u16,
2923 pub payload: T,
2924}
2925
2926impl<T> Envelope<T> {
2927 pub fn new(payload: T) -> Self {
2928 Self {
2929 protocol_version: crate::PROTOCOL_VERSION,
2930 payload,
2931 }
2932 }
2933}
2934
2935#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2937pub struct Hello {
2938 pub client_name: String,
2939 pub protocol_version: u16,
2940 #[serde(default)]
2941 pub auth: AuthCredential,
2942 #[serde(default)]
2944 pub character_id: Option<Uuid>,
2945}
2946
2947#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2950#[serde(rename_all = "snake_case")]
2951pub enum AuthCredential {
2952 DevLocal,
2953 Session { token: String },
2954 ApiToken { token: String, character_id: Uuid },
2955}
2956
2957impl Default for AuthCredential {
2958 fn default() -> Self {
2959 Self::DevLocal
2960 }
2961}
2962
2963#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2964pub struct Welcome {
2965 pub session_id: SessionId,
2966 pub entity_id: EntityId,
2967 pub snapshot: Snapshot,
2968}
2969
2970#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2971pub enum ServerMessage {
2972 Welcome(Welcome),
2973 ContentUpdated(Snapshot),
2975 Tick(TickDelta),
2976 IntentAck {
2977 entity_id: EntityId,
2978 seq: Seq,
2979 tick: Tick,
2980 },
2981 Chat(ChatMessage),
2982 HarvestResult(HarvestResult),
2983 UseResult(UseResult),
2984 CraftResult(CraftResult),
2985 Death(DeathNotice),
2986 Interaction(InteractionNotice),
2987 ShopOpened(ShopCatalog),
2988 NpcTalkOpened(NpcTalkOpened),
2989 NpcTalkPending(NpcTalkPending),
2990 NpcTalkReply(NpcTalkReply),
2991 NpcTalkClosed(NpcTalkClosed),
2992 NpcTalkError(NpcTalkError),
2993 QuestOffer(QuestOffer),
2994 QuestAccepted(QuestNotice),
2995 QuestWithdrawn(QuestNotice),
2996 QuestStepCompleted(QuestNotice),
2997 QuestCompleted(QuestNotice),
2998 BankOpened(BankPanel),
3000 StorageOpened(StoragePanel),
3002 TradeOpened(TradePanel),
3004 TradeClosed {
3006 reason: String,
3007 },
3008}
3009
3010#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3012pub struct TradePanel {
3013 pub peer_entity_id: EntityId,
3014 pub peer_name: String,
3015 pub my_presented: Vec<ItemStack>,
3016 pub their_presented: Vec<ItemStack>,
3017 pub i_ready: bool,
3018 pub they_ready: bool,
3019 pub my_mass_after: f32,
3021 pub my_mass_max: f32,
3022 pub my_encumbrance_after: EncumbranceState,
3023 pub overburden_warning: bool,
3025}
3026
3027#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3028pub enum ClientMessage {
3029 Hello(Hello),
3030 Intent(Intent),
3031 Disconnect,
3032}
3033
3034#[cfg(test)]
3035mod tests {
3036 use super::*;
3037
3038 #[test]
3039 fn pristine_vitals_state_yields_full_pools() {
3040 let attrs = PrimaryAttributes::default();
3041 let vitals = StoredVitalsState::default().apply_to(attrs);
3042 assert!(vitals.health > 0.0);
3043 assert_eq!(vitals.health, vitals.health_max);
3044 assert!((vitals.mana_max - 61.0).abs() < 0.01);
3045 }
3046
3047 #[test]
3048 fn humanize_snake_id_title_cases_parts() {
3049 assert_eq!(humanize_snake_id("heal_touch"), "Heal Touch");
3050 assert_eq!(humanize_snake_id("fireball"), "Fireball");
3051 assert_eq!(humanize_snake_id("cone_frost"), "Cone Frost");
3052 }
3053
3054 #[test]
3055 fn saved_vitals_scale_when_pool_max_increases() {
3056 let mut attrs = PrimaryAttributes::default();
3057 attrs.intelligence = 140;
3058 attrs.wisdom = 140;
3059 let saved = StoredVitalsState {
3060 health: 100.0,
3061 mana: 14.0,
3062 stamina: 100.0,
3063 ..StoredVitalsState::default()
3064 };
3065 let vitals = saved.apply_to(attrs);
3066 assert!(vitals.mana_max > 55.0);
3067 assert!(
3068 (vitals.mana - vitals.mana_max).abs() < 0.01,
3069 "full legacy mana bar migrates to full new bar"
3070 );
3071 }
3072
3073 #[test]
3074 fn empty_vitals_state_is_pristine() {
3075 let pristine = StoredVitalsState {
3076 health: 0.0,
3077 mana: 0.0,
3078 stamina: 0.0,
3079 hunger: 0.0,
3080 thirst: 0.0,
3081 coins: 0,
3082 deaths: 0,
3083 life_state: LifeState::Alive,
3084 };
3085 assert!(pristine.is_pristine());
3086 let vitals = pristine.apply_to(PrimaryAttributes::default());
3087 assert!(vitals.health > 0.0);
3088 }
3089
3090 #[test]
3091 fn stored_vitals_roundtrip_preserves_partial_pools() {
3092 let attrs = PrimaryAttributes::default();
3093 let mut live = PlayerVitals::from_attributes(attrs);
3094 live.health = 25.0;
3095 live.hunger = 77.0;
3096 live.deaths = 2;
3097 let stored = StoredVitalsState::from_live(&live);
3098 let restored = stored.apply_to(attrs);
3099 assert!(
3100 (restored.health - 25.0).abs() < 0.01,
3101 "partial HP below cap stays absolute"
3102 );
3103 assert_eq!(restored.hunger, 77.0);
3104 assert_eq!(restored.deaths, 2);
3105 }
3106
3107 #[test]
3108 fn skill_tiers_start_at_zero() {
3109 let skill = SkillProgress::default();
3110 assert_eq!(skill.level, 0);
3111 assert_eq!(skill.display_tier(), 0);
3112 let trained = SkillProgress {
3113 level: 250,
3114 last_trained_tick: 1,
3115 };
3116 assert_eq!(trained.display_tier(), 2);
3117 }
3118
3119 #[test]
3120 fn quest_server_messages_roundtrip_json() {
3121 use crate::codec::{Codec, PostcardCodec};
3122
3123 let offer = ServerMessage::QuestOffer(QuestOffer {
3124 quest_id: "ada_goblin_hunt".into(),
3125 title: "Goblin Trouble".into(),
3126 description: "Help Ada".into(),
3127 step_count: 3,
3128 });
3129 let notice = ServerMessage::QuestAccepted(QuestNotice {
3130 quest_id: "ada_goblin_hunt".into(),
3131 title: "Goblin Trouble".into(),
3132 message: "Quest accepted".into(),
3133 });
3134 for msg in [offer, notice] {
3135 let bytes = PostcardCodec.encode(&msg).unwrap();
3136 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
3137 assert_eq!(decoded, msg);
3138 }
3139 }
3140
3141 #[test]
3142 fn hotbar_consumable_binding_roundtrips() {
3143 let binding = hotbar_consumable_binding("bottle_of_water");
3144 assert_eq!(binding, "item:bottle_of_water");
3145 assert!(hotbar_binding_is_consumable(&binding));
3146 assert_eq!(
3147 hotbar_consumable_template(&binding),
3148 Some("bottle_of_water")
3149 );
3150 assert!(!hotbar_binding_is_consumable("fireball"));
3151 assert_eq!(hotbar_consumable_template("fireball"), None);
3152 }
3153}