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)]
510 pub worn: Vec<(BodySlot, ItemStack)>,
511 #[serde(default)]
513 pub rotation_presets: Vec<RotationPreset>,
514 #[serde(default)]
516 pub target_slots: Vec<StoredTargetSlot>,
517 #[serde(default)]
519 pub known_blueprint_ids: Vec<String>,
520 #[serde(default)]
522 pub keychain: Vec<ItemStack>,
523}
524
525fn default_auto_attack() -> bool {
526 true
527}
528
529impl Default for StoredCombatProfile {
530 fn default() -> Self {
531 Self {
532 combat_target_instance_id: None,
533 in_combat: false,
534 last_combat_tick: 0,
535 last_attack_tick: 0,
536 cooldowns_until_tick: BTreeMap::new(),
537 auto_attack_enabled: true,
538 mainhand_template_id: None,
539 worn: Vec::new(),
540 rotation_presets: Vec::new(),
541 target_slots: Vec::new(),
542 known_blueprint_ids: Vec::new(),
543 keychain: Vec::new(),
544 }
545 }
546}
547
548#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
549pub struct EntityState {
550 pub id: EntityId,
551 pub transform: Transform,
552 #[serde(default)]
554 pub label: String,
555 #[serde(default)]
556 pub vitals: Option<PlayerVitals>,
557 #[serde(default)]
559 pub attributes: Option<PrimaryAttributes>,
560 #[serde(default)]
561 pub skills: Option<PlayerSkills>,
562 #[serde(default)]
564 pub inside_building: Option<String>,
565 #[serde(default)]
567 pub tile_id: Option<String>,
568 #[serde(default)]
570 pub presentation_state: Option<String>,
571 #[serde(default)]
573 pub sprite_mode: Option<String>,
574 #[serde(default)]
576 pub progression_xp: Option<ProgressionXp>,
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 #[serde(default)]
1026 pub progression_xp: Option<ProgressionXp>,
1027 #[serde(default)]
1028 pub progression_baseline: u16,
1029 #[serde(default)]
1030 pub progression_xp_base: f64,
1031 #[serde(default)]
1032 pub progression_xp_growth: f64,
1033 #[serde(default)]
1034 pub attributes: Option<PrimaryAttributes>,
1035 #[serde(default)]
1036 pub skills: Option<PlayerSkills>,
1037}
1038
1039#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1041pub struct TickDelta {
1042 pub tick: Tick,
1043 pub entities: Vec<EntityState>,
1044 #[serde(default)]
1045 pub resource_nodes: Vec<ResourceNodeView>,
1046 #[serde(default)]
1047 pub buildings: Vec<BuildingView>,
1048 #[serde(default)]
1049 pub doors: Vec<DoorView>,
1050 #[serde(default)]
1051 pub npcs: Vec<NpcView>,
1052 #[serde(default)]
1054 pub inventory: Vec<ItemStack>,
1055 #[serde(default)]
1056 pub blueprints: Vec<BlueprintView>,
1057 #[serde(default)]
1058 pub world_clock: WorldClock,
1059 #[serde(default)]
1060 pub ground_drops: Vec<GroundDropView>,
1061 #[serde(default)]
1062 pub placed_containers: Vec<PlacedContainerView>,
1063 #[serde(default)]
1064 pub combat: Option<CombatHud>,
1065 #[serde(default)]
1066 pub interior_map: Option<InteriorMapView>,
1067 #[serde(default)]
1068 pub quest_log: Vec<QuestLogEntry>,
1069 #[serde(default)]
1070 pub interactables: Vec<InteractableView>,
1071}
1072#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1073pub struct GroundDropView {
1074 pub id: String,
1075 pub template_id: String,
1076 pub quantity: u32,
1077 pub x: f32,
1078 pub y: f32,
1079 pub z: f32,
1080 #[serde(default)]
1082 pub tile_id: Option<String>,
1083}
1084
1085#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1087pub struct Snapshot {
1088 pub tick: Tick,
1089 pub chunk_rev: u64,
1090 #[serde(default)]
1092 pub content_rev: u64,
1093 pub entities: Vec<EntityState>,
1094 #[serde(default)]
1095 pub resource_nodes: Vec<ResourceNodeView>,
1096 #[serde(default)]
1098 pub world_width_m: f32,
1099 #[serde(default)]
1100 pub world_height_m: f32,
1101 #[serde(default)]
1102 pub buildings: Vec<BuildingView>,
1103 #[serde(default)]
1104 pub doors: Vec<DoorView>,
1105 #[serde(default)]
1106 pub npcs: Vec<NpcView>,
1107 #[serde(default)]
1108 pub inventory: Vec<ItemStack>,
1109 #[serde(default)]
1110 pub blueprints: Vec<BlueprintView>,
1111 #[serde(default)]
1112 pub world_clock: WorldClock,
1113 #[serde(default)]
1114 pub terrain_zones: Vec<TerrainZoneView>,
1115 #[serde(default)]
1116 pub z_platforms: Vec<ZPlatformView>,
1117 #[serde(default)]
1118 pub z_transitions: Vec<ZTransitionView>,
1119 #[serde(default)]
1120 pub ground_drops: Vec<GroundDropView>,
1121 #[serde(default)]
1122 pub placed_containers: Vec<PlacedContainerView>,
1123 #[serde(default)]
1124 pub combat: Option<CombatHud>,
1125 #[serde(default)]
1126 pub interior_map: Option<InteriorMapView>,
1127 #[serde(default)]
1128 pub quest_log: Vec<QuestLogEntry>,
1129 #[serde(default)]
1130 pub interactables: Vec<InteractableView>,
1131}
1132
1133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1135pub struct ResourceNodeView {
1136 pub id: String,
1137 pub label: String,
1138 pub x: f32,
1139 pub y: f32,
1140 pub z: f32,
1141 pub item_template: String,
1142 #[serde(default = "default_node_state")]
1143 pub state: ResourceNodeState,
1144 #[serde(default = "default_blocking_view")]
1146 pub blocking: bool,
1147 #[serde(default = "default_blocking_radius_view")]
1149 pub blocking_radius_m: f32,
1150 #[serde(default)]
1152 pub tile_id: Option<String>,
1153 #[serde(default)]
1155 pub sprite_mode: Option<String>,
1156 #[serde(default)]
1158 pub presentation_state: Option<String>,
1159}
1160
1161fn default_blocking_radius_view() -> f32 {
1162 0.8
1163}
1164
1165fn default_blocking_view() -> bool {
1166 true
1167}
1168
1169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1170#[serde(rename_all = "snake_case")]
1171pub enum ResourceNodeState {
1172 Available,
1173 Harvesting,
1174 Cooldown,
1175}
1176
1177fn default_node_state() -> ResourceNodeState {
1178 ResourceNodeState::Available
1179}
1180
1181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1182pub struct ItemStack {
1183 pub template_id: String,
1184 pub quantity: u32,
1185 #[serde(default)]
1187 pub item_instance_id: Option<Uuid>,
1188 #[serde(default)]
1190 pub props: BTreeMap<String, String>,
1191 #[serde(default)]
1193 pub contents: Vec<ItemStack>,
1194 #[serde(default)]
1196 pub display_name: Option<String>,
1197 #[serde(default)]
1199 pub category: Option<String>,
1200 #[serde(default)]
1202 pub base_mass: Option<f32>,
1203 #[serde(default)]
1205 pub base_volume: Option<f32>,
1206 #[serde(default)]
1208 pub capacity_volume: Option<f32>,
1209 #[serde(default)]
1211 pub stackable: Option<bool>,
1212}
1213
1214impl ItemStack {
1215 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
1216 Self {
1217 template_id: template_id.into(),
1218 quantity,
1219 item_instance_id: None,
1220 props: BTreeMap::new(),
1221 contents: Vec::new(),
1222 display_name: None,
1223 category: None,
1224 base_mass: None,
1225 base_volume: None,
1226 capacity_volume: None,
1227 stackable: None,
1228 }
1229 }
1230}
1231
1232#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1234#[serde(rename_all = "snake_case")]
1235pub enum EncumbranceState {
1236 #[default]
1237 Light,
1238 Heavy,
1239 Over,
1240}
1241
1242#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
1246#[serde(rename_all = "snake_case")]
1247pub enum BodySlot {
1248 Head,
1249 Body,
1250 Arms,
1251 Legs,
1252 Feet,
1253 Back,
1254 Waist,
1255}
1256
1257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1259#[serde(rename_all = "snake_case")]
1260pub enum InventoryLocation {
1261 Root,
1263 Worn { slot: BodySlot },
1265 Placed { container_id: String },
1267 Keychain,
1269}
1270
1271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1273pub struct PlacedContainerView {
1274 pub id: String,
1275 pub template_id: String,
1276 pub display_name: String,
1277 pub x: f32,
1278 pub y: f32,
1279 pub z: f32,
1280 pub locked: bool,
1281 #[serde(default)]
1283 pub accessible: bool,
1284 #[serde(default)]
1285 pub owner_character_id: Option<Uuid>,
1286 #[serde(default)]
1288 pub contents: Vec<ItemStack>,
1289 #[serde(default)]
1291 pub lock_id: Option<String>,
1292 #[serde(default)]
1294 pub capacity_volume: Option<f32>,
1295 #[serde(default)]
1297 pub item_instance_id: Option<Uuid>,
1298 #[serde(default)]
1300 pub tile_id: Option<String>,
1301}
1302
1303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1304pub struct BlueprintIngredientView {
1305 pub template_id: String,
1306 pub quantity: u32,
1307 pub consumed: bool,
1309}
1310
1311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1312pub struct ToolRequirementView {
1313 pub item: String,
1314 pub consumed: bool,
1316}
1317
1318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1319pub struct SkillRequirementView {
1320 pub skill: String,
1321 pub level: u32,
1322}
1323
1324#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1325pub struct BlueprintView {
1326 pub id: String,
1327 pub label: String,
1328 pub output: String,
1329 pub output_qty: u32,
1330 pub craft_ticks: u32,
1331 pub inputs: Vec<BlueprintIngredientView>,
1332 #[serde(default)]
1334 pub station: Option<String>,
1335 #[serde(default)]
1336 pub category: Option<String>,
1337 #[serde(default)]
1338 pub required_tools: Vec<ToolRequirementView>,
1339 #[serde(default)]
1340 pub skill: Option<SkillRequirementView>,
1341 #[serde(default)]
1342 pub failure_chance: f32,
1343}
1344
1345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1347#[serde(rename_all = "snake_case")]
1348pub enum TerrainKindView {
1349 #[default]
1350 Grass,
1351 Hill,
1352 Bog,
1353 ShallowWater,
1354 DeepWater,
1355 Trail,
1356 Rock,
1357}
1358
1359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1360pub struct TerrainZoneView {
1361 pub id: String,
1362 pub x0: f32,
1363 pub y0: f32,
1364 pub x1: f32,
1365 pub y1: f32,
1366 #[serde(default)]
1367 pub kind: TerrainKindView,
1368 #[serde(default)]
1370 pub elevation: f32,
1371 #[serde(default)]
1374 pub glyph: Option<String>,
1375 #[serde(default)]
1377 pub color: Option<String>,
1378 #[serde(default)]
1380 pub tile_id: Option<String>,
1381}
1382
1383#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1385pub struct ZPlatformView {
1386 pub id: String,
1387 pub z: f32,
1388 pub x0: f32,
1389 pub y0: f32,
1390 pub x1: f32,
1391 pub y1: f32,
1392}
1393
1394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1396pub struct ZTransitionView {
1397 pub id: String,
1398 pub z_from: f32,
1399 pub z_to: f32,
1400 pub x0: f32,
1401 pub y0: f32,
1402 pub x1: f32,
1403 pub y1: f32,
1404}
1405
1406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1407pub struct BuildingView {
1408 pub id: String,
1409 pub label: String,
1410 pub x: f32,
1411 pub y: f32,
1412 pub width_m: f32,
1413 pub depth_m: f32,
1414 #[serde(default)]
1415 pub interior_blueprint: Option<String>,
1416 #[serde(default)]
1417 pub tags: Vec<String>,
1418}
1419
1420#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1421pub struct DoorView {
1422 pub id: String,
1423 pub building_id: String,
1424 pub x: f32,
1425 pub y: f32,
1426 #[serde(default)]
1427 pub open: bool,
1428 #[serde(default)]
1429 pub portal: Option<String>,
1430}
1431
1432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1433pub struct InteriorRoomView {
1434 pub id: String,
1435 pub label: String,
1436 pub floor: i32,
1437 pub x0: f32,
1438 pub y0: f32,
1439 pub x1: f32,
1440 pub y1: f32,
1441 #[serde(default)]
1442 pub floor_color: Option<String>,
1443 #[serde(default)]
1444 pub floor_glyph: Option<String>,
1445}
1446
1447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1448pub struct InteriorDoorView {
1449 pub id: String,
1450 pub room_a: String,
1451 pub room_b: String,
1452 pub x: f32,
1453 pub y: f32,
1454 pub kind: String,
1455}
1456
1457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1458pub struct InteriorMapView {
1459 pub building_id: String,
1460 pub blueprint_id: String,
1461 pub background_color: String,
1462 #[serde(default)]
1463 pub default_floor_color: Option<String>,
1464 #[serde(default = "default_floor_height_view")]
1465 pub floor_height_m: f32,
1466 pub rooms: Vec<InteriorRoomView>,
1467 #[serde(default)]
1468 pub room_doors: Vec<InteriorDoorView>,
1469}
1470
1471fn default_floor_height_view() -> f32 {
1472 3.0
1473}
1474
1475#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1476pub struct NpcView {
1477 pub id: String,
1478 pub label: String,
1479 pub role: String,
1480 pub x: f32,
1481 pub y: f32,
1482 #[serde(default)]
1484 pub building_id: Option<String>,
1485 #[serde(default)]
1487 pub entity_id: Option<EntityId>,
1488 #[serde(default)]
1489 pub life_state: Option<LifeState>,
1490 #[serde(default)]
1491 pub hp_pct: Option<f32>,
1492 #[serde(default)]
1494 pub can_trade: bool,
1495 #[serde(default)]
1497 pub tile_id: Option<String>,
1498 #[serde(default)]
1500 pub behavior_state: Option<String>,
1501 #[serde(default)]
1503 pub presentation_state: Option<String>,
1504 #[serde(default)]
1506 pub sprite_mode: Option<String>,
1507}
1508
1509#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1510pub struct UseResult {
1511 pub template_id: String,
1512 pub hunger_restored: f32,
1513 pub thirst_restored: f32,
1514 #[serde(default)]
1515 pub health_restored: f32,
1516 #[serde(default)]
1517 pub mana_restored: f32,
1518 #[serde(default)]
1519 pub cleared_dot_ids: Vec<String>,
1520}
1521
1522#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1523pub struct CraftResult {
1524 pub blueprint_id: String,
1525 pub outputs: Vec<ItemStack>,
1526 pub consumed: Vec<ItemStack>,
1527 #[serde(default = "default_one")]
1529 pub batch_index: u32,
1530 #[serde(default = "default_one")]
1532 pub batch_total: u32,
1533}
1534
1535fn default_one() -> u32 {
1536 1
1537}
1538
1539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1540pub struct DeathNotice {
1541 pub entity_id: EntityId,
1542 pub respawn_x: f32,
1543 pub respawn_y: f32,
1544 pub message: String,
1545}
1546
1547#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1548pub struct InteractionNotice {
1549 pub target_id: String,
1550 pub message: String,
1551 #[serde(default)]
1552 pub coins_delta: i32,
1553 #[serde(default)]
1554 pub inventory_delta: Vec<ItemStack>,
1555}
1556
1557#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1558#[serde(rename_all = "snake_case")]
1559pub enum NpcTalkTrustFlag {
1560 Stranger,
1561 Acquainted,
1562 Trusted,
1563}
1564
1565#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1566pub struct NpcTalkOpened {
1567 pub npc_id: String,
1568 pub npc_label: String,
1569 pub greeting: String,
1570 pub trust_flag: NpcTalkTrustFlag,
1571}
1572
1573#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1574pub struct NpcTalkPending {
1575 pub npc_id: String,
1576}
1577
1578#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1579pub struct NpcTalkReply {
1580 pub npc_id: String,
1581 pub line: String,
1582 pub trust_flag: NpcTalkTrustFlag,
1583}
1584
1585#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1586pub struct NpcTalkClosed {
1587 pub npc_id: String,
1588}
1589
1590#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1591pub struct NpcTalkError {
1592 pub npc_id: String,
1593 pub reason: String,
1594}
1595
1596#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1597#[serde(rename_all = "snake_case")]
1598pub enum QuestStatusView {
1599 Available,
1600 Active,
1601 Completed,
1602}
1603
1604#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1605pub struct QuestObjectiveProgress {
1606 pub label: String,
1607 pub current: u32,
1608 pub required: u32,
1609 pub done: bool,
1610}
1611
1612#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1613pub struct QuestLogEntry {
1614 pub quest_id: String,
1615 pub title: String,
1616 pub description: String,
1617 pub status: QuestStatusView,
1618 #[serde(default)]
1619 pub current_step_id: Option<String>,
1620 #[serde(default)]
1621 pub current_step_title: String,
1622 #[serde(default)]
1623 pub objectives: Vec<QuestObjectiveProgress>,
1624 #[serde(default)]
1625 pub is_tracked: bool,
1626 #[serde(default)]
1627 pub can_withdraw: bool,
1628}
1629
1630#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1631pub struct InteractableView {
1632 pub id: String,
1633 pub kind: String,
1634 pub label: String,
1635 pub x: f32,
1636 pub y: f32,
1637 pub z: f32,
1638 #[serde(default)]
1639 pub board_id: Option<String>,
1640}
1641
1642#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1643pub struct QuestOffer {
1644 pub quest_id: String,
1645 pub title: String,
1646 pub description: String,
1647 #[serde(default)]
1648 pub step_count: u32,
1649}
1650
1651#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1652pub struct QuestNotice {
1653 pub quest_id: String,
1654 pub title: String,
1655 pub message: String,
1656}
1657
1658#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1659#[serde(rename_all = "snake_case")]
1660pub enum ShopOfferKind {
1661 Item,
1662 Blueprint,
1663}
1664
1665#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1666pub struct ShopOffer {
1667 pub offer_id: String,
1668 pub kind: ShopOfferKind,
1669 pub label: String,
1670 #[serde(default)]
1671 pub template_id: Option<String>,
1672 #[serde(default)]
1673 pub blueprint_id: Option<String>,
1674 pub price_copper: u32,
1675 #[serde(default)]
1676 pub affordable: bool,
1677 #[serde(default)]
1678 pub already_owned: bool,
1679}
1680
1681#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1682pub struct ShopBuyLine {
1683 pub template_id: String,
1684 pub label: String,
1685 pub quantity: u32,
1686 pub price_copper: u32,
1687}
1688
1689#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1690pub struct ShopCatalog {
1691 pub npc_id: String,
1692 pub npc_label: String,
1693 #[serde(default)]
1694 pub sells: Vec<ShopOffer>,
1695 #[serde(default)]
1696 pub buys: Vec<ShopBuyLine>,
1697}
1698
1699#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1700pub struct HarvestResult {
1701 pub node_id: String,
1702 pub quantity: u32,
1704 pub item_template: String,
1705 #[serde(default)]
1708 pub item_instance_id: Option<Uuid>,
1709}
1710
1711#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1713pub struct Envelope<T> {
1714 pub protocol_version: u16,
1715 pub payload: T,
1716}
1717
1718impl<T> Envelope<T> {
1719 pub fn new(payload: T) -> Self {
1720 Self {
1721 protocol_version: crate::PROTOCOL_VERSION,
1722 payload,
1723 }
1724 }
1725}
1726
1727#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1729pub struct Hello {
1730 pub client_name: String,
1731 pub protocol_version: u16,
1732 #[serde(default)]
1733 pub auth: AuthCredential,
1734 #[serde(default)]
1736 pub character_id: Option<Uuid>,
1737}
1738
1739#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1742#[serde(rename_all = "snake_case")]
1743pub enum AuthCredential {
1744 DevLocal,
1745 Session { token: String },
1746 ApiToken { token: String, character_id: Uuid },
1747}
1748
1749impl Default for AuthCredential {
1750 fn default() -> Self {
1751 Self::DevLocal
1752 }
1753}
1754
1755#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1756pub struct Welcome {
1757 pub session_id: SessionId,
1758 pub entity_id: EntityId,
1759 pub snapshot: Snapshot,
1760}
1761
1762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1763pub enum ServerMessage {
1764 Welcome(Welcome),
1765 ContentUpdated(Snapshot),
1767 Tick(TickDelta),
1768 IntentAck {
1769 entity_id: EntityId,
1770 seq: Seq,
1771 tick: Tick,
1772 },
1773 Chat(ChatMessage),
1774 HarvestResult(HarvestResult),
1775 UseResult(UseResult),
1776 CraftResult(CraftResult),
1777 Death(DeathNotice),
1778 Interaction(InteractionNotice),
1779 ShopOpened(ShopCatalog),
1780 NpcTalkOpened(NpcTalkOpened),
1781 NpcTalkPending(NpcTalkPending),
1782 NpcTalkReply(NpcTalkReply),
1783 NpcTalkClosed(NpcTalkClosed),
1784 NpcTalkError(NpcTalkError),
1785 QuestOffer(QuestOffer),
1786 QuestAccepted(QuestNotice),
1787 QuestWithdrawn(QuestNotice),
1788 QuestStepCompleted(QuestNotice),
1789 QuestCompleted(QuestNotice),
1790}
1791
1792#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1793pub enum ClientMessage {
1794 Hello(Hello),
1795 Intent(Intent),
1796 Disconnect,
1797}
1798
1799#[cfg(test)]
1800mod tests {
1801 use super::*;
1802
1803 #[test]
1804 fn pristine_vitals_state_yields_full_pools() {
1805 let attrs = PrimaryAttributes::default();
1806 let vitals = StoredVitalsState::default().apply_to(attrs);
1807 assert!(vitals.health > 0.0);
1808 assert_eq!(vitals.health, vitals.health_max);
1809 assert!((vitals.mana_max - 61.0).abs() < 0.01);
1810 }
1811
1812 #[test]
1813 fn saved_vitals_scale_when_pool_max_increases() {
1814 let mut attrs = PrimaryAttributes::default();
1815 attrs.intelligence = 140;
1816 attrs.wisdom = 140;
1817 let saved = StoredVitalsState {
1818 health: 100.0,
1819 mana: 14.0,
1820 stamina: 100.0,
1821 ..StoredVitalsState::default()
1822 };
1823 let vitals = saved.apply_to(attrs);
1824 assert!(vitals.mana_max > 55.0);
1825 assert!(
1826 (vitals.mana - vitals.mana_max).abs() < 0.01,
1827 "full legacy mana bar migrates to full new bar"
1828 );
1829 }
1830
1831 #[test]
1832 fn empty_vitals_state_is_pristine() {
1833 let pristine = StoredVitalsState {
1834 health: 0.0,
1835 mana: 0.0,
1836 stamina: 0.0,
1837 hunger: 0.0,
1838 thirst: 0.0,
1839 coins: 0,
1840 deaths: 0,
1841 life_state: LifeState::Alive,
1842 };
1843 assert!(pristine.is_pristine());
1844 let vitals = pristine.apply_to(PrimaryAttributes::default());
1845 assert!(vitals.health > 0.0);
1846 }
1847
1848 #[test]
1849 fn stored_vitals_roundtrip_preserves_partial_pools() {
1850 let attrs = PrimaryAttributes::default();
1851 let mut live = PlayerVitals::from_attributes(attrs);
1852 live.health = 25.0;
1853 live.hunger = 77.0;
1854 live.deaths = 2;
1855 let stored = StoredVitalsState::from_live(&live);
1856 let restored = stored.apply_to(attrs);
1857 assert!(
1858 (restored.health - 25.0).abs() < 0.01,
1859 "partial HP below cap stays absolute"
1860 );
1861 assert_eq!(restored.hunger, 77.0);
1862 assert_eq!(restored.deaths, 2);
1863 }
1864
1865 #[test]
1866 fn skill_tiers_start_at_zero() {
1867 let skill = SkillProgress::default();
1868 assert_eq!(skill.level, 0);
1869 assert_eq!(skill.display_tier(), 0);
1870 let trained = SkillProgress {
1871 level: 250,
1872 last_trained_tick: 1,
1873 };
1874 assert_eq!(trained.display_tier(), 2);
1875 }
1876
1877 #[test]
1878 fn quest_server_messages_roundtrip_json() {
1879 use crate::codec::{Codec, PostcardCodec};
1880
1881 let offer = ServerMessage::QuestOffer(QuestOffer {
1882 quest_id: "ada_goblin_hunt".into(),
1883 title: "Goblin Trouble".into(),
1884 description: "Help Ada".into(),
1885 step_count: 3,
1886 });
1887 let notice = ServerMessage::QuestAccepted(QuestNotice {
1888 quest_id: "ada_goblin_hunt".into(),
1889 title: "Goblin Trouble".into(),
1890 message: "Quest accepted".into(),
1891 });
1892 for msg in [offer, notice] {
1893 let bytes = PostcardCodec.encode(&msg).unwrap();
1894 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
1895 assert_eq!(decoded, msg);
1896 }
1897 }
1898}