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