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: 500,
138 dexterity: 500,
139 intelligence: 500,
140 stamina: 500,
141 vitality: 500,
142 wisdom: 500,
143 charisma: 500,
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)]
208#[serde(default)]
209pub struct PlayerSkills {
210 pub logging: SkillProgress,
211 pub mining: SkillProgress,
212 pub evocation: SkillProgress,
213 pub swords: SkillProgress,
214 pub crafting: SkillProgress,
215 pub cartography: SkillProgress,
216}
217
218impl Default for PlayerSkills {
219 fn default() -> Self {
220 Self {
221 logging: SkillProgress::default(),
222 mining: SkillProgress::default(),
223 evocation: SkillProgress::default(),
224 swords: SkillProgress::default(),
225 crafting: SkillProgress::default(),
226 cartography: SkillProgress::default(),
227 }
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
233pub struct PlayerVitals {
234 pub health: f32,
235 pub health_max: f32,
236 pub mana: f32,
237 pub mana_max: f32,
238 pub stamina: f32,
239 pub stamina_max: f32,
240 #[serde(default = "default_survival_pool_max")]
241 pub hunger: f32,
242 #[serde(default = "default_survival_pool_max")]
243 pub hunger_max: f32,
244 #[serde(default = "default_survival_pool_max")]
245 pub thirst: f32,
246 #[serde(default = "default_survival_pool_max")]
247 pub thirst_max: f32,
248 #[serde(default)]
249 pub coins: u32,
250 #[serde(default)]
251 pub deaths: u32,
252 #[serde(default)]
253 pub life_state: LifeState,
254}
255
256fn default_survival_pool_max() -> f32 {
257 100.0
258}
259
260impl Default for PlayerVitals {
261 fn default() -> Self {
262 Self::from_attributes(PrimaryAttributes::default())
263 }
264}
265
266impl PlayerVitals {
267 pub fn from_attributes(attrs: PrimaryAttributes) -> Self {
274 let vit_d = PrimaryAttributes::display(attrs.vitality) as f32;
275 let int_d = PrimaryAttributes::display(attrs.intelligence) as f32;
276 let wis_d = PrimaryAttributes::display(attrs.wisdom) as f32;
277 let sta_d = PrimaryAttributes::display(attrs.stamina) as f32;
278
279 let health_max = 50.0 + vit_d * 2.0;
280 let stamina_max = 30.0 + sta_d * 1.4;
281 let mana_max = 25.0 + int_d * 1.1 + wis_d * 1.3;
282 let hunger_max = 100.0;
283 let thirst_max = 100.0;
284 Self {
285 health: health_max,
286 health_max,
287 mana: mana_max,
288 mana_max,
289 stamina: stamina_max,
290 stamina_max,
291 hunger: hunger_max,
292 hunger_max,
293 thirst: thirst_max,
294 thirst_max,
295 coins: 0,
296 deaths: 0,
297 life_state: LifeState::Alive,
298 }
299 }
300
301 pub fn legacy_maxima(attrs: PrimaryAttributes) -> (f32, f32, f32) {
303 (
304 attrs.vitality as f32 / 5.0,
305 attrs.stamina as f32 / 5.0,
306 (attrs.intelligence as f32 + attrs.wisdom as f32) / 20.0,
307 )
308 }
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
313#[serde(default)]
314pub struct StoredVitalsState {
315 pub health: f32,
316 pub mana: f32,
317 pub stamina: f32,
318 pub hunger: f32,
319 pub thirst: f32,
320 pub coins: u32,
321 pub deaths: u32,
322 pub life_state: LifeState,
323}
324
325impl StoredVitalsState {
326 pub fn from_live(v: &PlayerVitals) -> Self {
327 Self {
328 health: v.health,
329 mana: v.mana,
330 stamina: v.stamina,
331 hunger: v.hunger,
332 thirst: v.thirst,
333 coins: v.coins,
334 deaths: v.deaths,
335 life_state: v.life_state,
336 }
337 }
338
339 pub fn is_pristine(&self) -> bool {
341 self.health == 0.0
342 && self.mana == 0.0
343 && self.stamina == 0.0
344 && self.hunger == 0.0
345 && self.thirst == 0.0
346 && self.coins == 0
347 && self.deaths == 0
348 && self.life_state == LifeState::Alive
349 }
350
351 pub fn apply_to(self, attrs: PrimaryAttributes) -> PlayerVitals {
352 if self.is_pristine() {
353 return PlayerVitals::from_attributes(attrs);
354 }
355 let fresh = PlayerVitals::from_attributes(attrs);
356 let (legacy_hp, legacy_sta, legacy_mana) = PlayerVitals::legacy_maxima(attrs);
357
358 let scale = |current: f32, legacy_max: f32, new_max: f32| {
359 if legacy_max > 0.0
360 && new_max > legacy_max * 1.05
361 && current >= legacy_max * 0.95
362 {
363 let ratio = (current / legacy_max).clamp(0.0, 1.0);
364 (new_max * ratio).min(new_max)
365 } else {
366 current.min(new_max)
367 }
368 };
369
370 let mut v = fresh;
371 v.health = scale(self.health, legacy_hp, fresh.health_max);
372 v.mana = scale(self.mana, legacy_mana, fresh.mana_max);
373 v.stamina = scale(self.stamina, legacy_sta, fresh.stamina_max);
374 v.hunger = self.hunger.min(v.hunger_max);
375 v.thirst = self.thirst.min(v.thirst_max);
376 v.coins = self.coins;
377 v.deaths = self.deaths;
378 v.life_state = self.life_state;
379 v
380 }
381}
382
383impl Default for StoredVitalsState {
384 fn default() -> Self {
385 Self::from_live(&PlayerVitals::default())
386 }
387}
388
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
391pub struct RotationPreset {
392 pub id: String,
393 pub label: String,
394 #[serde(default)]
395 pub abilities: Vec<String>,
396}
397
398impl RotationPreset {
399 pub fn melee_default(ability_id: impl Into<String>) -> Self {
400 let id = ability_id.into();
401 Self {
402 id: "melee".into(),
403 label: "Melee".into(),
404 abilities: vec![id],
405 }
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
411pub struct StoredTargetSlot {
412 pub instance_id: Option<String>,
413 #[serde(default)]
414 pub preset_id: Option<String>,
415 #[serde(default)]
416 pub rotation_index: u32,
417 #[serde(default)]
418 pub auto_enabled: bool,
419}
420
421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
422#[serde(default)]
423pub struct StoredCombatProfile {
424 pub combat_target_instance_id: Option<String>,
426 pub in_combat: bool,
427 pub last_combat_tick: u64,
428 pub last_attack_tick: u64,
429 pub cooldowns_until_tick: BTreeMap<String, u64>,
430 #[serde(default = "default_auto_attack")]
431 pub auto_attack_enabled: bool,
432 #[serde(default)]
434 pub mainhand_template_id: Option<String>,
435 #[serde(default)]
438 pub worn: Vec<(BodySlot, ItemStack)>,
439 #[serde(default)]
441 pub rotation_presets: Vec<RotationPreset>,
442 #[serde(default)]
444 pub target_slots: Vec<StoredTargetSlot>,
445}
446
447fn default_auto_attack() -> bool {
448 true
449}
450
451impl Default for StoredCombatProfile {
452 fn default() -> Self {
453 Self {
454 combat_target_instance_id: None,
455 in_combat: false,
456 last_combat_tick: 0,
457 last_attack_tick: 0,
458 cooldowns_until_tick: BTreeMap::new(),
459 auto_attack_enabled: true,
460 mainhand_template_id: None,
461 worn: Vec::new(),
462 rotation_presets: Vec::new(),
463 target_slots: Vec::new(),
464 }
465 }
466}
467
468#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
469pub struct EntityState {
470 pub id: EntityId,
471 pub transform: Transform,
472 #[serde(default)]
474 pub label: String,
475 #[serde(default)]
476 pub vitals: Option<PlayerVitals>,
477 #[serde(default)]
479 pub attributes: Option<PrimaryAttributes>,
480 #[serde(default)]
481 pub skills: Option<PlayerSkills>,
482 #[serde(default)]
484 pub inside_building: Option<String>,
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
489#[serde(rename_all = "snake_case")]
490pub enum ChatChannel {
491 Nearby,
492 WhisperStone,
493}
494
495#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
496pub struct ChatMessage {
497 pub channel: ChatChannel,
498 pub from_entity: EntityId,
499 pub from_name: String,
500 pub text: String,
501 pub tick: Tick,
502}
503
504#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
506pub enum Intent {
507 Move {
508 entity_id: EntityId,
509 forward: f32,
510 strafe: f32,
511 #[serde(default)]
513 vertical: f32,
514 #[serde(default)]
516 sprint: bool,
517 seq: Seq,
518 },
519 Stop {
520 entity_id: EntityId,
521 seq: Seq,
522 },
523 Harvest {
524 entity_id: EntityId,
525 node_id: String,
526 seq: Seq,
527 },
528 Use {
529 entity_id: EntityId,
530 template_id: String,
531 seq: Seq,
532 },
533 Say {
534 entity_id: EntityId,
535 channel: ChatChannel,
536 text: String,
537 seq: Seq,
538 },
539 Craft {
541 entity_id: EntityId,
542 blueprint_id: String,
543 #[serde(default)]
545 count: Option<u32>,
546 seq: Seq,
547 },
548 Interact {
550 entity_id: EntityId,
551 target_id: String,
552 seq: Seq,
553 },
554 TestDamage {
556 entity_id: EntityId,
557 amount: f32,
558 seq: Seq,
559 },
560 SetTarget {
562 entity_id: EntityId,
563 target_id: EntityId,
564 seq: Seq,
565 },
566 SetTargetSlot {
568 entity_id: EntityId,
569 slot_index: u8,
570 target_id: EntityId,
571 seq: Seq,
572 },
573 ClearTarget {
574 entity_id: EntityId,
575 seq: Seq,
576 },
577 ClearTargetSlot {
578 entity_id: EntityId,
579 slot_index: u8,
580 seq: Seq,
581 },
582 SetAutoAttack {
584 entity_id: EntityId,
585 slot_index: u8,
586 enabled: bool,
587 seq: Seq,
588 },
589 Attack {
591 entity_id: EntityId,
592 #[serde(default)]
593 target_id: Option<EntityId>,
594 #[serde(default)]
595 weapon_slot: Option<u32>,
596 seq: Seq,
597 },
598 Pickup {
600 entity_id: EntityId,
601 #[serde(default)]
602 drop_id: Option<String>,
603 seq: Seq,
604 },
605 Cast {
607 entity_id: EntityId,
608 ability_id: String,
609 target_id: EntityId,
610 seq: Seq,
611 },
612 BindActionSlot {
614 entity_id: EntityId,
615 slot_index: u8,
616 ability_id: String,
617 #[serde(default = "default_auto_attack")]
618 auto_enabled: bool,
619 seq: Seq,
620 },
621 UseActionSlot {
623 entity_id: EntityId,
624 slot_index: u8,
625 seq: Seq,
626 },
627 Dodge {
629 entity_id: EntityId,
630 seq: Seq,
631 },
632 Lunge {
634 entity_id: EntityId,
635 #[serde(default)]
637 forward: f32,
638 #[serde(default)]
640 strafe: f32,
641 seq: Seq,
642 },
643 Block {
645 entity_id: EntityId,
646 #[serde(default = "default_block_enabled")]
647 enabled: bool,
648 seq: Seq,
649 },
650 EquipMainhand {
652 entity_id: EntityId,
653 #[serde(default)]
654 template_id: Option<String>,
655 seq: Seq,
656 },
657 EquipWorn {
660 entity_id: EntityId,
661 slot: BodySlot,
662 #[serde(default)]
663 instance_id: Option<Uuid>,
664 seq: Seq,
665 },
666 MoveItem {
668 entity_id: EntityId,
669 item_instance_id: Uuid,
670 from: InventoryLocation,
671 to: InventoryLocation,
672 #[serde(default)]
674 to_parent_instance_id: Option<Uuid>,
675 #[serde(default)]
677 quantity: Option<u32>,
678 seq: Seq,
679 },
680 PlaceContainer {
682 entity_id: EntityId,
683 item_instance_id: Uuid,
684 seq: Seq,
685 },
686 PickupContainer {
688 entity_id: EntityId,
689 container_id: String,
690 seq: Seq,
691 },
692 SetContainerLocked {
694 entity_id: EntityId,
695 location: InventoryLocation,
697 locked: bool,
698 seq: Seq,
699 },
700 DropItem {
702 entity_id: EntityId,
703 item_instance_id: Uuid,
704 from: InventoryLocation,
705 seq: Seq,
706 },
707 RenameContainer {
709 entity_id: EntityId,
710 item_instance_id: Uuid,
711 location: InventoryLocation,
712 name: String,
713 seq: Seq,
714 },
715 UpsertRotationPreset {
717 entity_id: EntityId,
718 preset: RotationPreset,
719 seq: Seq,
720 },
721 DeleteRotationPreset {
723 entity_id: EntityId,
724 preset_id: String,
725 seq: Seq,
726 },
727 AssignSlotPreset {
729 entity_id: EntityId,
730 slot_index: u8,
731 preset_id: String,
732 seq: Seq,
733 },
734 AdvanceRotation {
736 entity_id: EntityId,
737 slot_index: u8,
738 seq: Seq,
739 },
740}
741
742fn default_block_enabled() -> bool {
743 true
744}
745
746#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
748pub struct CombatTargetHud {
749 pub entity_id: EntityId,
750 #[serde(default)]
751 pub label: String,
752 #[serde(default)]
753 pub level: u32,
754 pub health: f32,
755 pub health_max: f32,
756 #[serde(default)]
757 pub life_state: LifeState,
758 #[serde(default)]
759 pub distance_m: f32,
760}
761
762#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
764pub struct CastProgressHud {
765 #[serde(default)]
766 pub ability_id: String,
767 #[serde(default)]
768 pub ability_label: String,
769 #[serde(default)]
770 pub ticks_remaining: u64,
771 #[serde(default)]
772 pub ticks_total: u64,
773}
774
775#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
777pub struct AbilityCooldownHud {
778 #[serde(default)]
779 pub ability_id: String,
780 #[serde(default)]
781 pub label: String,
782 #[serde(default)]
783 pub cd_ticks: u64,
784 #[serde(default)]
785 pub cd_total_ticks: u64,
786}
787
788#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
790pub struct CombatSlotHud {
791 pub slot_index: u8,
792 #[serde(default)]
793 pub target_entity_id: Option<EntityId>,
794 #[serde(default)]
795 pub target_label: Option<String>,
796 #[serde(default)]
797 pub target: Option<CombatTargetHud>,
798 #[serde(default)]
799 pub preset_id: Option<String>,
800 #[serde(default)]
801 pub preset_label: Option<String>,
802 #[serde(default)]
803 pub rotation: Vec<String>,
804 #[serde(default)]
805 pub rotation_index: u32,
806 #[serde(default)]
807 pub next_ability_id: Option<String>,
808 #[serde(default)]
809 pub auto_enabled: bool,
810}
811
812#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
814pub struct CombatHud {
815 pub in_combat: bool,
816 pub auto_attack: bool,
818 pub has_los: bool,
819 pub attack_cd_ticks: u64,
820 #[serde(default)]
821 pub ability_id: String,
822 #[serde(default)]
823 pub target_entity_id: Option<EntityId>,
824 #[serde(default)]
825 pub target_label: Option<String>,
826 #[serde(default)]
827 pub max_target_slots: u8,
828 #[serde(default)]
829 pub slots: Vec<CombatSlotHud>,
830 #[serde(default)]
831 pub rotation_presets: Vec<RotationPreset>,
832 #[serde(default)]
833 pub gcd_ticks: u64,
834 #[serde(default)]
835 pub mainhand_template_id: Option<String>,
836 #[serde(default)]
837 pub mainhand_label: Option<String>,
838 #[serde(default)]
840 pub worn: Vec<(BodySlot, ItemStack)>,
841 #[serde(default)]
842 pub carry_mass: f32,
843 #[serde(default)]
844 pub carry_mass_max: f32,
845 #[serde(default)]
846 pub encumbrance: EncumbranceState,
847 #[serde(default)]
848 pub target: Option<CombatTargetHud>,
849 #[serde(default)]
850 pub cast: Option<CastProgressHud>,
851 #[serde(default)]
852 pub ability_cooldowns: Vec<AbilityCooldownHud>,
853 #[serde(default)]
854 pub blocking_active: bool,
855}
856
857#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
859pub struct TickDelta {
860 pub tick: Tick,
861 pub entities: Vec<EntityState>,
862 #[serde(default)]
863 pub resource_nodes: Vec<ResourceNodeView>,
864 #[serde(default)]
865 pub buildings: Vec<BuildingView>,
866 #[serde(default)]
867 pub doors: Vec<DoorView>,
868 #[serde(default)]
869 pub npcs: Vec<NpcView>,
870 #[serde(default)]
872 pub inventory: Vec<ItemStack>,
873 #[serde(default)]
874 pub blueprints: Vec<BlueprintView>,
875 #[serde(default)]
876 pub world_clock: WorldClock,
877 #[serde(default)]
878 pub ground_drops: Vec<GroundDropView>,
879 #[serde(default)]
880 pub placed_containers: Vec<PlacedContainerView>,
881 #[serde(default)]
882 pub combat: Option<CombatHud>,
883}
884
885#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
887pub struct GroundDropView {
888 pub id: String,
889 pub template_id: String,
890 pub quantity: u32,
891 pub x: f32,
892 pub y: f32,
893 pub z: f32,
894}
895
896#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
898pub struct Snapshot {
899 pub tick: Tick,
900 pub chunk_rev: u64,
901 #[serde(default)]
903 pub content_rev: u64,
904 pub entities: Vec<EntityState>,
905 #[serde(default)]
906 pub resource_nodes: Vec<ResourceNodeView>,
907 #[serde(default)]
909 pub world_width_m: f32,
910 #[serde(default)]
911 pub world_height_m: f32,
912 #[serde(default)]
913 pub buildings: Vec<BuildingView>,
914 #[serde(default)]
915 pub doors: Vec<DoorView>,
916 #[serde(default)]
917 pub npcs: Vec<NpcView>,
918 #[serde(default)]
919 pub inventory: Vec<ItemStack>,
920 #[serde(default)]
921 pub blueprints: Vec<BlueprintView>,
922 #[serde(default)]
923 pub world_clock: WorldClock,
924 #[serde(default)]
925 pub terrain_zones: Vec<TerrainZoneView>,
926 #[serde(default)]
927 pub ground_drops: Vec<GroundDropView>,
928 #[serde(default)]
929 pub placed_containers: Vec<PlacedContainerView>,
930 #[serde(default)]
931 pub combat: Option<CombatHud>,
932}
933
934#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
936pub struct ResourceNodeView {
937 pub id: String,
938 pub label: String,
939 pub x: f32,
940 pub y: f32,
941 pub z: f32,
942 pub item_template: String,
943 #[serde(default = "default_node_state")]
944 pub state: ResourceNodeState,
945 #[serde(default = "default_blocking_view")]
947 pub blocking: bool,
948 #[serde(default = "default_blocking_radius_view")]
950 pub blocking_radius_m: f32,
951}
952
953fn default_blocking_radius_view() -> f32 {
954 0.8
955}
956
957fn default_blocking_view() -> bool {
958 true
959}
960
961#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
962#[serde(rename_all = "snake_case")]
963pub enum ResourceNodeState {
964 Available,
965 Harvesting,
966 Cooldown,
967}
968
969fn default_node_state() -> ResourceNodeState {
970 ResourceNodeState::Available
971}
972
973#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
974pub struct ItemStack {
975 pub template_id: String,
976 pub quantity: u32,
977 #[serde(default)]
979 pub item_instance_id: Option<Uuid>,
980 #[serde(default)]
982 pub props: BTreeMap<String, String>,
983 #[serde(default)]
985 pub contents: Vec<ItemStack>,
986 #[serde(default)]
988 pub display_name: Option<String>,
989 #[serde(default)]
991 pub category: Option<String>,
992 #[serde(default)]
994 pub base_mass: Option<f32>,
995 #[serde(default)]
997 pub base_volume: Option<f32>,
998 #[serde(default)]
1000 pub capacity_volume: Option<f32>,
1001 #[serde(default)]
1003 pub stackable: Option<bool>,
1004}
1005
1006impl ItemStack {
1007 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
1008 Self {
1009 template_id: template_id.into(),
1010 quantity,
1011 item_instance_id: None,
1012 props: BTreeMap::new(),
1013 contents: Vec::new(),
1014 display_name: None,
1015 category: None,
1016 base_mass: None,
1017 base_volume: None,
1018 capacity_volume: None,
1019 stackable: None,
1020 }
1021 }
1022}
1023
1024#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1026#[serde(rename_all = "snake_case")]
1027pub enum EncumbranceState {
1028 #[default]
1029 Light,
1030 Heavy,
1031 Over,
1032}
1033
1034#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
1038#[serde(rename_all = "snake_case")]
1039pub enum BodySlot {
1040 Head,
1041 Body,
1042 Arms,
1043 Legs,
1044 Feet,
1045 Back,
1046 Waist,
1047}
1048
1049#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1051#[serde(rename_all = "snake_case")]
1052pub enum InventoryLocation {
1053 Root,
1055 Worn {
1057 slot: BodySlot,
1058 },
1059 Placed {
1061 container_id: String,
1062 },
1063}
1064
1065#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1067pub struct PlacedContainerView {
1068 pub id: String,
1069 pub template_id: String,
1070 pub display_name: String,
1071 pub x: f32,
1072 pub y: f32,
1073 pub z: f32,
1074 pub locked: bool,
1075 #[serde(default)]
1077 pub accessible: bool,
1078 #[serde(default)]
1079 pub owner_character_id: Option<Uuid>,
1080 #[serde(default)]
1082 pub contents: Vec<ItemStack>,
1083 #[serde(default)]
1085 pub item_instance_id: Option<Uuid>,
1086}
1087
1088#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1089pub struct BlueprintIngredientView {
1090 pub template_id: String,
1091 pub quantity: u32,
1092 pub consumed: bool,
1094}
1095
1096#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1097pub struct ToolRequirementView {
1098 pub item: String,
1099 pub consumed: bool,
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1104pub struct SkillRequirementView {
1105 pub skill: String,
1106 pub level: u32,
1107}
1108
1109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1110pub struct BlueprintView {
1111 pub id: String,
1112 pub label: String,
1113 pub output: String,
1114 pub output_qty: u32,
1115 pub craft_ticks: u32,
1116 pub inputs: Vec<BlueprintIngredientView>,
1117 #[serde(default)]
1119 pub station: Option<String>,
1120 #[serde(default)]
1121 pub category: Option<String>,
1122 #[serde(default)]
1123 pub required_tools: Vec<ToolRequirementView>,
1124 #[serde(default)]
1125 pub skill: Option<SkillRequirementView>,
1126 #[serde(default)]
1127 pub failure_chance: f32,
1128}
1129
1130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1132#[serde(rename_all = "snake_case")]
1133pub enum TerrainKindView {
1134 #[default]
1135 Grass,
1136 Hill,
1137 Bog,
1138 ShallowWater,
1139 DeepWater,
1140}
1141
1142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1143pub struct TerrainZoneView {
1144 pub id: String,
1145 pub x0: f32,
1146 pub y0: f32,
1147 pub x1: f32,
1148 pub y1: f32,
1149 #[serde(default)]
1150 pub kind: TerrainKindView,
1151}
1152
1153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1154pub struct BuildingView {
1155 pub id: String,
1156 pub label: String,
1157 pub x: f32,
1158 pub y: f32,
1159 pub width_m: f32,
1160 pub depth_m: f32,
1161 #[serde(default)]
1163 pub interior_origin_x: f32,
1164 #[serde(default)]
1165 pub interior_origin_y: f32,
1166 #[serde(default)]
1167 pub tags: Vec<String>,
1168}
1169
1170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1171pub struct DoorView {
1172 pub id: String,
1173 pub building_id: String,
1174 pub x: f32,
1175 pub y: f32,
1176 #[serde(default)]
1177 pub open: bool,
1178 #[serde(default)]
1179 pub is_exit: bool,
1180}
1181
1182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1183pub struct NpcView {
1184 pub id: String,
1185 pub label: String,
1186 pub role: String,
1187 pub x: f32,
1188 pub y: f32,
1189 #[serde(default)]
1191 pub building_id: Option<String>,
1192 #[serde(default)]
1194 pub entity_id: Option<EntityId>,
1195 #[serde(default)]
1196 pub life_state: Option<LifeState>,
1197 #[serde(default)]
1198 pub hp_pct: Option<f32>,
1199}
1200
1201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1202pub struct UseResult {
1203 pub template_id: String,
1204 pub hunger_restored: f32,
1205 pub thirst_restored: f32,
1206}
1207
1208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1209pub struct CraftResult {
1210 pub blueprint_id: String,
1211 pub outputs: Vec<ItemStack>,
1212 pub consumed: Vec<ItemStack>,
1213 #[serde(default = "default_one")]
1215 pub batch_index: u32,
1216 #[serde(default = "default_one")]
1218 pub batch_total: u32,
1219}
1220
1221fn default_one() -> u32 {
1222 1
1223}
1224
1225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1226pub struct DeathNotice {
1227 pub entity_id: EntityId,
1228 pub respawn_x: f32,
1229 pub respawn_y: f32,
1230 pub message: String,
1231}
1232
1233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1234pub struct InteractionNotice {
1235 pub target_id: String,
1236 pub message: String,
1237 #[serde(default)]
1238 pub coins_delta: i32,
1239 #[serde(default)]
1240 pub inventory_delta: Vec<ItemStack>,
1241}
1242
1243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1244pub struct HarvestResult {
1245 pub node_id: String,
1246 pub quantity: u32,
1248 pub item_template: String,
1249 #[serde(default)]
1252 pub item_instance_id: Option<Uuid>,
1253}
1254
1255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1257pub struct Envelope<T> {
1258 pub protocol_version: u16,
1259 pub payload: T,
1260}
1261
1262impl<T> Envelope<T> {
1263 pub fn new(payload: T) -> Self {
1264 Self {
1265 protocol_version: crate::PROTOCOL_VERSION,
1266 payload,
1267 }
1268 }
1269}
1270
1271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1273pub struct Hello {
1274 pub client_name: String,
1275 pub protocol_version: u16,
1276 #[serde(default)]
1277 pub auth: AuthCredential,
1278 #[serde(default)]
1280 pub character_id: Option<Uuid>,
1281}
1282
1283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1286#[serde(rename_all = "snake_case")]
1287pub enum AuthCredential {
1288 DevLocal,
1289 Session { token: String },
1290 ApiToken { token: String, character_id: Uuid },
1291}
1292
1293impl Default for AuthCredential {
1294 fn default() -> Self {
1295 Self::DevLocal
1296 }
1297}
1298
1299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1300pub struct Welcome {
1301 pub session_id: SessionId,
1302 pub entity_id: EntityId,
1303 pub snapshot: Snapshot,
1304}
1305
1306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1307pub enum ServerMessage {
1308 Welcome(Welcome),
1309 ContentUpdated(Snapshot),
1311 Tick(TickDelta),
1312 IntentAck {
1313 entity_id: EntityId,
1314 seq: Seq,
1315 tick: Tick,
1316 },
1317 Chat(ChatMessage),
1318 HarvestResult(HarvestResult),
1319 UseResult(UseResult),
1320 CraftResult(CraftResult),
1321 Death(DeathNotice),
1322 Interaction(InteractionNotice),
1323}
1324
1325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1326pub enum ClientMessage {
1327 Hello(Hello),
1328 Intent(Intent),
1329 Disconnect,
1330}
1331
1332#[cfg(test)]
1333mod tests {
1334 use super::*;
1335
1336 #[test]
1337 fn pristine_vitals_state_yields_full_pools() {
1338 let attrs = PrimaryAttributes::default();
1339 let vitals = StoredVitalsState::default().apply_to(attrs);
1340 assert!(vitals.health > 0.0);
1341 assert_eq!(vitals.health, vitals.health_max);
1342 assert!((vitals.mana_max - 145.0).abs() < 0.01);
1343 }
1344
1345 #[test]
1346 fn saved_vitals_scale_when_pool_max_increases() {
1347 let mut attrs = PrimaryAttributes::default();
1348 attrs.intelligence = 140;
1349 attrs.wisdom = 140;
1350 let saved = StoredVitalsState {
1351 health: 100.0,
1352 mana: 14.0,
1353 stamina: 100.0,
1354 ..StoredVitalsState::default()
1355 };
1356 let vitals = saved.apply_to(attrs);
1357 assert!(vitals.mana_max > 55.0);
1358 assert!(
1359 (vitals.mana - vitals.mana_max).abs() < 0.01,
1360 "full legacy mana bar migrates to full new bar"
1361 );
1362 }
1363
1364 #[test]
1365 fn empty_vitals_state_is_pristine() {
1366 let pristine = StoredVitalsState {
1367 health: 0.0,
1368 mana: 0.0,
1369 stamina: 0.0,
1370 hunger: 0.0,
1371 thirst: 0.0,
1372 coins: 0,
1373 deaths: 0,
1374 life_state: LifeState::Alive,
1375 };
1376 assert!(pristine.is_pristine());
1377 let vitals = pristine.apply_to(PrimaryAttributes::default());
1378 assert!(vitals.health > 0.0);
1379 }
1380
1381 #[test]
1382 fn stored_vitals_roundtrip_preserves_partial_pools() {
1383 let attrs = PrimaryAttributes::default();
1384 let mut live = PlayerVitals::from_attributes(attrs);
1385 live.health = 42.0;
1386 live.hunger = 77.0;
1387 live.deaths = 2;
1388 let stored = StoredVitalsState::from_live(&live);
1389 let restored = stored.apply_to(attrs);
1390 assert!((restored.health - 42.0).abs() < 0.01, "partial HP below old cap stays absolute");
1391 assert_eq!(restored.hunger, 77.0);
1392 assert_eq!(restored.deaths, 2);
1393 }
1394
1395 #[test]
1396 fn skill_tiers_start_at_zero() {
1397 let skill = SkillProgress::default();
1398 assert_eq!(skill.level, 0);
1399 assert_eq!(skill.display_tier(), 0);
1400 let trained = SkillProgress {
1401 level: 250,
1402 last_trained_tick: 1,
1403 };
1404 assert_eq!(trained.display_tier(), 2);
1405 }
1406}