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 HireWorker {
919 entity_id: EntityId,
920 def_id: String,
921 wage_copper_per_interval: u32,
922 #[serde(default)]
923 lodging_container_id: Option<String>,
924 #[serde(default)]
925 job_yaml: Option<String>,
926 seq: Seq,
927 },
928 DismissWorker {
930 entity_id: EntityId,
931 worker_instance_id: String,
932 seq: Seq,
933 },
934 SetWorkerJob {
936 entity_id: EntityId,
937 worker_instance_id: String,
938 job_yaml: String,
939 seq: Seq,
940 },
941 AssignWorkerLodging {
943 entity_id: EntityId,
944 worker_instance_id: String,
945 lodging_container_id: String,
946 seq: Seq,
947 },
948 SetWorkerMode {
950 entity_id: EntityId,
951 worker_instance_id: String,
952 mode: String,
953 seq: Seq,
954 },
955 GiveWorkerItem {
958 entity_id: EntityId,
959 worker_instance_id: String,
960 item_instance_id: uuid::Uuid,
961 #[serde(default)]
962 quantity: Option<u32>,
963 seq: Seq,
964 },
965 TakeWorkerItem {
967 entity_id: EntityId,
968 worker_instance_id: String,
969 item_instance_id: uuid::Uuid,
970 #[serde(default)]
971 quantity: Option<u32>,
972 seq: Seq,
973 },
974 RenameHiredWorker {
976 entity_id: EntityId,
977 worker_instance_id: String,
978 name: String,
979 seq: Seq,
980 },
981 TeachWorkerBlueprint {
983 entity_id: EntityId,
984 worker_instance_id: String,
985 blueprint_id: String,
986 seq: Seq,
987 },
988}
989
990fn default_block_enabled() -> bool {
991 true
992}
993
994#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
996pub struct CombatTargetHud {
997 pub entity_id: EntityId,
998 #[serde(default)]
999 pub label: String,
1000 #[serde(default)]
1001 pub level: u32,
1002 pub health: f32,
1003 pub health_max: f32,
1004 #[serde(default)]
1005 pub life_state: LifeState,
1006 #[serde(default)]
1007 pub distance_m: f32,
1008}
1009
1010#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1012pub struct CastProgressHud {
1013 #[serde(default)]
1014 pub ability_id: String,
1015 #[serde(default)]
1016 pub ability_label: String,
1017 #[serde(default)]
1018 pub ticks_remaining: u64,
1019 #[serde(default)]
1020 pub ticks_total: u64,
1021}
1022
1023#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1025pub struct AbilityCooldownHud {
1026 #[serde(default)]
1027 pub ability_id: String,
1028 #[serde(default)]
1029 pub label: String,
1030 #[serde(default)]
1031 pub cd_ticks: u64,
1032 #[serde(default)]
1033 pub cd_total_ticks: u64,
1034}
1035
1036#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1038pub struct CombatSlotHud {
1039 pub slot_index: u8,
1040 #[serde(default)]
1041 pub target_entity_id: Option<EntityId>,
1042 #[serde(default)]
1043 pub target_label: Option<String>,
1044 #[serde(default)]
1045 pub target: Option<CombatTargetHud>,
1046 #[serde(default)]
1047 pub preset_id: Option<String>,
1048 #[serde(default)]
1049 pub preset_label: Option<String>,
1050 #[serde(default)]
1051 pub rotation: Vec<String>,
1052 #[serde(default)]
1053 pub rotation_index: u32,
1054 #[serde(default)]
1055 pub next_ability_id: Option<String>,
1056 #[serde(default)]
1057 pub auto_enabled: bool,
1058}
1059
1060#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1062pub struct CombatHud {
1063 pub in_combat: bool,
1064 pub auto_attack: bool,
1066 pub has_los: bool,
1067 pub attack_cd_ticks: u64,
1068 #[serde(default)]
1069 pub ability_id: String,
1070 #[serde(default)]
1071 pub target_entity_id: Option<EntityId>,
1072 #[serde(default)]
1073 pub target_label: Option<String>,
1074 #[serde(default)]
1075 pub max_target_slots: u8,
1076 #[serde(default)]
1077 pub slots: Vec<CombatSlotHud>,
1078 #[serde(default)]
1079 pub rotation_presets: Vec<RotationPreset>,
1080 #[serde(default)]
1081 pub gcd_ticks: u64,
1082 #[serde(default)]
1083 pub mainhand_template_id: Option<String>,
1084 #[serde(default)]
1085 pub mainhand_label: Option<String>,
1086 #[serde(default)]
1088 pub worn: Vec<(BodySlot, ItemStack)>,
1089 #[serde(default)]
1090 pub carry_mass: f32,
1091 #[serde(default)]
1092 pub carry_mass_max: f32,
1093 #[serde(default)]
1094 pub encumbrance: EncumbranceState,
1095 #[serde(default)]
1097 pub keychain: Vec<ItemStack>,
1098 #[serde(default)]
1099 pub target: Option<CombatTargetHud>,
1100 #[serde(default)]
1101 pub cast: Option<CastProgressHud>,
1102 #[serde(default)]
1103 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1104 #[serde(default)]
1105 pub blocking_active: bool,
1106 #[serde(default)]
1108 pub progression_xp: Option<ProgressionXp>,
1109 #[serde(default)]
1110 pub progression_baseline: u16,
1111 #[serde(default)]
1112 pub progression_xp_base: f64,
1113 #[serde(default)]
1114 pub progression_xp_growth: f64,
1115 #[serde(default)]
1116 pub attributes: Option<PrimaryAttributes>,
1117 #[serde(default)]
1118 pub skills: Option<PlayerSkills>,
1119}
1120
1121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1123#[serde(rename_all = "snake_case")]
1124pub enum WorkerModeView {
1125 Companion,
1126 JobLoop,
1127 Idle,
1130}
1131
1132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1134#[serde(rename_all = "snake_case")]
1135pub enum WorkerStateView {
1136 Idle,
1137 Traveling,
1138 Working,
1139 Resting,
1140 Waiting,
1141 Strike,
1142 Dismissed,
1143}
1144
1145#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
1147pub struct WorkerVitalsSummary {
1148 pub health_pct: f32,
1149 pub stamina_pct: f32,
1150}
1151
1152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1154#[serde(rename_all = "snake_case")]
1155pub enum WorkerRouteKindView {
1156 #[default]
1157 HarvestLoop,
1158 Ordered,
1159}
1160
1161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1163pub struct WorkerRouteView {
1164 #[serde(default)]
1165 pub kind: WorkerRouteKindView,
1166 #[serde(default)]
1167 pub lodging_container_id: Option<String>,
1168 #[serde(default)]
1170 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
1171 #[serde(default)]
1173 pub harvest_nodes: Vec<String>,
1174 #[serde(default = "default_route_carry_ratio")]
1175 pub carry_return_ratio: f32,
1176 #[serde(default)]
1178 pub stops: Vec<WorkerRouteStopView>,
1179}
1180
1181fn default_route_carry_ratio() -> f32 {
1182 0.90
1183}
1184
1185fn default_true_view() -> bool {
1186 true
1187}
1188
1189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1191pub struct WorkerWithdrawItemView {
1192 pub template: String,
1193 #[serde(default)]
1194 pub qty: u32,
1195 #[serde(default)]
1197 pub all: bool,
1198}
1199
1200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1201pub struct WorkerRouteWaypointView {
1202 pub x: f32,
1203 pub y: f32,
1204 pub z: f32,
1205}
1206
1207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1216#[serde(rename_all = "snake_case")]
1217pub enum WorkerRouteStopView {
1218 Waypoint {
1219 x: f32,
1220 y: f32,
1221 #[serde(default)]
1222 z: f32,
1223 },
1224 HarvestNode {
1225 node_id: String,
1226 },
1227 DepositAt {
1228 container_id: String,
1229 #[serde(default)]
1230 filter: Option<Vec<String>>,
1231 },
1232 TradeWith {
1233 #[serde(default)]
1234 npc_id: Option<String>,
1235 template: String,
1236 #[serde(default = "default_true_view")]
1237 sell_all: bool,
1238 },
1239 WithdrawFrom {
1240 container_id: String,
1241 items: Vec<WorkerWithdrawItemView>,
1242 },
1243 CraftAt {
1244 device: String,
1245 blueprint: String,
1246 #[serde(default)]
1247 qty: Option<u32>,
1248 },
1249 RestIfNeeded,
1250 Wait {
1251 #[serde(default)]
1252 wait_ticks: u64,
1253 },
1254}
1255
1256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1258pub struct HiredWorkerView {
1259 pub instance_id: String,
1260 pub entity_id: EntityId,
1261 pub def_id: String,
1262 pub label: String,
1264 pub x: f32,
1265 pub y: f32,
1266 pub z: f32,
1267 pub mode: WorkerModeView,
1268 pub state: WorkerStateView,
1269 #[serde(default)]
1270 pub step_label: String,
1271 pub vitals: WorkerVitalsSummary,
1272 #[serde(default)]
1273 pub carry_pct: f32,
1274 #[serde(default)]
1275 pub last_error: Option<String>,
1276 pub wage_copper_per_interval: u32,
1277 #[serde(default)]
1279 pub effective_wage_copper: u32,
1280 #[serde(default)]
1282 pub wage_meters_walked: f32,
1283 #[serde(default)]
1285 pub lodging_container_id: Option<String>,
1286 #[serde(default)]
1288 pub route: Option<WorkerRouteView>,
1289 #[serde(default)]
1291 pub known_blueprint_ids: Vec<String>,
1292 #[serde(default = "default_worker_view_level")]
1294 pub level: u32,
1295 #[serde(default)]
1297 pub worker_xp: f64,
1298 #[serde(default)]
1300 pub inventory: Vec<ItemStack>,
1301}
1302
1303fn default_worker_view_level() -> u32 {
1304 1
1305}
1306
1307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1309pub struct TickDelta {
1310 pub tick: Tick,
1311 pub entities: Vec<EntityState>,
1312 #[serde(default)]
1313 pub resource_nodes: Vec<ResourceNodeView>,
1314 #[serde(default)]
1315 pub buildings: Vec<BuildingView>,
1316 #[serde(default)]
1317 pub doors: Vec<DoorView>,
1318 #[serde(default)]
1319 pub npcs: Vec<NpcView>,
1320 #[serde(default)]
1322 pub inventory: Vec<ItemStack>,
1323 #[serde(default)]
1324 pub blueprints: Vec<BlueprintView>,
1325 #[serde(default)]
1326 pub world_clock: WorldClock,
1327 #[serde(default)]
1328 pub ground_drops: Vec<GroundDropView>,
1329 #[serde(default)]
1330 pub placed_containers: Vec<PlacedContainerView>,
1331 #[serde(default)]
1332 pub combat: Option<CombatHud>,
1333 #[serde(default)]
1334 pub interior_map: Option<InteriorMapView>,
1335 #[serde(default)]
1336 pub quest_log: Vec<QuestLogEntry>,
1337 #[serde(default)]
1338 pub hired_workers: Vec<HiredWorkerView>,
1339 #[serde(default)]
1340 pub interactables: Vec<InteractableView>,
1341}
1342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1343pub struct GroundDropView {
1344 pub id: String,
1345 pub template_id: String,
1346 pub quantity: u32,
1347 pub x: f32,
1348 pub y: f32,
1349 pub z: f32,
1350 #[serde(default)]
1352 pub tile_id: Option<String>,
1353}
1354
1355#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1357pub struct Snapshot {
1358 pub tick: Tick,
1359 pub chunk_rev: u64,
1360 #[serde(default)]
1362 pub content_rev: u64,
1363 #[serde(default)]
1365 pub publish_rev: u64,
1366 pub entities: Vec<EntityState>,
1367 #[serde(default)]
1368 pub resource_nodes: Vec<ResourceNodeView>,
1369 #[serde(default)]
1371 pub world_width_m: f32,
1372 #[serde(default)]
1373 pub world_height_m: f32,
1374 #[serde(default)]
1375 pub buildings: Vec<BuildingView>,
1376 #[serde(default)]
1377 pub doors: Vec<DoorView>,
1378 #[serde(default)]
1379 pub npcs: Vec<NpcView>,
1380 #[serde(default)]
1381 pub inventory: Vec<ItemStack>,
1382 #[serde(default)]
1383 pub blueprints: Vec<BlueprintView>,
1384 #[serde(default)]
1385 pub world_clock: WorldClock,
1386 #[serde(default)]
1387 pub terrain_zones: Vec<TerrainZoneView>,
1388 #[serde(default)]
1389 pub z_platforms: Vec<ZPlatformView>,
1390 #[serde(default)]
1391 pub z_transitions: Vec<ZTransitionView>,
1392 #[serde(default)]
1393 pub ground_drops: Vec<GroundDropView>,
1394 #[serde(default)]
1395 pub placed_containers: Vec<PlacedContainerView>,
1396 #[serde(default)]
1397 pub combat: Option<CombatHud>,
1398 #[serde(default)]
1399 pub interior_map: Option<InteriorMapView>,
1400 #[serde(default)]
1401 pub quest_log: Vec<QuestLogEntry>,
1402 #[serde(default)]
1403 pub hired_workers: Vec<HiredWorkerView>,
1404 #[serde(default)]
1405 pub interactables: Vec<InteractableView>,
1406}
1407
1408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1410pub struct ResourceNodeView {
1411 pub id: String,
1412 pub label: String,
1413 pub x: f32,
1414 pub y: f32,
1415 pub z: f32,
1416 pub item_template: String,
1417 #[serde(default = "default_node_state")]
1418 pub state: ResourceNodeState,
1419 #[serde(default = "default_blocking_view")]
1421 pub blocking: bool,
1422 #[serde(default = "default_blocking_radius_view")]
1424 pub blocking_radius_m: f32,
1425 #[serde(default)]
1427 pub tile_id: Option<String>,
1428 #[serde(default)]
1430 pub sprite_mode: Option<String>,
1431 #[serde(default)]
1433 pub presentation_state: Option<String>,
1434}
1435
1436fn default_blocking_radius_view() -> f32 {
1437 0.8
1438}
1439
1440fn default_blocking_view() -> bool {
1441 true
1442}
1443
1444#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1445#[serde(rename_all = "snake_case")]
1446pub enum ResourceNodeState {
1447 Available,
1448 Harvesting,
1449 Cooldown,
1450}
1451
1452fn default_node_state() -> ResourceNodeState {
1453 ResourceNodeState::Available
1454}
1455
1456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1457pub struct ItemStack {
1458 pub template_id: String,
1459 pub quantity: u32,
1460 #[serde(default)]
1462 pub item_instance_id: Option<Uuid>,
1463 #[serde(default)]
1465 pub props: BTreeMap<String, String>,
1466 #[serde(default)]
1468 pub contents: Vec<ItemStack>,
1469 #[serde(default)]
1471 pub display_name: Option<String>,
1472 #[serde(default)]
1474 pub category: Option<String>,
1475 #[serde(default)]
1477 pub base_mass: Option<f32>,
1478 #[serde(default)]
1480 pub base_volume: Option<f32>,
1481 #[serde(default)]
1483 pub capacity_volume: Option<f32>,
1484 #[serde(default)]
1486 pub stackable: Option<bool>,
1487 #[serde(default)]
1489 pub world_placeable: Option<bool>,
1490 #[serde(default)]
1492 pub worker_lodging_capacity: Option<u32>,
1493}
1494
1495impl ItemStack {
1496 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
1497 Self {
1498 template_id: template_id.into(),
1499 quantity,
1500 ..Default::default()
1501 }
1502 }
1503}
1504
1505impl Default for ItemStack {
1506 fn default() -> Self {
1507 Self {
1508 template_id: String::new(),
1509 quantity: 0,
1510 item_instance_id: None,
1511 props: BTreeMap::new(),
1512 contents: Vec::new(),
1513 display_name: None,
1514 category: None,
1515 base_mass: None,
1516 base_volume: None,
1517 capacity_volume: None,
1518 stackable: None,
1519 world_placeable: None,
1520 worker_lodging_capacity: None,
1521 }
1522 }
1523}
1524
1525#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1527#[serde(rename_all = "snake_case")]
1528pub enum EncumbranceState {
1529 #[default]
1530 Light,
1531 Heavy,
1532 Over,
1533}
1534
1535#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
1539#[serde(rename_all = "snake_case")]
1540pub enum BodySlot {
1541 Head,
1542 Body,
1543 Arms,
1544 Legs,
1545 Feet,
1546 Back,
1547 Waist,
1548}
1549
1550#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1552#[serde(rename_all = "snake_case")]
1553pub enum InventoryLocation {
1554 Root,
1556 Worn { slot: BodySlot },
1558 Placed { container_id: String },
1560 Keychain,
1562}
1563
1564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1566pub struct PlacedContainerView {
1567 pub id: String,
1568 pub template_id: String,
1569 pub display_name: String,
1570 pub x: f32,
1571 pub y: f32,
1572 pub z: f32,
1573 pub locked: bool,
1574 #[serde(default)]
1576 pub accessible: bool,
1577 #[serde(default)]
1578 pub owner_character_id: Option<Uuid>,
1579 #[serde(default)]
1581 pub contents: Vec<ItemStack>,
1582 #[serde(default)]
1584 pub lock_id: Option<String>,
1585 #[serde(default)]
1587 pub capacity_volume: Option<f32>,
1588 #[serde(default)]
1590 pub item_instance_id: Option<Uuid>,
1591 #[serde(default)]
1593 pub tile_id: Option<String>,
1594 #[serde(default)]
1596 pub worker_lodging_capacity: Option<u32>,
1597}
1598
1599#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1600pub struct BlueprintIngredientView {
1601 pub template_id: String,
1602 pub quantity: u32,
1603 pub consumed: bool,
1605}
1606
1607#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1608pub struct ToolRequirementView {
1609 pub item: String,
1610 pub consumed: bool,
1612}
1613
1614#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1615pub struct SkillRequirementView {
1616 pub skill: String,
1617 pub level: u32,
1618}
1619
1620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1621pub struct BlueprintView {
1622 pub id: String,
1623 pub label: String,
1624 pub output: String,
1625 pub output_qty: u32,
1626 pub craft_ticks: u32,
1627 pub inputs: Vec<BlueprintIngredientView>,
1628 #[serde(default)]
1630 pub station: Option<String>,
1631 #[serde(default)]
1632 pub category: Option<String>,
1633 #[serde(default)]
1634 pub required_tools: Vec<ToolRequirementView>,
1635 #[serde(default)]
1636 pub skill: Option<SkillRequirementView>,
1637 #[serde(default)]
1638 pub failure_chance: f32,
1639 #[serde(default)]
1641 pub worker_train_copper: u64,
1642}
1643
1644#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1646#[serde(rename_all = "snake_case")]
1647pub enum TerrainKindView {
1648 #[default]
1649 Grass,
1650 Hill,
1651 Bog,
1652 ShallowWater,
1653 DeepWater,
1654 Trail,
1655 Rock,
1656}
1657
1658#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1659pub struct TerrainZoneView {
1660 pub id: String,
1661 pub x0: f32,
1662 pub y0: f32,
1663 pub x1: f32,
1664 pub y1: f32,
1665 #[serde(default)]
1666 pub kind: TerrainKindView,
1667 #[serde(default)]
1669 pub elevation: f32,
1670 #[serde(default)]
1673 pub glyph: Option<String>,
1674 #[serde(default)]
1676 pub color: Option<String>,
1677 #[serde(default)]
1679 pub tile_id: Option<String>,
1680 #[serde(default)]
1682 pub z_order: i32,
1683}
1684
1685#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1687pub struct ZPlatformView {
1688 pub id: String,
1689 pub z: f32,
1690 pub x0: f32,
1691 pub y0: f32,
1692 pub x1: f32,
1693 pub y1: f32,
1694}
1695
1696#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1698pub struct ZTransitionView {
1699 pub id: String,
1700 pub z_from: f32,
1701 pub z_to: f32,
1702 pub x0: f32,
1703 pub y0: f32,
1704 pub x1: f32,
1705 pub y1: f32,
1706}
1707
1708#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1709pub struct BuildingView {
1710 pub id: String,
1711 pub label: String,
1712 pub x: f32,
1713 pub y: f32,
1714 pub width_m: f32,
1715 pub depth_m: f32,
1716 #[serde(default)]
1717 pub interior_blueprint: Option<String>,
1718 #[serde(default)]
1719 pub tags: Vec<String>,
1720}
1721
1722#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1723pub struct DoorView {
1724 pub id: String,
1725 pub building_id: String,
1726 pub x: f32,
1727 pub y: f32,
1728 #[serde(default)]
1729 pub open: bool,
1730 #[serde(default)]
1731 pub portal: Option<String>,
1732}
1733
1734#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1735pub struct InteriorRoomView {
1736 pub id: String,
1737 pub label: String,
1738 pub floor: i32,
1739 pub x0: f32,
1740 pub y0: f32,
1741 pub x1: f32,
1742 pub y1: f32,
1743 #[serde(default)]
1744 pub floor_color: Option<String>,
1745 #[serde(default)]
1746 pub floor_glyph: Option<String>,
1747}
1748
1749#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1750pub struct InteriorDoorView {
1751 pub id: String,
1752 pub room_a: String,
1753 pub room_b: String,
1754 pub x: f32,
1755 pub y: f32,
1756 pub kind: String,
1757}
1758
1759#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1760pub struct InteriorMapView {
1761 pub building_id: String,
1762 pub blueprint_id: String,
1763 pub background_color: String,
1764 #[serde(default)]
1765 pub default_floor_color: Option<String>,
1766 #[serde(default = "default_floor_height_view")]
1767 pub floor_height_m: f32,
1768 pub rooms: Vec<InteriorRoomView>,
1769 #[serde(default)]
1770 pub room_doors: Vec<InteriorDoorView>,
1771}
1772
1773fn default_floor_height_view() -> f32 {
1774 3.0
1775}
1776
1777#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1778pub struct NpcView {
1779 pub id: String,
1780 pub label: String,
1781 pub role: String,
1782 pub x: f32,
1783 pub y: f32,
1784 #[serde(default)]
1786 pub building_id: Option<String>,
1787 #[serde(default)]
1789 pub entity_id: Option<EntityId>,
1790 #[serde(default)]
1791 pub life_state: Option<LifeState>,
1792 #[serde(default)]
1793 pub hp_pct: Option<f32>,
1794 #[serde(default)]
1796 pub can_trade: bool,
1797 #[serde(default)]
1799 pub tile_id: Option<String>,
1800 #[serde(default)]
1802 pub behavior_state: Option<String>,
1803 #[serde(default)]
1805 pub presentation_state: Option<String>,
1806 #[serde(default)]
1808 pub sprite_mode: Option<String>,
1809}
1810
1811#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1812pub struct UseResult {
1813 pub template_id: String,
1814 pub hunger_restored: f32,
1815 pub thirst_restored: f32,
1816 #[serde(default)]
1817 pub health_restored: f32,
1818 #[serde(default)]
1819 pub mana_restored: f32,
1820 #[serde(default)]
1821 pub cleared_dot_ids: Vec<String>,
1822}
1823
1824#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1825pub struct CraftResult {
1826 pub blueprint_id: String,
1827 pub outputs: Vec<ItemStack>,
1828 pub consumed: Vec<ItemStack>,
1829 #[serde(default = "default_one")]
1831 pub batch_index: u32,
1832 #[serde(default = "default_one")]
1834 pub batch_total: u32,
1835}
1836
1837fn default_one() -> u32 {
1838 1
1839}
1840
1841#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1842pub struct DeathNotice {
1843 pub entity_id: EntityId,
1844 pub respawn_x: f32,
1845 pub respawn_y: f32,
1846 pub message: String,
1847}
1848
1849#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1850pub struct InteractionNotice {
1851 pub target_id: String,
1852 pub message: String,
1853 #[serde(default)]
1854 pub coins_delta: i32,
1855 #[serde(default)]
1856 pub inventory_delta: Vec<ItemStack>,
1857}
1858
1859#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1860#[serde(rename_all = "snake_case")]
1861pub enum NpcTalkTrustFlag {
1862 Stranger,
1863 Acquainted,
1864 Trusted,
1865}
1866
1867#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1868#[serde(rename_all = "snake_case")]
1869pub enum NpcTalkDepth {
1870 #[default]
1871 Full,
1872 Brief,
1873 Unavailable,
1874}
1875
1876#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1877pub struct NpcTalkOpened {
1878 pub npc_id: String,
1879 pub npc_label: String,
1880 pub greeting: String,
1881 pub trust_flag: NpcTalkTrustFlag,
1882 #[serde(default)]
1883 pub talk_depth: NpcTalkDepth,
1884 #[serde(default = "default_true")]
1885 pub trade_allowed: bool,
1886}
1887
1888fn default_true() -> bool {
1889 true
1890}
1891
1892#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1893pub struct NpcTalkPending {
1894 pub npc_id: String,
1895}
1896
1897#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1898pub struct NpcTalkReply {
1899 pub npc_id: String,
1900 pub line: String,
1901 pub trust_flag: NpcTalkTrustFlag,
1902 #[serde(default)]
1903 pub wind_down: bool,
1904 #[serde(default)]
1905 pub trade_disabled: bool,
1906}
1907
1908#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1909pub struct NpcTalkClosed {
1910 pub npc_id: String,
1911}
1912
1913#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1914pub struct NpcTalkError {
1915 pub npc_id: String,
1916 pub reason: String,
1917}
1918
1919#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1920#[serde(rename_all = "snake_case")]
1921pub enum QuestStatusView {
1922 Available,
1923 Active,
1924 Completed,
1925}
1926
1927#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1928pub struct QuestObjectiveProgress {
1929 pub label: String,
1930 pub current: u32,
1931 pub required: u32,
1932 pub done: bool,
1933}
1934
1935#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1936pub struct QuestLogEntry {
1937 pub quest_id: String,
1938 pub title: String,
1939 pub description: String,
1940 pub status: QuestStatusView,
1941 #[serde(default)]
1942 pub current_step_id: Option<String>,
1943 #[serde(default)]
1944 pub current_step_title: String,
1945 #[serde(default)]
1946 pub objectives: Vec<QuestObjectiveProgress>,
1947 #[serde(default)]
1948 pub is_tracked: bool,
1949 #[serde(default)]
1950 pub can_withdraw: bool,
1951}
1952
1953#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1954pub struct InteractableView {
1955 pub id: String,
1956 pub kind: String,
1957 pub label: String,
1958 pub x: f32,
1959 pub y: f32,
1960 pub z: f32,
1961 #[serde(default)]
1962 pub board_id: Option<String>,
1963}
1964
1965#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1966pub struct QuestOffer {
1967 pub quest_id: String,
1968 pub title: String,
1969 pub description: String,
1970 #[serde(default)]
1971 pub step_count: u32,
1972}
1973
1974#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1975pub struct QuestNotice {
1976 pub quest_id: String,
1977 pub title: String,
1978 pub message: String,
1979}
1980
1981#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1982#[serde(rename_all = "snake_case")]
1983pub enum ShopOfferKind {
1984 Item,
1985 Blueprint,
1986}
1987
1988#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1989pub struct ShopOffer {
1990 pub offer_id: String,
1991 pub kind: ShopOfferKind,
1992 pub label: String,
1993 #[serde(default)]
1994 pub template_id: Option<String>,
1995 #[serde(default)]
1996 pub blueprint_id: Option<String>,
1997 pub price_copper: u32,
1998 #[serde(default)]
1999 pub affordable: bool,
2000 #[serde(default)]
2001 pub already_owned: bool,
2002}
2003
2004#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2005pub struct ShopBuyLine {
2006 pub template_id: String,
2007 pub label: String,
2008 pub quantity: u32,
2009 pub price_copper: u32,
2010}
2011
2012#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2013pub struct ShopCatalog {
2014 pub npc_id: String,
2015 pub npc_label: String,
2016 #[serde(default)]
2017 pub sells: Vec<ShopOffer>,
2018 #[serde(default)]
2019 pub buys: Vec<ShopBuyLine>,
2020}
2021
2022#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2023pub struct HarvestResult {
2024 pub node_id: String,
2025 pub quantity: u32,
2027 pub item_template: String,
2028 #[serde(default)]
2031 pub item_instance_id: Option<Uuid>,
2032}
2033
2034#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2036pub struct Envelope<T> {
2037 pub protocol_version: u16,
2038 pub payload: T,
2039}
2040
2041impl<T> Envelope<T> {
2042 pub fn new(payload: T) -> Self {
2043 Self {
2044 protocol_version: crate::PROTOCOL_VERSION,
2045 payload,
2046 }
2047 }
2048}
2049
2050#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2052pub struct Hello {
2053 pub client_name: String,
2054 pub protocol_version: u16,
2055 #[serde(default)]
2056 pub auth: AuthCredential,
2057 #[serde(default)]
2059 pub character_id: Option<Uuid>,
2060}
2061
2062#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2065#[serde(rename_all = "snake_case")]
2066pub enum AuthCredential {
2067 DevLocal,
2068 Session { token: String },
2069 ApiToken { token: String, character_id: Uuid },
2070}
2071
2072impl Default for AuthCredential {
2073 fn default() -> Self {
2074 Self::DevLocal
2075 }
2076}
2077
2078#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2079pub struct Welcome {
2080 pub session_id: SessionId,
2081 pub entity_id: EntityId,
2082 pub snapshot: Snapshot,
2083}
2084
2085#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2086pub enum ServerMessage {
2087 Welcome(Welcome),
2088 ContentUpdated(Snapshot),
2090 Tick(TickDelta),
2091 IntentAck {
2092 entity_id: EntityId,
2093 seq: Seq,
2094 tick: Tick,
2095 },
2096 Chat(ChatMessage),
2097 HarvestResult(HarvestResult),
2098 UseResult(UseResult),
2099 CraftResult(CraftResult),
2100 Death(DeathNotice),
2101 Interaction(InteractionNotice),
2102 ShopOpened(ShopCatalog),
2103 NpcTalkOpened(NpcTalkOpened),
2104 NpcTalkPending(NpcTalkPending),
2105 NpcTalkReply(NpcTalkReply),
2106 NpcTalkClosed(NpcTalkClosed),
2107 NpcTalkError(NpcTalkError),
2108 QuestOffer(QuestOffer),
2109 QuestAccepted(QuestNotice),
2110 QuestWithdrawn(QuestNotice),
2111 QuestStepCompleted(QuestNotice),
2112 QuestCompleted(QuestNotice),
2113}
2114
2115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2116pub enum ClientMessage {
2117 Hello(Hello),
2118 Intent(Intent),
2119 Disconnect,
2120}
2121
2122#[cfg(test)]
2123mod tests {
2124 use super::*;
2125
2126 #[test]
2127 fn pristine_vitals_state_yields_full_pools() {
2128 let attrs = PrimaryAttributes::default();
2129 let vitals = StoredVitalsState::default().apply_to(attrs);
2130 assert!(vitals.health > 0.0);
2131 assert_eq!(vitals.health, vitals.health_max);
2132 assert!((vitals.mana_max - 61.0).abs() < 0.01);
2133 }
2134
2135 #[test]
2136 fn saved_vitals_scale_when_pool_max_increases() {
2137 let mut attrs = PrimaryAttributes::default();
2138 attrs.intelligence = 140;
2139 attrs.wisdom = 140;
2140 let saved = StoredVitalsState {
2141 health: 100.0,
2142 mana: 14.0,
2143 stamina: 100.0,
2144 ..StoredVitalsState::default()
2145 };
2146 let vitals = saved.apply_to(attrs);
2147 assert!(vitals.mana_max > 55.0);
2148 assert!(
2149 (vitals.mana - vitals.mana_max).abs() < 0.01,
2150 "full legacy mana bar migrates to full new bar"
2151 );
2152 }
2153
2154 #[test]
2155 fn empty_vitals_state_is_pristine() {
2156 let pristine = StoredVitalsState {
2157 health: 0.0,
2158 mana: 0.0,
2159 stamina: 0.0,
2160 hunger: 0.0,
2161 thirst: 0.0,
2162 coins: 0,
2163 deaths: 0,
2164 life_state: LifeState::Alive,
2165 };
2166 assert!(pristine.is_pristine());
2167 let vitals = pristine.apply_to(PrimaryAttributes::default());
2168 assert!(vitals.health > 0.0);
2169 }
2170
2171 #[test]
2172 fn stored_vitals_roundtrip_preserves_partial_pools() {
2173 let attrs = PrimaryAttributes::default();
2174 let mut live = PlayerVitals::from_attributes(attrs);
2175 live.health = 25.0;
2176 live.hunger = 77.0;
2177 live.deaths = 2;
2178 let stored = StoredVitalsState::from_live(&live);
2179 let restored = stored.apply_to(attrs);
2180 assert!(
2181 (restored.health - 25.0).abs() < 0.01,
2182 "partial HP below cap stays absolute"
2183 );
2184 assert_eq!(restored.hunger, 77.0);
2185 assert_eq!(restored.deaths, 2);
2186 }
2187
2188 #[test]
2189 fn skill_tiers_start_at_zero() {
2190 let skill = SkillProgress::default();
2191 assert_eq!(skill.level, 0);
2192 assert_eq!(skill.display_tier(), 0);
2193 let trained = SkillProgress {
2194 level: 250,
2195 last_trained_tick: 1,
2196 };
2197 assert_eq!(trained.display_tier(), 2);
2198 }
2199
2200 #[test]
2201 fn quest_server_messages_roundtrip_json() {
2202 use crate::codec::{Codec, PostcardCodec};
2203
2204 let offer = ServerMessage::QuestOffer(QuestOffer {
2205 quest_id: "ada_goblin_hunt".into(),
2206 title: "Goblin Trouble".into(),
2207 description: "Help Ada".into(),
2208 step_count: 3,
2209 });
2210 let notice = ServerMessage::QuestAccepted(QuestNotice {
2211 quest_id: "ada_goblin_hunt".into(),
2212 title: "Goblin Trouble".into(),
2213 message: "Quest accepted".into(),
2214 });
2215 for msg in [offer, notice] {
2216 let bytes = PostcardCodec.encode(&msg).unwrap();
2217 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
2218 assert_eq!(decoded, msg);
2219 }
2220 }
2221}