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