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 #[serde(default)]
447 pub known_blueprint_ids: Vec<String>,
448}
449
450fn default_auto_attack() -> bool {
451 true
452}
453
454impl Default for StoredCombatProfile {
455 fn default() -> Self {
456 Self {
457 combat_target_instance_id: None,
458 in_combat: false,
459 last_combat_tick: 0,
460 last_attack_tick: 0,
461 cooldowns_until_tick: BTreeMap::new(),
462 auto_attack_enabled: true,
463 mainhand_template_id: None,
464 worn: Vec::new(),
465 rotation_presets: Vec::new(),
466 target_slots: Vec::new(),
467 known_blueprint_ids: Vec::new(),
468 }
469 }
470}
471
472#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
473pub struct EntityState {
474 pub id: EntityId,
475 pub transform: Transform,
476 #[serde(default)]
478 pub label: String,
479 #[serde(default)]
480 pub vitals: Option<PlayerVitals>,
481 #[serde(default)]
483 pub attributes: Option<PrimaryAttributes>,
484 #[serde(default)]
485 pub skills: Option<PlayerSkills>,
486 #[serde(default)]
488 pub inside_building: Option<String>,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(rename_all = "snake_case")]
494pub enum ChatChannel {
495 Nearby,
496 WhisperStone,
497}
498
499#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
500pub struct ChatMessage {
501 pub channel: ChatChannel,
502 pub from_entity: EntityId,
503 pub from_name: String,
504 pub text: String,
505 pub tick: Tick,
506}
507
508#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
510pub enum Intent {
511 Move {
512 entity_id: EntityId,
513 forward: f32,
514 strafe: f32,
515 #[serde(default)]
517 vertical: f32,
518 #[serde(default)]
520 sprint: bool,
521 seq: Seq,
522 },
523 Stop {
524 entity_id: EntityId,
525 seq: Seq,
526 },
527 Harvest {
528 entity_id: EntityId,
529 node_id: String,
530 seq: Seq,
531 },
532 Use {
533 entity_id: EntityId,
534 template_id: String,
535 seq: Seq,
536 },
537 Say {
538 entity_id: EntityId,
539 channel: ChatChannel,
540 text: String,
541 seq: Seq,
542 },
543 Craft {
545 entity_id: EntityId,
546 blueprint_id: String,
547 #[serde(default)]
549 count: Option<u32>,
550 seq: Seq,
551 },
552 Interact {
554 entity_id: EntityId,
555 target_id: String,
556 seq: Seq,
557 },
558 ShopBuy {
560 entity_id: EntityId,
561 npc_id: String,
562 offer_id: String,
563 #[serde(default = "default_one")]
564 quantity: u32,
565 seq: Seq,
566 },
567 ShopSell {
569 entity_id: EntityId,
570 npc_id: String,
571 template_id: String,
572 #[serde(default = "default_one")]
573 quantity: u32,
574 seq: Seq,
575 },
576 TestDamage {
578 entity_id: EntityId,
579 amount: f32,
580 seq: Seq,
581 },
582 SetTarget {
584 entity_id: EntityId,
585 target_id: EntityId,
586 seq: Seq,
587 },
588 SetTargetSlot {
590 entity_id: EntityId,
591 slot_index: u8,
592 target_id: EntityId,
593 seq: Seq,
594 },
595 ClearTarget {
596 entity_id: EntityId,
597 seq: Seq,
598 },
599 ClearTargetSlot {
600 entity_id: EntityId,
601 slot_index: u8,
602 seq: Seq,
603 },
604 SetAutoAttack {
606 entity_id: EntityId,
607 slot_index: u8,
608 enabled: bool,
609 seq: Seq,
610 },
611 Attack {
613 entity_id: EntityId,
614 #[serde(default)]
615 target_id: Option<EntityId>,
616 #[serde(default)]
617 weapon_slot: Option<u32>,
618 seq: Seq,
619 },
620 Pickup {
622 entity_id: EntityId,
623 #[serde(default)]
624 drop_id: Option<String>,
625 seq: Seq,
626 },
627 Cast {
629 entity_id: EntityId,
630 ability_id: String,
631 target_id: EntityId,
632 seq: Seq,
633 },
634 BindActionSlot {
636 entity_id: EntityId,
637 slot_index: u8,
638 ability_id: String,
639 #[serde(default = "default_auto_attack")]
640 auto_enabled: bool,
641 seq: Seq,
642 },
643 UseActionSlot {
645 entity_id: EntityId,
646 slot_index: u8,
647 seq: Seq,
648 },
649 Dodge {
651 entity_id: EntityId,
652 seq: Seq,
653 },
654 Lunge {
656 entity_id: EntityId,
657 #[serde(default)]
659 forward: f32,
660 #[serde(default)]
662 strafe: f32,
663 seq: Seq,
664 },
665 Block {
667 entity_id: EntityId,
668 #[serde(default = "default_block_enabled")]
669 enabled: bool,
670 seq: Seq,
671 },
672 EquipMainhand {
674 entity_id: EntityId,
675 #[serde(default)]
676 template_id: Option<String>,
677 seq: Seq,
678 },
679 EquipWorn {
682 entity_id: EntityId,
683 slot: BodySlot,
684 #[serde(default)]
685 instance_id: Option<Uuid>,
686 seq: Seq,
687 },
688 MoveItem {
690 entity_id: EntityId,
691 item_instance_id: Uuid,
692 from: InventoryLocation,
693 to: InventoryLocation,
694 #[serde(default)]
696 to_parent_instance_id: Option<Uuid>,
697 #[serde(default)]
699 quantity: Option<u32>,
700 seq: Seq,
701 },
702 PlaceContainer {
704 entity_id: EntityId,
705 item_instance_id: Uuid,
706 seq: Seq,
707 },
708 PickupContainer {
710 entity_id: EntityId,
711 container_id: String,
712 seq: Seq,
713 },
714 SetContainerLocked {
716 entity_id: EntityId,
717 location: InventoryLocation,
719 locked: bool,
720 seq: Seq,
721 },
722 DropItem {
724 entity_id: EntityId,
725 item_instance_id: Uuid,
726 from: InventoryLocation,
727 seq: Seq,
728 },
729 DestroyItem {
731 entity_id: EntityId,
732 item_instance_id: Uuid,
733 from: InventoryLocation,
734 #[serde(default)]
736 quantity: Option<u32>,
737 seq: Seq,
738 },
739 RenameContainer {
741 entity_id: EntityId,
742 item_instance_id: Uuid,
743 location: InventoryLocation,
744 name: String,
745 seq: Seq,
746 },
747 UpsertRotationPreset {
749 entity_id: EntityId,
750 preset: RotationPreset,
751 seq: Seq,
752 },
753 DeleteRotationPreset {
755 entity_id: EntityId,
756 preset_id: String,
757 seq: Seq,
758 },
759 AssignSlotPreset {
761 entity_id: EntityId,
762 slot_index: u8,
763 preset_id: String,
764 seq: Seq,
765 },
766 AdvanceRotation {
768 entity_id: EntityId,
769 slot_index: u8,
770 seq: Seq,
771 },
772}
773
774fn default_block_enabled() -> bool {
775 true
776}
777
778#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
780pub struct CombatTargetHud {
781 pub entity_id: EntityId,
782 #[serde(default)]
783 pub label: String,
784 #[serde(default)]
785 pub level: u32,
786 pub health: f32,
787 pub health_max: f32,
788 #[serde(default)]
789 pub life_state: LifeState,
790 #[serde(default)]
791 pub distance_m: f32,
792}
793
794#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
796pub struct CastProgressHud {
797 #[serde(default)]
798 pub ability_id: String,
799 #[serde(default)]
800 pub ability_label: String,
801 #[serde(default)]
802 pub ticks_remaining: u64,
803 #[serde(default)]
804 pub ticks_total: u64,
805}
806
807#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
809pub struct AbilityCooldownHud {
810 #[serde(default)]
811 pub ability_id: String,
812 #[serde(default)]
813 pub label: String,
814 #[serde(default)]
815 pub cd_ticks: u64,
816 #[serde(default)]
817 pub cd_total_ticks: u64,
818}
819
820#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
822pub struct CombatSlotHud {
823 pub slot_index: u8,
824 #[serde(default)]
825 pub target_entity_id: Option<EntityId>,
826 #[serde(default)]
827 pub target_label: Option<String>,
828 #[serde(default)]
829 pub target: Option<CombatTargetHud>,
830 #[serde(default)]
831 pub preset_id: Option<String>,
832 #[serde(default)]
833 pub preset_label: Option<String>,
834 #[serde(default)]
835 pub rotation: Vec<String>,
836 #[serde(default)]
837 pub rotation_index: u32,
838 #[serde(default)]
839 pub next_ability_id: Option<String>,
840 #[serde(default)]
841 pub auto_enabled: bool,
842}
843
844#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
846pub struct CombatHud {
847 pub in_combat: bool,
848 pub auto_attack: bool,
850 pub has_los: bool,
851 pub attack_cd_ticks: u64,
852 #[serde(default)]
853 pub ability_id: String,
854 #[serde(default)]
855 pub target_entity_id: Option<EntityId>,
856 #[serde(default)]
857 pub target_label: Option<String>,
858 #[serde(default)]
859 pub max_target_slots: u8,
860 #[serde(default)]
861 pub slots: Vec<CombatSlotHud>,
862 #[serde(default)]
863 pub rotation_presets: Vec<RotationPreset>,
864 #[serde(default)]
865 pub gcd_ticks: u64,
866 #[serde(default)]
867 pub mainhand_template_id: Option<String>,
868 #[serde(default)]
869 pub mainhand_label: Option<String>,
870 #[serde(default)]
872 pub worn: Vec<(BodySlot, ItemStack)>,
873 #[serde(default)]
874 pub carry_mass: f32,
875 #[serde(default)]
876 pub carry_mass_max: f32,
877 #[serde(default)]
878 pub encumbrance: EncumbranceState,
879 #[serde(default)]
880 pub target: Option<CombatTargetHud>,
881 #[serde(default)]
882 pub cast: Option<CastProgressHud>,
883 #[serde(default)]
884 pub ability_cooldowns: Vec<AbilityCooldownHud>,
885 #[serde(default)]
886 pub blocking_active: bool,
887}
888
889#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
891pub struct TickDelta {
892 pub tick: Tick,
893 pub entities: Vec<EntityState>,
894 #[serde(default)]
895 pub resource_nodes: Vec<ResourceNodeView>,
896 #[serde(default)]
897 pub buildings: Vec<BuildingView>,
898 #[serde(default)]
899 pub doors: Vec<DoorView>,
900 #[serde(default)]
901 pub npcs: Vec<NpcView>,
902 #[serde(default)]
904 pub inventory: Vec<ItemStack>,
905 #[serde(default)]
906 pub blueprints: Vec<BlueprintView>,
907 #[serde(default)]
908 pub world_clock: WorldClock,
909 #[serde(default)]
910 pub ground_drops: Vec<GroundDropView>,
911 #[serde(default)]
912 pub placed_containers: Vec<PlacedContainerView>,
913 #[serde(default)]
914 pub combat: Option<CombatHud>,
915}
916
917#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
919pub struct GroundDropView {
920 pub id: String,
921 pub template_id: String,
922 pub quantity: u32,
923 pub x: f32,
924 pub y: f32,
925 pub z: f32,
926}
927
928#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
930pub struct Snapshot {
931 pub tick: Tick,
932 pub chunk_rev: u64,
933 #[serde(default)]
935 pub content_rev: u64,
936 pub entities: Vec<EntityState>,
937 #[serde(default)]
938 pub resource_nodes: Vec<ResourceNodeView>,
939 #[serde(default)]
941 pub world_width_m: f32,
942 #[serde(default)]
943 pub world_height_m: f32,
944 #[serde(default)]
945 pub buildings: Vec<BuildingView>,
946 #[serde(default)]
947 pub doors: Vec<DoorView>,
948 #[serde(default)]
949 pub npcs: Vec<NpcView>,
950 #[serde(default)]
951 pub inventory: Vec<ItemStack>,
952 #[serde(default)]
953 pub blueprints: Vec<BlueprintView>,
954 #[serde(default)]
955 pub world_clock: WorldClock,
956 #[serde(default)]
957 pub terrain_zones: Vec<TerrainZoneView>,
958 #[serde(default)]
959 pub ground_drops: Vec<GroundDropView>,
960 #[serde(default)]
961 pub placed_containers: Vec<PlacedContainerView>,
962 #[serde(default)]
963 pub combat: Option<CombatHud>,
964}
965
966#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
968pub struct ResourceNodeView {
969 pub id: String,
970 pub label: String,
971 pub x: f32,
972 pub y: f32,
973 pub z: f32,
974 pub item_template: String,
975 #[serde(default = "default_node_state")]
976 pub state: ResourceNodeState,
977 #[serde(default = "default_blocking_view")]
979 pub blocking: bool,
980 #[serde(default = "default_blocking_radius_view")]
982 pub blocking_radius_m: f32,
983}
984
985fn default_blocking_radius_view() -> f32 {
986 0.8
987}
988
989fn default_blocking_view() -> bool {
990 true
991}
992
993#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
994#[serde(rename_all = "snake_case")]
995pub enum ResourceNodeState {
996 Available,
997 Harvesting,
998 Cooldown,
999}
1000
1001fn default_node_state() -> ResourceNodeState {
1002 ResourceNodeState::Available
1003}
1004
1005#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1006pub struct ItemStack {
1007 pub template_id: String,
1008 pub quantity: u32,
1009 #[serde(default)]
1011 pub item_instance_id: Option<Uuid>,
1012 #[serde(default)]
1014 pub props: BTreeMap<String, String>,
1015 #[serde(default)]
1017 pub contents: Vec<ItemStack>,
1018 #[serde(default)]
1020 pub display_name: Option<String>,
1021 #[serde(default)]
1023 pub category: Option<String>,
1024 #[serde(default)]
1026 pub base_mass: Option<f32>,
1027 #[serde(default)]
1029 pub base_volume: Option<f32>,
1030 #[serde(default)]
1032 pub capacity_volume: Option<f32>,
1033 #[serde(default)]
1035 pub stackable: Option<bool>,
1036}
1037
1038impl ItemStack {
1039 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
1040 Self {
1041 template_id: template_id.into(),
1042 quantity,
1043 item_instance_id: None,
1044 props: BTreeMap::new(),
1045 contents: Vec::new(),
1046 display_name: None,
1047 category: None,
1048 base_mass: None,
1049 base_volume: None,
1050 capacity_volume: None,
1051 stackable: None,
1052 }
1053 }
1054}
1055
1056#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1058#[serde(rename_all = "snake_case")]
1059pub enum EncumbranceState {
1060 #[default]
1061 Light,
1062 Heavy,
1063 Over,
1064}
1065
1066#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
1070#[serde(rename_all = "snake_case")]
1071pub enum BodySlot {
1072 Head,
1073 Body,
1074 Arms,
1075 Legs,
1076 Feet,
1077 Back,
1078 Waist,
1079}
1080
1081#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1083#[serde(rename_all = "snake_case")]
1084pub enum InventoryLocation {
1085 Root,
1087 Worn {
1089 slot: BodySlot,
1090 },
1091 Placed {
1093 container_id: String,
1094 },
1095}
1096
1097#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1099pub struct PlacedContainerView {
1100 pub id: String,
1101 pub template_id: String,
1102 pub display_name: String,
1103 pub x: f32,
1104 pub y: f32,
1105 pub z: f32,
1106 pub locked: bool,
1107 #[serde(default)]
1109 pub accessible: bool,
1110 #[serde(default)]
1111 pub owner_character_id: Option<Uuid>,
1112 #[serde(default)]
1114 pub contents: Vec<ItemStack>,
1115 #[serde(default)]
1117 pub lock_id: Option<String>,
1118 #[serde(default)]
1120 pub capacity_volume: Option<f32>,
1121 #[serde(default)]
1123 pub item_instance_id: Option<Uuid>,
1124}
1125
1126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1127pub struct BlueprintIngredientView {
1128 pub template_id: String,
1129 pub quantity: u32,
1130 pub consumed: bool,
1132}
1133
1134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1135pub struct ToolRequirementView {
1136 pub item: String,
1137 pub consumed: bool,
1139}
1140
1141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1142pub struct SkillRequirementView {
1143 pub skill: String,
1144 pub level: u32,
1145}
1146
1147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1148pub struct BlueprintView {
1149 pub id: String,
1150 pub label: String,
1151 pub output: String,
1152 pub output_qty: u32,
1153 pub craft_ticks: u32,
1154 pub inputs: Vec<BlueprintIngredientView>,
1155 #[serde(default)]
1157 pub station: Option<String>,
1158 #[serde(default)]
1159 pub category: Option<String>,
1160 #[serde(default)]
1161 pub required_tools: Vec<ToolRequirementView>,
1162 #[serde(default)]
1163 pub skill: Option<SkillRequirementView>,
1164 #[serde(default)]
1165 pub failure_chance: f32,
1166}
1167
1168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1170#[serde(rename_all = "snake_case")]
1171pub enum TerrainKindView {
1172 #[default]
1173 Grass,
1174 Hill,
1175 Bog,
1176 ShallowWater,
1177 DeepWater,
1178}
1179
1180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1181pub struct TerrainZoneView {
1182 pub id: String,
1183 pub x0: f32,
1184 pub y0: f32,
1185 pub x1: f32,
1186 pub y1: f32,
1187 #[serde(default)]
1188 pub kind: TerrainKindView,
1189}
1190
1191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1192pub struct BuildingView {
1193 pub id: String,
1194 pub label: String,
1195 pub x: f32,
1196 pub y: f32,
1197 pub width_m: f32,
1198 pub depth_m: f32,
1199 #[serde(default)]
1201 pub interior_origin_x: f32,
1202 #[serde(default)]
1203 pub interior_origin_y: f32,
1204 #[serde(default)]
1205 pub tags: Vec<String>,
1206}
1207
1208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1209pub struct DoorView {
1210 pub id: String,
1211 pub building_id: String,
1212 pub x: f32,
1213 pub y: f32,
1214 #[serde(default)]
1215 pub open: bool,
1216 #[serde(default)]
1217 pub is_exit: bool,
1218}
1219
1220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1221pub struct NpcView {
1222 pub id: String,
1223 pub label: String,
1224 pub role: String,
1225 pub x: f32,
1226 pub y: f32,
1227 #[serde(default)]
1229 pub building_id: Option<String>,
1230 #[serde(default)]
1232 pub entity_id: Option<EntityId>,
1233 #[serde(default)]
1234 pub life_state: Option<LifeState>,
1235 #[serde(default)]
1236 pub hp_pct: Option<f32>,
1237}
1238
1239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1240pub struct UseResult {
1241 pub template_id: String,
1242 pub hunger_restored: f32,
1243 pub thirst_restored: f32,
1244}
1245
1246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1247pub struct CraftResult {
1248 pub blueprint_id: String,
1249 pub outputs: Vec<ItemStack>,
1250 pub consumed: Vec<ItemStack>,
1251 #[serde(default = "default_one")]
1253 pub batch_index: u32,
1254 #[serde(default = "default_one")]
1256 pub batch_total: u32,
1257}
1258
1259fn default_one() -> u32 {
1260 1
1261}
1262
1263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1264pub struct DeathNotice {
1265 pub entity_id: EntityId,
1266 pub respawn_x: f32,
1267 pub respawn_y: f32,
1268 pub message: String,
1269}
1270
1271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1272pub struct InteractionNotice {
1273 pub target_id: String,
1274 pub message: String,
1275 #[serde(default)]
1276 pub coins_delta: i32,
1277 #[serde(default)]
1278 pub inventory_delta: Vec<ItemStack>,
1279}
1280
1281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1282#[serde(rename_all = "snake_case")]
1283pub enum ShopOfferKind {
1284 Item,
1285 Blueprint,
1286}
1287
1288#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1289pub struct ShopOffer {
1290 pub offer_id: String,
1291 pub kind: ShopOfferKind,
1292 pub label: String,
1293 #[serde(default)]
1294 pub template_id: Option<String>,
1295 #[serde(default)]
1296 pub blueprint_id: Option<String>,
1297 pub price_copper: u32,
1298 #[serde(default)]
1299 pub affordable: bool,
1300 #[serde(default)]
1301 pub already_owned: bool,
1302}
1303
1304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1305pub struct ShopBuyLine {
1306 pub template_id: String,
1307 pub label: String,
1308 pub quantity: u32,
1309 pub price_copper: u32,
1310}
1311
1312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1313pub struct ShopCatalog {
1314 pub npc_id: String,
1315 pub npc_label: String,
1316 #[serde(default)]
1317 pub sells: Vec<ShopOffer>,
1318 #[serde(default)]
1319 pub buys: Vec<ShopBuyLine>,
1320}
1321
1322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1323pub struct HarvestResult {
1324 pub node_id: String,
1325 pub quantity: u32,
1327 pub item_template: String,
1328 #[serde(default)]
1331 pub item_instance_id: Option<Uuid>,
1332}
1333
1334#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1336pub struct Envelope<T> {
1337 pub protocol_version: u16,
1338 pub payload: T,
1339}
1340
1341impl<T> Envelope<T> {
1342 pub fn new(payload: T) -> Self {
1343 Self {
1344 protocol_version: crate::PROTOCOL_VERSION,
1345 payload,
1346 }
1347 }
1348}
1349
1350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1352pub struct Hello {
1353 pub client_name: String,
1354 pub protocol_version: u16,
1355 #[serde(default)]
1356 pub auth: AuthCredential,
1357 #[serde(default)]
1359 pub character_id: Option<Uuid>,
1360}
1361
1362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1365#[serde(rename_all = "snake_case")]
1366pub enum AuthCredential {
1367 DevLocal,
1368 Session { token: String },
1369 ApiToken { token: String, character_id: Uuid },
1370}
1371
1372impl Default for AuthCredential {
1373 fn default() -> Self {
1374 Self::DevLocal
1375 }
1376}
1377
1378#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1379pub struct Welcome {
1380 pub session_id: SessionId,
1381 pub entity_id: EntityId,
1382 pub snapshot: Snapshot,
1383}
1384
1385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1386pub enum ServerMessage {
1387 Welcome(Welcome),
1388 ContentUpdated(Snapshot),
1390 Tick(TickDelta),
1391 IntentAck {
1392 entity_id: EntityId,
1393 seq: Seq,
1394 tick: Tick,
1395 },
1396 Chat(ChatMessage),
1397 HarvestResult(HarvestResult),
1398 UseResult(UseResult),
1399 CraftResult(CraftResult),
1400 Death(DeathNotice),
1401 Interaction(InteractionNotice),
1402 ShopOpened(ShopCatalog),
1403}
1404
1405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1406pub enum ClientMessage {
1407 Hello(Hello),
1408 Intent(Intent),
1409 Disconnect,
1410}
1411
1412#[cfg(test)]
1413mod tests {
1414 use super::*;
1415
1416 #[test]
1417 fn pristine_vitals_state_yields_full_pools() {
1418 let attrs = PrimaryAttributes::default();
1419 let vitals = StoredVitalsState::default().apply_to(attrs);
1420 assert!(vitals.health > 0.0);
1421 assert_eq!(vitals.health, vitals.health_max);
1422 assert!((vitals.mana_max - 145.0).abs() < 0.01);
1423 }
1424
1425 #[test]
1426 fn saved_vitals_scale_when_pool_max_increases() {
1427 let mut attrs = PrimaryAttributes::default();
1428 attrs.intelligence = 140;
1429 attrs.wisdom = 140;
1430 let saved = StoredVitalsState {
1431 health: 100.0,
1432 mana: 14.0,
1433 stamina: 100.0,
1434 ..StoredVitalsState::default()
1435 };
1436 let vitals = saved.apply_to(attrs);
1437 assert!(vitals.mana_max > 55.0);
1438 assert!(
1439 (vitals.mana - vitals.mana_max).abs() < 0.01,
1440 "full legacy mana bar migrates to full new bar"
1441 );
1442 }
1443
1444 #[test]
1445 fn empty_vitals_state_is_pristine() {
1446 let pristine = StoredVitalsState {
1447 health: 0.0,
1448 mana: 0.0,
1449 stamina: 0.0,
1450 hunger: 0.0,
1451 thirst: 0.0,
1452 coins: 0,
1453 deaths: 0,
1454 life_state: LifeState::Alive,
1455 };
1456 assert!(pristine.is_pristine());
1457 let vitals = pristine.apply_to(PrimaryAttributes::default());
1458 assert!(vitals.health > 0.0);
1459 }
1460
1461 #[test]
1462 fn stored_vitals_roundtrip_preserves_partial_pools() {
1463 let attrs = PrimaryAttributes::default();
1464 let mut live = PlayerVitals::from_attributes(attrs);
1465 live.health = 42.0;
1466 live.hunger = 77.0;
1467 live.deaths = 2;
1468 let stored = StoredVitalsState::from_live(&live);
1469 let restored = stored.apply_to(attrs);
1470 assert!((restored.health - 42.0).abs() < 0.01, "partial HP below old cap stays absolute");
1471 assert_eq!(restored.hunger, 77.0);
1472 assert_eq!(restored.deaths, 2);
1473 }
1474
1475 #[test]
1476 fn skill_tiers_start_at_zero() {
1477 let skill = SkillProgress::default();
1478 assert_eq!(skill.level, 0);
1479 assert_eq!(skill.display_tier(), 0);
1480 let trained = SkillProgress {
1481 level: 250,
1482 last_trained_tick: 1,
1483 };
1484 assert_eq!(trained.display_tier(), 2);
1485 }
1486}