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, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1258#[serde(rename_all = "snake_case")]
1259pub enum LedgerCategory {
1260 Workers,
1261 Hire,
1262 Train,
1263 ShopBuy,
1264 Taxes,
1265 WorkerSales,
1266 TraderSales,
1267 Other,
1268}
1269
1270impl LedgerCategory {
1271 pub fn as_str(self) -> &'static str {
1272 match self {
1273 Self::Workers => "workers",
1274 Self::Hire => "hire",
1275 Self::Train => "train",
1276 Self::ShopBuy => "shop_buy",
1277 Self::Taxes => "taxes",
1278 Self::WorkerSales => "worker_sales",
1279 Self::TraderSales => "trader_sales",
1280 Self::Other => "other",
1281 }
1282 }
1283}
1284
1285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1286pub struct LedgerEntryView {
1287 pub id: uuid::Uuid,
1288 pub game_day: u64,
1289 pub signed_copper: i64,
1290 pub category: LedgerCategory,
1291 #[serde(default)]
1292 pub label: String,
1293}
1294
1295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1296pub struct LedgerPeriodTotals {
1297 #[serde(default)]
1299 pub expenses: std::collections::HashMap<String, u64>,
1300 #[serde(default)]
1302 pub income: std::collections::HashMap<String, u64>,
1303 pub expense_copper: u64,
1304 pub income_copper: u64,
1305 pub cash_flow_copper: i64,
1307}
1308
1309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1310pub struct PlayerLedgerView {
1311 pub current_game_day: u64,
1312 #[serde(default)]
1313 pub period_day: LedgerPeriodTotals,
1314 #[serde(default)]
1315 pub period_week: LedgerPeriodTotals,
1316 #[serde(default)]
1317 pub period_month: LedgerPeriodTotals,
1318 #[serde(default)]
1319 pub period_lifetime: LedgerPeriodTotals,
1320 #[serde(default)]
1321 pub recent: Vec<LedgerEntryView>,
1322 #[serde(default)]
1324 pub wealth_on_person_copper: u64,
1325 #[serde(default)]
1327 pub wealth_in_storage_copper: u64,
1328 #[serde(default)]
1330 pub wealth_total_copper: u64,
1331 #[serde(default)]
1333 pub live_expense_per_interval_copper: u64,
1334 #[serde(default)]
1336 pub live_income_route_est_per_loop_copper: u64,
1337 #[serde(default)]
1339 pub live_income_avg_per_interval_copper: u64,
1340 #[serde(default)]
1342 pub live_income_avg_window_intervals: u32,
1343 #[serde(default)]
1345 pub live_net_avg_per_interval_copper: i64,
1346}
1347
1348#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1350#[serde(rename_all = "snake_case")]
1351pub enum AnalyticsMetric {
1352 NpcKill,
1353 WildlifeKill,
1354 Harvest,
1355 QuestComplete,
1356 QuestAccept,
1357 QuestAbandon,
1358 PlayerDeath,
1359 Craft,
1360 WorkerHire,
1361 WorkerDismiss,
1362 WorkerTeach,
1363 NpcTalk,
1364 ShopBuy,
1365 ShopSell,
1366 PlaceContainer,
1367 PickupContainer,
1368 PickupDrop,
1369 ConsumableUse,
1370 AbilityUse,
1371 DistanceWalkedM,
1372 DoorUse,
1373 BuildingEnter,
1374}
1375
1376impl AnalyticsMetric {
1377 pub fn as_str(self) -> &'static str {
1378 match self {
1379 Self::NpcKill => "npc_kill",
1380 Self::WildlifeKill => "wildlife_kill",
1381 Self::Harvest => "harvest",
1382 Self::QuestComplete => "quest_complete",
1383 Self::QuestAccept => "quest_accept",
1384 Self::QuestAbandon => "quest_abandon",
1385 Self::PlayerDeath => "player_death",
1386 Self::Craft => "craft",
1387 Self::WorkerHire => "worker_hire",
1388 Self::WorkerDismiss => "worker_dismiss",
1389 Self::WorkerTeach => "worker_teach",
1390 Self::NpcTalk => "npc_talk",
1391 Self::ShopBuy => "shop_buy",
1392 Self::ShopSell => "shop_sell",
1393 Self::PlaceContainer => "place_container",
1394 Self::PickupContainer => "pickup_container",
1395 Self::PickupDrop => "pickup_drop",
1396 Self::ConsumableUse => "consumable_use",
1397 Self::AbilityUse => "ability_use",
1398 Self::DistanceWalkedM => "distance_walked_m",
1399 Self::DoorUse => "door_use",
1400 Self::BuildingEnter => "building_enter",
1401 }
1402 }
1403
1404 pub fn from_str_key(s: &str) -> Option<Self> {
1405 Some(match s {
1406 "npc_kill" => Self::NpcKill,
1407 "wildlife_kill" => Self::WildlifeKill,
1408 "harvest" => Self::Harvest,
1409 "quest_complete" => Self::QuestComplete,
1410 "quest_accept" => Self::QuestAccept,
1411 "quest_abandon" => Self::QuestAbandon,
1412 "player_death" => Self::PlayerDeath,
1413 "craft" => Self::Craft,
1414 "worker_hire" => Self::WorkerHire,
1415 "worker_dismiss" => Self::WorkerDismiss,
1416 "worker_teach" => Self::WorkerTeach,
1417 "npc_talk" => Self::NpcTalk,
1418 "shop_buy" => Self::ShopBuy,
1419 "shop_sell" => Self::ShopSell,
1420 "place_container" => Self::PlaceContainer,
1421 "pickup_container" => Self::PickupContainer,
1422 "pickup_drop" => Self::PickupDrop,
1423 "consumable_use" => Self::ConsumableUse,
1424 "ability_use" => Self::AbilityUse,
1425 "distance_walked_m" => Self::DistanceWalkedM,
1426 "door_use" => Self::DoorUse,
1427 "building_enter" => Self::BuildingEnter,
1428 _ => return None,
1429 })
1430 }
1431}
1432
1433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1434pub struct CareerMetricRow {
1435 pub subject_id: String,
1436 pub amount: u64,
1437}
1438
1439#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1441pub struct PlayerCareerView {
1442 pub current_game_day: u64,
1443 #[serde(default)]
1444 pub kills: Vec<CareerMetricRow>,
1445 #[serde(default)]
1446 pub harvests: Vec<CareerMetricRow>,
1447 pub quests_completed: u64,
1448 #[serde(default)]
1449 pub crafts: Vec<CareerMetricRow>,
1450 pub deaths: u64,
1451 pub npc_talks: u64,
1452 pub shop_buys: u64,
1453 pub shop_sells: u64,
1454 pub distance_m: u64,
1455 #[serde(default)]
1456 pub other: Vec<CareerMetricRow>,
1457}
1458
1459#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1461pub struct HiredWorkerView {
1462 pub instance_id: String,
1463 pub entity_id: EntityId,
1464 pub def_id: String,
1465 pub label: String,
1467 pub x: f32,
1468 pub y: f32,
1469 pub z: f32,
1470 pub mode: WorkerModeView,
1471 pub state: WorkerStateView,
1472 #[serde(default)]
1473 pub step_label: String,
1474 pub vitals: WorkerVitalsSummary,
1475 #[serde(default)]
1476 pub carry_pct: f32,
1477 #[serde(default)]
1478 pub last_error: Option<String>,
1479 pub wage_copper_per_interval: u32,
1480 #[serde(default)]
1482 pub effective_wage_copper: u32,
1483 #[serde(default)]
1485 pub wage_meters_walked: f32,
1486 #[serde(default)]
1488 pub lodging_container_id: Option<String>,
1489 #[serde(default)]
1491 pub route: Option<WorkerRouteView>,
1492 #[serde(default)]
1494 pub known_blueprint_ids: Vec<String>,
1495 #[serde(default = "default_worker_view_level")]
1497 pub level: u32,
1498 #[serde(default)]
1500 pub worker_xp: f64,
1501 #[serde(default)]
1503 pub inventory: Vec<ItemStack>,
1504}
1505
1506fn default_worker_view_level() -> u32 {
1507 1
1508}
1509
1510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1512pub struct TickDelta {
1513 pub tick: Tick,
1514 pub entities: Vec<EntityState>,
1515 #[serde(default)]
1516 pub resource_nodes: Vec<ResourceNodeView>,
1517 #[serde(default)]
1518 pub buildings: Vec<BuildingView>,
1519 #[serde(default)]
1520 pub doors: Vec<DoorView>,
1521 #[serde(default)]
1522 pub npcs: Vec<NpcView>,
1523 #[serde(default)]
1525 pub inventory: Vec<ItemStack>,
1526 #[serde(default)]
1527 pub blueprints: Vec<BlueprintView>,
1528 #[serde(default)]
1529 pub world_clock: WorldClock,
1530 #[serde(default)]
1531 pub ground_drops: Vec<GroundDropView>,
1532 #[serde(default)]
1533 pub placed_containers: Vec<PlacedContainerView>,
1534 #[serde(default)]
1535 pub combat: Option<CombatHud>,
1536 #[serde(default)]
1537 pub interior_map: Option<InteriorMapView>,
1538 #[serde(default)]
1539 pub quest_log: Vec<QuestLogEntry>,
1540 #[serde(default)]
1541 pub hired_workers: Vec<HiredWorkerView>,
1542 #[serde(default)]
1543 pub interactables: Vec<InteractableView>,
1544 #[serde(default)]
1545 pub ledger: Option<PlayerLedgerView>,
1546 #[serde(default)]
1547 pub career: Option<PlayerCareerView>,
1548}
1549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1550pub struct GroundDropView {
1551 pub id: String,
1552 pub template_id: String,
1553 pub quantity: u32,
1554 pub x: f32,
1555 pub y: f32,
1556 pub z: f32,
1557 #[serde(default)]
1559 pub tile_id: Option<String>,
1560}
1561
1562#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1564pub struct Snapshot {
1565 pub tick: Tick,
1566 pub chunk_rev: u64,
1567 #[serde(default)]
1569 pub content_rev: u64,
1570 #[serde(default)]
1572 pub publish_rev: u64,
1573 pub entities: Vec<EntityState>,
1574 #[serde(default)]
1575 pub resource_nodes: Vec<ResourceNodeView>,
1576 #[serde(default)]
1578 pub world_width_m: f32,
1579 #[serde(default)]
1580 pub world_height_m: f32,
1581 #[serde(default)]
1582 pub buildings: Vec<BuildingView>,
1583 #[serde(default)]
1584 pub doors: Vec<DoorView>,
1585 #[serde(default)]
1586 pub npcs: Vec<NpcView>,
1587 #[serde(default)]
1588 pub inventory: Vec<ItemStack>,
1589 #[serde(default)]
1590 pub blueprints: Vec<BlueprintView>,
1591 #[serde(default)]
1592 pub world_clock: WorldClock,
1593 #[serde(default)]
1594 pub terrain_zones: Vec<TerrainZoneView>,
1595 #[serde(default)]
1596 pub z_platforms: Vec<ZPlatformView>,
1597 #[serde(default)]
1598 pub z_transitions: Vec<ZTransitionView>,
1599 #[serde(default)]
1600 pub ground_drops: Vec<GroundDropView>,
1601 #[serde(default)]
1602 pub placed_containers: Vec<PlacedContainerView>,
1603 #[serde(default)]
1604 pub combat: Option<CombatHud>,
1605 #[serde(default)]
1606 pub interior_map: Option<InteriorMapView>,
1607 #[serde(default)]
1608 pub quest_log: Vec<QuestLogEntry>,
1609 #[serde(default)]
1610 pub hired_workers: Vec<HiredWorkerView>,
1611 #[serde(default)]
1612 pub interactables: Vec<InteractableView>,
1613 #[serde(default)]
1614 pub ledger: Option<PlayerLedgerView>,
1615 #[serde(default)]
1616 pub career: Option<PlayerCareerView>,
1617}
1618
1619#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1621pub struct ResourceNodeView {
1622 pub id: String,
1623 pub label: String,
1624 pub x: f32,
1625 pub y: f32,
1626 pub z: f32,
1627 pub item_template: String,
1628 #[serde(default = "default_node_state")]
1629 pub state: ResourceNodeState,
1630 #[serde(default = "default_blocking_view")]
1632 pub blocking: bool,
1633 #[serde(default = "default_blocking_radius_view")]
1635 pub blocking_radius_m: f32,
1636 #[serde(default)]
1638 pub tile_id: Option<String>,
1639 #[serde(default)]
1641 pub sprite_mode: Option<String>,
1642 #[serde(default)]
1644 pub presentation_state: Option<String>,
1645}
1646
1647fn default_blocking_radius_view() -> f32 {
1648 0.8
1649}
1650
1651fn default_blocking_view() -> bool {
1652 true
1653}
1654
1655#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1656#[serde(rename_all = "snake_case")]
1657pub enum ResourceNodeState {
1658 Available,
1659 Harvesting,
1660 Cooldown,
1661}
1662
1663fn default_node_state() -> ResourceNodeState {
1664 ResourceNodeState::Available
1665}
1666
1667#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1668pub struct ItemStack {
1669 pub template_id: String,
1670 pub quantity: u32,
1671 #[serde(default)]
1673 pub item_instance_id: Option<Uuid>,
1674 #[serde(default)]
1676 pub props: BTreeMap<String, String>,
1677 #[serde(default)]
1679 pub contents: Vec<ItemStack>,
1680 #[serde(default)]
1682 pub display_name: Option<String>,
1683 #[serde(default)]
1685 pub category: Option<String>,
1686 #[serde(default)]
1688 pub base_mass: Option<f32>,
1689 #[serde(default)]
1691 pub base_volume: Option<f32>,
1692 #[serde(default)]
1694 pub capacity_volume: Option<f32>,
1695 #[serde(default)]
1697 pub stackable: Option<bool>,
1698 #[serde(default)]
1700 pub world_placeable: Option<bool>,
1701 #[serde(default)]
1703 pub worker_lodging_capacity: Option<u32>,
1704}
1705
1706impl ItemStack {
1707 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
1708 Self {
1709 template_id: template_id.into(),
1710 quantity,
1711 ..Default::default()
1712 }
1713 }
1714}
1715
1716impl Default for ItemStack {
1717 fn default() -> Self {
1718 Self {
1719 template_id: String::new(),
1720 quantity: 0,
1721 item_instance_id: None,
1722 props: BTreeMap::new(),
1723 contents: Vec::new(),
1724 display_name: None,
1725 category: None,
1726 base_mass: None,
1727 base_volume: None,
1728 capacity_volume: None,
1729 stackable: None,
1730 world_placeable: None,
1731 worker_lodging_capacity: None,
1732 }
1733 }
1734}
1735
1736#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1738#[serde(rename_all = "snake_case")]
1739pub enum EncumbranceState {
1740 #[default]
1741 Light,
1742 Heavy,
1743 Over,
1744}
1745
1746#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
1750#[serde(rename_all = "snake_case")]
1751pub enum BodySlot {
1752 Head,
1753 Body,
1754 Arms,
1755 Legs,
1756 Feet,
1757 Back,
1758 Waist,
1759}
1760
1761#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1763#[serde(rename_all = "snake_case")]
1764pub enum InventoryLocation {
1765 Root,
1767 Worn { slot: BodySlot },
1769 Placed { container_id: String },
1771 Keychain,
1773}
1774
1775#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1777pub struct PlacedContainerView {
1778 pub id: String,
1779 pub template_id: String,
1780 pub display_name: String,
1781 pub x: f32,
1782 pub y: f32,
1783 pub z: f32,
1784 pub locked: bool,
1785 #[serde(default)]
1787 pub accessible: bool,
1788 #[serde(default)]
1789 pub owner_character_id: Option<Uuid>,
1790 #[serde(default)]
1792 pub contents: Vec<ItemStack>,
1793 #[serde(default)]
1795 pub lock_id: Option<String>,
1796 #[serde(default)]
1798 pub capacity_volume: Option<f32>,
1799 #[serde(default)]
1801 pub item_instance_id: Option<Uuid>,
1802 #[serde(default)]
1804 pub tile_id: Option<String>,
1805 #[serde(default)]
1807 pub worker_lodging_capacity: Option<u32>,
1808}
1809
1810#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1811pub struct BlueprintIngredientView {
1812 pub template_id: String,
1813 pub quantity: u32,
1814 pub consumed: bool,
1816}
1817
1818#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1819pub struct ToolRequirementView {
1820 pub item: String,
1821 pub consumed: bool,
1823}
1824
1825#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1826pub struct SkillRequirementView {
1827 pub skill: String,
1828 pub level: u32,
1829}
1830
1831#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1832pub struct BlueprintView {
1833 pub id: String,
1834 pub label: String,
1835 pub output: String,
1836 pub output_qty: u32,
1837 pub craft_ticks: u32,
1838 pub inputs: Vec<BlueprintIngredientView>,
1839 #[serde(default)]
1841 pub station: Option<String>,
1842 #[serde(default)]
1843 pub category: Option<String>,
1844 #[serde(default)]
1845 pub required_tools: Vec<ToolRequirementView>,
1846 #[serde(default)]
1847 pub skill: Option<SkillRequirementView>,
1848 #[serde(default)]
1849 pub failure_chance: f32,
1850 #[serde(default)]
1852 pub worker_train_copper: u64,
1853}
1854
1855#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1857#[serde(rename_all = "snake_case")]
1858pub enum TerrainKindView {
1859 #[default]
1860 Grass,
1861 Hill,
1862 Bog,
1863 ShallowWater,
1864 DeepWater,
1865 Trail,
1866 Rock,
1867}
1868
1869#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1870pub struct TerrainZoneView {
1871 pub id: String,
1872 pub x0: f32,
1873 pub y0: f32,
1874 pub x1: f32,
1875 pub y1: f32,
1876 #[serde(default)]
1877 pub kind: TerrainKindView,
1878 #[serde(default)]
1880 pub elevation: f32,
1881 #[serde(default)]
1884 pub glyph: Option<String>,
1885 #[serde(default)]
1887 pub color: Option<String>,
1888 #[serde(default)]
1890 pub tile_id: Option<String>,
1891 #[serde(default)]
1893 pub z_order: i32,
1894}
1895
1896#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1898pub struct ZPlatformView {
1899 pub id: String,
1900 pub z: f32,
1901 pub x0: f32,
1902 pub y0: f32,
1903 pub x1: f32,
1904 pub y1: f32,
1905}
1906
1907#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1909pub struct ZTransitionView {
1910 pub id: String,
1911 pub z_from: f32,
1912 pub z_to: f32,
1913 pub x0: f32,
1914 pub y0: f32,
1915 pub x1: f32,
1916 pub y1: f32,
1917}
1918
1919#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1920pub struct BuildingView {
1921 pub id: String,
1922 pub label: String,
1923 pub x: f32,
1924 pub y: f32,
1925 pub width_m: f32,
1926 pub depth_m: f32,
1927 #[serde(default)]
1928 pub interior_blueprint: Option<String>,
1929 #[serde(default)]
1930 pub tags: Vec<String>,
1931}
1932
1933#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1934pub struct DoorView {
1935 pub id: String,
1936 pub building_id: String,
1937 pub x: f32,
1938 pub y: f32,
1939 #[serde(default)]
1940 pub open: bool,
1941 #[serde(default)]
1942 pub portal: Option<String>,
1943}
1944
1945#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1946pub struct InteriorRoomView {
1947 pub id: String,
1948 pub label: String,
1949 pub floor: i32,
1950 pub x0: f32,
1951 pub y0: f32,
1952 pub x1: f32,
1953 pub y1: f32,
1954 #[serde(default)]
1955 pub floor_color: Option<String>,
1956 #[serde(default)]
1957 pub floor_glyph: Option<String>,
1958}
1959
1960#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1961pub struct InteriorDoorView {
1962 pub id: String,
1963 pub room_a: String,
1964 pub room_b: String,
1965 pub x: f32,
1966 pub y: f32,
1967 pub kind: String,
1968}
1969
1970#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1971pub struct InteriorMapView {
1972 pub building_id: String,
1973 pub blueprint_id: String,
1974 pub background_color: String,
1975 #[serde(default)]
1976 pub default_floor_color: Option<String>,
1977 #[serde(default = "default_floor_height_view")]
1978 pub floor_height_m: f32,
1979 pub rooms: Vec<InteriorRoomView>,
1980 #[serde(default)]
1981 pub room_doors: Vec<InteriorDoorView>,
1982}
1983
1984fn default_floor_height_view() -> f32 {
1985 3.0
1986}
1987
1988#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1989pub struct NpcView {
1990 pub id: String,
1991 pub label: String,
1992 pub role: String,
1993 pub x: f32,
1994 pub y: f32,
1995 #[serde(default)]
1997 pub building_id: Option<String>,
1998 #[serde(default)]
2000 pub entity_id: Option<EntityId>,
2001 #[serde(default)]
2002 pub life_state: Option<LifeState>,
2003 #[serde(default)]
2004 pub hp_pct: Option<f32>,
2005 #[serde(default)]
2007 pub can_trade: bool,
2008 #[serde(default)]
2010 pub tile_id: Option<String>,
2011 #[serde(default)]
2013 pub behavior_state: Option<String>,
2014 #[serde(default)]
2016 pub presentation_state: Option<String>,
2017 #[serde(default)]
2019 pub sprite_mode: Option<String>,
2020}
2021
2022#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2023pub struct UseResult {
2024 pub template_id: String,
2025 pub hunger_restored: f32,
2026 pub thirst_restored: f32,
2027 #[serde(default)]
2028 pub health_restored: f32,
2029 #[serde(default)]
2030 pub mana_restored: f32,
2031 #[serde(default)]
2032 pub cleared_dot_ids: Vec<String>,
2033}
2034
2035#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2036pub struct CraftResult {
2037 pub blueprint_id: String,
2038 pub outputs: Vec<ItemStack>,
2039 pub consumed: Vec<ItemStack>,
2040 #[serde(default = "default_one")]
2042 pub batch_index: u32,
2043 #[serde(default = "default_one")]
2045 pub batch_total: u32,
2046}
2047
2048fn default_one() -> u32 {
2049 1
2050}
2051
2052#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2053pub struct DeathNotice {
2054 pub entity_id: EntityId,
2055 pub respawn_x: f32,
2056 pub respawn_y: f32,
2057 pub message: String,
2058}
2059
2060#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2061pub struct InteractionNotice {
2062 pub target_id: String,
2063 pub message: String,
2064 #[serde(default)]
2065 pub coins_delta: i32,
2066 #[serde(default)]
2067 pub inventory_delta: Vec<ItemStack>,
2068}
2069
2070#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2071#[serde(rename_all = "snake_case")]
2072pub enum NpcTalkTrustFlag {
2073 Stranger,
2074 Acquainted,
2075 Trusted,
2076}
2077
2078#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
2079#[serde(rename_all = "snake_case")]
2080pub enum NpcTalkDepth {
2081 #[default]
2082 Full,
2083 Brief,
2084 Unavailable,
2085}
2086
2087#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2088pub struct NpcTalkOpened {
2089 pub npc_id: String,
2090 pub npc_label: String,
2091 pub greeting: String,
2092 pub trust_flag: NpcTalkTrustFlag,
2093 #[serde(default)]
2094 pub talk_depth: NpcTalkDepth,
2095 #[serde(default = "default_true")]
2096 pub trade_allowed: bool,
2097}
2098
2099fn default_true() -> bool {
2100 true
2101}
2102
2103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2104pub struct NpcTalkPending {
2105 pub npc_id: String,
2106}
2107
2108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2109pub struct NpcTalkReply {
2110 pub npc_id: String,
2111 pub line: String,
2112 pub trust_flag: NpcTalkTrustFlag,
2113 #[serde(default)]
2114 pub wind_down: bool,
2115 #[serde(default)]
2116 pub trade_disabled: bool,
2117}
2118
2119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2120pub struct NpcTalkClosed {
2121 pub npc_id: String,
2122}
2123
2124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2125pub struct NpcTalkError {
2126 pub npc_id: String,
2127 pub reason: String,
2128}
2129
2130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2131#[serde(rename_all = "snake_case")]
2132pub enum QuestStatusView {
2133 Available,
2134 Active,
2135 Completed,
2136}
2137
2138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2139pub struct QuestObjectiveProgress {
2140 pub label: String,
2141 pub current: u32,
2142 pub required: u32,
2143 pub done: bool,
2144}
2145
2146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2147pub struct QuestLogEntry {
2148 pub quest_id: String,
2149 pub title: String,
2150 pub description: String,
2151 pub status: QuestStatusView,
2152 #[serde(default)]
2153 pub current_step_id: Option<String>,
2154 #[serde(default)]
2155 pub current_step_title: String,
2156 #[serde(default)]
2157 pub objectives: Vec<QuestObjectiveProgress>,
2158 #[serde(default)]
2159 pub is_tracked: bool,
2160 #[serde(default)]
2161 pub can_withdraw: bool,
2162}
2163
2164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2165pub struct InteractableView {
2166 pub id: String,
2167 pub kind: String,
2168 pub label: String,
2169 pub x: f32,
2170 pub y: f32,
2171 pub z: f32,
2172 #[serde(default)]
2173 pub board_id: Option<String>,
2174}
2175
2176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2177pub struct QuestOffer {
2178 pub quest_id: String,
2179 pub title: String,
2180 pub description: String,
2181 #[serde(default)]
2182 pub step_count: u32,
2183}
2184
2185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2186pub struct QuestNotice {
2187 pub quest_id: String,
2188 pub title: String,
2189 pub message: String,
2190}
2191
2192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2193#[serde(rename_all = "snake_case")]
2194pub enum ShopOfferKind {
2195 Item,
2196 Blueprint,
2197}
2198
2199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2200pub struct ShopOffer {
2201 pub offer_id: String,
2202 pub kind: ShopOfferKind,
2203 pub label: String,
2204 #[serde(default)]
2205 pub template_id: Option<String>,
2206 #[serde(default)]
2207 pub blueprint_id: Option<String>,
2208 pub price_copper: u32,
2209 #[serde(default)]
2210 pub affordable: bool,
2211 #[serde(default)]
2212 pub already_owned: bool,
2213}
2214
2215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2216pub struct ShopBuyLine {
2217 pub template_id: String,
2218 pub label: String,
2219 pub quantity: u32,
2220 pub price_copper: u32,
2221}
2222
2223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2224pub struct ShopCatalog {
2225 pub npc_id: String,
2226 pub npc_label: String,
2227 #[serde(default)]
2228 pub sells: Vec<ShopOffer>,
2229 #[serde(default)]
2230 pub buys: Vec<ShopBuyLine>,
2231}
2232
2233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2234pub struct HarvestResult {
2235 pub node_id: String,
2236 pub quantity: u32,
2238 pub item_template: String,
2239 #[serde(default)]
2242 pub item_instance_id: Option<Uuid>,
2243}
2244
2245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2247pub struct Envelope<T> {
2248 pub protocol_version: u16,
2249 pub payload: T,
2250}
2251
2252impl<T> Envelope<T> {
2253 pub fn new(payload: T) -> Self {
2254 Self {
2255 protocol_version: crate::PROTOCOL_VERSION,
2256 payload,
2257 }
2258 }
2259}
2260
2261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2263pub struct Hello {
2264 pub client_name: String,
2265 pub protocol_version: u16,
2266 #[serde(default)]
2267 pub auth: AuthCredential,
2268 #[serde(default)]
2270 pub character_id: Option<Uuid>,
2271}
2272
2273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2276#[serde(rename_all = "snake_case")]
2277pub enum AuthCredential {
2278 DevLocal,
2279 Session { token: String },
2280 ApiToken { token: String, character_id: Uuid },
2281}
2282
2283impl Default for AuthCredential {
2284 fn default() -> Self {
2285 Self::DevLocal
2286 }
2287}
2288
2289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2290pub struct Welcome {
2291 pub session_id: SessionId,
2292 pub entity_id: EntityId,
2293 pub snapshot: Snapshot,
2294}
2295
2296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2297pub enum ServerMessage {
2298 Welcome(Welcome),
2299 ContentUpdated(Snapshot),
2301 Tick(TickDelta),
2302 IntentAck {
2303 entity_id: EntityId,
2304 seq: Seq,
2305 tick: Tick,
2306 },
2307 Chat(ChatMessage),
2308 HarvestResult(HarvestResult),
2309 UseResult(UseResult),
2310 CraftResult(CraftResult),
2311 Death(DeathNotice),
2312 Interaction(InteractionNotice),
2313 ShopOpened(ShopCatalog),
2314 NpcTalkOpened(NpcTalkOpened),
2315 NpcTalkPending(NpcTalkPending),
2316 NpcTalkReply(NpcTalkReply),
2317 NpcTalkClosed(NpcTalkClosed),
2318 NpcTalkError(NpcTalkError),
2319 QuestOffer(QuestOffer),
2320 QuestAccepted(QuestNotice),
2321 QuestWithdrawn(QuestNotice),
2322 QuestStepCompleted(QuestNotice),
2323 QuestCompleted(QuestNotice),
2324}
2325
2326#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2327pub enum ClientMessage {
2328 Hello(Hello),
2329 Intent(Intent),
2330 Disconnect,
2331}
2332
2333#[cfg(test)]
2334mod tests {
2335 use super::*;
2336
2337 #[test]
2338 fn pristine_vitals_state_yields_full_pools() {
2339 let attrs = PrimaryAttributes::default();
2340 let vitals = StoredVitalsState::default().apply_to(attrs);
2341 assert!(vitals.health > 0.0);
2342 assert_eq!(vitals.health, vitals.health_max);
2343 assert!((vitals.mana_max - 61.0).abs() < 0.01);
2344 }
2345
2346 #[test]
2347 fn saved_vitals_scale_when_pool_max_increases() {
2348 let mut attrs = PrimaryAttributes::default();
2349 attrs.intelligence = 140;
2350 attrs.wisdom = 140;
2351 let saved = StoredVitalsState {
2352 health: 100.0,
2353 mana: 14.0,
2354 stamina: 100.0,
2355 ..StoredVitalsState::default()
2356 };
2357 let vitals = saved.apply_to(attrs);
2358 assert!(vitals.mana_max > 55.0);
2359 assert!(
2360 (vitals.mana - vitals.mana_max).abs() < 0.01,
2361 "full legacy mana bar migrates to full new bar"
2362 );
2363 }
2364
2365 #[test]
2366 fn empty_vitals_state_is_pristine() {
2367 let pristine = StoredVitalsState {
2368 health: 0.0,
2369 mana: 0.0,
2370 stamina: 0.0,
2371 hunger: 0.0,
2372 thirst: 0.0,
2373 coins: 0,
2374 deaths: 0,
2375 life_state: LifeState::Alive,
2376 };
2377 assert!(pristine.is_pristine());
2378 let vitals = pristine.apply_to(PrimaryAttributes::default());
2379 assert!(vitals.health > 0.0);
2380 }
2381
2382 #[test]
2383 fn stored_vitals_roundtrip_preserves_partial_pools() {
2384 let attrs = PrimaryAttributes::default();
2385 let mut live = PlayerVitals::from_attributes(attrs);
2386 live.health = 25.0;
2387 live.hunger = 77.0;
2388 live.deaths = 2;
2389 let stored = StoredVitalsState::from_live(&live);
2390 let restored = stored.apply_to(attrs);
2391 assert!(
2392 (restored.health - 25.0).abs() < 0.01,
2393 "partial HP below cap stays absolute"
2394 );
2395 assert_eq!(restored.hunger, 77.0);
2396 assert_eq!(restored.deaths, 2);
2397 }
2398
2399 #[test]
2400 fn skill_tiers_start_at_zero() {
2401 let skill = SkillProgress::default();
2402 assert_eq!(skill.level, 0);
2403 assert_eq!(skill.display_tier(), 0);
2404 let trained = SkillProgress {
2405 level: 250,
2406 last_trained_tick: 1,
2407 };
2408 assert_eq!(trained.display_tier(), 2);
2409 }
2410
2411 #[test]
2412 fn quest_server_messages_roundtrip_json() {
2413 use crate::codec::{Codec, PostcardCodec};
2414
2415 let offer = ServerMessage::QuestOffer(QuestOffer {
2416 quest_id: "ada_goblin_hunt".into(),
2417 title: "Goblin Trouble".into(),
2418 description: "Help Ada".into(),
2419 step_count: 3,
2420 });
2421 let notice = ServerMessage::QuestAccepted(QuestNotice {
2422 quest_id: "ada_goblin_hunt".into(),
2423 title: "Goblin Trouble".into(),
2424 message: "Quest accepted".into(),
2425 });
2426 for msg in [offer, notice] {
2427 let bytes = PostcardCodec.encode(&msg).unwrap();
2428 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
2429 assert_eq!(decoded, msg);
2430 }
2431 }
2432}