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 DirectionalJump {
755 entity_id: EntityId,
756 #[serde(default)]
758 forward: f32,
759 #[serde(default)]
761 strafe: f32,
762 seq: Seq,
763 },
764 Block {
766 entity_id: EntityId,
767 #[serde(default = "default_block_enabled")]
768 enabled: bool,
769 seq: Seq,
770 },
771 EquipMainhand {
773 entity_id: EntityId,
774 #[serde(default)]
775 template_id: Option<String>,
776 seq: Seq,
777 },
778 EquipWorn {
781 entity_id: EntityId,
782 slot: BodySlot,
783 #[serde(default)]
784 instance_id: Option<Uuid>,
785 seq: Seq,
786 },
787 MoveItem {
789 entity_id: EntityId,
790 item_instance_id: Uuid,
791 from: InventoryLocation,
792 to: InventoryLocation,
793 #[serde(default)]
795 to_parent_instance_id: Option<Uuid>,
796 #[serde(default)]
798 quantity: Option<u32>,
799 seq: Seq,
800 },
801 PlaceContainer {
803 entity_id: EntityId,
804 item_instance_id: Uuid,
805 seq: Seq,
806 },
807 PickupContainer {
809 entity_id: EntityId,
810 container_id: String,
811 seq: Seq,
812 },
813 SetContainerLocked {
815 entity_id: EntityId,
816 location: InventoryLocation,
818 locked: bool,
819 seq: Seq,
820 },
821 DropItem {
823 entity_id: EntityId,
824 item_instance_id: Uuid,
825 from: InventoryLocation,
826 seq: Seq,
827 },
828 DestroyItem {
830 entity_id: EntityId,
831 item_instance_id: Uuid,
832 from: InventoryLocation,
833 #[serde(default)]
835 quantity: Option<u32>,
836 seq: Seq,
837 },
838 RenameContainer {
840 entity_id: EntityId,
841 item_instance_id: Uuid,
842 location: InventoryLocation,
843 name: String,
844 seq: Seq,
845 },
846 UpsertRotationPreset {
848 entity_id: EntityId,
849 preset: RotationPreset,
850 seq: Seq,
851 },
852 DeleteRotationPreset {
854 entity_id: EntityId,
855 preset_id: String,
856 seq: Seq,
857 },
858 AssignSlotPreset {
860 entity_id: EntityId,
861 slot_index: u8,
862 preset_id: String,
863 seq: Seq,
864 },
865 AdvanceRotation {
867 entity_id: EntityId,
868 slot_index: u8,
869 seq: Seq,
870 },
871 NpcTalkOpen {
873 entity_id: EntityId,
874 npc_id: String,
875 seq: Seq,
876 },
877 NpcTalkSay {
879 entity_id: EntityId,
880 npc_id: String,
881 message: String,
882 seq: Seq,
883 },
884 NpcTalkClose {
886 entity_id: EntityId,
887 npc_id: String,
888 seq: Seq,
889 },
890 AcceptQuest {
892 entity_id: EntityId,
893 quest_id: String,
894 seq: Seq,
895 },
896 WithdrawQuest {
898 entity_id: EntityId,
899 quest_id: String,
900 seq: Seq,
901 },
902 TrackQuest {
904 entity_id: EntityId,
905 quest_id: String,
906 seq: Seq,
907 },
908 QuestGiveItem {
910 entity_id: EntityId,
911 npc_id: String,
912 template_id: String,
913 #[serde(default = "default_one")]
914 quantity: u32,
915 seq: Seq,
916 },
917}
918
919fn default_block_enabled() -> bool {
920 true
921}
922
923#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
925pub struct CombatTargetHud {
926 pub entity_id: EntityId,
927 #[serde(default)]
928 pub label: String,
929 #[serde(default)]
930 pub level: u32,
931 pub health: f32,
932 pub health_max: f32,
933 #[serde(default)]
934 pub life_state: LifeState,
935 #[serde(default)]
936 pub distance_m: f32,
937}
938
939#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
941pub struct CastProgressHud {
942 #[serde(default)]
943 pub ability_id: String,
944 #[serde(default)]
945 pub ability_label: String,
946 #[serde(default)]
947 pub ticks_remaining: u64,
948 #[serde(default)]
949 pub ticks_total: u64,
950}
951
952#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
954pub struct AbilityCooldownHud {
955 #[serde(default)]
956 pub ability_id: String,
957 #[serde(default)]
958 pub label: String,
959 #[serde(default)]
960 pub cd_ticks: u64,
961 #[serde(default)]
962 pub cd_total_ticks: u64,
963}
964
965#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
967pub struct CombatSlotHud {
968 pub slot_index: u8,
969 #[serde(default)]
970 pub target_entity_id: Option<EntityId>,
971 #[serde(default)]
972 pub target_label: Option<String>,
973 #[serde(default)]
974 pub target: Option<CombatTargetHud>,
975 #[serde(default)]
976 pub preset_id: Option<String>,
977 #[serde(default)]
978 pub preset_label: Option<String>,
979 #[serde(default)]
980 pub rotation: Vec<String>,
981 #[serde(default)]
982 pub rotation_index: u32,
983 #[serde(default)]
984 pub next_ability_id: Option<String>,
985 #[serde(default)]
986 pub auto_enabled: bool,
987}
988
989#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
991pub struct CombatHud {
992 pub in_combat: bool,
993 pub auto_attack: bool,
995 pub has_los: bool,
996 pub attack_cd_ticks: u64,
997 #[serde(default)]
998 pub ability_id: String,
999 #[serde(default)]
1000 pub target_entity_id: Option<EntityId>,
1001 #[serde(default)]
1002 pub target_label: Option<String>,
1003 #[serde(default)]
1004 pub max_target_slots: u8,
1005 #[serde(default)]
1006 pub slots: Vec<CombatSlotHud>,
1007 #[serde(default)]
1008 pub rotation_presets: Vec<RotationPreset>,
1009 #[serde(default)]
1010 pub gcd_ticks: u64,
1011 #[serde(default)]
1012 pub mainhand_template_id: Option<String>,
1013 #[serde(default)]
1014 pub mainhand_label: Option<String>,
1015 #[serde(default)]
1017 pub worn: Vec<(BodySlot, ItemStack)>,
1018 #[serde(default)]
1019 pub carry_mass: f32,
1020 #[serde(default)]
1021 pub carry_mass_max: f32,
1022 #[serde(default)]
1023 pub encumbrance: EncumbranceState,
1024 #[serde(default)]
1026 pub keychain: Vec<ItemStack>,
1027 #[serde(default)]
1028 pub target: Option<CombatTargetHud>,
1029 #[serde(default)]
1030 pub cast: Option<CastProgressHud>,
1031 #[serde(default)]
1032 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1033 #[serde(default)]
1034 pub blocking_active: bool,
1035 #[serde(default)]
1037 pub progression_xp: Option<ProgressionXp>,
1038 #[serde(default)]
1039 pub progression_baseline: u16,
1040 #[serde(default)]
1041 pub progression_xp_base: f64,
1042 #[serde(default)]
1043 pub progression_xp_growth: f64,
1044 #[serde(default)]
1045 pub attributes: Option<PrimaryAttributes>,
1046 #[serde(default)]
1047 pub skills: Option<PlayerSkills>,
1048}
1049
1050#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1052pub struct TickDelta {
1053 pub tick: Tick,
1054 pub entities: Vec<EntityState>,
1055 #[serde(default)]
1056 pub resource_nodes: Vec<ResourceNodeView>,
1057 #[serde(default)]
1058 pub buildings: Vec<BuildingView>,
1059 #[serde(default)]
1060 pub doors: Vec<DoorView>,
1061 #[serde(default)]
1062 pub npcs: Vec<NpcView>,
1063 #[serde(default)]
1065 pub inventory: Vec<ItemStack>,
1066 #[serde(default)]
1067 pub blueprints: Vec<BlueprintView>,
1068 #[serde(default)]
1069 pub world_clock: WorldClock,
1070 #[serde(default)]
1071 pub ground_drops: Vec<GroundDropView>,
1072 #[serde(default)]
1073 pub placed_containers: Vec<PlacedContainerView>,
1074 #[serde(default)]
1075 pub combat: Option<CombatHud>,
1076 #[serde(default)]
1077 pub interior_map: Option<InteriorMapView>,
1078 #[serde(default)]
1079 pub quest_log: Vec<QuestLogEntry>,
1080 #[serde(default)]
1081 pub interactables: Vec<InteractableView>,
1082}
1083#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1084pub struct GroundDropView {
1085 pub id: String,
1086 pub template_id: String,
1087 pub quantity: u32,
1088 pub x: f32,
1089 pub y: f32,
1090 pub z: f32,
1091 #[serde(default)]
1093 pub tile_id: Option<String>,
1094}
1095
1096#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1098pub struct Snapshot {
1099 pub tick: Tick,
1100 pub chunk_rev: u64,
1101 #[serde(default)]
1103 pub content_rev: u64,
1104 #[serde(default)]
1106 pub publish_rev: u64,
1107 pub entities: Vec<EntityState>,
1108 #[serde(default)]
1109 pub resource_nodes: Vec<ResourceNodeView>,
1110 #[serde(default)]
1112 pub world_width_m: f32,
1113 #[serde(default)]
1114 pub world_height_m: f32,
1115 #[serde(default)]
1116 pub buildings: Vec<BuildingView>,
1117 #[serde(default)]
1118 pub doors: Vec<DoorView>,
1119 #[serde(default)]
1120 pub npcs: Vec<NpcView>,
1121 #[serde(default)]
1122 pub inventory: Vec<ItemStack>,
1123 #[serde(default)]
1124 pub blueprints: Vec<BlueprintView>,
1125 #[serde(default)]
1126 pub world_clock: WorldClock,
1127 #[serde(default)]
1128 pub terrain_zones: Vec<TerrainZoneView>,
1129 #[serde(default)]
1130 pub z_platforms: Vec<ZPlatformView>,
1131 #[serde(default)]
1132 pub z_transitions: Vec<ZTransitionView>,
1133 #[serde(default)]
1134 pub ground_drops: Vec<GroundDropView>,
1135 #[serde(default)]
1136 pub placed_containers: Vec<PlacedContainerView>,
1137 #[serde(default)]
1138 pub combat: Option<CombatHud>,
1139 #[serde(default)]
1140 pub interior_map: Option<InteriorMapView>,
1141 #[serde(default)]
1142 pub quest_log: Vec<QuestLogEntry>,
1143 #[serde(default)]
1144 pub interactables: Vec<InteractableView>,
1145}
1146
1147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1149pub struct ResourceNodeView {
1150 pub id: String,
1151 pub label: String,
1152 pub x: f32,
1153 pub y: f32,
1154 pub z: f32,
1155 pub item_template: String,
1156 #[serde(default = "default_node_state")]
1157 pub state: ResourceNodeState,
1158 #[serde(default = "default_blocking_view")]
1160 pub blocking: bool,
1161 #[serde(default = "default_blocking_radius_view")]
1163 pub blocking_radius_m: f32,
1164 #[serde(default)]
1166 pub tile_id: Option<String>,
1167 #[serde(default)]
1169 pub sprite_mode: Option<String>,
1170 #[serde(default)]
1172 pub presentation_state: Option<String>,
1173}
1174
1175fn default_blocking_radius_view() -> f32 {
1176 0.8
1177}
1178
1179fn default_blocking_view() -> bool {
1180 true
1181}
1182
1183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1184#[serde(rename_all = "snake_case")]
1185pub enum ResourceNodeState {
1186 Available,
1187 Harvesting,
1188 Cooldown,
1189}
1190
1191fn default_node_state() -> ResourceNodeState {
1192 ResourceNodeState::Available
1193}
1194
1195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1196pub struct ItemStack {
1197 pub template_id: String,
1198 pub quantity: u32,
1199 #[serde(default)]
1201 pub item_instance_id: Option<Uuid>,
1202 #[serde(default)]
1204 pub props: BTreeMap<String, String>,
1205 #[serde(default)]
1207 pub contents: Vec<ItemStack>,
1208 #[serde(default)]
1210 pub display_name: Option<String>,
1211 #[serde(default)]
1213 pub category: Option<String>,
1214 #[serde(default)]
1216 pub base_mass: Option<f32>,
1217 #[serde(default)]
1219 pub base_volume: Option<f32>,
1220 #[serde(default)]
1222 pub capacity_volume: Option<f32>,
1223 #[serde(default)]
1225 pub stackable: Option<bool>,
1226}
1227
1228impl ItemStack {
1229 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
1230 Self {
1231 template_id: template_id.into(),
1232 quantity,
1233 item_instance_id: None,
1234 props: BTreeMap::new(),
1235 contents: Vec::new(),
1236 display_name: None,
1237 category: None,
1238 base_mass: None,
1239 base_volume: None,
1240 capacity_volume: None,
1241 stackable: None,
1242 }
1243 }
1244}
1245
1246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1248#[serde(rename_all = "snake_case")]
1249pub enum EncumbranceState {
1250 #[default]
1251 Light,
1252 Heavy,
1253 Over,
1254}
1255
1256#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
1260#[serde(rename_all = "snake_case")]
1261pub enum BodySlot {
1262 Head,
1263 Body,
1264 Arms,
1265 Legs,
1266 Feet,
1267 Back,
1268 Waist,
1269}
1270
1271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1273#[serde(rename_all = "snake_case")]
1274pub enum InventoryLocation {
1275 Root,
1277 Worn { slot: BodySlot },
1279 Placed { container_id: String },
1281 Keychain,
1283}
1284
1285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1287pub struct PlacedContainerView {
1288 pub id: String,
1289 pub template_id: String,
1290 pub display_name: String,
1291 pub x: f32,
1292 pub y: f32,
1293 pub z: f32,
1294 pub locked: bool,
1295 #[serde(default)]
1297 pub accessible: bool,
1298 #[serde(default)]
1299 pub owner_character_id: Option<Uuid>,
1300 #[serde(default)]
1302 pub contents: Vec<ItemStack>,
1303 #[serde(default)]
1305 pub lock_id: Option<String>,
1306 #[serde(default)]
1308 pub capacity_volume: Option<f32>,
1309 #[serde(default)]
1311 pub item_instance_id: Option<Uuid>,
1312 #[serde(default)]
1314 pub tile_id: Option<String>,
1315}
1316
1317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1318pub struct BlueprintIngredientView {
1319 pub template_id: String,
1320 pub quantity: u32,
1321 pub consumed: bool,
1323}
1324
1325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1326pub struct ToolRequirementView {
1327 pub item: String,
1328 pub consumed: bool,
1330}
1331
1332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1333pub struct SkillRequirementView {
1334 pub skill: String,
1335 pub level: u32,
1336}
1337
1338#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1339pub struct BlueprintView {
1340 pub id: String,
1341 pub label: String,
1342 pub output: String,
1343 pub output_qty: u32,
1344 pub craft_ticks: u32,
1345 pub inputs: Vec<BlueprintIngredientView>,
1346 #[serde(default)]
1348 pub station: Option<String>,
1349 #[serde(default)]
1350 pub category: Option<String>,
1351 #[serde(default)]
1352 pub required_tools: Vec<ToolRequirementView>,
1353 #[serde(default)]
1354 pub skill: Option<SkillRequirementView>,
1355 #[serde(default)]
1356 pub failure_chance: f32,
1357}
1358
1359#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1361#[serde(rename_all = "snake_case")]
1362pub enum TerrainKindView {
1363 #[default]
1364 Grass,
1365 Hill,
1366 Bog,
1367 ShallowWater,
1368 DeepWater,
1369 Trail,
1370 Rock,
1371}
1372
1373#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1374pub struct TerrainZoneView {
1375 pub id: String,
1376 pub x0: f32,
1377 pub y0: f32,
1378 pub x1: f32,
1379 pub y1: f32,
1380 #[serde(default)]
1381 pub kind: TerrainKindView,
1382 #[serde(default)]
1384 pub elevation: f32,
1385 #[serde(default)]
1388 pub glyph: Option<String>,
1389 #[serde(default)]
1391 pub color: Option<String>,
1392 #[serde(default)]
1394 pub tile_id: Option<String>,
1395 #[serde(default)]
1397 pub z_order: i32,
1398}
1399
1400#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1402pub struct ZPlatformView {
1403 pub id: String,
1404 pub z: f32,
1405 pub x0: f32,
1406 pub y0: f32,
1407 pub x1: f32,
1408 pub y1: f32,
1409}
1410
1411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1413pub struct ZTransitionView {
1414 pub id: String,
1415 pub z_from: f32,
1416 pub z_to: f32,
1417 pub x0: f32,
1418 pub y0: f32,
1419 pub x1: f32,
1420 pub y1: f32,
1421}
1422
1423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1424pub struct BuildingView {
1425 pub id: String,
1426 pub label: String,
1427 pub x: f32,
1428 pub y: f32,
1429 pub width_m: f32,
1430 pub depth_m: f32,
1431 #[serde(default)]
1432 pub interior_blueprint: Option<String>,
1433 #[serde(default)]
1434 pub tags: Vec<String>,
1435}
1436
1437#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1438pub struct DoorView {
1439 pub id: String,
1440 pub building_id: String,
1441 pub x: f32,
1442 pub y: f32,
1443 #[serde(default)]
1444 pub open: bool,
1445 #[serde(default)]
1446 pub portal: Option<String>,
1447}
1448
1449#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1450pub struct InteriorRoomView {
1451 pub id: String,
1452 pub label: String,
1453 pub floor: i32,
1454 pub x0: f32,
1455 pub y0: f32,
1456 pub x1: f32,
1457 pub y1: f32,
1458 #[serde(default)]
1459 pub floor_color: Option<String>,
1460 #[serde(default)]
1461 pub floor_glyph: Option<String>,
1462}
1463
1464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1465pub struct InteriorDoorView {
1466 pub id: String,
1467 pub room_a: String,
1468 pub room_b: String,
1469 pub x: f32,
1470 pub y: f32,
1471 pub kind: String,
1472}
1473
1474#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1475pub struct InteriorMapView {
1476 pub building_id: String,
1477 pub blueprint_id: String,
1478 pub background_color: String,
1479 #[serde(default)]
1480 pub default_floor_color: Option<String>,
1481 #[serde(default = "default_floor_height_view")]
1482 pub floor_height_m: f32,
1483 pub rooms: Vec<InteriorRoomView>,
1484 #[serde(default)]
1485 pub room_doors: Vec<InteriorDoorView>,
1486}
1487
1488fn default_floor_height_view() -> f32 {
1489 3.0
1490}
1491
1492#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1493pub struct NpcView {
1494 pub id: String,
1495 pub label: String,
1496 pub role: String,
1497 pub x: f32,
1498 pub y: f32,
1499 #[serde(default)]
1501 pub building_id: Option<String>,
1502 #[serde(default)]
1504 pub entity_id: Option<EntityId>,
1505 #[serde(default)]
1506 pub life_state: Option<LifeState>,
1507 #[serde(default)]
1508 pub hp_pct: Option<f32>,
1509 #[serde(default)]
1511 pub can_trade: bool,
1512 #[serde(default)]
1514 pub tile_id: Option<String>,
1515 #[serde(default)]
1517 pub behavior_state: Option<String>,
1518 #[serde(default)]
1520 pub presentation_state: Option<String>,
1521 #[serde(default)]
1523 pub sprite_mode: Option<String>,
1524}
1525
1526#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1527pub struct UseResult {
1528 pub template_id: String,
1529 pub hunger_restored: f32,
1530 pub thirst_restored: f32,
1531 #[serde(default)]
1532 pub health_restored: f32,
1533 #[serde(default)]
1534 pub mana_restored: f32,
1535 #[serde(default)]
1536 pub cleared_dot_ids: Vec<String>,
1537}
1538
1539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1540pub struct CraftResult {
1541 pub blueprint_id: String,
1542 pub outputs: Vec<ItemStack>,
1543 pub consumed: Vec<ItemStack>,
1544 #[serde(default = "default_one")]
1546 pub batch_index: u32,
1547 #[serde(default = "default_one")]
1549 pub batch_total: u32,
1550}
1551
1552fn default_one() -> u32 {
1553 1
1554}
1555
1556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1557pub struct DeathNotice {
1558 pub entity_id: EntityId,
1559 pub respawn_x: f32,
1560 pub respawn_y: f32,
1561 pub message: String,
1562}
1563
1564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1565pub struct InteractionNotice {
1566 pub target_id: String,
1567 pub message: String,
1568 #[serde(default)]
1569 pub coins_delta: i32,
1570 #[serde(default)]
1571 pub inventory_delta: Vec<ItemStack>,
1572}
1573
1574#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1575#[serde(rename_all = "snake_case")]
1576pub enum NpcTalkTrustFlag {
1577 Stranger,
1578 Acquainted,
1579 Trusted,
1580}
1581
1582#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1583pub struct NpcTalkOpened {
1584 pub npc_id: String,
1585 pub npc_label: String,
1586 pub greeting: String,
1587 pub trust_flag: NpcTalkTrustFlag,
1588}
1589
1590#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1591pub struct NpcTalkPending {
1592 pub npc_id: String,
1593}
1594
1595#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1596pub struct NpcTalkReply {
1597 pub npc_id: String,
1598 pub line: String,
1599 pub trust_flag: NpcTalkTrustFlag,
1600}
1601
1602#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1603pub struct NpcTalkClosed {
1604 pub npc_id: String,
1605}
1606
1607#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1608pub struct NpcTalkError {
1609 pub npc_id: String,
1610 pub reason: String,
1611}
1612
1613#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1614#[serde(rename_all = "snake_case")]
1615pub enum QuestStatusView {
1616 Available,
1617 Active,
1618 Completed,
1619}
1620
1621#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1622pub struct QuestObjectiveProgress {
1623 pub label: String,
1624 pub current: u32,
1625 pub required: u32,
1626 pub done: bool,
1627}
1628
1629#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1630pub struct QuestLogEntry {
1631 pub quest_id: String,
1632 pub title: String,
1633 pub description: String,
1634 pub status: QuestStatusView,
1635 #[serde(default)]
1636 pub current_step_id: Option<String>,
1637 #[serde(default)]
1638 pub current_step_title: String,
1639 #[serde(default)]
1640 pub objectives: Vec<QuestObjectiveProgress>,
1641 #[serde(default)]
1642 pub is_tracked: bool,
1643 #[serde(default)]
1644 pub can_withdraw: bool,
1645}
1646
1647#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1648pub struct InteractableView {
1649 pub id: String,
1650 pub kind: String,
1651 pub label: String,
1652 pub x: f32,
1653 pub y: f32,
1654 pub z: f32,
1655 #[serde(default)]
1656 pub board_id: Option<String>,
1657}
1658
1659#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1660pub struct QuestOffer {
1661 pub quest_id: String,
1662 pub title: String,
1663 pub description: String,
1664 #[serde(default)]
1665 pub step_count: u32,
1666}
1667
1668#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1669pub struct QuestNotice {
1670 pub quest_id: String,
1671 pub title: String,
1672 pub message: String,
1673}
1674
1675#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1676#[serde(rename_all = "snake_case")]
1677pub enum ShopOfferKind {
1678 Item,
1679 Blueprint,
1680}
1681
1682#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1683pub struct ShopOffer {
1684 pub offer_id: String,
1685 pub kind: ShopOfferKind,
1686 pub label: String,
1687 #[serde(default)]
1688 pub template_id: Option<String>,
1689 #[serde(default)]
1690 pub blueprint_id: Option<String>,
1691 pub price_copper: u32,
1692 #[serde(default)]
1693 pub affordable: bool,
1694 #[serde(default)]
1695 pub already_owned: bool,
1696}
1697
1698#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1699pub struct ShopBuyLine {
1700 pub template_id: String,
1701 pub label: String,
1702 pub quantity: u32,
1703 pub price_copper: u32,
1704}
1705
1706#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1707pub struct ShopCatalog {
1708 pub npc_id: String,
1709 pub npc_label: String,
1710 #[serde(default)]
1711 pub sells: Vec<ShopOffer>,
1712 #[serde(default)]
1713 pub buys: Vec<ShopBuyLine>,
1714}
1715
1716#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1717pub struct HarvestResult {
1718 pub node_id: String,
1719 pub quantity: u32,
1721 pub item_template: String,
1722 #[serde(default)]
1725 pub item_instance_id: Option<Uuid>,
1726}
1727
1728#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1730pub struct Envelope<T> {
1731 pub protocol_version: u16,
1732 pub payload: T,
1733}
1734
1735impl<T> Envelope<T> {
1736 pub fn new(payload: T) -> Self {
1737 Self {
1738 protocol_version: crate::PROTOCOL_VERSION,
1739 payload,
1740 }
1741 }
1742}
1743
1744#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1746pub struct Hello {
1747 pub client_name: String,
1748 pub protocol_version: u16,
1749 #[serde(default)]
1750 pub auth: AuthCredential,
1751 #[serde(default)]
1753 pub character_id: Option<Uuid>,
1754}
1755
1756#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1759#[serde(rename_all = "snake_case")]
1760pub enum AuthCredential {
1761 DevLocal,
1762 Session { token: String },
1763 ApiToken { token: String, character_id: Uuid },
1764}
1765
1766impl Default for AuthCredential {
1767 fn default() -> Self {
1768 Self::DevLocal
1769 }
1770}
1771
1772#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1773pub struct Welcome {
1774 pub session_id: SessionId,
1775 pub entity_id: EntityId,
1776 pub snapshot: Snapshot,
1777}
1778
1779#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1780pub enum ServerMessage {
1781 Welcome(Welcome),
1782 ContentUpdated(Snapshot),
1784 Tick(TickDelta),
1785 IntentAck {
1786 entity_id: EntityId,
1787 seq: Seq,
1788 tick: Tick,
1789 },
1790 Chat(ChatMessage),
1791 HarvestResult(HarvestResult),
1792 UseResult(UseResult),
1793 CraftResult(CraftResult),
1794 Death(DeathNotice),
1795 Interaction(InteractionNotice),
1796 ShopOpened(ShopCatalog),
1797 NpcTalkOpened(NpcTalkOpened),
1798 NpcTalkPending(NpcTalkPending),
1799 NpcTalkReply(NpcTalkReply),
1800 NpcTalkClosed(NpcTalkClosed),
1801 NpcTalkError(NpcTalkError),
1802 QuestOffer(QuestOffer),
1803 QuestAccepted(QuestNotice),
1804 QuestWithdrawn(QuestNotice),
1805 QuestStepCompleted(QuestNotice),
1806 QuestCompleted(QuestNotice),
1807}
1808
1809#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1810pub enum ClientMessage {
1811 Hello(Hello),
1812 Intent(Intent),
1813 Disconnect,
1814}
1815
1816#[cfg(test)]
1817mod tests {
1818 use super::*;
1819
1820 #[test]
1821 fn pristine_vitals_state_yields_full_pools() {
1822 let attrs = PrimaryAttributes::default();
1823 let vitals = StoredVitalsState::default().apply_to(attrs);
1824 assert!(vitals.health > 0.0);
1825 assert_eq!(vitals.health, vitals.health_max);
1826 assert!((vitals.mana_max - 61.0).abs() < 0.01);
1827 }
1828
1829 #[test]
1830 fn saved_vitals_scale_when_pool_max_increases() {
1831 let mut attrs = PrimaryAttributes::default();
1832 attrs.intelligence = 140;
1833 attrs.wisdom = 140;
1834 let saved = StoredVitalsState {
1835 health: 100.0,
1836 mana: 14.0,
1837 stamina: 100.0,
1838 ..StoredVitalsState::default()
1839 };
1840 let vitals = saved.apply_to(attrs);
1841 assert!(vitals.mana_max > 55.0);
1842 assert!(
1843 (vitals.mana - vitals.mana_max).abs() < 0.01,
1844 "full legacy mana bar migrates to full new bar"
1845 );
1846 }
1847
1848 #[test]
1849 fn empty_vitals_state_is_pristine() {
1850 let pristine = StoredVitalsState {
1851 health: 0.0,
1852 mana: 0.0,
1853 stamina: 0.0,
1854 hunger: 0.0,
1855 thirst: 0.0,
1856 coins: 0,
1857 deaths: 0,
1858 life_state: LifeState::Alive,
1859 };
1860 assert!(pristine.is_pristine());
1861 let vitals = pristine.apply_to(PrimaryAttributes::default());
1862 assert!(vitals.health > 0.0);
1863 }
1864
1865 #[test]
1866 fn stored_vitals_roundtrip_preserves_partial_pools() {
1867 let attrs = PrimaryAttributes::default();
1868 let mut live = PlayerVitals::from_attributes(attrs);
1869 live.health = 25.0;
1870 live.hunger = 77.0;
1871 live.deaths = 2;
1872 let stored = StoredVitalsState::from_live(&live);
1873 let restored = stored.apply_to(attrs);
1874 assert!(
1875 (restored.health - 25.0).abs() < 0.01,
1876 "partial HP below cap stays absolute"
1877 );
1878 assert_eq!(restored.hunger, 77.0);
1879 assert_eq!(restored.deaths, 2);
1880 }
1881
1882 #[test]
1883 fn skill_tiers_start_at_zero() {
1884 let skill = SkillProgress::default();
1885 assert_eq!(skill.level, 0);
1886 assert_eq!(skill.display_tier(), 0);
1887 let trained = SkillProgress {
1888 level: 250,
1889 last_trained_tick: 1,
1890 };
1891 assert_eq!(trained.display_tier(), 2);
1892 }
1893
1894 #[test]
1895 fn quest_server_messages_roundtrip_json() {
1896 use crate::codec::{Codec, PostcardCodec};
1897
1898 let offer = ServerMessage::QuestOffer(QuestOffer {
1899 quest_id: "ada_goblin_hunt".into(),
1900 title: "Goblin Trouble".into(),
1901 description: "Help Ada".into(),
1902 step_count: 3,
1903 });
1904 let notice = ServerMessage::QuestAccepted(QuestNotice {
1905 quest_id: "ada_goblin_hunt".into(),
1906 title: "Goblin Trouble".into(),
1907 message: "Quest accepted".into(),
1908 });
1909 for msg in [offer, notice] {
1910 let bytes = PostcardCodec.encode(&msg).unwrap();
1911 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
1912 assert_eq!(decoded, msg);
1913 }
1914 }
1915}