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
435 && new_max > legacy_max * 1.05
436 && current >= legacy_max * 0.95
437 {
438 let ratio = (current / legacy_max).clamp(0.0, 1.0);
439 (new_max * ratio).min(new_max)
440 } else {
441 current.min(new_max)
442 }
443 };
444
445 let mut v = fresh;
446 v.health = scale(self.health, legacy_hp, fresh.health_max);
447 v.mana = scale(self.mana, legacy_mana, fresh.mana_max);
448 v.stamina = scale(self.stamina, legacy_sta, fresh.stamina_max);
449 v.hunger = self.hunger.min(v.hunger_max);
450 v.thirst = self.thirst.min(v.thirst_max);
451 v.coins = self.coins;
452 v.deaths = self.deaths;
453 v.life_state = self.life_state;
454 v
455 }
456}
457
458impl Default for StoredVitalsState {
459 fn default() -> Self {
460 Self::from_live(&PlayerVitals::default())
461 }
462}
463
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
466pub struct RotationPreset {
467 pub id: String,
468 pub label: String,
469 #[serde(default)]
470 pub abilities: Vec<String>,
471}
472
473impl RotationPreset {
474 pub fn melee_default(ability_id: impl Into<String>) -> Self {
475 let id = ability_id.into();
476 Self {
477 id: "melee".into(),
478 label: "Melee".into(),
479 abilities: vec![id],
480 }
481 }
482}
483
484#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
486pub struct StoredTargetSlot {
487 pub instance_id: Option<String>,
488 #[serde(default)]
489 pub preset_id: Option<String>,
490 #[serde(default)]
491 pub rotation_index: u32,
492 #[serde(default)]
493 pub auto_enabled: bool,
494}
495
496#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
497#[serde(default)]
498pub struct StoredCombatProfile {
499 pub combat_target_instance_id: Option<String>,
501 pub in_combat: bool,
502 pub last_combat_tick: u64,
503 pub last_attack_tick: u64,
504 pub cooldowns_until_tick: BTreeMap<String, u64>,
505 #[serde(default = "default_auto_attack")]
506 pub auto_attack_enabled: bool,
507 #[serde(default)]
509 pub mainhand_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 worn: Vec::new(),
543 rotation_presets: Vec::new(),
544 target_slots: Vec::new(),
545 known_blueprint_ids: Vec::new(),
546 keychain: Vec::new(),
547 }
548 }
549}
550
551#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
552pub struct EntityState {
553 pub id: EntityId,
554 pub transform: Transform,
555 #[serde(default)]
557 pub label: String,
558 #[serde(default)]
559 pub vitals: Option<PlayerVitals>,
560 #[serde(default)]
562 pub attributes: Option<PrimaryAttributes>,
563 #[serde(default)]
564 pub skills: Option<PlayerSkills>,
565 #[serde(default)]
567 pub inside_building: Option<String>,
568 #[serde(default)]
570 pub tile_id: Option<String>,
571 #[serde(default)]
573 pub presentation_state: Option<String>,
574 #[serde(default)]
576 pub sprite_mode: Option<String>,
577}
578
579#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
581#[serde(rename_all = "snake_case")]
582pub enum ChatChannel {
583 Nearby,
584 WhisperStone,
585}
586
587#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
588pub struct ChatMessage {
589 pub channel: ChatChannel,
590 pub from_entity: EntityId,
591 pub from_name: String,
592 pub text: String,
593 pub tick: Tick,
594}
595
596#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
598pub enum Intent {
599 Move {
600 entity_id: EntityId,
601 forward: f32,
602 strafe: f32,
603 #[serde(default)]
605 vertical: f32,
606 #[serde(default)]
608 sprint: bool,
609 seq: Seq,
610 },
611 Stop {
612 entity_id: EntityId,
613 seq: Seq,
614 },
615 Harvest {
616 entity_id: EntityId,
617 node_id: String,
618 seq: Seq,
619 },
620 Use {
621 entity_id: EntityId,
622 template_id: String,
623 seq: Seq,
624 },
625 Say {
626 entity_id: EntityId,
627 channel: ChatChannel,
628 text: String,
629 seq: Seq,
630 },
631 Craft {
633 entity_id: EntityId,
634 blueprint_id: String,
635 #[serde(default)]
637 count: Option<u32>,
638 seq: Seq,
639 },
640 Interact {
642 entity_id: EntityId,
643 target_id: String,
644 seq: Seq,
645 },
646 ShopBuy {
648 entity_id: EntityId,
649 npc_id: String,
650 offer_id: String,
651 #[serde(default = "default_one")]
652 quantity: u32,
653 seq: Seq,
654 },
655 ShopSell {
657 entity_id: EntityId,
658 npc_id: String,
659 template_id: String,
660 #[serde(default = "default_one")]
661 quantity: u32,
662 seq: Seq,
663 },
664 TestDamage {
666 entity_id: EntityId,
667 amount: f32,
668 seq: Seq,
669 },
670 SetTarget {
672 entity_id: EntityId,
673 target_id: EntityId,
674 seq: Seq,
675 },
676 SetTargetSlot {
678 entity_id: EntityId,
679 slot_index: u8,
680 target_id: EntityId,
681 seq: Seq,
682 },
683 ClearTarget {
684 entity_id: EntityId,
685 seq: Seq,
686 },
687 ClearTargetSlot {
688 entity_id: EntityId,
689 slot_index: u8,
690 seq: Seq,
691 },
692 SetAutoAttack {
694 entity_id: EntityId,
695 slot_index: u8,
696 enabled: bool,
697 seq: Seq,
698 },
699 Attack {
701 entity_id: EntityId,
702 #[serde(default)]
703 target_id: Option<EntityId>,
704 #[serde(default)]
705 weapon_slot: Option<u32>,
706 seq: Seq,
707 },
708 Pickup {
710 entity_id: EntityId,
711 #[serde(default)]
712 drop_id: Option<String>,
713 seq: Seq,
714 },
715 Cast {
717 entity_id: EntityId,
718 ability_id: String,
719 target_id: EntityId,
720 seq: Seq,
721 },
722 BindActionSlot {
724 entity_id: EntityId,
725 slot_index: u8,
726 ability_id: String,
727 #[serde(default = "default_auto_attack")]
728 auto_enabled: bool,
729 seq: Seq,
730 },
731 UseActionSlot {
733 entity_id: EntityId,
734 slot_index: u8,
735 seq: Seq,
736 },
737 Dodge {
739 entity_id: EntityId,
740 seq: Seq,
741 },
742 Lunge {
744 entity_id: EntityId,
745 #[serde(default)]
747 forward: f32,
748 #[serde(default)]
750 strafe: f32,
751 seq: Seq,
752 },
753 Block {
755 entity_id: EntityId,
756 #[serde(default = "default_block_enabled")]
757 enabled: bool,
758 seq: Seq,
759 },
760 EquipMainhand {
762 entity_id: EntityId,
763 #[serde(default)]
764 template_id: Option<String>,
765 seq: Seq,
766 },
767 EquipWorn {
770 entity_id: EntityId,
771 slot: BodySlot,
772 #[serde(default)]
773 instance_id: Option<Uuid>,
774 seq: Seq,
775 },
776 MoveItem {
778 entity_id: EntityId,
779 item_instance_id: Uuid,
780 from: InventoryLocation,
781 to: InventoryLocation,
782 #[serde(default)]
784 to_parent_instance_id: Option<Uuid>,
785 #[serde(default)]
787 quantity: Option<u32>,
788 seq: Seq,
789 },
790 PlaceContainer {
792 entity_id: EntityId,
793 item_instance_id: Uuid,
794 seq: Seq,
795 },
796 PickupContainer {
798 entity_id: EntityId,
799 container_id: String,
800 seq: Seq,
801 },
802 SetContainerLocked {
804 entity_id: EntityId,
805 location: InventoryLocation,
807 locked: bool,
808 seq: Seq,
809 },
810 DropItem {
812 entity_id: EntityId,
813 item_instance_id: Uuid,
814 from: InventoryLocation,
815 seq: Seq,
816 },
817 DestroyItem {
819 entity_id: EntityId,
820 item_instance_id: Uuid,
821 from: InventoryLocation,
822 #[serde(default)]
824 quantity: Option<u32>,
825 seq: Seq,
826 },
827 RenameContainer {
829 entity_id: EntityId,
830 item_instance_id: Uuid,
831 location: InventoryLocation,
832 name: String,
833 seq: Seq,
834 },
835 UpsertRotationPreset {
837 entity_id: EntityId,
838 preset: RotationPreset,
839 seq: Seq,
840 },
841 DeleteRotationPreset {
843 entity_id: EntityId,
844 preset_id: String,
845 seq: Seq,
846 },
847 AssignSlotPreset {
849 entity_id: EntityId,
850 slot_index: u8,
851 preset_id: String,
852 seq: Seq,
853 },
854 AdvanceRotation {
856 entity_id: EntityId,
857 slot_index: u8,
858 seq: Seq,
859 },
860 NpcTalkOpen {
862 entity_id: EntityId,
863 npc_id: String,
864 seq: Seq,
865 },
866 NpcTalkSay {
868 entity_id: EntityId,
869 npc_id: String,
870 message: String,
871 seq: Seq,
872 },
873 NpcTalkClose {
875 entity_id: EntityId,
876 npc_id: String,
877 seq: Seq,
878 },
879 AcceptQuest {
881 entity_id: EntityId,
882 quest_id: String,
883 seq: Seq,
884 },
885 WithdrawQuest {
887 entity_id: EntityId,
888 quest_id: String,
889 seq: Seq,
890 },
891 TrackQuest {
893 entity_id: EntityId,
894 quest_id: String,
895 seq: Seq,
896 },
897 QuestGiveItem {
899 entity_id: EntityId,
900 npc_id: String,
901 template_id: String,
902 #[serde(default = "default_one")]
903 quantity: u32,
904 seq: Seq,
905 },
906}
907
908fn default_block_enabled() -> bool {
909 true
910}
911
912#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
914pub struct CombatTargetHud {
915 pub entity_id: EntityId,
916 #[serde(default)]
917 pub label: String,
918 #[serde(default)]
919 pub level: u32,
920 pub health: f32,
921 pub health_max: f32,
922 #[serde(default)]
923 pub life_state: LifeState,
924 #[serde(default)]
925 pub distance_m: f32,
926}
927
928#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
930pub struct CastProgressHud {
931 #[serde(default)]
932 pub ability_id: String,
933 #[serde(default)]
934 pub ability_label: String,
935 #[serde(default)]
936 pub ticks_remaining: u64,
937 #[serde(default)]
938 pub ticks_total: u64,
939}
940
941#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
943pub struct AbilityCooldownHud {
944 #[serde(default)]
945 pub ability_id: String,
946 #[serde(default)]
947 pub label: String,
948 #[serde(default)]
949 pub cd_ticks: u64,
950 #[serde(default)]
951 pub cd_total_ticks: u64,
952}
953
954#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
956pub struct CombatSlotHud {
957 pub slot_index: u8,
958 #[serde(default)]
959 pub target_entity_id: Option<EntityId>,
960 #[serde(default)]
961 pub target_label: Option<String>,
962 #[serde(default)]
963 pub target: Option<CombatTargetHud>,
964 #[serde(default)]
965 pub preset_id: Option<String>,
966 #[serde(default)]
967 pub preset_label: Option<String>,
968 #[serde(default)]
969 pub rotation: Vec<String>,
970 #[serde(default)]
971 pub rotation_index: u32,
972 #[serde(default)]
973 pub next_ability_id: Option<String>,
974 #[serde(default)]
975 pub auto_enabled: bool,
976}
977
978#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
980pub struct CombatHud {
981 pub in_combat: bool,
982 pub auto_attack: bool,
984 pub has_los: bool,
985 pub attack_cd_ticks: u64,
986 #[serde(default)]
987 pub ability_id: String,
988 #[serde(default)]
989 pub target_entity_id: Option<EntityId>,
990 #[serde(default)]
991 pub target_label: Option<String>,
992 #[serde(default)]
993 pub max_target_slots: u8,
994 #[serde(default)]
995 pub slots: Vec<CombatSlotHud>,
996 #[serde(default)]
997 pub rotation_presets: Vec<RotationPreset>,
998 #[serde(default)]
999 pub gcd_ticks: u64,
1000 #[serde(default)]
1001 pub mainhand_template_id: Option<String>,
1002 #[serde(default)]
1003 pub mainhand_label: Option<String>,
1004 #[serde(default)]
1006 pub worn: Vec<(BodySlot, ItemStack)>,
1007 #[serde(default)]
1008 pub carry_mass: f32,
1009 #[serde(default)]
1010 pub carry_mass_max: f32,
1011 #[serde(default)]
1012 pub encumbrance: EncumbranceState,
1013 #[serde(default)]
1015 pub keychain: Vec<ItemStack>,
1016 #[serde(default)]
1017 pub target: Option<CombatTargetHud>,
1018 #[serde(default)]
1019 pub cast: Option<CastProgressHud>,
1020 #[serde(default)]
1021 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1022 #[serde(default)]
1023 pub blocking_active: bool,
1024}
1025
1026#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1028pub struct TickDelta {
1029 pub tick: Tick,
1030 pub entities: Vec<EntityState>,
1031 #[serde(default)]
1032 pub resource_nodes: Vec<ResourceNodeView>,
1033 #[serde(default)]
1034 pub buildings: Vec<BuildingView>,
1035 #[serde(default)]
1036 pub doors: Vec<DoorView>,
1037 #[serde(default)]
1038 pub npcs: Vec<NpcView>,
1039 #[serde(default)]
1041 pub inventory: Vec<ItemStack>,
1042 #[serde(default)]
1043 pub blueprints: Vec<BlueprintView>,
1044 #[serde(default)]
1045 pub world_clock: WorldClock,
1046 #[serde(default)]
1047 pub ground_drops: Vec<GroundDropView>,
1048 #[serde(default)]
1049 pub placed_containers: Vec<PlacedContainerView>,
1050 #[serde(default)]
1051 pub combat: Option<CombatHud>,
1052 #[serde(default)]
1053 pub interior_map: Option<InteriorMapView>,
1054 #[serde(default)]
1055 pub quest_log: Vec<QuestLogEntry>,
1056 #[serde(default)]
1057 pub interactables: Vec<InteractableView>,
1058}
1059#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1060pub struct GroundDropView {
1061 pub id: String,
1062 pub template_id: String,
1063 pub quantity: u32,
1064 pub x: f32,
1065 pub y: f32,
1066 pub z: f32,
1067 #[serde(default)]
1069 pub tile_id: Option<String>,
1070}
1071
1072#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1074pub struct Snapshot {
1075 pub tick: Tick,
1076 pub chunk_rev: u64,
1077 #[serde(default)]
1079 pub content_rev: u64,
1080 pub entities: Vec<EntityState>,
1081 #[serde(default)]
1082 pub resource_nodes: Vec<ResourceNodeView>,
1083 #[serde(default)]
1085 pub world_width_m: f32,
1086 #[serde(default)]
1087 pub world_height_m: f32,
1088 #[serde(default)]
1089 pub buildings: Vec<BuildingView>,
1090 #[serde(default)]
1091 pub doors: Vec<DoorView>,
1092 #[serde(default)]
1093 pub npcs: Vec<NpcView>,
1094 #[serde(default)]
1095 pub inventory: Vec<ItemStack>,
1096 #[serde(default)]
1097 pub blueprints: Vec<BlueprintView>,
1098 #[serde(default)]
1099 pub world_clock: WorldClock,
1100 #[serde(default)]
1101 pub terrain_zones: Vec<TerrainZoneView>,
1102 #[serde(default)]
1103 pub z_platforms: Vec<ZPlatformView>,
1104 #[serde(default)]
1105 pub z_transitions: Vec<ZTransitionView>,
1106 #[serde(default)]
1107 pub ground_drops: Vec<GroundDropView>,
1108 #[serde(default)]
1109 pub placed_containers: Vec<PlacedContainerView>,
1110 #[serde(default)]
1111 pub combat: Option<CombatHud>,
1112 #[serde(default)]
1113 pub interior_map: Option<InteriorMapView>,
1114 #[serde(default)]
1115 pub quest_log: Vec<QuestLogEntry>,
1116 #[serde(default)]
1117 pub interactables: Vec<InteractableView>,
1118}
1119
1120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1122pub struct ResourceNodeView {
1123 pub id: String,
1124 pub label: String,
1125 pub x: f32,
1126 pub y: f32,
1127 pub z: f32,
1128 pub item_template: String,
1129 #[serde(default = "default_node_state")]
1130 pub state: ResourceNodeState,
1131 #[serde(default = "default_blocking_view")]
1133 pub blocking: bool,
1134 #[serde(default = "default_blocking_radius_view")]
1136 pub blocking_radius_m: f32,
1137 #[serde(default)]
1139 pub tile_id: Option<String>,
1140 #[serde(default)]
1142 pub sprite_mode: Option<String>,
1143 #[serde(default)]
1145 pub presentation_state: Option<String>,
1146}
1147
1148fn default_blocking_radius_view() -> f32 {
1149 0.8
1150}
1151
1152fn default_blocking_view() -> bool {
1153 true
1154}
1155
1156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1157#[serde(rename_all = "snake_case")]
1158pub enum ResourceNodeState {
1159 Available,
1160 Harvesting,
1161 Cooldown,
1162}
1163
1164fn default_node_state() -> ResourceNodeState {
1165 ResourceNodeState::Available
1166}
1167
1168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1169pub struct ItemStack {
1170 pub template_id: String,
1171 pub quantity: u32,
1172 #[serde(default)]
1174 pub item_instance_id: Option<Uuid>,
1175 #[serde(default)]
1177 pub props: BTreeMap<String, String>,
1178 #[serde(default)]
1180 pub contents: Vec<ItemStack>,
1181 #[serde(default)]
1183 pub display_name: Option<String>,
1184 #[serde(default)]
1186 pub category: Option<String>,
1187 #[serde(default)]
1189 pub base_mass: Option<f32>,
1190 #[serde(default)]
1192 pub base_volume: Option<f32>,
1193 #[serde(default)]
1195 pub capacity_volume: Option<f32>,
1196 #[serde(default)]
1198 pub stackable: Option<bool>,
1199}
1200
1201impl ItemStack {
1202 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
1203 Self {
1204 template_id: template_id.into(),
1205 quantity,
1206 item_instance_id: None,
1207 props: BTreeMap::new(),
1208 contents: Vec::new(),
1209 display_name: None,
1210 category: None,
1211 base_mass: None,
1212 base_volume: None,
1213 capacity_volume: None,
1214 stackable: None,
1215 }
1216 }
1217}
1218
1219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1221#[serde(rename_all = "snake_case")]
1222pub enum EncumbranceState {
1223 #[default]
1224 Light,
1225 Heavy,
1226 Over,
1227}
1228
1229#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
1233#[serde(rename_all = "snake_case")]
1234pub enum BodySlot {
1235 Head,
1236 Body,
1237 Arms,
1238 Legs,
1239 Feet,
1240 Back,
1241 Waist,
1242}
1243
1244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1246#[serde(rename_all = "snake_case")]
1247pub enum InventoryLocation {
1248 Root,
1250 Worn {
1252 slot: BodySlot,
1253 },
1254 Placed {
1256 container_id: String,
1257 },
1258 Keychain,
1260}
1261
1262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1264pub struct PlacedContainerView {
1265 pub id: String,
1266 pub template_id: String,
1267 pub display_name: String,
1268 pub x: f32,
1269 pub y: f32,
1270 pub z: f32,
1271 pub locked: bool,
1272 #[serde(default)]
1274 pub accessible: bool,
1275 #[serde(default)]
1276 pub owner_character_id: Option<Uuid>,
1277 #[serde(default)]
1279 pub contents: Vec<ItemStack>,
1280 #[serde(default)]
1282 pub lock_id: Option<String>,
1283 #[serde(default)]
1285 pub capacity_volume: Option<f32>,
1286 #[serde(default)]
1288 pub item_instance_id: Option<Uuid>,
1289 #[serde(default)]
1291 pub tile_id: Option<String>,
1292}
1293
1294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1295pub struct BlueprintIngredientView {
1296 pub template_id: String,
1297 pub quantity: u32,
1298 pub consumed: bool,
1300}
1301
1302#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1303pub struct ToolRequirementView {
1304 pub item: String,
1305 pub consumed: bool,
1307}
1308
1309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1310pub struct SkillRequirementView {
1311 pub skill: String,
1312 pub level: u32,
1313}
1314
1315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1316pub struct BlueprintView {
1317 pub id: String,
1318 pub label: String,
1319 pub output: String,
1320 pub output_qty: u32,
1321 pub craft_ticks: u32,
1322 pub inputs: Vec<BlueprintIngredientView>,
1323 #[serde(default)]
1325 pub station: Option<String>,
1326 #[serde(default)]
1327 pub category: Option<String>,
1328 #[serde(default)]
1329 pub required_tools: Vec<ToolRequirementView>,
1330 #[serde(default)]
1331 pub skill: Option<SkillRequirementView>,
1332 #[serde(default)]
1333 pub failure_chance: f32,
1334}
1335
1336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1338#[serde(rename_all = "snake_case")]
1339pub enum TerrainKindView {
1340 #[default]
1341 Grass,
1342 Hill,
1343 Bog,
1344 ShallowWater,
1345 DeepWater,
1346 Trail,
1347 Rock,
1348}
1349
1350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1351pub struct TerrainZoneView {
1352 pub id: String,
1353 pub x0: f32,
1354 pub y0: f32,
1355 pub x1: f32,
1356 pub y1: f32,
1357 #[serde(default)]
1358 pub kind: TerrainKindView,
1359 #[serde(default)]
1361 pub elevation: f32,
1362 #[serde(default)]
1365 pub glyph: Option<String>,
1366 #[serde(default)]
1368 pub color: Option<String>,
1369 #[serde(default)]
1371 pub tile_id: Option<String>,
1372}
1373
1374#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1376pub struct ZPlatformView {
1377 pub id: String,
1378 pub z: f32,
1379 pub x0: f32,
1380 pub y0: f32,
1381 pub x1: f32,
1382 pub y1: f32,
1383}
1384
1385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1387pub struct ZTransitionView {
1388 pub id: String,
1389 pub z_from: f32,
1390 pub z_to: f32,
1391 pub x0: f32,
1392 pub y0: f32,
1393 pub x1: f32,
1394 pub y1: f32,
1395}
1396
1397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1398pub struct BuildingView {
1399 pub id: String,
1400 pub label: String,
1401 pub x: f32,
1402 pub y: f32,
1403 pub width_m: f32,
1404 pub depth_m: f32,
1405 #[serde(default)]
1406 pub interior_blueprint: Option<String>,
1407 #[serde(default)]
1408 pub tags: Vec<String>,
1409}
1410
1411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1412pub struct DoorView {
1413 pub id: String,
1414 pub building_id: String,
1415 pub x: f32,
1416 pub y: f32,
1417 #[serde(default)]
1418 pub open: bool,
1419 #[serde(default)]
1420 pub portal: Option<String>,
1421}
1422
1423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1424pub struct InteriorRoomView {
1425 pub id: String,
1426 pub label: String,
1427 pub floor: i32,
1428 pub x0: f32,
1429 pub y0: f32,
1430 pub x1: f32,
1431 pub y1: f32,
1432 #[serde(default)]
1433 pub floor_color: Option<String>,
1434 #[serde(default)]
1435 pub floor_glyph: Option<String>,
1436}
1437
1438#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1439pub struct InteriorDoorView {
1440 pub id: String,
1441 pub room_a: String,
1442 pub room_b: String,
1443 pub x: f32,
1444 pub y: f32,
1445 pub kind: String,
1446}
1447
1448#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1449pub struct InteriorMapView {
1450 pub building_id: String,
1451 pub blueprint_id: String,
1452 pub background_color: String,
1453 #[serde(default)]
1454 pub default_floor_color: Option<String>,
1455 #[serde(default = "default_floor_height_view")]
1456 pub floor_height_m: f32,
1457 pub rooms: Vec<InteriorRoomView>,
1458 #[serde(default)]
1459 pub room_doors: Vec<InteriorDoorView>,
1460}
1461
1462fn default_floor_height_view() -> f32 {
1463 3.0
1464}
1465
1466#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1467pub struct NpcView {
1468 pub id: String,
1469 pub label: String,
1470 pub role: String,
1471 pub x: f32,
1472 pub y: f32,
1473 #[serde(default)]
1475 pub building_id: Option<String>,
1476 #[serde(default)]
1478 pub entity_id: Option<EntityId>,
1479 #[serde(default)]
1480 pub life_state: Option<LifeState>,
1481 #[serde(default)]
1482 pub hp_pct: Option<f32>,
1483 #[serde(default)]
1485 pub can_trade: bool,
1486 #[serde(default)]
1488 pub tile_id: Option<String>,
1489 #[serde(default)]
1491 pub behavior_state: Option<String>,
1492 #[serde(default)]
1494 pub presentation_state: Option<String>,
1495 #[serde(default)]
1497 pub sprite_mode: Option<String>,
1498}
1499
1500#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1501pub struct UseResult {
1502 pub template_id: String,
1503 pub hunger_restored: f32,
1504 pub thirst_restored: f32,
1505 #[serde(default)]
1506 pub health_restored: f32,
1507 #[serde(default)]
1508 pub mana_restored: f32,
1509 #[serde(default)]
1510 pub cleared_dot_ids: Vec<String>,
1511}
1512
1513#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1514pub struct CraftResult {
1515 pub blueprint_id: String,
1516 pub outputs: Vec<ItemStack>,
1517 pub consumed: Vec<ItemStack>,
1518 #[serde(default = "default_one")]
1520 pub batch_index: u32,
1521 #[serde(default = "default_one")]
1523 pub batch_total: u32,
1524}
1525
1526fn default_one() -> u32 {
1527 1
1528}
1529
1530#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1531pub struct DeathNotice {
1532 pub entity_id: EntityId,
1533 pub respawn_x: f32,
1534 pub respawn_y: f32,
1535 pub message: String,
1536}
1537
1538#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1539pub struct InteractionNotice {
1540 pub target_id: String,
1541 pub message: String,
1542 #[serde(default)]
1543 pub coins_delta: i32,
1544 #[serde(default)]
1545 pub inventory_delta: Vec<ItemStack>,
1546}
1547
1548#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1549#[serde(rename_all = "snake_case")]
1550pub enum NpcTalkTrustFlag {
1551 Stranger,
1552 Acquainted,
1553 Trusted,
1554}
1555
1556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1557pub struct NpcTalkOpened {
1558 pub npc_id: String,
1559 pub npc_label: String,
1560 pub greeting: String,
1561 pub trust_flag: NpcTalkTrustFlag,
1562}
1563
1564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1565pub struct NpcTalkPending {
1566 pub npc_id: String,
1567}
1568
1569#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1570pub struct NpcTalkReply {
1571 pub npc_id: String,
1572 pub line: String,
1573 pub trust_flag: NpcTalkTrustFlag,
1574}
1575
1576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1577pub struct NpcTalkClosed {
1578 pub npc_id: String,
1579}
1580
1581#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1582pub struct NpcTalkError {
1583 pub npc_id: String,
1584 pub reason: String,
1585}
1586
1587#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1588#[serde(rename_all = "snake_case")]
1589pub enum QuestStatusView {
1590 Available,
1591 Active,
1592 Completed,
1593}
1594
1595#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1596pub struct QuestObjectiveProgress {
1597 pub label: String,
1598 pub current: u32,
1599 pub required: u32,
1600 pub done: bool,
1601}
1602
1603#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1604pub struct QuestLogEntry {
1605 pub quest_id: String,
1606 pub title: String,
1607 pub description: String,
1608 pub status: QuestStatusView,
1609 #[serde(default)]
1610 pub current_step_id: Option<String>,
1611 #[serde(default)]
1612 pub current_step_title: String,
1613 #[serde(default)]
1614 pub objectives: Vec<QuestObjectiveProgress>,
1615 #[serde(default)]
1616 pub is_tracked: bool,
1617 #[serde(default)]
1618 pub can_withdraw: bool,
1619}
1620
1621#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1622pub struct InteractableView {
1623 pub id: String,
1624 pub kind: String,
1625 pub label: String,
1626 pub x: f32,
1627 pub y: f32,
1628 pub z: f32,
1629 #[serde(default)]
1630 pub board_id: Option<String>,
1631}
1632
1633#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1634pub struct QuestOffer {
1635 pub quest_id: String,
1636 pub title: String,
1637 pub description: String,
1638 #[serde(default)]
1639 pub step_count: u32,
1640}
1641
1642#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1643pub struct QuestNotice {
1644 pub quest_id: String,
1645 pub title: String,
1646 pub message: String,
1647}
1648
1649#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1650#[serde(rename_all = "snake_case")]
1651pub enum ShopOfferKind {
1652 Item,
1653 Blueprint,
1654}
1655
1656#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1657pub struct ShopOffer {
1658 pub offer_id: String,
1659 pub kind: ShopOfferKind,
1660 pub label: String,
1661 #[serde(default)]
1662 pub template_id: Option<String>,
1663 #[serde(default)]
1664 pub blueprint_id: Option<String>,
1665 pub price_copper: u32,
1666 #[serde(default)]
1667 pub affordable: bool,
1668 #[serde(default)]
1669 pub already_owned: bool,
1670}
1671
1672#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1673pub struct ShopBuyLine {
1674 pub template_id: String,
1675 pub label: String,
1676 pub quantity: u32,
1677 pub price_copper: u32,
1678}
1679
1680#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1681pub struct ShopCatalog {
1682 pub npc_id: String,
1683 pub npc_label: String,
1684 #[serde(default)]
1685 pub sells: Vec<ShopOffer>,
1686 #[serde(default)]
1687 pub buys: Vec<ShopBuyLine>,
1688}
1689
1690#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1691pub struct HarvestResult {
1692 pub node_id: String,
1693 pub quantity: u32,
1695 pub item_template: String,
1696 #[serde(default)]
1699 pub item_instance_id: Option<Uuid>,
1700}
1701
1702#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1704pub struct Envelope<T> {
1705 pub protocol_version: u16,
1706 pub payload: T,
1707}
1708
1709impl<T> Envelope<T> {
1710 pub fn new(payload: T) -> Self {
1711 Self {
1712 protocol_version: crate::PROTOCOL_VERSION,
1713 payload,
1714 }
1715 }
1716}
1717
1718#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1720pub struct Hello {
1721 pub client_name: String,
1722 pub protocol_version: u16,
1723 #[serde(default)]
1724 pub auth: AuthCredential,
1725 #[serde(default)]
1727 pub character_id: Option<Uuid>,
1728}
1729
1730#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1733#[serde(rename_all = "snake_case")]
1734pub enum AuthCredential {
1735 DevLocal,
1736 Session { token: String },
1737 ApiToken { token: String, character_id: Uuid },
1738}
1739
1740impl Default for AuthCredential {
1741 fn default() -> Self {
1742 Self::DevLocal
1743 }
1744}
1745
1746#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1747pub struct Welcome {
1748 pub session_id: SessionId,
1749 pub entity_id: EntityId,
1750 pub snapshot: Snapshot,
1751}
1752
1753#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1754pub enum ServerMessage {
1755 Welcome(Welcome),
1756 ContentUpdated(Snapshot),
1758 Tick(TickDelta),
1759 IntentAck {
1760 entity_id: EntityId,
1761 seq: Seq,
1762 tick: Tick,
1763 },
1764 Chat(ChatMessage),
1765 HarvestResult(HarvestResult),
1766 UseResult(UseResult),
1767 CraftResult(CraftResult),
1768 Death(DeathNotice),
1769 Interaction(InteractionNotice),
1770 ShopOpened(ShopCatalog),
1771 NpcTalkOpened(NpcTalkOpened),
1772 NpcTalkPending(NpcTalkPending),
1773 NpcTalkReply(NpcTalkReply),
1774 NpcTalkClosed(NpcTalkClosed),
1775 NpcTalkError(NpcTalkError),
1776 QuestOffer(QuestOffer),
1777 QuestAccepted(QuestNotice),
1778 QuestWithdrawn(QuestNotice),
1779 QuestStepCompleted(QuestNotice),
1780 QuestCompleted(QuestNotice),
1781}
1782
1783#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1784pub enum ClientMessage {
1785 Hello(Hello),
1786 Intent(Intent),
1787 Disconnect,
1788}
1789
1790#[cfg(test)]
1791mod tests {
1792 use super::*;
1793
1794 #[test]
1795 fn pristine_vitals_state_yields_full_pools() {
1796 let attrs = PrimaryAttributes::default();
1797 let vitals = StoredVitalsState::default().apply_to(attrs);
1798 assert!(vitals.health > 0.0);
1799 assert_eq!(vitals.health, vitals.health_max);
1800 assert!((vitals.mana_max - 61.0).abs() < 0.01);
1801 }
1802
1803 #[test]
1804 fn saved_vitals_scale_when_pool_max_increases() {
1805 let mut attrs = PrimaryAttributes::default();
1806 attrs.intelligence = 140;
1807 attrs.wisdom = 140;
1808 let saved = StoredVitalsState {
1809 health: 100.0,
1810 mana: 14.0,
1811 stamina: 100.0,
1812 ..StoredVitalsState::default()
1813 };
1814 let vitals = saved.apply_to(attrs);
1815 assert!(vitals.mana_max > 55.0);
1816 assert!(
1817 (vitals.mana - vitals.mana_max).abs() < 0.01,
1818 "full legacy mana bar migrates to full new bar"
1819 );
1820 }
1821
1822 #[test]
1823 fn empty_vitals_state_is_pristine() {
1824 let pristine = StoredVitalsState {
1825 health: 0.0,
1826 mana: 0.0,
1827 stamina: 0.0,
1828 hunger: 0.0,
1829 thirst: 0.0,
1830 coins: 0,
1831 deaths: 0,
1832 life_state: LifeState::Alive,
1833 };
1834 assert!(pristine.is_pristine());
1835 let vitals = pristine.apply_to(PrimaryAttributes::default());
1836 assert!(vitals.health > 0.0);
1837 }
1838
1839 #[test]
1840 fn stored_vitals_roundtrip_preserves_partial_pools() {
1841 let attrs = PrimaryAttributes::default();
1842 let mut live = PlayerVitals::from_attributes(attrs);
1843 live.health = 25.0;
1844 live.hunger = 77.0;
1845 live.deaths = 2;
1846 let stored = StoredVitalsState::from_live(&live);
1847 let restored = stored.apply_to(attrs);
1848 assert!((restored.health - 25.0).abs() < 0.01, "partial HP below cap stays absolute");
1849 assert_eq!(restored.hunger, 77.0);
1850 assert_eq!(restored.deaths, 2);
1851 }
1852
1853 #[test]
1854 fn skill_tiers_start_at_zero() {
1855 let skill = SkillProgress::default();
1856 assert_eq!(skill.level, 0);
1857 assert_eq!(skill.display_tier(), 0);
1858 let trained = SkillProgress {
1859 level: 250,
1860 last_trained_tick: 1,
1861 };
1862 assert_eq!(trained.display_tier(), 2);
1863 }
1864
1865 #[test]
1866 fn quest_server_messages_roundtrip_json() {
1867 use crate::codec::{Codec, PostcardCodec};
1868
1869 let offer = ServerMessage::QuestOffer(QuestOffer {
1870 quest_id: "ada_goblin_hunt".into(),
1871 title: "Goblin Trouble".into(),
1872 description: "Help Ada".into(),
1873 step_count: 3,
1874 });
1875 let notice = ServerMessage::QuestAccepted(QuestNotice {
1876 quest_id: "ada_goblin_hunt".into(),
1877 title: "Goblin Trouble".into(),
1878 message: "Quest accepted".into(),
1879 });
1880 for msg in [offer, notice] {
1881 let bytes = PostcardCodec.encode(&msg).unwrap();
1882 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
1883 assert_eq!(decoded, msg);
1884 }
1885 }
1886}