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 RotationPreset {
464 pub id: String,
465 pub label: String,
466 #[serde(default)]
467 pub abilities: Vec<String>,
468}
469
470impl RotationPreset {
471 pub fn melee_default(ability_id: impl Into<String>) -> Self {
472 let id = ability_id.into();
473 Self {
474 id: "melee".into(),
475 label: "Melee".into(),
476 abilities: vec![id],
477 }
478 }
479}
480
481#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
483pub struct StoredTargetSlot {
484 pub instance_id: Option<String>,
485 #[serde(default)]
486 pub preset_id: Option<String>,
487 #[serde(default)]
488 pub rotation_index: u32,
489 #[serde(default)]
490 pub auto_enabled: bool,
491}
492
493#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
494#[serde(default)]
495pub struct StoredCombatProfile {
496 pub combat_target_instance_id: Option<String>,
498 pub in_combat: bool,
499 pub last_combat_tick: u64,
500 pub last_attack_tick: u64,
501 pub cooldowns_until_tick: BTreeMap<String, u64>,
502 #[serde(default = "default_auto_attack")]
503 pub auto_attack_enabled: bool,
504 #[serde(default)]
506 pub mainhand_template_id: Option<String>,
507 #[serde(default)]
509 pub mainhand_instance_id: Option<Uuid>,
510 #[serde(default)]
512 pub offhand_template_id: Option<String>,
513 #[serde(default)]
515 pub offhand_instance_id: Option<Uuid>,
516 #[serde(default)]
519 pub worn: Vec<(BodySlot, ItemStack)>,
520 #[serde(default)]
522 pub rotation_presets: Vec<RotationPreset>,
523 #[serde(default)]
525 pub target_slots: Vec<StoredTargetSlot>,
526 #[serde(default)]
528 pub known_blueprint_ids: Vec<String>,
529 #[serde(default)]
531 pub keychain: Vec<ItemStack>,
532}
533
534fn default_auto_attack() -> bool {
535 true
536}
537
538impl Default for StoredCombatProfile {
539 fn default() -> Self {
540 Self {
541 combat_target_instance_id: None,
542 in_combat: false,
543 last_combat_tick: 0,
544 last_attack_tick: 0,
545 cooldowns_until_tick: BTreeMap::new(),
546 auto_attack_enabled: true,
547 mainhand_template_id: None,
548 mainhand_instance_id: None,
549 offhand_template_id: None,
550 offhand_instance_id: None,
551 worn: Vec::new(),
552 rotation_presets: Vec::new(),
553 target_slots: Vec::new(),
554 known_blueprint_ids: Vec::new(),
555 keychain: Vec::new(),
556 }
557 }
558}
559
560#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
561pub struct EntityState {
562 pub id: EntityId,
563 pub transform: Transform,
564 #[serde(default)]
566 pub label: String,
567 #[serde(default)]
568 pub vitals: Option<PlayerVitals>,
569 #[serde(default)]
571 pub attributes: Option<PrimaryAttributes>,
572 #[serde(default)]
573 pub skills: Option<PlayerSkills>,
574 #[serde(default)]
576 pub inside_building: Option<String>,
577 #[serde(default)]
579 pub tile_id: Option<String>,
580 #[serde(default)]
582 pub presentation_state: Option<String>,
583 #[serde(default)]
585 pub sprite_mode: Option<String>,
586 #[serde(default)]
588 pub progression_xp: Option<ProgressionXp>,
589}
590
591#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
593#[serde(rename_all = "snake_case")]
594pub enum ChatChannel {
595 Nearby,
596 WhisperStone,
597}
598
599#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
600pub struct ChatMessage {
601 pub channel: ChatChannel,
602 pub from_entity: EntityId,
603 pub from_name: String,
604 pub text: String,
605 pub tick: Tick,
606}
607
608#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
610pub enum Intent {
611 Move {
612 entity_id: EntityId,
613 forward: f32,
614 strafe: f32,
615 #[serde(default)]
617 vertical: f32,
618 #[serde(default)]
620 sprint: bool,
621 seq: Seq,
622 },
623 Stop {
624 entity_id: EntityId,
625 seq: Seq,
626 },
627 Harvest {
628 entity_id: EntityId,
629 node_id: String,
630 seq: Seq,
631 },
632 Use {
633 entity_id: EntityId,
634 template_id: String,
635 seq: Seq,
636 },
637 UseGrant {
639 entity_id: EntityId,
640 grant_instance_id: Uuid,
641 target_instance_id: Uuid,
642 seq: Seq,
643 },
644 Say {
645 entity_id: EntityId,
646 channel: ChatChannel,
647 text: String,
648 seq: Seq,
649 },
650 Craft {
652 entity_id: EntityId,
653 blueprint_id: String,
654 #[serde(default)]
656 count: Option<u32>,
657 seq: Seq,
658 },
659 Interact {
661 entity_id: EntityId,
662 target_id: String,
663 seq: Seq,
664 },
665 ShopBuy {
667 entity_id: EntityId,
668 npc_id: String,
669 offer_id: String,
670 #[serde(default = "default_one")]
671 quantity: u32,
672 seq: Seq,
673 },
674 ShopSell {
676 entity_id: EntityId,
677 npc_id: String,
678 template_id: String,
679 #[serde(default = "default_one")]
680 quantity: u32,
681 seq: Seq,
682 },
683 ShopClose {
685 entity_id: EntityId,
686 npc_id: String,
687 seq: Seq,
688 },
689 TestDamage {
691 entity_id: EntityId,
692 amount: f32,
693 seq: Seq,
694 },
695 SetTarget {
697 entity_id: EntityId,
698 target_id: EntityId,
699 seq: Seq,
700 },
701 SetTargetSlot {
703 entity_id: EntityId,
704 slot_index: u8,
705 target_id: EntityId,
706 seq: Seq,
707 },
708 ClearTarget {
709 entity_id: EntityId,
710 seq: Seq,
711 },
712 ClearTargetSlot {
713 entity_id: EntityId,
714 slot_index: u8,
715 seq: Seq,
716 },
717 SetAutoAttack {
719 entity_id: EntityId,
720 slot_index: u8,
721 enabled: bool,
722 seq: Seq,
723 },
724 Attack {
726 entity_id: EntityId,
727 #[serde(default)]
728 target_id: Option<EntityId>,
729 #[serde(default)]
730 weapon_slot: Option<u32>,
731 seq: Seq,
732 },
733 Pickup {
735 entity_id: EntityId,
736 #[serde(default)]
737 drop_id: Option<String>,
738 seq: Seq,
739 },
740 Cast {
742 entity_id: EntityId,
743 ability_id: String,
744 target_id: EntityId,
745 seq: Seq,
746 },
747 BindActionSlot {
749 entity_id: EntityId,
750 slot_index: u8,
751 ability_id: String,
752 #[serde(default = "default_auto_attack")]
753 auto_enabled: bool,
754 seq: Seq,
755 },
756 UseActionSlot {
758 entity_id: EntityId,
759 slot_index: u8,
760 seq: Seq,
761 },
762 Dodge {
764 entity_id: EntityId,
765 seq: Seq,
766 },
767 Lunge {
769 entity_id: EntityId,
770 #[serde(default)]
772 forward: f32,
773 #[serde(default)]
775 strafe: f32,
776 seq: Seq,
777 },
778 DirectionalJump {
780 entity_id: EntityId,
781 #[serde(default)]
783 forward: f32,
784 #[serde(default)]
786 strafe: f32,
787 seq: Seq,
788 },
789 Block {
791 entity_id: EntityId,
792 #[serde(default = "default_block_enabled")]
793 enabled: bool,
794 seq: Seq,
795 },
796 EquipMainhand {
800 entity_id: EntityId,
801 #[serde(default)]
802 template_id: Option<String>,
803 #[serde(default)]
804 instance_id: Option<Uuid>,
805 seq: Seq,
806 },
807 EquipOffhand {
809 entity_id: EntityId,
810 #[serde(default)]
811 template_id: Option<String>,
812 #[serde(default)]
813 instance_id: Option<Uuid>,
814 seq: Seq,
815 },
816 EquipWorn {
819 entity_id: EntityId,
820 slot: BodySlot,
821 #[serde(default)]
822 instance_id: Option<Uuid>,
823 seq: Seq,
824 },
825 MoveItem {
827 entity_id: EntityId,
828 item_instance_id: Uuid,
829 from: InventoryLocation,
830 to: InventoryLocation,
831 #[serde(default)]
833 to_parent_instance_id: Option<Uuid>,
834 #[serde(default)]
836 quantity: Option<u32>,
837 seq: Seq,
838 },
839 PlaceContainer {
841 entity_id: EntityId,
842 item_instance_id: Uuid,
843 seq: Seq,
844 },
845 PickupContainer {
847 entity_id: EntityId,
848 container_id: String,
849 seq: Seq,
850 },
851 SetContainerLocked {
853 entity_id: EntityId,
854 location: InventoryLocation,
856 locked: bool,
857 seq: Seq,
858 },
859 DropItem {
861 entity_id: EntityId,
862 item_instance_id: Uuid,
863 from: InventoryLocation,
864 seq: Seq,
865 },
866 DestroyItem {
868 entity_id: EntityId,
869 item_instance_id: Uuid,
870 from: InventoryLocation,
871 #[serde(default)]
873 quantity: Option<u32>,
874 seq: Seq,
875 },
876 RenameContainer {
878 entity_id: EntityId,
879 item_instance_id: Uuid,
880 location: InventoryLocation,
881 name: String,
882 seq: Seq,
883 },
884 UpsertRotationPreset {
886 entity_id: EntityId,
887 preset: RotationPreset,
888 seq: Seq,
889 },
890 DeleteRotationPreset {
892 entity_id: EntityId,
893 preset_id: String,
894 seq: Seq,
895 },
896 AssignSlotPreset {
898 entity_id: EntityId,
899 slot_index: u8,
900 preset_id: String,
901 seq: Seq,
902 },
903 AdvanceRotation {
905 entity_id: EntityId,
906 slot_index: u8,
907 seq: Seq,
908 },
909 NpcTalkOpen {
911 entity_id: EntityId,
912 npc_id: String,
913 seq: Seq,
914 },
915 NpcTalkSay {
917 entity_id: EntityId,
918 npc_id: String,
919 message: String,
920 seq: Seq,
921 },
922 NpcTalkClose {
924 entity_id: EntityId,
925 npc_id: String,
926 seq: Seq,
927 },
928 AcceptQuest {
930 entity_id: EntityId,
931 quest_id: String,
932 seq: Seq,
933 },
934 WithdrawQuest {
936 entity_id: EntityId,
937 quest_id: String,
938 seq: Seq,
939 },
940 TrackQuest {
942 entity_id: EntityId,
943 quest_id: String,
944 seq: Seq,
945 },
946 QuestGiveItem {
948 entity_id: EntityId,
949 npc_id: String,
950 template_id: String,
951 #[serde(default = "default_one")]
952 quantity: u32,
953 seq: Seq,
954 },
955 HireWorker {
957 entity_id: EntityId,
958 def_id: String,
959 wage_copper_per_interval: u32,
960 #[serde(default)]
961 lodging_container_id: Option<String>,
962 #[serde(default)]
963 job_yaml: Option<String>,
964 seq: Seq,
965 },
966 DismissWorker {
968 entity_id: EntityId,
969 worker_instance_id: String,
970 seq: Seq,
971 },
972 SetWorkerJob {
974 entity_id: EntityId,
975 worker_instance_id: String,
976 job_yaml: String,
977 seq: Seq,
978 },
979 AssignWorkerLodging {
981 entity_id: EntityId,
982 worker_instance_id: String,
983 lodging_container_id: String,
984 seq: Seq,
985 },
986 SetWorkerMode {
988 entity_id: EntityId,
989 worker_instance_id: String,
990 mode: String,
991 seq: Seq,
992 },
993 GiveWorkerItem {
996 entity_id: EntityId,
997 worker_instance_id: String,
998 item_instance_id: uuid::Uuid,
999 #[serde(default)]
1000 quantity: Option<u32>,
1001 seq: Seq,
1002 },
1003 TakeWorkerItem {
1005 entity_id: EntityId,
1006 worker_instance_id: String,
1007 item_instance_id: uuid::Uuid,
1008 #[serde(default)]
1009 quantity: Option<u32>,
1010 seq: Seq,
1011 },
1012 RenameHiredWorker {
1014 entity_id: EntityId,
1015 worker_instance_id: String,
1016 name: String,
1017 seq: Seq,
1018 },
1019 TeachWorkerBlueprint {
1021 entity_id: EntityId,
1022 worker_instance_id: String,
1023 blueprint_id: String,
1024 seq: Seq,
1025 },
1026}
1027
1028fn default_block_enabled() -> bool {
1029 true
1030}
1031
1032#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1034pub struct StatusEffectHud {
1035 pub effect_id: String,
1036 pub label: String,
1037 #[serde(default)]
1038 pub polarity: String,
1039 #[serde(default)]
1040 pub icon_tile_id: Option<String>,
1041 #[serde(default)]
1043 pub remaining_sec: Option<f32>,
1044}
1045
1046#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1048pub struct CombatTargetHud {
1049 pub entity_id: EntityId,
1050 #[serde(default)]
1051 pub label: String,
1052 #[serde(default)]
1053 pub level: u32,
1054 pub health: f32,
1055 pub health_max: f32,
1056 #[serde(default)]
1057 pub life_state: LifeState,
1058 #[serde(default)]
1059 pub distance_m: f32,
1060 #[serde(default)]
1061 pub statuses: Vec<StatusEffectHud>,
1062}
1063
1064#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1066pub struct CastProgressHud {
1067 #[serde(default)]
1068 pub ability_id: String,
1069 #[serde(default)]
1070 pub ability_label: String,
1071 #[serde(default)]
1072 pub ticks_remaining: u64,
1073 #[serde(default)]
1074 pub ticks_total: u64,
1075}
1076
1077#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1079pub struct AbilityCooldownHud {
1080 #[serde(default)]
1081 pub ability_id: String,
1082 #[serde(default)]
1083 pub label: String,
1084 #[serde(default)]
1085 pub cd_ticks: u64,
1086 #[serde(default)]
1087 pub cd_total_ticks: u64,
1088}
1089
1090#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1092pub struct CombatSlotHud {
1093 pub slot_index: u8,
1094 #[serde(default)]
1095 pub target_entity_id: Option<EntityId>,
1096 #[serde(default)]
1097 pub target_label: Option<String>,
1098 #[serde(default)]
1099 pub target: Option<CombatTargetHud>,
1100 #[serde(default)]
1101 pub preset_id: Option<String>,
1102 #[serde(default)]
1103 pub preset_label: Option<String>,
1104 #[serde(default)]
1105 pub rotation: Vec<String>,
1106 #[serde(default)]
1107 pub rotation_index: u32,
1108 #[serde(default)]
1109 pub next_ability_id: Option<String>,
1110 #[serde(default)]
1111 pub auto_enabled: bool,
1112}
1113
1114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1116pub struct DefensePieceHud {
1117 pub slot: BodySlot,
1118 pub label: String,
1119 pub template_id: String,
1120 #[serde(default)]
1121 pub armor_physical: f32,
1122 #[serde(default)]
1123 pub resists: Vec<(String, f32)>,
1124}
1125
1126impl Default for DefensePieceHud {
1127 fn default() -> Self {
1128 Self {
1129 slot: BodySlot::Head,
1130 label: String::new(),
1131 template_id: String::new(),
1132 armor_physical: 0.0,
1133 resists: Vec::new(),
1134 }
1135 }
1136}
1137
1138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1140pub struct DefenseHud {
1141 pub armor_physical: f32,
1142 pub vitality_contribution: f32,
1143 pub total_mitigation_rating: f32,
1144 pub estimated_physical_dr: f32,
1146 #[serde(default)]
1147 pub resists: Vec<(String, f32)>,
1148 #[serde(default)]
1149 pub pieces: Vec<DefensePieceHud>,
1150}
1151
1152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1154pub struct CombatHud {
1155 pub in_combat: bool,
1156 pub auto_attack: bool,
1158 pub has_los: bool,
1159 pub attack_cd_ticks: u64,
1160 #[serde(default)]
1161 pub ability_id: String,
1162 #[serde(default)]
1163 pub target_entity_id: Option<EntityId>,
1164 #[serde(default)]
1165 pub target_label: Option<String>,
1166 #[serde(default)]
1167 pub max_target_slots: u8,
1168 #[serde(default)]
1169 pub slots: Vec<CombatSlotHud>,
1170 #[serde(default)]
1171 pub rotation_presets: Vec<RotationPreset>,
1172 #[serde(default)]
1173 pub gcd_ticks: u64,
1174 #[serde(default)]
1175 pub mainhand_template_id: Option<String>,
1176 #[serde(default)]
1177 pub mainhand_label: Option<String>,
1178 #[serde(default)]
1179 pub offhand_template_id: Option<String>,
1180 #[serde(default)]
1181 pub offhand_label: Option<String>,
1182 #[serde(default)]
1184 pub mainhand_hand_slots: u8,
1185 #[serde(default)]
1187 pub worn: Vec<(BodySlot, ItemStack)>,
1188 #[serde(default)]
1190 pub defense: Option<DefenseHud>,
1191 #[serde(default)]
1192 pub carry_mass: f32,
1193 #[serde(default)]
1194 pub carry_mass_max: f32,
1195 #[serde(default)]
1196 pub encumbrance: EncumbranceState,
1197 #[serde(default)]
1199 pub keychain: Vec<ItemStack>,
1200 #[serde(default)]
1201 pub target: Option<CombatTargetHud>,
1202 #[serde(default)]
1203 pub cast: Option<CastProgressHud>,
1204 #[serde(default)]
1205 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1206 #[serde(default)]
1207 pub blocking_active: bool,
1208 #[serde(default)]
1210 pub progression_xp: Option<ProgressionXp>,
1211 #[serde(default)]
1212 pub progression_baseline: u16,
1213 #[serde(default)]
1214 pub progression_xp_base: f64,
1215 #[serde(default)]
1216 pub progression_xp_growth: f64,
1217 #[serde(default)]
1218 pub attributes: Option<PrimaryAttributes>,
1219 #[serde(default)]
1220 pub skills: Option<PlayerSkills>,
1221 #[serde(default)]
1223 pub statuses: Vec<StatusEffectHud>,
1224}
1225
1226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1228#[serde(rename_all = "snake_case")]
1229pub enum WorkerModeView {
1230 Companion,
1231 JobLoop,
1232 Idle,
1235}
1236
1237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1239#[serde(rename_all = "snake_case")]
1240pub enum WorkerStateView {
1241 Idle,
1242 Traveling,
1243 Working,
1244 Resting,
1245 Waiting,
1246 Strike,
1247 Dismissed,
1248}
1249
1250#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
1252pub struct WorkerVitalsSummary {
1253 pub health_pct: f32,
1254 pub stamina_pct: f32,
1255}
1256
1257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1259#[serde(rename_all = "snake_case")]
1260pub enum WorkerRouteKindView {
1261 #[default]
1262 HarvestLoop,
1263 Ordered,
1264}
1265
1266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1268pub struct WorkerRouteView {
1269 #[serde(default)]
1270 pub kind: WorkerRouteKindView,
1271 #[serde(default)]
1272 pub lodging_container_id: Option<String>,
1273 #[serde(default)]
1275 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
1276 #[serde(default)]
1278 pub harvest_nodes: Vec<String>,
1279 #[serde(default = "default_route_carry_ratio")]
1280 pub carry_return_ratio: f32,
1281 #[serde(default)]
1283 pub stops: Vec<WorkerRouteStopView>,
1284}
1285
1286fn default_route_carry_ratio() -> f32 {
1287 0.90
1288}
1289
1290fn default_true_view() -> bool {
1291 true
1292}
1293
1294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1296pub struct WorkerWithdrawItemView {
1297 pub template: String,
1298 #[serde(default)]
1299 pub qty: u32,
1300 #[serde(default)]
1302 pub all: bool,
1303}
1304
1305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1306pub struct WorkerRouteWaypointView {
1307 pub x: f32,
1308 pub y: f32,
1309 pub z: f32,
1310}
1311
1312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1321#[serde(rename_all = "snake_case")]
1322pub enum WorkerRouteStopView {
1323 Waypoint {
1324 x: f32,
1325 y: f32,
1326 #[serde(default)]
1327 z: f32,
1328 },
1329 HarvestNode {
1330 node_id: String,
1331 },
1332 DepositAt {
1333 container_id: String,
1334 #[serde(default)]
1335 filter: Option<Vec<String>>,
1336 },
1337 TradeWith {
1338 #[serde(default)]
1339 npc_id: Option<String>,
1340 template: String,
1341 #[serde(default = "default_true_view")]
1342 sell_all: bool,
1343 },
1344 WithdrawFrom {
1345 container_id: String,
1346 items: Vec<WorkerWithdrawItemView>,
1347 },
1348 CraftAt {
1349 device: String,
1350 blueprint: String,
1351 #[serde(default)]
1352 qty: Option<u32>,
1353 },
1354 RestIfNeeded,
1355 Wait {
1356 #[serde(default)]
1357 wait_ticks: u64,
1358 },
1359}
1360
1361#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1363#[serde(rename_all = "snake_case")]
1364pub enum LedgerCategory {
1365 Workers,
1366 Hire,
1367 Train,
1368 ShopBuy,
1369 Taxes,
1370 WorkerSales,
1371 TraderSales,
1372 Other,
1373}
1374
1375impl LedgerCategory {
1376 pub fn as_str(self) -> &'static str {
1377 match self {
1378 Self::Workers => "workers",
1379 Self::Hire => "hire",
1380 Self::Train => "train",
1381 Self::ShopBuy => "shop_buy",
1382 Self::Taxes => "taxes",
1383 Self::WorkerSales => "worker_sales",
1384 Self::TraderSales => "trader_sales",
1385 Self::Other => "other",
1386 }
1387 }
1388}
1389
1390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1391pub struct LedgerEntryView {
1392 pub id: uuid::Uuid,
1393 pub game_day: u64,
1394 pub signed_copper: i64,
1395 pub category: LedgerCategory,
1396 #[serde(default)]
1397 pub label: String,
1398}
1399
1400#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1401pub struct LedgerPeriodTotals {
1402 #[serde(default)]
1404 pub expenses: std::collections::HashMap<String, u64>,
1405 #[serde(default)]
1407 pub income: std::collections::HashMap<String, u64>,
1408 pub expense_copper: u64,
1409 pub income_copper: u64,
1410 pub cash_flow_copper: i64,
1412}
1413
1414#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1415pub struct PlayerLedgerView {
1416 pub current_game_day: u64,
1417 #[serde(default)]
1418 pub period_day: LedgerPeriodTotals,
1419 #[serde(default)]
1420 pub period_week: LedgerPeriodTotals,
1421 #[serde(default)]
1422 pub period_month: LedgerPeriodTotals,
1423 #[serde(default)]
1424 pub period_lifetime: LedgerPeriodTotals,
1425 #[serde(default)]
1426 pub recent: Vec<LedgerEntryView>,
1427 #[serde(default)]
1429 pub wealth_on_person_copper: u64,
1430 #[serde(default)]
1432 pub wealth_in_storage_copper: u64,
1433 #[serde(default)]
1435 pub wealth_total_copper: u64,
1436 #[serde(default)]
1438 pub live_expense_per_interval_copper: u64,
1439 #[serde(default)]
1441 pub live_income_route_est_per_loop_copper: u64,
1442 #[serde(default)]
1444 pub live_income_avg_per_interval_copper: u64,
1445 #[serde(default)]
1447 pub live_income_avg_window_intervals: u32,
1448 #[serde(default)]
1450 pub live_net_avg_per_interval_copper: i64,
1451}
1452
1453#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1455#[serde(rename_all = "snake_case")]
1456pub enum AnalyticsMetric {
1457 NpcKill,
1458 WildlifeKill,
1459 Harvest,
1460 QuestComplete,
1461 QuestAccept,
1462 QuestAbandon,
1463 PlayerDeath,
1464 Craft,
1465 WorkerHire,
1466 WorkerDismiss,
1467 WorkerTeach,
1468 NpcTalk,
1469 ShopBuy,
1470 ShopSell,
1471 PlaceContainer,
1472 PickupContainer,
1473 PickupDrop,
1474 ConsumableUse,
1475 AbilityUse,
1476 DistanceWalkedM,
1477 DoorUse,
1478 BuildingEnter,
1479}
1480
1481impl AnalyticsMetric {
1482 pub fn as_str(self) -> &'static str {
1483 match self {
1484 Self::NpcKill => "npc_kill",
1485 Self::WildlifeKill => "wildlife_kill",
1486 Self::Harvest => "harvest",
1487 Self::QuestComplete => "quest_complete",
1488 Self::QuestAccept => "quest_accept",
1489 Self::QuestAbandon => "quest_abandon",
1490 Self::PlayerDeath => "player_death",
1491 Self::Craft => "craft",
1492 Self::WorkerHire => "worker_hire",
1493 Self::WorkerDismiss => "worker_dismiss",
1494 Self::WorkerTeach => "worker_teach",
1495 Self::NpcTalk => "npc_talk",
1496 Self::ShopBuy => "shop_buy",
1497 Self::ShopSell => "shop_sell",
1498 Self::PlaceContainer => "place_container",
1499 Self::PickupContainer => "pickup_container",
1500 Self::PickupDrop => "pickup_drop",
1501 Self::ConsumableUse => "consumable_use",
1502 Self::AbilityUse => "ability_use",
1503 Self::DistanceWalkedM => "distance_walked_m",
1504 Self::DoorUse => "door_use",
1505 Self::BuildingEnter => "building_enter",
1506 }
1507 }
1508
1509 pub fn from_str_key(s: &str) -> Option<Self> {
1510 Some(match s {
1511 "npc_kill" => Self::NpcKill,
1512 "wildlife_kill" => Self::WildlifeKill,
1513 "harvest" => Self::Harvest,
1514 "quest_complete" => Self::QuestComplete,
1515 "quest_accept" => Self::QuestAccept,
1516 "quest_abandon" => Self::QuestAbandon,
1517 "player_death" => Self::PlayerDeath,
1518 "craft" => Self::Craft,
1519 "worker_hire" => Self::WorkerHire,
1520 "worker_dismiss" => Self::WorkerDismiss,
1521 "worker_teach" => Self::WorkerTeach,
1522 "npc_talk" => Self::NpcTalk,
1523 "shop_buy" => Self::ShopBuy,
1524 "shop_sell" => Self::ShopSell,
1525 "place_container" => Self::PlaceContainer,
1526 "pickup_container" => Self::PickupContainer,
1527 "pickup_drop" => Self::PickupDrop,
1528 "consumable_use" => Self::ConsumableUse,
1529 "ability_use" => Self::AbilityUse,
1530 "distance_walked_m" => Self::DistanceWalkedM,
1531 "door_use" => Self::DoorUse,
1532 "building_enter" => Self::BuildingEnter,
1533 _ => return None,
1534 })
1535 }
1536}
1537
1538#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1539pub struct CareerMetricRow {
1540 pub subject_id: String,
1541 pub amount: u64,
1542}
1543
1544#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1546pub struct PlayerCareerView {
1547 pub current_game_day: u64,
1548 #[serde(default)]
1549 pub kills: Vec<CareerMetricRow>,
1550 #[serde(default)]
1551 pub harvests: Vec<CareerMetricRow>,
1552 pub quests_completed: u64,
1553 #[serde(default)]
1554 pub crafts: Vec<CareerMetricRow>,
1555 pub deaths: u64,
1556 pub npc_talks: u64,
1557 pub shop_buys: u64,
1558 pub shop_sells: u64,
1559 pub distance_m: u64,
1560 #[serde(default)]
1561 pub other: Vec<CareerMetricRow>,
1562}
1563
1564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1566pub struct HiredWorkerView {
1567 pub instance_id: String,
1568 pub entity_id: EntityId,
1569 pub def_id: String,
1570 pub label: String,
1572 pub x: f32,
1573 pub y: f32,
1574 pub z: f32,
1575 pub mode: WorkerModeView,
1576 pub state: WorkerStateView,
1577 #[serde(default)]
1578 pub step_label: String,
1579 pub vitals: WorkerVitalsSummary,
1580 #[serde(default)]
1581 pub carry_pct: f32,
1582 #[serde(default)]
1583 pub last_error: Option<String>,
1584 pub wage_copper_per_interval: u32,
1585 #[serde(default)]
1587 pub effective_wage_copper: u32,
1588 #[serde(default)]
1590 pub wage_meters_walked: f32,
1591 #[serde(default)]
1593 pub lodging_container_id: Option<String>,
1594 #[serde(default)]
1596 pub route: Option<WorkerRouteView>,
1597 #[serde(default)]
1599 pub known_blueprint_ids: Vec<String>,
1600 #[serde(default = "default_worker_view_level")]
1602 pub level: u32,
1603 #[serde(default)]
1605 pub worker_xp: f64,
1606 #[serde(default)]
1608 pub inventory: Vec<ItemStack>,
1609}
1610
1611fn default_worker_view_level() -> u32 {
1612 1
1613}
1614
1615#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1617pub struct TickDelta {
1618 pub tick: Tick,
1619 pub entities: Vec<EntityState>,
1620 #[serde(default)]
1621 pub resource_nodes: Vec<ResourceNodeView>,
1622 #[serde(default)]
1623 pub buildings: Vec<BuildingView>,
1624 #[serde(default)]
1625 pub doors: Vec<DoorView>,
1626 #[serde(default)]
1627 pub npcs: Vec<NpcView>,
1628 #[serde(default)]
1630 pub inventory: Vec<ItemStack>,
1631 #[serde(default)]
1632 pub blueprints: Vec<BlueprintView>,
1633 #[serde(default)]
1634 pub world_clock: WorldClock,
1635 #[serde(default)]
1636 pub ground_drops: Vec<GroundDropView>,
1637 #[serde(default)]
1638 pub placed_containers: Vec<PlacedContainerView>,
1639 #[serde(default)]
1640 pub combat: Option<CombatHud>,
1641 #[serde(default)]
1642 pub interior_map: Option<InteriorMapView>,
1643 #[serde(default)]
1644 pub quest_log: Vec<QuestLogEntry>,
1645 #[serde(default)]
1646 pub hired_workers: Vec<HiredWorkerView>,
1647 #[serde(default)]
1648 pub interactables: Vec<InteractableView>,
1649 #[serde(default)]
1650 pub ledger: Option<PlayerLedgerView>,
1651 #[serde(default)]
1652 pub career: Option<PlayerCareerView>,
1653}
1654#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1655pub struct GroundDropView {
1656 pub id: String,
1657 pub template_id: String,
1658 pub quantity: u32,
1659 pub x: f32,
1660 pub y: f32,
1661 pub z: f32,
1662 #[serde(default)]
1664 pub tile_id: Option<String>,
1665}
1666
1667#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1669pub struct Snapshot {
1670 pub tick: Tick,
1671 pub chunk_rev: u64,
1672 #[serde(default)]
1674 pub content_rev: u64,
1675 #[serde(default)]
1677 pub publish_rev: u64,
1678 pub entities: Vec<EntityState>,
1679 #[serde(default)]
1680 pub resource_nodes: Vec<ResourceNodeView>,
1681 #[serde(default)]
1683 pub world_width_m: f32,
1684 #[serde(default)]
1685 pub world_height_m: f32,
1686 #[serde(default)]
1687 pub buildings: Vec<BuildingView>,
1688 #[serde(default)]
1689 pub doors: Vec<DoorView>,
1690 #[serde(default)]
1691 pub npcs: Vec<NpcView>,
1692 #[serde(default)]
1693 pub inventory: Vec<ItemStack>,
1694 #[serde(default)]
1695 pub blueprints: Vec<BlueprintView>,
1696 #[serde(default)]
1697 pub world_clock: WorldClock,
1698 #[serde(default)]
1699 pub terrain_zones: Vec<TerrainZoneView>,
1700 #[serde(default)]
1701 pub z_platforms: Vec<ZPlatformView>,
1702 #[serde(default)]
1703 pub z_transitions: Vec<ZTransitionView>,
1704 #[serde(default)]
1705 pub ground_drops: Vec<GroundDropView>,
1706 #[serde(default)]
1707 pub placed_containers: Vec<PlacedContainerView>,
1708 #[serde(default)]
1709 pub combat: Option<CombatHud>,
1710 #[serde(default)]
1711 pub interior_map: Option<InteriorMapView>,
1712 #[serde(default)]
1713 pub quest_log: Vec<QuestLogEntry>,
1714 #[serde(default)]
1715 pub hired_workers: Vec<HiredWorkerView>,
1716 #[serde(default)]
1717 pub interactables: Vec<InteractableView>,
1718 #[serde(default)]
1719 pub ledger: Option<PlayerLedgerView>,
1720 #[serde(default)]
1721 pub career: Option<PlayerCareerView>,
1722}
1723
1724#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1726pub struct ResourceNodeView {
1727 pub id: String,
1728 pub label: String,
1729 pub x: f32,
1730 pub y: f32,
1731 pub z: f32,
1732 pub item_template: String,
1733 #[serde(default = "default_node_state")]
1734 pub state: ResourceNodeState,
1735 #[serde(default = "default_blocking_view")]
1737 pub blocking: bool,
1738 #[serde(default = "default_blocking_radius_view")]
1740 pub blocking_radius_m: f32,
1741 #[serde(default)]
1743 pub tile_id: Option<String>,
1744 #[serde(default)]
1746 pub sprite_mode: Option<String>,
1747 #[serde(default)]
1749 pub presentation_state: Option<String>,
1750}
1751
1752fn default_blocking_radius_view() -> f32 {
1753 0.8
1754}
1755
1756fn default_blocking_view() -> bool {
1757 true
1758}
1759
1760#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1761#[serde(rename_all = "snake_case")]
1762pub enum ResourceNodeState {
1763 Available,
1764 Harvesting,
1765 Cooldown,
1766}
1767
1768fn default_node_state() -> ResourceNodeState {
1769 ResourceNodeState::Available
1770}
1771
1772#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1773#[serde(rename_all = "snake_case")]
1774pub enum ItemStatusBindingMode {
1775 OnHit,
1776 WhileEquipped,
1777}
1778
1779impl Default for ItemStatusBindingMode {
1780 fn default() -> Self {
1781 Self::OnHit
1782 }
1783}
1784
1785#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1787pub struct ItemStatusBinding {
1788 pub effect_id: String,
1789 #[serde(default)]
1790 pub mode: ItemStatusBindingMode,
1791 #[serde(default)]
1793 pub source: String,
1794 #[serde(default)]
1795 pub applied_at_tick: u64,
1796 #[serde(default, skip_serializing_if = "Option::is_none")]
1798 pub expires_at_tick: Option<u64>,
1799}
1800
1801#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1802pub struct ItemStack {
1803 pub template_id: String,
1804 pub quantity: u32,
1805 #[serde(default)]
1807 pub item_instance_id: Option<Uuid>,
1808 #[serde(default)]
1810 pub props: BTreeMap<String, String>,
1811 #[serde(default)]
1813 pub status_bindings: Vec<ItemStatusBinding>,
1814 #[serde(default)]
1816 pub contents: Vec<ItemStack>,
1817 #[serde(default)]
1819 pub display_name: Option<String>,
1820 #[serde(default)]
1822 pub category: Option<String>,
1823 #[serde(default)]
1825 pub base_mass: Option<f32>,
1826 #[serde(default)]
1828 pub base_volume: Option<f32>,
1829 #[serde(default)]
1831 pub capacity_volume: Option<f32>,
1832 #[serde(default)]
1834 pub stackable: Option<bool>,
1835 #[serde(default)]
1837 pub world_placeable: Option<bool>,
1838 #[serde(default)]
1840 pub worker_lodging_capacity: Option<u32>,
1841 #[serde(default)]
1843 pub equip_slot: Option<BodySlot>,
1844 #[serde(default)]
1846 pub armor_physical: Option<f32>,
1847 #[serde(default)]
1849 pub resists: Vec<(String, f32)>,
1850 #[serde(default)]
1852 pub hand_slots: Option<u8>,
1853}
1854
1855impl ItemStack {
1856 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
1857 Self {
1858 template_id: template_id.into(),
1859 quantity,
1860 ..Default::default()
1861 }
1862 }
1863}
1864
1865impl Default for ItemStack {
1866 fn default() -> Self {
1867 Self {
1868 template_id: String::new(),
1869 quantity: 0,
1870 item_instance_id: None,
1871 props: BTreeMap::new(),
1872 status_bindings: Vec::new(),
1873 contents: Vec::new(),
1874 display_name: None,
1875 category: None,
1876 base_mass: None,
1877 base_volume: None,
1878 capacity_volume: None,
1879 stackable: None,
1880 world_placeable: None,
1881 worker_lodging_capacity: None,
1882 equip_slot: None,
1883 armor_physical: None,
1884 resists: Vec::new(),
1885 hand_slots: None,
1886 }
1887 }
1888}
1889
1890#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1892#[serde(rename_all = "snake_case")]
1893pub enum EncumbranceState {
1894 #[default]
1895 Light,
1896 Heavy,
1897 Over,
1898}
1899
1900#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
1904#[serde(rename_all = "snake_case")]
1905pub enum BodySlot {
1906 Head,
1907 #[serde(alias = "body")]
1909 Chest,
1910 #[serde(alias = "arms")]
1912 Forearms,
1913 Legs,
1914 Feet,
1915 Cloak,
1916 Back,
1917 Waist,
1918 Earrings,
1919 Necklace,
1920 Eyeglasses,
1921 #[serde(rename = "ring_left_1", alias = "ring_left1")]
1923 RingLeft1,
1924 #[serde(rename = "ring_left_2", alias = "ring_left2")]
1925 RingLeft2,
1926 #[serde(rename = "ring_right_1", alias = "ring_right1")]
1927 RingRight1,
1928 #[serde(rename = "ring_right_2", alias = "ring_right2")]
1929 RingRight2,
1930}
1931
1932impl BodySlot {
1933 pub const ALL: [BodySlot; 15] = [
1935 BodySlot::Head,
1936 BodySlot::Chest,
1937 BodySlot::Forearms,
1938 BodySlot::Legs,
1939 BodySlot::Feet,
1940 BodySlot::Cloak,
1941 BodySlot::Back,
1942 BodySlot::Waist,
1943 BodySlot::Earrings,
1944 BodySlot::Necklace,
1945 BodySlot::Eyeglasses,
1946 BodySlot::RingLeft1,
1947 BodySlot::RingLeft2,
1948 BodySlot::RingRight1,
1949 BodySlot::RingRight2,
1950 ];
1951
1952 pub fn as_str(self) -> &'static str {
1953 match self {
1954 BodySlot::Head => "head",
1955 BodySlot::Chest => "chest",
1956 BodySlot::Forearms => "forearms",
1957 BodySlot::Legs => "legs",
1958 BodySlot::Feet => "feet",
1959 BodySlot::Cloak => "cloak",
1960 BodySlot::Back => "back",
1961 BodySlot::Waist => "waist",
1962 BodySlot::Earrings => "earrings",
1963 BodySlot::Necklace => "necklace",
1964 BodySlot::Eyeglasses => "eyeglasses",
1965 BodySlot::RingLeft1 => "ring_left_1",
1966 BodySlot::RingLeft2 => "ring_left_2",
1967 BodySlot::RingRight1 => "ring_right_1",
1968 BodySlot::RingRight2 => "ring_right_2",
1969 }
1970 }
1971}
1972
1973#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1975#[serde(rename_all = "snake_case")]
1976pub enum InventoryLocation {
1977 Root,
1979 Worn { slot: BodySlot },
1981 Placed { container_id: String },
1983 Keychain,
1985}
1986
1987#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1989pub struct PlacedContainerView {
1990 pub id: String,
1991 pub template_id: String,
1992 pub display_name: String,
1993 pub x: f32,
1994 pub y: f32,
1995 pub z: f32,
1996 pub locked: bool,
1997 #[serde(default)]
1999 pub accessible: bool,
2000 #[serde(default)]
2001 pub owner_character_id: Option<Uuid>,
2002 #[serde(default)]
2004 pub contents: Vec<ItemStack>,
2005 #[serde(default)]
2007 pub lock_id: Option<String>,
2008 #[serde(default)]
2010 pub capacity_volume: Option<f32>,
2011 #[serde(default)]
2013 pub item_instance_id: Option<Uuid>,
2014 #[serde(default)]
2016 pub tile_id: Option<String>,
2017 #[serde(default)]
2019 pub worker_lodging_capacity: Option<u32>,
2020}
2021
2022#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2023pub struct BlueprintIngredientView {
2024 pub template_id: String,
2025 pub quantity: u32,
2026 pub consumed: bool,
2028}
2029
2030#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2031pub struct ToolRequirementView {
2032 pub item: String,
2033 pub consumed: bool,
2035}
2036
2037#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2038pub struct SkillRequirementView {
2039 pub skill: String,
2040 pub level: u32,
2041}
2042
2043#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2044pub struct BlueprintView {
2045 pub id: String,
2046 pub label: String,
2047 pub output: String,
2048 pub output_qty: u32,
2049 pub craft_ticks: u32,
2050 pub inputs: Vec<BlueprintIngredientView>,
2051 #[serde(default)]
2053 pub station: Option<String>,
2054 #[serde(default)]
2055 pub category: Option<String>,
2056 #[serde(default)]
2057 pub required_tools: Vec<ToolRequirementView>,
2058 #[serde(default)]
2059 pub skill: Option<SkillRequirementView>,
2060 #[serde(default)]
2061 pub failure_chance: f32,
2062 #[serde(default)]
2064 pub worker_train_copper: u64,
2065}
2066
2067#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2069#[serde(rename_all = "snake_case")]
2070pub enum TerrainKindView {
2071 #[default]
2072 Grass,
2073 Hill,
2074 Bog,
2075 ShallowWater,
2076 DeepWater,
2077 Trail,
2078 Road,
2079 Rock,
2080}
2081
2082#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2083pub struct TerrainZoneView {
2084 pub id: String,
2085 pub x0: f32,
2086 pub y0: f32,
2087 pub x1: f32,
2088 pub y1: f32,
2089 #[serde(default)]
2090 pub kind: TerrainKindView,
2091 #[serde(default)]
2093 pub elevation: f32,
2094 #[serde(default)]
2097 pub glyph: Option<String>,
2098 #[serde(default)]
2100 pub color: Option<String>,
2101 #[serde(default)]
2103 pub tile_id: Option<String>,
2104 #[serde(default)]
2106 pub z_order: i32,
2107}
2108
2109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2111pub struct ZPlatformView {
2112 pub id: String,
2113 pub z: f32,
2114 pub x0: f32,
2115 pub y0: f32,
2116 pub x1: f32,
2117 pub y1: f32,
2118}
2119
2120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2122pub struct ZTransitionView {
2123 pub id: String,
2124 pub z_from: f32,
2125 pub z_to: f32,
2126 pub x0: f32,
2127 pub y0: f32,
2128 pub x1: f32,
2129 pub y1: f32,
2130}
2131
2132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2133pub struct BuildingView {
2134 pub id: String,
2135 pub label: String,
2136 pub x: f32,
2137 pub y: f32,
2138 pub width_m: f32,
2139 pub depth_m: f32,
2140 #[serde(default)]
2141 pub interior_blueprint: Option<String>,
2142 #[serde(default)]
2143 pub tags: Vec<String>,
2144}
2145
2146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2147pub struct DoorView {
2148 pub id: String,
2149 pub building_id: String,
2150 pub x: f32,
2151 pub y: f32,
2152 #[serde(default)]
2153 pub open: bool,
2154 #[serde(default)]
2155 pub portal: Option<String>,
2156}
2157
2158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2159pub struct InteriorRoomView {
2160 pub id: String,
2161 pub label: String,
2162 pub floor: i32,
2163 pub x0: f32,
2164 pub y0: f32,
2165 pub x1: f32,
2166 pub y1: f32,
2167 #[serde(default)]
2168 pub floor_color: Option<String>,
2169 #[serde(default)]
2170 pub floor_glyph: Option<String>,
2171}
2172
2173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2174pub struct InteriorDoorView {
2175 pub id: String,
2176 pub room_a: String,
2177 pub room_b: String,
2178 pub x: f32,
2179 pub y: f32,
2180 pub kind: String,
2181}
2182
2183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2184pub struct InteriorMapView {
2185 pub building_id: String,
2186 pub blueprint_id: String,
2187 pub background_color: String,
2188 #[serde(default)]
2189 pub default_floor_color: Option<String>,
2190 #[serde(default = "default_floor_height_view")]
2191 pub floor_height_m: f32,
2192 pub rooms: Vec<InteriorRoomView>,
2193 #[serde(default)]
2194 pub room_doors: Vec<InteriorDoorView>,
2195}
2196
2197fn default_floor_height_view() -> f32 {
2198 3.0
2199}
2200
2201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2202pub struct NpcView {
2203 pub id: String,
2204 pub label: String,
2205 pub role: String,
2206 pub x: f32,
2207 pub y: f32,
2208 #[serde(default)]
2210 pub building_id: Option<String>,
2211 #[serde(default)]
2213 pub entity_id: Option<EntityId>,
2214 #[serde(default)]
2215 pub life_state: Option<LifeState>,
2216 #[serde(default)]
2217 pub hp_pct: Option<f32>,
2218 #[serde(default)]
2220 pub can_trade: bool,
2221 #[serde(default)]
2223 pub tile_id: Option<String>,
2224 #[serde(default)]
2226 pub behavior_state: Option<String>,
2227 #[serde(default)]
2229 pub presentation_state: Option<String>,
2230 #[serde(default)]
2232 pub sprite_mode: Option<String>,
2233}
2234
2235#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2236pub struct UseResult {
2237 pub template_id: String,
2238 pub hunger_restored: f32,
2239 pub thirst_restored: f32,
2240 #[serde(default)]
2241 pub health_restored: f32,
2242 #[serde(default)]
2243 pub mana_restored: f32,
2244 #[serde(default)]
2245 pub cleared_dot_ids: Vec<String>,
2246}
2247
2248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2249pub struct CraftResult {
2250 pub blueprint_id: String,
2251 pub outputs: Vec<ItemStack>,
2252 pub consumed: Vec<ItemStack>,
2253 #[serde(default = "default_one")]
2255 pub batch_index: u32,
2256 #[serde(default = "default_one")]
2258 pub batch_total: u32,
2259}
2260
2261fn default_one() -> u32 {
2262 1
2263}
2264
2265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2266pub struct DeathNotice {
2267 pub entity_id: EntityId,
2268 pub respawn_x: f32,
2269 pub respawn_y: f32,
2270 pub message: String,
2271}
2272
2273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2274pub struct InteractionNotice {
2275 pub target_id: String,
2276 pub message: String,
2277 #[serde(default)]
2278 pub coins_delta: i32,
2279 #[serde(default)]
2280 pub inventory_delta: Vec<ItemStack>,
2281}
2282
2283#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2284#[serde(rename_all = "snake_case")]
2285pub enum NpcTalkTrustFlag {
2286 Stranger,
2287 Acquainted,
2288 Trusted,
2289}
2290
2291#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
2292#[serde(rename_all = "snake_case")]
2293pub enum NpcTalkDepth {
2294 #[default]
2295 Full,
2296 Brief,
2297 Unavailable,
2298}
2299
2300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2301pub struct NpcTalkOpened {
2302 pub npc_id: String,
2303 pub npc_label: String,
2304 pub greeting: String,
2305 pub trust_flag: NpcTalkTrustFlag,
2306 #[serde(default)]
2307 pub talk_depth: NpcTalkDepth,
2308 #[serde(default = "default_true")]
2309 pub trade_allowed: bool,
2310}
2311
2312fn default_true() -> bool {
2313 true
2314}
2315
2316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2317pub struct NpcTalkPending {
2318 pub npc_id: String,
2319}
2320
2321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2322pub struct NpcTalkReply {
2323 pub npc_id: String,
2324 pub line: String,
2325 pub trust_flag: NpcTalkTrustFlag,
2326 #[serde(default)]
2327 pub wind_down: bool,
2328 #[serde(default)]
2329 pub trade_disabled: bool,
2330}
2331
2332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2333pub struct NpcTalkClosed {
2334 pub npc_id: String,
2335}
2336
2337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2338pub struct NpcTalkError {
2339 pub npc_id: String,
2340 pub reason: String,
2341}
2342
2343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2344#[serde(rename_all = "snake_case")]
2345pub enum QuestStatusView {
2346 Available,
2347 Active,
2348 Completed,
2349}
2350
2351#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2352pub struct QuestObjectiveProgress {
2353 pub label: String,
2354 pub current: u32,
2355 pub required: u32,
2356 pub done: bool,
2357}
2358
2359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2360pub struct QuestLogEntry {
2361 pub quest_id: String,
2362 pub title: String,
2363 pub description: String,
2364 pub status: QuestStatusView,
2365 #[serde(default)]
2366 pub current_step_id: Option<String>,
2367 #[serde(default)]
2368 pub current_step_title: String,
2369 #[serde(default)]
2370 pub objectives: Vec<QuestObjectiveProgress>,
2371 #[serde(default)]
2372 pub is_tracked: bool,
2373 #[serde(default)]
2374 pub can_withdraw: bool,
2375}
2376
2377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2378pub struct InteractableView {
2379 pub id: String,
2380 pub kind: String,
2381 pub label: String,
2382 pub x: f32,
2383 pub y: f32,
2384 pub z: f32,
2385 #[serde(default)]
2386 pub board_id: Option<String>,
2387}
2388
2389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2390pub struct QuestOffer {
2391 pub quest_id: String,
2392 pub title: String,
2393 pub description: String,
2394 #[serde(default)]
2395 pub step_count: u32,
2396}
2397
2398#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2399pub struct QuestNotice {
2400 pub quest_id: String,
2401 pub title: String,
2402 pub message: String,
2403}
2404
2405#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2406#[serde(rename_all = "snake_case")]
2407pub enum ShopOfferKind {
2408 Item,
2409 Blueprint,
2410}
2411
2412#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2413pub struct ShopOffer {
2414 pub offer_id: String,
2415 pub kind: ShopOfferKind,
2416 pub label: String,
2417 #[serde(default)]
2418 pub template_id: Option<String>,
2419 #[serde(default)]
2420 pub blueprint_id: Option<String>,
2421 pub price_copper: u32,
2422 #[serde(default)]
2423 pub affordable: bool,
2424 #[serde(default)]
2425 pub already_owned: bool,
2426}
2427
2428#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2429pub struct ShopBuyLine {
2430 pub template_id: String,
2431 pub label: String,
2432 pub quantity: u32,
2433 pub price_copper: u32,
2434}
2435
2436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2437pub struct ShopCatalog {
2438 pub npc_id: String,
2439 pub npc_label: String,
2440 #[serde(default)]
2441 pub sells: Vec<ShopOffer>,
2442 #[serde(default)]
2443 pub buys: Vec<ShopBuyLine>,
2444}
2445
2446#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2447pub struct HarvestResult {
2448 pub node_id: String,
2449 pub quantity: u32,
2451 pub item_template: String,
2452 #[serde(default)]
2455 pub item_instance_id: Option<Uuid>,
2456}
2457
2458#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2460pub struct Envelope<T> {
2461 pub protocol_version: u16,
2462 pub payload: T,
2463}
2464
2465impl<T> Envelope<T> {
2466 pub fn new(payload: T) -> Self {
2467 Self {
2468 protocol_version: crate::PROTOCOL_VERSION,
2469 payload,
2470 }
2471 }
2472}
2473
2474#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2476pub struct Hello {
2477 pub client_name: String,
2478 pub protocol_version: u16,
2479 #[serde(default)]
2480 pub auth: AuthCredential,
2481 #[serde(default)]
2483 pub character_id: Option<Uuid>,
2484}
2485
2486#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2489#[serde(rename_all = "snake_case")]
2490pub enum AuthCredential {
2491 DevLocal,
2492 Session { token: String },
2493 ApiToken { token: String, character_id: Uuid },
2494}
2495
2496impl Default for AuthCredential {
2497 fn default() -> Self {
2498 Self::DevLocal
2499 }
2500}
2501
2502#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2503pub struct Welcome {
2504 pub session_id: SessionId,
2505 pub entity_id: EntityId,
2506 pub snapshot: Snapshot,
2507}
2508
2509#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2510pub enum ServerMessage {
2511 Welcome(Welcome),
2512 ContentUpdated(Snapshot),
2514 Tick(TickDelta),
2515 IntentAck {
2516 entity_id: EntityId,
2517 seq: Seq,
2518 tick: Tick,
2519 },
2520 Chat(ChatMessage),
2521 HarvestResult(HarvestResult),
2522 UseResult(UseResult),
2523 CraftResult(CraftResult),
2524 Death(DeathNotice),
2525 Interaction(InteractionNotice),
2526 ShopOpened(ShopCatalog),
2527 NpcTalkOpened(NpcTalkOpened),
2528 NpcTalkPending(NpcTalkPending),
2529 NpcTalkReply(NpcTalkReply),
2530 NpcTalkClosed(NpcTalkClosed),
2531 NpcTalkError(NpcTalkError),
2532 QuestOffer(QuestOffer),
2533 QuestAccepted(QuestNotice),
2534 QuestWithdrawn(QuestNotice),
2535 QuestStepCompleted(QuestNotice),
2536 QuestCompleted(QuestNotice),
2537}
2538
2539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2540pub enum ClientMessage {
2541 Hello(Hello),
2542 Intent(Intent),
2543 Disconnect,
2544}
2545
2546#[cfg(test)]
2547mod tests {
2548 use super::*;
2549
2550 #[test]
2551 fn pristine_vitals_state_yields_full_pools() {
2552 let attrs = PrimaryAttributes::default();
2553 let vitals = StoredVitalsState::default().apply_to(attrs);
2554 assert!(vitals.health > 0.0);
2555 assert_eq!(vitals.health, vitals.health_max);
2556 assert!((vitals.mana_max - 61.0).abs() < 0.01);
2557 }
2558
2559 #[test]
2560 fn saved_vitals_scale_when_pool_max_increases() {
2561 let mut attrs = PrimaryAttributes::default();
2562 attrs.intelligence = 140;
2563 attrs.wisdom = 140;
2564 let saved = StoredVitalsState {
2565 health: 100.0,
2566 mana: 14.0,
2567 stamina: 100.0,
2568 ..StoredVitalsState::default()
2569 };
2570 let vitals = saved.apply_to(attrs);
2571 assert!(vitals.mana_max > 55.0);
2572 assert!(
2573 (vitals.mana - vitals.mana_max).abs() < 0.01,
2574 "full legacy mana bar migrates to full new bar"
2575 );
2576 }
2577
2578 #[test]
2579 fn empty_vitals_state_is_pristine() {
2580 let pristine = StoredVitalsState {
2581 health: 0.0,
2582 mana: 0.0,
2583 stamina: 0.0,
2584 hunger: 0.0,
2585 thirst: 0.0,
2586 coins: 0,
2587 deaths: 0,
2588 life_state: LifeState::Alive,
2589 };
2590 assert!(pristine.is_pristine());
2591 let vitals = pristine.apply_to(PrimaryAttributes::default());
2592 assert!(vitals.health > 0.0);
2593 }
2594
2595 #[test]
2596 fn stored_vitals_roundtrip_preserves_partial_pools() {
2597 let attrs = PrimaryAttributes::default();
2598 let mut live = PlayerVitals::from_attributes(attrs);
2599 live.health = 25.0;
2600 live.hunger = 77.0;
2601 live.deaths = 2;
2602 let stored = StoredVitalsState::from_live(&live);
2603 let restored = stored.apply_to(attrs);
2604 assert!(
2605 (restored.health - 25.0).abs() < 0.01,
2606 "partial HP below cap stays absolute"
2607 );
2608 assert_eq!(restored.hunger, 77.0);
2609 assert_eq!(restored.deaths, 2);
2610 }
2611
2612 #[test]
2613 fn skill_tiers_start_at_zero() {
2614 let skill = SkillProgress::default();
2615 assert_eq!(skill.level, 0);
2616 assert_eq!(skill.display_tier(), 0);
2617 let trained = SkillProgress {
2618 level: 250,
2619 last_trained_tick: 1,
2620 };
2621 assert_eq!(trained.display_tier(), 2);
2622 }
2623
2624 #[test]
2625 fn quest_server_messages_roundtrip_json() {
2626 use crate::codec::{Codec, PostcardCodec};
2627
2628 let offer = ServerMessage::QuestOffer(QuestOffer {
2629 quest_id: "ada_goblin_hunt".into(),
2630 title: "Goblin Trouble".into(),
2631 description: "Help Ada".into(),
2632 step_count: 3,
2633 });
2634 let notice = ServerMessage::QuestAccepted(QuestNotice {
2635 quest_id: "ada_goblin_hunt".into(),
2636 title: "Goblin Trouble".into(),
2637 message: "Quest accepted".into(),
2638 });
2639 for msg in [offer, notice] {
2640 let bytes = PostcardCodec.encode(&msg).unwrap();
2641 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
2642 assert_eq!(decoded, msg);
2643 }
2644 }
2645}