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