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