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
21#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
23pub struct AimPoint {
24 pub x: f32,
25 pub y: f32,
26 #[serde(default)]
27 pub z: f32,
28}
29
30impl AimPoint {
31 pub fn xy(x: f32, y: f32) -> Self {
32 Self { x, y, z: 0.0 }
33 }
34}
35
36impl WorldCoord {
37 pub fn surface(x: f32, y: f32) -> Self {
38 Self {
39 x,
40 y,
41 z: 0.0,
42 w: 0,
43 t: 0,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
49pub struct Velocity2D {
50 pub vx: f32,
51 pub vy: f32,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
55pub struct Transform {
56 pub position: WorldCoord,
57 pub yaw: f32,
58 pub velocity: Velocity2D,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum LifeState {
64 Alive,
65 Dead,
66}
67
68impl Default for LifeState {
69 fn default() -> Self {
70 Self::Alive
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum TimeOfDayPhase {
78 Night,
79 Dawn,
80 Morning,
81 Midday,
82 Afternoon,
83 Evening,
84}
85
86impl TimeOfDayPhase {
87 pub fn label(self) -> &'static str {
88 match self {
89 Self::Night => "Night",
90 Self::Dawn => "Dawn",
91 Self::Morning => "Morning",
92 Self::Midday => "Midday",
93 Self::Afternoon => "Afternoon",
94 Self::Evening => "Evening",
95 }
96 }
97
98 pub fn from_name(name: &str) -> Self {
99 match name.to_ascii_lowercase().as_str() {
100 "dawn" => Self::Dawn,
101 "morning" => Self::Morning,
102 "midday" | "mid_day" | "noon" => Self::Midday,
103 "afternoon" => Self::Afternoon,
104 "evening" | "dusk" => Self::Evening,
105 _ => Self::Night,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
112pub struct WorldClock {
113 pub day: u64,
115 pub hour: u8,
116 pub minute: u8,
117 pub phase: TimeOfDayPhase,
118}
119
120impl Default for WorldClock {
121 fn default() -> Self {
122 Self {
123 day: 0,
124 hour: 8,
125 minute: 0,
126 phase: TimeOfDayPhase::Morning,
127 }
128 }
129}
130
131impl WorldClock {
132 pub fn display_time(self) -> String {
133 format!("{:02}:{:02}", self.hour, self.minute)
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
139pub struct PrimaryAttributes {
140 pub strength: u16,
141 pub dexterity: u16,
142 pub intelligence: u16,
143 pub stamina: u16,
144 pub vitality: u16,
145 pub wisdom: u16,
146 pub charisma: u16,
147}
148
149impl Default for PrimaryAttributes {
150 fn default() -> Self {
151 Self {
152 strength: 150,
153 dexterity: 150,
154 intelligence: 150,
155 stamina: 150,
156 vitality: 150,
157 wisdom: 150,
158 charisma: 150,
159 }
160 }
161}
162
163impl PrimaryAttributes {
164 pub fn display(value: u16) -> u16 {
166 (value / 10).clamp(1, 100)
167 }
168
169 pub fn derived_preview(&self) -> DerivedPreview {
171 let str_d = Self::display(self.strength) as f32;
172 let dex_d = Self::display(self.dexterity) as f32;
173 let int_d = Self::display(self.intelligence) as f32;
174 let wis_d = Self::display(self.wisdom) as f32;
175 DerivedPreview {
176 attack_power: str_d * 1.2 + dex_d * 0.3,
177 spell_power: int_d * 1.1 + wis_d * 0.4,
178 evasion: dex_d * 0.8 + wis_d * 0.2,
179 carry_mass_max: str_d * 2.5,
180 sight_range_m: 12.0 + wis_d * 0.15 + dex_d * 0.05,
181 fov_deg: 120.0 + wis_d * 0.2,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq)]
188pub struct DerivedPreview {
189 pub attack_power: f32,
190 pub spell_power: f32,
191 pub evasion: f32,
192 pub carry_mass_max: f32,
193 pub sight_range_m: f32,
194 pub fov_deg: f32,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
199pub struct SkillProgress {
200 pub level: u16,
201 #[serde(default)]
202 pub last_trained_tick: u64,
203}
204
205impl Default for SkillProgress {
206 fn default() -> Self {
207 Self {
208 level: 0,
209 last_trained_tick: 0,
210 }
211 }
212}
213
214impl SkillProgress {
215 pub fn display_tier(&self) -> u16 {
217 (self.level / 100).min(10)
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
223#[serde(default)]
224pub struct ProgressionXp {
225 pub strength: f64,
226 pub dexterity: f64,
227 pub intelligence: f64,
228 pub stamina: f64,
229 pub vitality: f64,
230 pub wisdom: f64,
231 pub charisma: f64,
232 pub logging: f64,
233 pub mining: f64,
234 pub evocation: f64,
235 pub restoration: f64,
236 pub swords: f64,
237 pub archery: f64,
238 pub crafting: f64,
239 pub alchemy: f64,
240 pub cartography: f64,
241 #[serde(default)]
243 pub ability: std::collections::BTreeMap<String, f64>,
244}
245
246impl ProgressionXp {
247 pub fn bootstrap_new(baseline_display: u16, xp_base: f64, xp_growth: f64) -> Self {
249 let bootstrap = |display: f64| {
250 if display <= 1.0 {
251 0.0
252 } else {
253 xp_base * xp_growth.powf(display - 1.0)
254 }
255 };
256 let b = baseline_display as f64;
257 let primary = bootstrap(b);
258 Self {
259 strength: primary,
260 dexterity: primary,
261 intelligence: primary,
262 stamina: primary,
263 vitality: primary,
264 wisdom: primary,
265 charisma: primary,
266 ..Self::default()
267 }
268 }
269
270 pub fn is_empty(&self) -> bool {
271 self.strength == 0.0
272 && self.dexterity == 0.0
273 && self.intelligence == 0.0
274 && self.stamina == 0.0
275 && self.vitality == 0.0
276 && self.wisdom == 0.0
277 && self.charisma == 0.0
278 && self.logging == 0.0
279 && self.mining == 0.0
280 && self.evocation == 0.0
281 && self.restoration == 0.0
282 && self.swords == 0.0
283 && self.archery == 0.0
284 && self.crafting == 0.0
285 && self.alchemy == 0.0
286 && self.cartography == 0.0
287 && self.ability.is_empty()
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
293pub struct AbilityMasteryHud {
294 pub ability_id: String,
295 pub tier: u16,
297 pub level: u16,
299 pub xp: f64,
301 pub xp_to_next: f64,
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307#[serde(default)]
308pub struct PlayerSkills {
309 pub logging: SkillProgress,
310 pub mining: SkillProgress,
311 pub evocation: SkillProgress,
312 #[serde(default)]
313 pub restoration: SkillProgress,
314 pub swords: SkillProgress,
315 #[serde(default)]
316 pub archery: SkillProgress,
317 pub crafting: SkillProgress,
318 #[serde(default)]
319 pub alchemy: SkillProgress,
320 pub cartography: SkillProgress,
321}
322
323impl Default for PlayerSkills {
324 fn default() -> Self {
325 Self {
326 logging: SkillProgress::default(),
327 mining: SkillProgress::default(),
328 evocation: SkillProgress::default(),
329 restoration: SkillProgress::default(),
330 swords: SkillProgress::default(),
331 archery: SkillProgress::default(),
332 crafting: SkillProgress::default(),
333 alchemy: SkillProgress::default(),
334 cartography: SkillProgress::default(),
335 }
336 }
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
341pub struct PlayerVitals {
342 pub health: f32,
343 pub health_max: f32,
344 pub mana: f32,
345 pub mana_max: f32,
346 pub stamina: f32,
347 pub stamina_max: f32,
348 #[serde(default = "default_survival_pool_max")]
349 pub hunger: f32,
350 #[serde(default = "default_survival_pool_max")]
351 pub hunger_max: f32,
352 #[serde(default = "default_survival_pool_max")]
353 pub thirst: f32,
354 #[serde(default = "default_survival_pool_max")]
355 pub thirst_max: f32,
356 #[serde(default)]
357 pub coins: u32,
358 #[serde(default)]
359 pub deaths: u32,
360 #[serde(default)]
361 pub life_state: LifeState,
362}
363
364fn default_survival_pool_max() -> f32 {
365 100.0
366}
367
368impl Default for PlayerVitals {
369 fn default() -> Self {
370 Self::from_attributes(PrimaryAttributes::default())
371 }
372}
373
374impl PlayerVitals {
375 pub fn from_attributes(attrs: PrimaryAttributes) -> Self {
382 let vit_d = PrimaryAttributes::display(attrs.vitality) as f32;
383 let int_d = PrimaryAttributes::display(attrs.intelligence) as f32;
384 let wis_d = PrimaryAttributes::display(attrs.wisdom) as f32;
385 let sta_d = PrimaryAttributes::display(attrs.stamina) as f32;
386
387 let health_max = 50.0 + vit_d * 2.0;
388 let stamina_max = 30.0 + sta_d * 1.4;
389 let mana_max = 25.0 + int_d * 1.1 + wis_d * 1.3;
390 let hunger_max = 100.0;
391 let thirst_max = 100.0;
392 Self {
393 health: health_max,
394 health_max,
395 mana: mana_max,
396 mana_max,
397 stamina: stamina_max,
398 stamina_max,
399 hunger: hunger_max,
400 hunger_max,
401 thirst: thirst_max,
402 thirst_max,
403 coins: 0,
404 deaths: 0,
405 life_state: LifeState::Alive,
406 }
407 }
408
409 pub fn legacy_maxima(attrs: PrimaryAttributes) -> (f32, f32, f32) {
411 (
412 attrs.vitality as f32 / 5.0,
413 attrs.stamina as f32 / 5.0,
414 (attrs.intelligence as f32 + attrs.wisdom as f32) / 20.0,
415 )
416 }
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
421#[serde(default)]
422pub struct StoredVitalsState {
423 pub health: f32,
424 pub mana: f32,
425 pub stamina: f32,
426 pub hunger: f32,
427 pub thirst: f32,
428 pub coins: u32,
429 pub deaths: u32,
430 pub life_state: LifeState,
431}
432
433impl StoredVitalsState {
434 pub fn from_live(v: &PlayerVitals) -> Self {
435 Self {
436 health: v.health,
437 mana: v.mana,
438 stamina: v.stamina,
439 hunger: v.hunger,
440 thirst: v.thirst,
441 coins: v.coins,
442 deaths: v.deaths,
443 life_state: v.life_state,
444 }
445 }
446
447 pub fn is_pristine(&self) -> bool {
449 self.health == 0.0
450 && self.mana == 0.0
451 && self.stamina == 0.0
452 && self.hunger == 0.0
453 && self.thirst == 0.0
454 && self.coins == 0
455 && self.deaths == 0
456 && self.life_state == LifeState::Alive
457 }
458
459 pub fn apply_to(self, attrs: PrimaryAttributes) -> PlayerVitals {
460 if self.is_pristine() {
461 return PlayerVitals::from_attributes(attrs);
462 }
463 let fresh = PlayerVitals::from_attributes(attrs);
464 let (legacy_hp, legacy_sta, legacy_mana) = PlayerVitals::legacy_maxima(attrs);
465
466 let scale = |current: f32, legacy_max: f32, new_max: f32| {
467 if legacy_max > 0.0 && new_max > legacy_max * 1.05 && current >= legacy_max * 0.95 {
468 let ratio = (current / legacy_max).clamp(0.0, 1.0);
469 (new_max * ratio).min(new_max)
470 } else {
471 current.min(new_max)
472 }
473 };
474
475 let mut v = fresh;
476 v.health = scale(self.health, legacy_hp, fresh.health_max);
477 v.mana = scale(self.mana, legacy_mana, fresh.mana_max);
478 v.stamina = scale(self.stamina, legacy_sta, fresh.stamina_max);
479 v.hunger = self.hunger.min(v.hunger_max);
480 v.thirst = self.thirst.min(v.thirst_max);
481 v.coins = self.coins;
482 v.deaths = self.deaths;
483 v.life_state = self.life_state;
484 v
485 }
486}
487
488impl Default for StoredVitalsState {
489 fn default() -> Self {
490 Self::from_live(&PlayerVitals::default())
491 }
492}
493
494pub fn humanize_snake_id(id: &str) -> String {
498 id.split('_')
499 .filter(|part| !part.is_empty())
500 .map(|part| {
501 let mut chars = part.chars();
502 match chars.next() {
503 None => String::new(),
504 Some(first) => first.to_uppercase().chain(chars).collect(),
505 }
506 })
507 .collect::<Vec<_>>()
508 .join(" ")
509}
510
511#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
513pub struct KnownAbility {
514 pub ability_id: String,
515 #[serde(default = "default_known_permanent")]
517 pub permanent: bool,
518 #[serde(default)]
520 pub expires_at_tick: Option<u64>,
521}
522
523fn default_known_permanent() -> bool {
524 true
525}
526
527impl KnownAbility {
528 pub fn permanent(ability_id: impl Into<String>) -> Self {
529 Self {
530 ability_id: ability_id.into(),
531 permanent: true,
532 expires_at_tick: None,
533 }
534 }
535
536 pub fn is_active(&self, tick: u64) -> bool {
537 if self.permanent {
538 return true;
539 }
540 match self.expires_at_tick {
541 Some(exp) => tick < exp,
542 None => false,
543 }
544 }
545}
546
547#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
549pub struct RotationPreset {
550 pub id: String,
551 pub label: String,
552 #[serde(default)]
553 pub abilities: Vec<String>,
554}
555
556impl RotationPreset {
557 pub fn melee_default(ability_id: impl Into<String>) -> Self {
558 let id = ability_id.into();
559 Self {
560 id: "melee".into(),
561 label: "Weapon".into(),
563 abilities: vec![id],
564 }
565 }
566
567 pub fn is_weapon_preset(&self) -> bool {
568 self.id == "melee"
569 }
570}
571
572#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
574pub struct StoredTargetSlot {
575 pub instance_id: Option<String>,
576 #[serde(default)]
577 pub preset_id: Option<String>,
578 #[serde(default)]
579 pub rotation_index: u32,
580 #[serde(default)]
581 pub auto_enabled: bool,
582}
583
584#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
585#[serde(default)]
586pub struct StoredCombatProfile {
587 pub combat_target_instance_id: Option<String>,
589 pub in_combat: bool,
590 pub last_combat_tick: u64,
591 pub last_attack_tick: u64,
592 pub cooldowns_until_tick: BTreeMap<String, u64>,
593 #[serde(default = "default_auto_attack")]
594 pub auto_attack_enabled: bool,
595 #[serde(default)]
597 pub mainhand_template_id: Option<String>,
598 #[serde(default)]
600 pub mainhand_instance_id: Option<Uuid>,
601 #[serde(default)]
603 pub offhand_template_id: Option<String>,
604 #[serde(default)]
606 pub offhand_instance_id: Option<Uuid>,
607 #[serde(default)]
610 pub worn: Vec<(BodySlot, ItemStack)>,
611 #[serde(default)]
613 pub rotation_presets: Vec<RotationPreset>,
614 #[serde(default)]
616 pub target_slots: Vec<StoredTargetSlot>,
617 #[serde(default)]
619 pub known_blueprint_ids: Vec<String>,
620 #[serde(default)]
622 pub keychain: Vec<ItemStack>,
623 #[serde(default)]
625 pub whisper_pouch: Vec<ItemStack>,
626 #[serde(default)]
628 pub known_abilities: Vec<KnownAbility>,
629 #[serde(default)]
631 pub hotbar: Vec<Option<String>>,
632 #[serde(default)]
634 pub abilities_schema_version: u32,
635 #[serde(default)]
637 pub bank_balance_copper: u64,
638}
639
640fn default_auto_attack() -> bool {
641 true
642}
643
644impl Default for StoredCombatProfile {
645 fn default() -> Self {
646 Self {
647 combat_target_instance_id: None,
648 in_combat: false,
649 last_combat_tick: 0,
650 last_attack_tick: 0,
651 cooldowns_until_tick: BTreeMap::new(),
652 auto_attack_enabled: true,
653 mainhand_template_id: None,
654 mainhand_instance_id: None,
655 offhand_template_id: None,
656 offhand_instance_id: None,
657 worn: Vec::new(),
658 rotation_presets: Vec::new(),
659 target_slots: Vec::new(),
660 known_blueprint_ids: Vec::new(),
661 keychain: Vec::new(),
662 whisper_pouch: Vec::new(),
663 known_abilities: Vec::new(),
664 hotbar: Vec::new(),
665 abilities_schema_version: 0,
666 bank_balance_copper: 0,
667 }
668 }
669}
670
671#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
673#[serde(rename_all = "snake_case")]
674pub enum CombatCueKind {
675 Dodge,
676 Block,
677 AttackTelegraph,
678}
679
680#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
681pub struct CombatCueView {
682 pub kind: CombatCueKind,
683 pub until_tick: Tick,
685 #[serde(default)]
687 pub start_tick: Tick,
688 #[serde(default)]
690 pub ability_id: Option<String>,
691}
692
693#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
694pub struct EntityState {
695 pub id: EntityId,
696 pub transform: Transform,
697 #[serde(default)]
699 pub label: String,
700 #[serde(default)]
701 pub vitals: Option<PlayerVitals>,
702 #[serde(default)]
704 pub attributes: Option<PrimaryAttributes>,
705 #[serde(default)]
706 pub skills: Option<PlayerSkills>,
707 #[serde(default)]
709 pub inside_building: Option<String>,
710 #[serde(default)]
712 pub tile_id: Option<String>,
713 #[serde(default)]
715 pub paperdoll_ref: Option<String>,
716 #[serde(default)]
718 pub presentation_state: Option<String>,
719 #[serde(default)]
721 pub sprite_mode: Option<String>,
722 #[serde(default)]
724 pub progression_xp: Option<ProgressionXp>,
725 #[serde(default)]
727 pub combat_cues: Vec<CombatCueView>,
728 #[serde(default)]
730 pub statuses: Vec<StatusEffectHud>,
731}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
735#[serde(rename_all = "snake_case")]
736pub enum ChatChannel {
737 Nearby,
739 Direct,
741 Whisper,
743 WhisperStone,
745}
746
747#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
749#[serde(rename_all = "snake_case")]
750pub enum ChatClarity {
751 #[default]
752 Clear,
753 Partial,
754 Heavy,
755}
756
757#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
758pub struct ChatMessage {
759 pub channel: ChatChannel,
760 pub from_entity: EntityId,
761 pub from_name: String,
762 pub text: String,
764 pub tick: Tick,
765 #[serde(default)]
767 pub to_entity: Option<EntityId>,
768 #[serde(default)]
769 pub clarity: ChatClarity,
770}
771
772#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
774pub enum Intent {
775 Move {
776 entity_id: EntityId,
777 forward: f32,
778 strafe: f32,
779 #[serde(default)]
781 vertical: f32,
782 #[serde(default)]
784 sprint: bool,
785 seq: Seq,
786 },
787 Stop {
788 entity_id: EntityId,
789 seq: Seq,
790 },
791 Harvest {
792 entity_id: EntityId,
793 node_id: String,
794 seq: Seq,
795 },
796 Use {
797 entity_id: EntityId,
798 template_id: String,
799 seq: Seq,
800 },
801 UseGrant {
803 entity_id: EntityId,
804 grant_instance_id: Uuid,
805 target_instance_id: Uuid,
806 seq: Seq,
807 },
808 Say {
809 entity_id: EntityId,
810 channel: ChatChannel,
811 text: String,
812 #[serde(default)]
814 to_entity: Option<EntityId>,
815 seq: Seq,
816 },
817 Craft {
819 entity_id: EntityId,
820 blueprint_id: String,
821 #[serde(default)]
823 count: Option<u32>,
824 seq: Seq,
825 },
826 Interact {
828 entity_id: EntityId,
829 target_id: String,
830 seq: Seq,
831 },
832 ShopBuy {
834 entity_id: EntityId,
835 npc_id: String,
836 offer_id: String,
837 #[serde(default = "default_one")]
838 quantity: u32,
839 seq: Seq,
840 },
841 ShopSell {
843 entity_id: EntityId,
844 npc_id: String,
845 template_id: String,
846 #[serde(default = "default_one")]
847 quantity: u32,
848 seq: Seq,
849 },
850 ShopClose {
852 entity_id: EntityId,
853 npc_id: String,
854 seq: Seq,
855 },
856 TestDamage {
858 entity_id: EntityId,
859 amount: f32,
860 seq: Seq,
861 },
862 SetTarget {
864 entity_id: EntityId,
865 target_id: EntityId,
866 seq: Seq,
867 },
868 SetTargetSlot {
870 entity_id: EntityId,
871 slot_index: u8,
872 target_id: EntityId,
873 seq: Seq,
874 },
875 ClearTarget {
876 entity_id: EntityId,
877 seq: Seq,
878 },
879 ClearTargetSlot {
880 entity_id: EntityId,
881 slot_index: u8,
882 seq: Seq,
883 },
884 SetAutoAttack {
886 entity_id: EntityId,
887 slot_index: u8,
888 enabled: bool,
889 seq: Seq,
890 },
891 Attack {
893 entity_id: EntityId,
894 #[serde(default)]
895 target_id: Option<EntityId>,
896 #[serde(default)]
897 weapon_slot: Option<u32>,
898 seq: Seq,
899 },
900 Pickup {
902 entity_id: EntityId,
903 #[serde(default)]
904 drop_id: Option<String>,
905 seq: Seq,
906 },
907 Cast {
910 entity_id: EntityId,
911 ability_id: String,
912 target_id: EntityId,
913 #[serde(default)]
914 target_point: Option<AimPoint>,
915 seq: Seq,
916 },
917 BindActionSlot {
919 entity_id: EntityId,
920 slot_index: u8,
921 ability_id: String,
922 #[serde(default = "default_auto_attack")]
923 auto_enabled: bool,
924 seq: Seq,
925 },
926 UseActionSlot {
928 entity_id: EntityId,
929 slot_index: u8,
930 seq: Seq,
931 },
932 Dodge {
934 entity_id: EntityId,
935 seq: Seq,
936 },
937 Lunge {
939 entity_id: EntityId,
940 #[serde(default)]
942 forward: f32,
943 #[serde(default)]
945 strafe: f32,
946 seq: Seq,
947 },
948 DirectionalJump {
950 entity_id: EntityId,
951 #[serde(default)]
953 forward: f32,
954 #[serde(default)]
956 strafe: f32,
957 seq: Seq,
958 },
959 Block {
961 entity_id: EntityId,
962 #[serde(default = "default_block_enabled")]
963 enabled: bool,
964 seq: Seq,
965 },
966 EquipMainhand {
970 entity_id: EntityId,
971 #[serde(default)]
972 template_id: Option<String>,
973 #[serde(default)]
974 instance_id: Option<Uuid>,
975 seq: Seq,
976 },
977 EquipOffhand {
979 entity_id: EntityId,
980 #[serde(default)]
981 template_id: Option<String>,
982 #[serde(default)]
983 instance_id: Option<Uuid>,
984 seq: Seq,
985 },
986 EquipWorn {
989 entity_id: EntityId,
990 slot: BodySlot,
991 #[serde(default)]
992 instance_id: Option<Uuid>,
993 seq: Seq,
994 },
995 MoveItem {
997 entity_id: EntityId,
998 item_instance_id: Uuid,
999 from: InventoryLocation,
1000 to: InventoryLocation,
1001 #[serde(default)]
1003 to_parent_instance_id: Option<Uuid>,
1004 #[serde(default)]
1006 quantity: Option<u32>,
1007 seq: Seq,
1008 },
1009 PlaceContainer {
1011 entity_id: EntityId,
1012 item_instance_id: Uuid,
1013 seq: Seq,
1014 },
1015 PickupContainer {
1017 entity_id: EntityId,
1018 container_id: String,
1019 seq: Seq,
1020 },
1021 MovePlacedContainer {
1023 entity_id: EntityId,
1024 container_id: String,
1025 x: f32,
1026 y: f32,
1027 seq: Seq,
1028 },
1029 SetContainerLocked {
1031 entity_id: EntityId,
1032 location: InventoryLocation,
1034 locked: bool,
1035 seq: Seq,
1036 },
1037 DropItem {
1039 entity_id: EntityId,
1040 item_instance_id: Uuid,
1041 from: InventoryLocation,
1042 seq: Seq,
1043 },
1044 DestroyItem {
1046 entity_id: EntityId,
1047 item_instance_id: Uuid,
1048 from: InventoryLocation,
1049 #[serde(default)]
1051 quantity: Option<u32>,
1052 seq: Seq,
1053 },
1054 RenameContainer {
1056 entity_id: EntityId,
1057 item_instance_id: Uuid,
1058 location: InventoryLocation,
1059 name: String,
1060 seq: Seq,
1061 },
1062 UpsertRotationPreset {
1064 entity_id: EntityId,
1065 preset: RotationPreset,
1066 seq: Seq,
1067 },
1068 DeleteRotationPreset {
1070 entity_id: EntityId,
1071 preset_id: String,
1072 seq: Seq,
1073 },
1074 AssignSlotPreset {
1076 entity_id: EntityId,
1077 slot_index: u8,
1078 preset_id: String,
1079 seq: Seq,
1080 },
1081 SetHotbarSlot {
1084 entity_id: EntityId,
1085 slot: u8,
1087 #[serde(default)]
1089 ability_id: Option<String>,
1090 seq: Seq,
1091 },
1092 AdvanceRotation {
1094 entity_id: EntityId,
1095 slot_index: u8,
1096 seq: Seq,
1097 },
1098 NpcTalkOpen {
1100 entity_id: EntityId,
1101 npc_id: String,
1102 seq: Seq,
1103 },
1104 NpcTalkSay {
1106 entity_id: EntityId,
1107 npc_id: String,
1108 message: String,
1109 seq: Seq,
1110 },
1111 NpcTalkClose {
1113 entity_id: EntityId,
1114 npc_id: String,
1115 seq: Seq,
1116 },
1117 AcceptQuest {
1119 entity_id: EntityId,
1120 quest_id: String,
1121 seq: Seq,
1122 },
1123 WithdrawQuest {
1125 entity_id: EntityId,
1126 quest_id: String,
1127 seq: Seq,
1128 },
1129 TrackQuest {
1131 entity_id: EntityId,
1132 quest_id: String,
1133 seq: Seq,
1134 },
1135 QuestGiveItem {
1137 entity_id: EntityId,
1138 npc_id: String,
1139 template_id: String,
1140 #[serde(default = "default_one")]
1141 quantity: u32,
1142 seq: Seq,
1143 },
1144 HireWorker {
1146 entity_id: EntityId,
1147 def_id: String,
1148 wage_copper_per_interval: u32,
1149 #[serde(default)]
1150 lodging_container_id: Option<String>,
1151 #[serde(default)]
1152 job_yaml: Option<String>,
1153 seq: Seq,
1154 },
1155 DismissWorker {
1157 entity_id: EntityId,
1158 worker_instance_id: String,
1159 seq: Seq,
1160 },
1161 SetWorkerJob {
1163 entity_id: EntityId,
1164 worker_instance_id: String,
1165 job_yaml: String,
1166 seq: Seq,
1167 },
1168 AssignWorkerLodging {
1170 entity_id: EntityId,
1171 worker_instance_id: String,
1172 lodging_container_id: String,
1173 seq: Seq,
1174 },
1175 SetWorkerMode {
1177 entity_id: EntityId,
1178 worker_instance_id: String,
1179 mode: String,
1180 seq: Seq,
1181 },
1182 GiveWorkerItem {
1185 entity_id: EntityId,
1186 worker_instance_id: String,
1187 item_instance_id: uuid::Uuid,
1188 #[serde(default)]
1189 quantity: Option<u32>,
1190 seq: Seq,
1191 },
1192 TakeWorkerItem {
1194 entity_id: EntityId,
1195 worker_instance_id: String,
1196 item_instance_id: uuid::Uuid,
1197 #[serde(default)]
1198 quantity: Option<u32>,
1199 seq: Seq,
1200 },
1201 RenameHiredWorker {
1203 entity_id: EntityId,
1204 worker_instance_id: String,
1205 name: String,
1206 seq: Seq,
1207 },
1208 TeachWorkerBlueprint {
1210 entity_id: EntityId,
1211 worker_instance_id: String,
1212 blueprint_id: String,
1213 seq: Seq,
1214 },
1215 AttendHiredWorker {
1217 entity_id: EntityId,
1218 worker_instance_id: String,
1219 attending: bool,
1220 seq: Seq,
1221 },
1222 BuyPlot {
1224 entity_id: EntityId,
1225 zone_id: String,
1226 x0: f32,
1227 y0: f32,
1228 x1: f32,
1229 y1: f32,
1230 seq: Seq,
1231 },
1232 BuyPlotAllFree {
1234 entity_id: EntityId,
1235 zone_id: String,
1236 seq: Seq,
1237 },
1238 SellPlotToCrown {
1240 entity_id: EntityId,
1241 plot_id: Uuid,
1242 seq: Seq,
1243 },
1244 Cultivate {
1246 entity_id: EntityId,
1247 x: f32,
1249 y: f32,
1250 seq: Seq,
1251 },
1252 PlantSeeds {
1254 entity_id: EntityId,
1255 seed_template_id: String,
1256 quantity: u32,
1257 seq: Seq,
1258 },
1259 SetPlotFarmPublic {
1261 entity_id: EntityId,
1262 plot_id: Uuid,
1263 public: bool,
1264 #[serde(default)]
1265 public_tax_discount_bps: u32,
1266 seq: Seq,
1267 },
1268 PlotFarmAllowUpsert {
1270 entity_id: EntityId,
1271 plot_id: Uuid,
1272 #[serde(default)]
1274 character_id: Option<Uuid>,
1275 #[serde(default)]
1277 character_name: String,
1278 #[serde(default)]
1279 tax_discount_bps: u32,
1280 seq: Seq,
1281 },
1282 PlotFarmAllowRemove {
1284 entity_id: EntityId,
1285 plot_id: Uuid,
1286 character_id: Uuid,
1287 seq: Seq,
1288 },
1289 StartPlotBuild {
1292 entity_id: EntityId,
1293 plot_id: Uuid,
1294 wall_material_id: String,
1295 roof_material_id: String,
1296 seq: Seq,
1297 },
1298 CancelPlotBuild {
1299 entity_id: EntityId,
1300 seq: Seq,
1301 },
1302 SetDoorLocked {
1305 entity_id: EntityId,
1306 door_id: String,
1307 locked: bool,
1308 seq: Seq,
1309 },
1310 EnterBuildingDoor {
1313 entity_id: EntityId,
1314 door_id: String,
1315 seq: Seq,
1316 },
1317 ExitBuildingDoor {
1320 entity_id: EntityId,
1321 door_id: String,
1322 seq: Seq,
1323 },
1324 ConfirmInteriorEdit {
1326 entity_id: EntityId,
1327 building_id: String,
1328 rooms: Vec<InteriorRoomEdit>,
1329 room_doors: Vec<InteriorRoomDoorEdit>,
1330 seq: Seq,
1331 },
1332 CancelInteriorEdit {
1333 entity_id: EntityId,
1334 building_id: String,
1335 seq: Seq,
1336 },
1337 BankDeposit {
1339 entity_id: EntityId,
1340 npc_id: String,
1341 #[serde(default)]
1343 amount_copper: u64,
1344 seq: Seq,
1345 },
1346 BankWithdraw {
1348 entity_id: EntityId,
1349 npc_id: String,
1350 #[serde(default)]
1352 amount_copper: u64,
1353 seq: Seq,
1354 },
1355 BankClose {
1357 entity_id: EntityId,
1358 npc_id: String,
1359 seq: Seq,
1360 },
1361 BankTransfer {
1363 entity_id: EntityId,
1364 npc_id: String,
1365 #[serde(default)]
1367 to_character_id: Option<Uuid>,
1368 #[serde(default)]
1370 to_name: String,
1371 amount_copper: u64,
1373 seq: Seq,
1374 },
1375 StorageStore {
1377 entity_id: EntityId,
1378 npc_id: String,
1379 item_instance_id: Uuid,
1380 #[serde(default)]
1381 quantity: Option<u32>,
1382 seq: Seq,
1383 },
1384 StorageTake {
1386 entity_id: EntityId,
1387 npc_id: String,
1388 item_instance_id: Uuid,
1389 #[serde(default)]
1390 quantity: Option<u32>,
1391 seq: Seq,
1392 },
1393 StorageShip {
1395 entity_id: EntityId,
1396 npc_id: String,
1397 dest_building_id: String,
1398 item_instance_id: Uuid,
1399 #[serde(default)]
1400 quantity: Option<u32>,
1401 seq: Seq,
1402 },
1403 StorageClose {
1405 entity_id: EntityId,
1406 npc_id: String,
1407 seq: Seq,
1408 },
1409 MarketList {
1412 entity_id: EntityId,
1413 npc_id: String,
1414 source: GoodsLocation,
1415 item_instance_id: Uuid,
1416 #[serde(default)]
1417 quantity: Option<u32>,
1418 unit_price_copper: u64,
1419 #[serde(default)]
1421 npc_price: bool,
1422 seq: Seq,
1423 },
1424 MarketReprice {
1426 entity_id: EntityId,
1427 npc_id: String,
1428 listing_id: Uuid,
1429 unit_price_copper: u64,
1430 seq: Seq,
1431 },
1432 MarketDelist {
1434 entity_id: EntityId,
1435 npc_id: String,
1436 listing_id: Uuid,
1437 dest: GoodsLocation,
1438 seq: Seq,
1439 },
1440 MarketBuy {
1442 entity_id: EntityId,
1443 npc_id: String,
1444 listing_id: Uuid,
1445 #[serde(default = "default_one")]
1446 quantity: u32,
1447 dest: GoodsLocation,
1448 seq: Seq,
1449 },
1450 MarketClose {
1452 entity_id: EntityId,
1453 npc_id: String,
1454 seq: Seq,
1455 },
1456 TradeRequest {
1458 entity_id: EntityId,
1459 peer_entity_id: EntityId,
1460 seq: Seq,
1461 },
1462 TradeRespond {
1464 entity_id: EntityId,
1465 peer_entity_id: EntityId,
1466 accept: bool,
1467 seq: Seq,
1468 },
1469 TradePresent {
1471 entity_id: EntityId,
1472 item_instance_id: Uuid,
1473 #[serde(default)]
1474 quantity: Option<u32>,
1475 seq: Seq,
1476 },
1477 TradeUnpresent {
1479 entity_id: EntityId,
1480 item_instance_id: Uuid,
1481 seq: Seq,
1482 },
1483 TradeSetReady {
1485 entity_id: EntityId,
1486 ready: bool,
1487 seq: Seq,
1488 },
1489 TradeCancel {
1491 entity_id: EntityId,
1492 seq: Seq,
1493 },
1494 DestroyWhisperStone {
1496 entity_id: EntityId,
1497 item_instance_id: Uuid,
1498 seq: Seq,
1499 },
1500 StowWhisperStone {
1502 entity_id: EntityId,
1503 item_instance_id: Uuid,
1504 seq: Seq,
1505 },
1506}
1507
1508fn default_block_enabled() -> bool {
1509 true
1510}
1511
1512#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1514pub struct StatusEffectHud {
1515 pub effect_id: String,
1516 pub label: String,
1517 #[serde(default)]
1518 pub polarity: String,
1519 #[serde(default)]
1520 pub icon_tile_id: Option<String>,
1521 #[serde(default)]
1523 pub remaining_sec: Option<f32>,
1524}
1525
1526#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1528pub struct CombatTargetHud {
1529 pub entity_id: EntityId,
1530 #[serde(default)]
1531 pub label: String,
1532 #[serde(default)]
1533 pub level: u32,
1534 pub health: f32,
1535 pub health_max: f32,
1536 #[serde(default)]
1537 pub life_state: LifeState,
1538 #[serde(default)]
1539 pub distance_m: f32,
1540 #[serde(default)]
1541 pub statuses: Vec<StatusEffectHud>,
1542}
1543
1544#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1546#[serde(rename_all = "snake_case")]
1547pub enum TimedChannelKind {
1548 #[default]
1549 Cultivate,
1550 Plant,
1551 Harvest,
1552 Build,
1554}
1555
1556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1558pub struct TimedChannelHud {
1559 #[serde(default)]
1560 pub label: String,
1561 #[serde(default)]
1562 pub channel: TimedChannelKind,
1563 #[serde(default)]
1564 pub cell_x: i32,
1565 #[serde(default)]
1566 pub cell_y: i32,
1567 #[serde(default)]
1569 pub x0: f32,
1570 #[serde(default)]
1571 pub y0: f32,
1572 #[serde(default)]
1573 pub x1: f32,
1574 #[serde(default)]
1575 pub y1: f32,
1576 #[serde(default)]
1577 pub ticks_remaining: u64,
1578 #[serde(default)]
1579 pub ticks_total: u64,
1580}
1581
1582impl TimedChannelHud {
1583 pub fn has_footprint(&self) -> bool {
1585 self.x1 > self.x0 && self.y1 > self.y0
1586 }
1587}
1588
1589#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1591#[serde(rename_all = "snake_case")]
1592pub enum PlotBuildMaterialSource {
1593 #[default]
1594 None,
1595 TownStorage,
1596 NearbyContainer,
1597}
1598
1599#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1601pub struct BuildingMaterialView {
1602 pub id: String,
1603 pub display_name: String,
1604 #[serde(default)]
1605 pub can_wall: bool,
1606 #[serde(default)]
1607 pub can_roof: bool,
1608 #[serde(default)]
1609 pub wall_set: String,
1610 #[serde(default)]
1611 pub roof_set: String,
1612 #[serde(default = "default_material_tick_mult")]
1613 pub tick_mult: f32,
1614 #[serde(default)]
1615 pub wall_bom: Vec<BuildingBomLineView>,
1616 #[serde(default)]
1617 pub roof_bom: Vec<BuildingBomLineView>,
1618}
1619
1620fn default_material_tick_mult() -> f32 {
1621 1.0
1622}
1623
1624#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1625pub struct BuildingBomLineView {
1626 pub template_id: String,
1627 #[serde(default)]
1628 pub display_name: String,
1629 pub per_m2: f32,
1630}
1631
1632#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1634pub struct PlotBuildStockView {
1635 pub template_id: String,
1636 #[serde(default)]
1637 pub display_name: String,
1638 pub quantity: u32,
1639}
1640
1641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1643pub struct PlotBuildOfferHud {
1644 pub plot_id: Uuid,
1645 #[serde(default)]
1646 pub pad_width_m: f32,
1647 #[serde(default)]
1648 pub pad_depth_m: f32,
1649 #[serde(default)]
1650 pub pad_ok: bool,
1651 #[serde(default)]
1652 pub pad_error: String,
1653 #[serde(default)]
1654 pub source: PlotBuildMaterialSource,
1655 #[serde(default)]
1656 pub source_label: String,
1657 #[serde(default)]
1658 pub available: Vec<PlotBuildStockView>,
1659 #[serde(default)]
1660 pub base_ticks: u32,
1661 #[serde(default)]
1662 pub tick_per_m2: u32,
1663}
1664
1665#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1667pub struct CastProgressHud {
1668 #[serde(default)]
1669 pub ability_id: String,
1670 #[serde(default)]
1671 pub ability_label: String,
1672 #[serde(default)]
1673 pub ticks_remaining: u64,
1674 #[serde(default)]
1675 pub ticks_total: u64,
1676}
1677
1678#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1680pub struct AbilityCooldownHud {
1681 #[serde(default)]
1682 pub ability_id: String,
1683 #[serde(default)]
1684 pub label: String,
1685 #[serde(default)]
1686 pub cd_ticks: u64,
1687 #[serde(default)]
1688 pub cd_total_ticks: u64,
1689}
1690
1691#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1693pub struct CombatSlotHud {
1694 pub slot_index: u8,
1695 #[serde(default)]
1696 pub target_entity_id: Option<EntityId>,
1697 #[serde(default)]
1698 pub target_label: Option<String>,
1699 #[serde(default)]
1700 pub target: Option<CombatTargetHud>,
1701 #[serde(default)]
1702 pub preset_id: Option<String>,
1703 #[serde(default)]
1704 pub preset_label: Option<String>,
1705 #[serde(default)]
1706 pub rotation: Vec<String>,
1707 #[serde(default)]
1708 pub rotation_index: u32,
1709 #[serde(default)]
1710 pub next_ability_id: Option<String>,
1711 #[serde(default)]
1712 pub auto_enabled: bool,
1713}
1714
1715#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1717pub struct DefensePieceHud {
1718 pub slot: BodySlot,
1719 pub label: String,
1720 pub template_id: String,
1721 #[serde(default)]
1722 pub armor_physical: f32,
1723 #[serde(default)]
1724 pub resists: Vec<(String, f32)>,
1725}
1726
1727impl Default for DefensePieceHud {
1728 fn default() -> Self {
1729 Self {
1730 slot: BodySlot::Head,
1731 label: String::new(),
1732 template_id: String::new(),
1733 armor_physical: 0.0,
1734 resists: Vec::new(),
1735 }
1736 }
1737}
1738
1739#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1741pub struct DefenseHud {
1742 pub armor_physical: f32,
1743 pub vitality_contribution: f32,
1744 pub total_mitigation_rating: f32,
1745 pub estimated_physical_dr: f32,
1747 #[serde(default)]
1748 pub resists: Vec<(String, f32)>,
1749 #[serde(default)]
1750 pub pieces: Vec<DefensePieceHud>,
1751}
1752
1753#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1755pub struct CombatHud {
1756 pub in_combat: bool,
1757 pub auto_attack: bool,
1759 pub has_los: bool,
1760 pub attack_cd_ticks: u64,
1761 #[serde(default)]
1762 pub ability_id: String,
1763 #[serde(default)]
1764 pub target_entity_id: Option<EntityId>,
1765 #[serde(default)]
1766 pub target_label: Option<String>,
1767 #[serde(default)]
1768 pub max_target_slots: u8,
1769 #[serde(default)]
1770 pub slots: Vec<CombatSlotHud>,
1771 #[serde(default)]
1772 pub rotation_presets: Vec<RotationPreset>,
1773 #[serde(default)]
1774 pub gcd_ticks: u64,
1775 #[serde(default)]
1776 pub mainhand_template_id: Option<String>,
1777 #[serde(default)]
1778 pub mainhand_label: Option<String>,
1779 #[serde(default)]
1781 pub mainhand_instance_id: Option<Uuid>,
1782 #[serde(default)]
1783 pub offhand_template_id: Option<String>,
1784 #[serde(default)]
1785 pub offhand_label: Option<String>,
1786 #[serde(default)]
1788 pub offhand_instance_id: Option<Uuid>,
1789 #[serde(default)]
1791 pub mainhand_hand_slots: u8,
1792 #[serde(default)]
1794 pub worn: Vec<(BodySlot, ItemStack)>,
1795 #[serde(default)]
1797 pub defense: Option<DefenseHud>,
1798 #[serde(default)]
1799 pub carry_mass: f32,
1800 #[serde(default)]
1801 pub carry_mass_max: f32,
1802 #[serde(default)]
1803 pub encumbrance: EncumbranceState,
1804 #[serde(default)]
1806 pub keychain: Vec<ItemStack>,
1807 #[serde(default)]
1809 pub whisper_pouch: Vec<ItemStack>,
1810 #[serde(default)]
1811 pub target: Option<CombatTargetHud>,
1812 #[serde(default)]
1813 pub cast: Option<CastProgressHud>,
1814 #[serde(default)]
1816 pub timed_channel: Option<TimedChannelHud>,
1817 #[serde(default)]
1819 pub plot_build: Option<PlotBuildOfferHud>,
1820 #[serde(default)]
1821 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1822 #[serde(default)]
1823 pub blocking_active: bool,
1824 #[serde(default)]
1826 pub progression_xp: Option<ProgressionXp>,
1827 #[serde(default)]
1828 pub progression_baseline: u16,
1829 #[serde(default)]
1830 pub progression_xp_base: f64,
1831 #[serde(default)]
1832 pub progression_xp_growth: f64,
1833 #[serde(default)]
1834 pub attributes: Option<PrimaryAttributes>,
1835 #[serde(default)]
1836 pub skills: Option<PlayerSkills>,
1837 #[serde(default)]
1839 pub statuses: Vec<StatusEffectHud>,
1840 #[serde(default)]
1842 pub known_abilities: Vec<String>,
1843 #[serde(default)]
1845 pub ability_meta: Vec<AbilityMetaHud>,
1846 #[serde(default)]
1848 pub ability_mastery: Vec<AbilityMasteryHud>,
1849 #[serde(default)]
1852 pub hotbar: Vec<Option<String>>,
1853 #[serde(default)]
1855 pub max_abilities_per_rotation: u8,
1856}
1857
1858#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1860pub struct AbilityMetaHud {
1861 pub id: String,
1862 #[serde(default = "default_aim_mode_entity")]
1864 pub aim_mode: String,
1865 #[serde(default)]
1866 pub blast_radius_m: f32,
1867 #[serde(default)]
1868 pub allows_self: bool,
1869 #[serde(default)]
1870 pub is_heal: bool,
1871 #[serde(default = "default_auto_rotation_eligible")]
1874 pub auto_rotation_eligible: bool,
1875}
1876
1877fn default_auto_rotation_eligible() -> bool {
1878 true
1879}
1880
1881fn default_aim_mode_entity() -> String {
1882 "entity".into()
1883}
1884
1885pub const HOTBAR_ITEM_PREFIX: &str = "item:";
1887
1888pub fn hotbar_consumable_binding(template_id: &str) -> String {
1890 format!("{HOTBAR_ITEM_PREFIX}{}", template_id.trim())
1891}
1892
1893pub fn hotbar_consumable_template(binding: &str) -> Option<&str> {
1895 binding
1896 .strip_prefix(HOTBAR_ITEM_PREFIX)
1897 .map(str::trim)
1898 .filter(|id| !id.is_empty())
1899}
1900
1901pub fn hotbar_binding_is_consumable(binding: &str) -> bool {
1903 hotbar_consumable_template(binding).is_some()
1904}
1905
1906#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1908#[serde(rename_all = "snake_case")]
1909pub enum CombatFxKind {
1910 MeleeArc,
1911 Cone,
1912 Sphere,
1913 Beam,
1914 HitMarker,
1915}
1916
1917#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1919#[serde(rename_all = "snake_case")]
1920pub enum CombatFxHitOutcome {
1921 #[default]
1922 Hit,
1923 Blocked,
1924 Miss,
1925 Glance,
1926}
1927
1928#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1930pub struct CombatFxHit {
1931 pub entity_id: EntityId,
1932 pub x: f32,
1933 pub y: f32,
1934 pub z: f32,
1935 #[serde(default)]
1936 pub outcome: CombatFxHitOutcome,
1937}
1938
1939#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1941pub struct CombatFx {
1942 pub id: u64,
1943 pub kind: CombatFxKind,
1944 pub ability_id: String,
1945 pub caster_id: EntityId,
1946 pub origin_x: f32,
1947 pub origin_y: f32,
1948 pub origin_z: f32,
1949 #[serde(default)]
1950 pub end_x: Option<f32>,
1951 #[serde(default)]
1952 pub end_y: Option<f32>,
1953 #[serde(default)]
1954 pub end_z: Option<f32>,
1955 #[serde(default)]
1956 pub yaw: Option<f32>,
1957 #[serde(default)]
1958 pub reach_m: Option<f32>,
1959 #[serde(default)]
1960 pub arc_deg: Option<f32>,
1961 #[serde(default)]
1962 pub radius_m: Option<f32>,
1963 #[serde(default)]
1964 pub hits: Vec<CombatFxHit>,
1965 pub until_tick: u64,
1967 #[serde(default)]
1968 pub damage_type: String,
1969}
1970
1971#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1973#[serde(rename_all = "snake_case")]
1974pub enum WorkerModeView {
1975 Companion,
1976 JobLoop,
1977 Idle,
1980}
1981
1982#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1984#[serde(rename_all = "snake_case")]
1985pub enum WorkerStateView {
1986 Idle,
1987 Traveling,
1988 Working,
1989 Resting,
1990 Waiting,
1991 Strike,
1992 Dismissed,
1993}
1994
1995#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
1997pub struct WorkerVitalsSummary {
1998 pub health_pct: f32,
1999 pub stamina_pct: f32,
2000}
2001
2002#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2004#[serde(rename_all = "snake_case")]
2005pub enum WorkerRouteKindView {
2006 #[default]
2007 HarvestLoop,
2008 Ordered,
2009}
2010
2011#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2013pub struct WorkerRouteView {
2014 #[serde(default)]
2015 pub kind: WorkerRouteKindView,
2016 #[serde(default)]
2017 pub lodging_container_id: Option<String>,
2018 #[serde(default)]
2020 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
2021 #[serde(default)]
2023 pub harvest_nodes: Vec<String>,
2024 #[serde(default = "default_route_carry_ratio")]
2025 pub carry_return_ratio: f32,
2026 #[serde(default)]
2028 pub stops: Vec<WorkerRouteStopView>,
2029}
2030
2031fn default_route_carry_ratio() -> f32 {
2032 0.90
2033}
2034
2035fn default_true_view() -> bool {
2036 true
2037}
2038
2039#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2041pub struct WorkerWithdrawItemView {
2042 pub template: String,
2043 #[serde(default)]
2045 pub qty: u32,
2046 #[serde(default)]
2048 pub all: bool,
2049}
2050
2051#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2052pub struct WorkerRouteWaypointView {
2053 pub x: f32,
2054 pub y: f32,
2055 pub z: f32,
2056}
2057
2058#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2067#[serde(rename_all = "snake_case")]
2068pub enum WorkerRouteStopView {
2069 Waypoint {
2070 x: f32,
2071 y: f32,
2072 #[serde(default)]
2073 z: f32,
2074 },
2075 HarvestNode {
2076 node_id: String,
2077 },
2078 DepositAt {
2079 container_id: String,
2080 #[serde(default)]
2081 filter: Option<Vec<String>>,
2082 },
2083 TradeWith {
2084 #[serde(default)]
2085 npc_id: Option<String>,
2086 template: String,
2087 #[serde(default = "default_true_view")]
2088 sell_all: bool,
2089 },
2090 WithdrawFrom {
2091 container_id: String,
2092 items: Vec<WorkerWithdrawItemView>,
2093 },
2094 CraftAt {
2095 device: String,
2096 blueprint: String,
2097 #[serde(default)]
2098 qty: Option<u32>,
2099 },
2100 CultivatePlot {
2101 plot_id: uuid::Uuid,
2102 },
2103 PlantPlot {
2104 plot_id: uuid::Uuid,
2105 seed_template: String,
2106 },
2107 HarvestPlot {
2108 plot_id: uuid::Uuid,
2109 },
2110 RestIfNeeded,
2111 Wait {
2112 #[serde(default)]
2113 wait_ticks: u64,
2114 },
2115}
2116
2117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2119#[serde(rename_all = "snake_case")]
2120pub enum LedgerCategory {
2121 Workers,
2122 Hire,
2123 Train,
2124 ShopBuy,
2125 Taxes,
2126 WorkerSales,
2127 TraderSales,
2128 BankDeposit,
2129 BankWithdraw,
2130 BankTransferOut,
2131 BankTransferIn,
2132 BankTransferFee,
2133 StorageShipFee,
2134 PropertyBuy,
2136 PropertySell,
2138 TaxShare,
2140 MarketBuy,
2142 MarketSell,
2144 Other,
2145}
2146
2147impl LedgerCategory {
2148 pub fn as_str(self) -> &'static str {
2149 match self {
2150 Self::Workers => "workers",
2151 Self::Hire => "hire",
2152 Self::Train => "train",
2153 Self::ShopBuy => "shop_buy",
2154 Self::Taxes => "taxes",
2155 Self::WorkerSales => "worker_sales",
2156 Self::TraderSales => "trader_sales",
2157 Self::BankDeposit => "bank_deposit",
2158 Self::BankWithdraw => "bank_withdraw",
2159 Self::BankTransferOut => "bank_transfer_out",
2160 Self::BankTransferIn => "bank_transfer_in",
2161 Self::BankTransferFee => "bank_transfer_fee",
2162 Self::StorageShipFee => "storage_ship_fee",
2163 Self::PropertyBuy => "property_buy",
2164 Self::PropertySell => "property_sell",
2165 Self::TaxShare => "tax_share",
2166 Self::MarketBuy => "market_buy",
2167 Self::MarketSell => "market_sell",
2168 Self::Other => "other",
2169 }
2170 }
2171}
2172
2173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2174pub struct LedgerEntryView {
2175 pub id: uuid::Uuid,
2176 pub game_day: u64,
2177 pub signed_copper: i64,
2178 pub category: LedgerCategory,
2179 #[serde(default)]
2180 pub label: String,
2181}
2182
2183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2184pub struct LedgerPeriodTotals {
2185 #[serde(default)]
2187 pub expenses: std::collections::HashMap<String, u64>,
2188 #[serde(default)]
2190 pub income: std::collections::HashMap<String, u64>,
2191 pub expense_copper: u64,
2192 pub income_copper: u64,
2193 pub cash_flow_copper: i64,
2195}
2196
2197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2198pub struct PlayerLedgerView {
2199 pub current_game_day: u64,
2200 #[serde(default)]
2201 pub period_day: LedgerPeriodTotals,
2202 #[serde(default)]
2203 pub period_week: LedgerPeriodTotals,
2204 #[serde(default)]
2205 pub period_month: LedgerPeriodTotals,
2206 #[serde(default)]
2207 pub period_lifetime: LedgerPeriodTotals,
2208 #[serde(default)]
2209 pub recent: Vec<LedgerEntryView>,
2210 #[serde(default)]
2212 pub wealth_on_person_copper: u64,
2213 #[serde(default)]
2215 pub wealth_in_storage_copper: u64,
2216 #[serde(default)]
2218 pub wealth_in_bank_copper: u64,
2219 #[serde(default)]
2221 pub wealth_total_copper: u64,
2222 #[serde(default)]
2224 pub wealth_in_property_copper: u64,
2225 #[serde(default)]
2227 pub wealth_net_worth_copper: u64,
2228 #[serde(default)]
2230 pub property_assets: Vec<PropertyAssetView>,
2231 #[serde(default)]
2233 pub property_market_nearby: Vec<PropertyMarketCompView>,
2234 #[serde(default)]
2236 pub live_expense_per_interval_copper: u64,
2237 #[serde(default)]
2239 pub live_income_route_est_per_loop_copper: u64,
2240 #[serde(default)]
2242 pub live_income_avg_per_interval_copper: u64,
2243 #[serde(default)]
2245 pub live_income_avg_window_intervals: u32,
2246 #[serde(default)]
2248 pub live_net_avg_per_interval_copper: i64,
2249}
2250
2251#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2253pub struct PropertyAssetView {
2254 pub plot_id: Uuid,
2255 pub label: String,
2257 pub zone_id: String,
2258 #[serde(default)]
2259 pub zone_label: Option<String>,
2260 pub area_m2: f32,
2261 pub purchase_basis_copper: u64,
2263 pub upkeep_copper_per_day: u64,
2264}
2265
2266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2268pub struct PropertyMarketCompView {
2269 pub day: u64,
2270 pub zone_id: String,
2271 #[serde(default)]
2272 pub zone_label: Option<String>,
2273 pub area_m2: f32,
2274 pub price_copper: u64,
2275 pub price_per_m2_copper: u64,
2277 pub kind: String,
2279 pub distance_m: f32,
2281}
2282
2283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2285#[serde(rename_all = "snake_case")]
2286pub enum AnalyticsMetric {
2287 NpcKill,
2288 WildlifeKill,
2289 Harvest,
2290 QuestComplete,
2291 QuestAccept,
2292 QuestAbandon,
2293 PlayerDeath,
2294 Craft,
2295 WorkerHire,
2296 WorkerDismiss,
2297 WorkerTeach,
2298 NpcTalk,
2299 ShopBuy,
2300 ShopSell,
2301 PlaceContainer,
2302 PickupContainer,
2303 PickupDrop,
2304 ConsumableUse,
2305 AbilityUse,
2306 DistanceWalkedM,
2307 DoorUse,
2308 BuildingEnter,
2309}
2310
2311impl AnalyticsMetric {
2312 pub fn as_str(self) -> &'static str {
2313 match self {
2314 Self::NpcKill => "npc_kill",
2315 Self::WildlifeKill => "wildlife_kill",
2316 Self::Harvest => "harvest",
2317 Self::QuestComplete => "quest_complete",
2318 Self::QuestAccept => "quest_accept",
2319 Self::QuestAbandon => "quest_abandon",
2320 Self::PlayerDeath => "player_death",
2321 Self::Craft => "craft",
2322 Self::WorkerHire => "worker_hire",
2323 Self::WorkerDismiss => "worker_dismiss",
2324 Self::WorkerTeach => "worker_teach",
2325 Self::NpcTalk => "npc_talk",
2326 Self::ShopBuy => "shop_buy",
2327 Self::ShopSell => "shop_sell",
2328 Self::PlaceContainer => "place_container",
2329 Self::PickupContainer => "pickup_container",
2330 Self::PickupDrop => "pickup_drop",
2331 Self::ConsumableUse => "consumable_use",
2332 Self::AbilityUse => "ability_use",
2333 Self::DistanceWalkedM => "distance_walked_m",
2334 Self::DoorUse => "door_use",
2335 Self::BuildingEnter => "building_enter",
2336 }
2337 }
2338
2339 pub fn from_str_key(s: &str) -> Option<Self> {
2340 Some(match s {
2341 "npc_kill" => Self::NpcKill,
2342 "wildlife_kill" => Self::WildlifeKill,
2343 "harvest" => Self::Harvest,
2344 "quest_complete" => Self::QuestComplete,
2345 "quest_accept" => Self::QuestAccept,
2346 "quest_abandon" => Self::QuestAbandon,
2347 "player_death" => Self::PlayerDeath,
2348 "craft" => Self::Craft,
2349 "worker_hire" => Self::WorkerHire,
2350 "worker_dismiss" => Self::WorkerDismiss,
2351 "worker_teach" => Self::WorkerTeach,
2352 "npc_talk" => Self::NpcTalk,
2353 "shop_buy" => Self::ShopBuy,
2354 "shop_sell" => Self::ShopSell,
2355 "place_container" => Self::PlaceContainer,
2356 "pickup_container" => Self::PickupContainer,
2357 "pickup_drop" => Self::PickupDrop,
2358 "consumable_use" => Self::ConsumableUse,
2359 "ability_use" => Self::AbilityUse,
2360 "distance_walked_m" => Self::DistanceWalkedM,
2361 "door_use" => Self::DoorUse,
2362 "building_enter" => Self::BuildingEnter,
2363 _ => return None,
2364 })
2365 }
2366}
2367
2368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2369pub struct CareerMetricRow {
2370 pub subject_id: String,
2371 pub amount: u64,
2372}
2373
2374#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2376pub struct PlayerCareerView {
2377 pub current_game_day: u64,
2378 #[serde(default)]
2379 pub kills: Vec<CareerMetricRow>,
2380 #[serde(default)]
2381 pub harvests: Vec<CareerMetricRow>,
2382 pub quests_completed: u64,
2383 #[serde(default)]
2384 pub crafts: Vec<CareerMetricRow>,
2385 pub deaths: u64,
2386 pub npc_talks: u64,
2387 pub shop_buys: u64,
2388 pub shop_sells: u64,
2389 pub distance_m: u64,
2390 #[serde(default)]
2391 pub other: Vec<CareerMetricRow>,
2392}
2393
2394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2396pub struct HiredWorkerView {
2397 pub instance_id: String,
2398 pub entity_id: EntityId,
2399 pub def_id: String,
2400 pub label: String,
2402 pub x: f32,
2403 pub y: f32,
2404 pub z: f32,
2405 pub mode: WorkerModeView,
2406 pub state: WorkerStateView,
2407 #[serde(default)]
2408 pub step_label: String,
2409 pub vitals: WorkerVitalsSummary,
2410 #[serde(default)]
2411 pub carry_pct: f32,
2412 #[serde(default)]
2413 pub last_error: Option<String>,
2414 pub wage_copper_per_interval: u32,
2415 #[serde(default)]
2417 pub effective_wage_copper: u32,
2418 #[serde(default)]
2420 pub wage_meters_walked: f32,
2421 #[serde(default)]
2423 pub lodging_container_id: Option<String>,
2424 #[serde(default)]
2426 pub route: Option<WorkerRouteView>,
2427 #[serde(default)]
2430 pub route_stop_index: Option<u32>,
2431 #[serde(default)]
2433 pub known_blueprint_ids: Vec<String>,
2434 #[serde(default = "default_worker_view_level")]
2436 pub level: u32,
2437 #[serde(default)]
2439 pub worker_xp: f64,
2440 #[serde(default)]
2442 pub inventory: Vec<ItemStack>,
2443}
2444
2445fn default_worker_view_level() -> u32 {
2446 1
2447}
2448
2449#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2451pub struct TickDelta {
2452 pub tick: Tick,
2453 pub entities: Vec<EntityState>,
2454 #[serde(default)]
2455 pub resource_nodes: Vec<ResourceNodeView>,
2456 #[serde(default)]
2457 pub buildings: Vec<BuildingView>,
2458 #[serde(default)]
2459 pub doors: Vec<DoorView>,
2460 #[serde(default)]
2461 pub npcs: Vec<NpcView>,
2462 #[serde(default)]
2464 pub inventory: Vec<ItemStack>,
2465 #[serde(default)]
2466 pub blueprints: Vec<BlueprintView>,
2467 #[serde(default)]
2469 pub building_materials: Vec<BuildingMaterialView>,
2470 #[serde(default)]
2471 pub world_clock: WorldClock,
2472 #[serde(default)]
2473 pub ground_drops: Vec<GroundDropView>,
2474 #[serde(default)]
2475 pub placed_containers: Vec<PlacedContainerView>,
2476 #[serde(default)]
2477 pub combat: Option<CombatHud>,
2478 #[serde(default)]
2479 pub interior_map: Option<InteriorMapView>,
2480 #[serde(default)]
2481 pub quest_log: Vec<QuestLogEntry>,
2482 #[serde(default)]
2483 pub hired_workers: Vec<HiredWorkerView>,
2484 #[serde(default)]
2485 pub interactables: Vec<InteractableView>,
2486 #[serde(default)]
2487 pub ledger: Option<PlayerLedgerView>,
2488 #[serde(default)]
2489 pub career: Option<PlayerCareerView>,
2490 #[serde(default)]
2492 pub combat_fx: Vec<CombatFx>,
2493 #[serde(default)]
2495 pub property_plots: Vec<PropertyPlotView>,
2496 #[serde(default)]
2498 pub terrain_overlays: Vec<TerrainZoneView>,
2499}
2500#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2501pub struct GroundDropView {
2502 pub id: String,
2503 pub template_id: String,
2504 pub quantity: u32,
2505 pub x: f32,
2506 pub y: f32,
2507 pub z: f32,
2508 #[serde(default)]
2510 pub tile_id: Option<String>,
2511 #[serde(default)]
2513 pub display_name: Option<String>,
2514 #[serde(default)]
2516 pub yaw: f32,
2517 #[serde(default)]
2519 pub pitch: f32,
2520 #[serde(default)]
2522 pub roll: f32,
2523 #[serde(default = "default_draw_scale")]
2525 pub draw_scale: f32,
2526}
2527
2528fn default_draw_scale() -> f32 {
2529 1.0
2530}
2531
2532#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2534pub struct Snapshot {
2535 pub tick: Tick,
2536 pub chunk_rev: u64,
2537 #[serde(default)]
2539 pub content_rev: u64,
2540 #[serde(default)]
2542 pub publish_rev: u64,
2543 pub entities: Vec<EntityState>,
2544 #[serde(default)]
2545 pub resource_nodes: Vec<ResourceNodeView>,
2546 #[serde(default)]
2548 pub world_x0: f32,
2549 #[serde(default)]
2550 pub world_y0: f32,
2551 #[serde(default)]
2553 pub world_width_m: f32,
2554 #[serde(default)]
2555 pub world_height_m: f32,
2556 #[serde(default)]
2557 pub buildings: Vec<BuildingView>,
2558 #[serde(default)]
2559 pub doors: Vec<DoorView>,
2560 #[serde(default)]
2561 pub npcs: Vec<NpcView>,
2562 #[serde(default)]
2563 pub inventory: Vec<ItemStack>,
2564 #[serde(default)]
2565 pub blueprints: Vec<BlueprintView>,
2566 #[serde(default)]
2568 pub building_materials: Vec<BuildingMaterialView>,
2569 #[serde(default)]
2570 pub world_clock: WorldClock,
2571 #[serde(default)]
2572 pub terrain_zones: Vec<TerrainZoneView>,
2573 #[serde(default)]
2574 pub z_platforms: Vec<ZPlatformView>,
2575 #[serde(default)]
2576 pub z_transitions: Vec<ZTransitionView>,
2577 #[serde(default)]
2578 pub ground_drops: Vec<GroundDropView>,
2579 #[serde(default)]
2580 pub placed_containers: Vec<PlacedContainerView>,
2581 #[serde(default)]
2582 pub combat: Option<CombatHud>,
2583 #[serde(default)]
2584 pub interior_map: Option<InteriorMapView>,
2585 #[serde(default)]
2586 pub quest_log: Vec<QuestLogEntry>,
2587 #[serde(default)]
2588 pub hired_workers: Vec<HiredWorkerView>,
2589 #[serde(default)]
2590 pub interactables: Vec<InteractableView>,
2591 #[serde(default)]
2592 pub ledger: Option<PlayerLedgerView>,
2593 #[serde(default)]
2594 pub career: Option<PlayerCareerView>,
2595 #[serde(default)]
2597 pub combat_fx: Vec<CombatFx>,
2598 #[serde(default)]
2600 pub property_zones: Vec<PropertyZoneView>,
2601 #[serde(default)]
2603 pub tax_zones: Vec<TaxZoneView>,
2604 #[serde(default)]
2606 pub boundary_zones: Vec<BoundaryZoneView>,
2607 #[serde(default)]
2609 pub encounter_zones: Vec<EncounterZoneView>,
2610 #[serde(default)]
2612 pub growth_zones: Vec<GrowthZoneView>,
2613 #[serde(default)]
2615 pub biome_zones: Vec<BiomeZoneView>,
2616 #[serde(default)]
2618 pub terrain_kind_nav: Vec<TerrainKindNavView>,
2619 #[serde(default)]
2621 pub property_plots: Vec<PropertyPlotView>,
2622 #[serde(default)]
2624 pub property_plot_settings: Option<PropertyPlotSettingsView>,
2625}
2626
2627#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2629pub struct ResourceNodeView {
2630 pub id: String,
2631 pub label: String,
2632 pub x: f32,
2633 pub y: f32,
2634 pub z: f32,
2635 pub item_template: String,
2636 #[serde(default = "default_node_state")]
2637 pub state: ResourceNodeState,
2638 #[serde(default = "default_blocking_view")]
2640 pub blocking: bool,
2641 #[serde(default = "default_blocking_radius_view")]
2643 pub blocking_radius_m: f32,
2644 #[serde(default)]
2646 pub harvest_off: bool,
2647 #[serde(default)]
2649 pub tile_id: Option<String>,
2650 #[serde(default)]
2652 pub yaw: f32,
2653 #[serde(default)]
2655 pub pitch: f32,
2656 #[serde(default)]
2658 pub roll: f32,
2659 #[serde(default = "default_draw_scale")]
2661 pub draw_scale: f32,
2662 #[serde(default)]
2664 pub sprite_mode: Option<String>,
2665 #[serde(default)]
2667 pub presentation_state: Option<String>,
2668 #[serde(default)]
2671 pub growth_progress: Option<f32>,
2672 #[serde(default)]
2674 pub channel_start_tick: Option<Tick>,
2675 #[serde(default)]
2676 pub channel_end_tick: Option<Tick>,
2677 #[serde(default)]
2679 pub harvest_drop_templates: Vec<String>,
2680}
2681
2682fn default_blocking_radius_view() -> f32 {
2683 0.8
2684}
2685
2686fn default_blocking_view() -> bool {
2687 true
2688}
2689
2690#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2691#[serde(rename_all = "snake_case")]
2692pub enum ResourceNodeState {
2693 Available,
2694 Harvesting,
2695 Cooldown,
2696}
2697fn default_node_state() -> ResourceNodeState {
2698 ResourceNodeState::Available
2699}
2700
2701#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2703#[serde(rename_all = "snake_case")]
2704pub enum ItemSpawnStateView {
2705 Spawned,
2706 PickedUp {
2707 respawn_at_tick: u64,
2708 },
2709 Consumed,
2710}
2711
2712#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2714pub struct ItemSpawnView {
2715 pub id: String,
2716 pub label: String,
2717 pub item_template: String,
2718 pub quantity: u32,
2719 pub x: f32,
2720 pub y: f32,
2721 pub z: f32,
2722 pub respawn_ticks: u32,
2723 #[serde(default)]
2724 pub building_id: Option<String>,
2725 pub state: ItemSpawnStateView,
2726}
2727
2728#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2729#[serde(rename_all = "snake_case")]
2730pub enum ItemStatusBindingMode {
2731 OnHit,
2732 WhileEquipped,
2733}
2734
2735impl Default for ItemStatusBindingMode {
2736 fn default() -> Self {
2737 Self::OnHit
2738 }
2739}
2740
2741#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2743pub struct ItemStatusBinding {
2744 pub effect_id: String,
2745 #[serde(default)]
2746 pub mode: ItemStatusBindingMode,
2747 #[serde(default)]
2749 pub source: String,
2750 #[serde(default)]
2751 pub applied_at_tick: u64,
2752 #[serde(default, skip_serializing_if = "Option::is_none")]
2754 pub expires_at_tick: Option<u64>,
2755}
2756
2757#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2758pub struct ItemStack {
2759 pub template_id: String,
2760 pub quantity: u32,
2761 #[serde(default)]
2763 pub item_instance_id: Option<Uuid>,
2764 #[serde(default)]
2766 pub props: BTreeMap<String, String>,
2767 #[serde(default)]
2769 pub status_bindings: Vec<ItemStatusBinding>,
2770 #[serde(default)]
2772 pub contents: Vec<ItemStack>,
2773 #[serde(default)]
2775 pub display_name: Option<String>,
2776 #[serde(default)]
2778 pub category: Option<String>,
2779 #[serde(default)]
2781 pub base_mass: Option<f32>,
2782 #[serde(default)]
2784 pub base_volume: Option<f32>,
2785 #[serde(default)]
2787 pub capacity_volume: Option<f32>,
2788 #[serde(default)]
2790 pub stackable: Option<bool>,
2791 #[serde(default)]
2793 pub world_placeable: Option<bool>,
2794 #[serde(default)]
2796 pub worker_lodging_capacity: Option<u32>,
2797 #[serde(default)]
2799 pub equip_slot: Option<BodySlot>,
2800 #[serde(default)]
2802 pub armor_physical: Option<f32>,
2803 #[serde(default)]
2805 pub resists: Vec<(String, f32)>,
2806 #[serde(default)]
2808 pub hand_slots: Option<u8>,
2809 #[serde(default)]
2811 pub listable: Option<bool>,
2812}
2813
2814impl ItemStack {
2815 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
2816 Self {
2817 template_id: template_id.into(),
2818 quantity,
2819 ..Default::default()
2820 }
2821 }
2822}
2823
2824impl Default for ItemStack {
2825 fn default() -> Self {
2826 Self {
2827 template_id: String::new(),
2828 quantity: 0,
2829 item_instance_id: None,
2830 props: BTreeMap::new(),
2831 status_bindings: Vec::new(),
2832 contents: Vec::new(),
2833 display_name: None,
2834 category: None,
2835 base_mass: None,
2836 base_volume: None,
2837 capacity_volume: None,
2838 stackable: None,
2839 world_placeable: None,
2840 worker_lodging_capacity: None,
2841 equip_slot: None,
2842 armor_physical: None,
2843 resists: Vec::new(),
2844 hand_slots: None,
2845 listable: None,
2846 }
2847 }
2848}
2849
2850#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2852#[serde(rename_all = "snake_case")]
2853pub enum EncumbranceState {
2854 #[default]
2855 Light,
2856 Heavy,
2857 Over,
2858}
2859
2860#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
2864#[serde(rename_all = "snake_case")]
2865pub enum BodySlot {
2866 Head,
2867 #[serde(alias = "body")]
2869 Chest,
2870 #[serde(alias = "arms")]
2872 Forearms,
2873 Legs,
2874 Feet,
2875 Cloak,
2876 Back,
2877 Waist,
2878 Earrings,
2879 Necklace,
2880 Eyeglasses,
2881 #[serde(rename = "ring_left_1", alias = "ring_left1")]
2883 RingLeft1,
2884 #[serde(rename = "ring_left_2", alias = "ring_left2")]
2885 RingLeft2,
2886 #[serde(rename = "ring_right_1", alias = "ring_right1")]
2887 RingRight1,
2888 #[serde(rename = "ring_right_2", alias = "ring_right2")]
2889 RingRight2,
2890}
2891
2892impl BodySlot {
2893 pub const ALL: [BodySlot; 15] = [
2895 BodySlot::Head,
2896 BodySlot::Chest,
2897 BodySlot::Forearms,
2898 BodySlot::Legs,
2899 BodySlot::Feet,
2900 BodySlot::Cloak,
2901 BodySlot::Back,
2902 BodySlot::Waist,
2903 BodySlot::Earrings,
2904 BodySlot::Necklace,
2905 BodySlot::Eyeglasses,
2906 BodySlot::RingLeft1,
2907 BodySlot::RingLeft2,
2908 BodySlot::RingRight1,
2909 BodySlot::RingRight2,
2910 ];
2911
2912 pub fn as_str(self) -> &'static str {
2913 match self {
2914 BodySlot::Head => "head",
2915 BodySlot::Chest => "chest",
2916 BodySlot::Forearms => "forearms",
2917 BodySlot::Legs => "legs",
2918 BodySlot::Feet => "feet",
2919 BodySlot::Cloak => "cloak",
2920 BodySlot::Back => "back",
2921 BodySlot::Waist => "waist",
2922 BodySlot::Earrings => "earrings",
2923 BodySlot::Necklace => "necklace",
2924 BodySlot::Eyeglasses => "eyeglasses",
2925 BodySlot::RingLeft1 => "ring_left_1",
2926 BodySlot::RingLeft2 => "ring_left_2",
2927 BodySlot::RingRight1 => "ring_right_1",
2928 BodySlot::RingRight2 => "ring_right_2",
2929 }
2930 }
2931}
2932
2933#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2935#[serde(rename_all = "snake_case")]
2936pub enum InventoryLocation {
2937 Root,
2939 Worn { slot: BodySlot },
2941 Placed { container_id: String },
2943 Keychain,
2945 WhisperPouch,
2947}
2948
2949#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2951pub struct PlacedContainerView {
2952 pub id: String,
2953 pub template_id: String,
2954 pub display_name: String,
2955 pub x: f32,
2956 pub y: f32,
2957 pub z: f32,
2958 pub locked: bool,
2959 #[serde(default)]
2961 pub accessible: bool,
2962 #[serde(default)]
2963 pub owner_character_id: Option<Uuid>,
2964 #[serde(default)]
2966 pub contents: Vec<ItemStack>,
2967 #[serde(default)]
2969 pub lock_id: Option<String>,
2970 #[serde(default)]
2972 pub capacity_volume: Option<f32>,
2973 #[serde(default)]
2975 pub item_instance_id: Option<Uuid>,
2976 #[serde(default)]
2978 pub tile_id: Option<String>,
2979 #[serde(default)]
2981 pub worker_lodging_capacity: Option<u32>,
2982 #[serde(default)]
2984 pub blocking: bool,
2985 #[serde(default)]
2987 pub blocking_radius_m: f32,
2988 #[serde(default)]
2991 pub building_id: Option<String>,
2992}
2993
2994#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2995pub struct BlueprintIngredientView {
2996 pub template_id: String,
2997 pub quantity: u32,
2998 pub consumed: bool,
3000 #[serde(default)]
3002 pub display_name: String,
3003}
3004
3005#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3006pub struct ToolRequirementView {
3007 pub item: String,
3008 pub consumed: bool,
3010 #[serde(default)]
3012 pub display_name: String,
3013}
3014
3015#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3016pub struct SkillRequirementView {
3017 pub skill: String,
3018 pub level: u32,
3019}
3020
3021#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3022pub struct BlueprintView {
3023 pub id: String,
3024 pub label: String,
3025 pub output: String,
3026 pub output_qty: u32,
3027 pub craft_ticks: u32,
3028 pub inputs: Vec<BlueprintIngredientView>,
3029 #[serde(default)]
3031 pub station: Option<String>,
3032 #[serde(default)]
3033 pub category: Option<String>,
3034 #[serde(default)]
3035 pub required_tools: Vec<ToolRequirementView>,
3036 #[serde(default)]
3037 pub skill: Option<SkillRequirementView>,
3038 #[serde(default)]
3039 pub failure_chance: f32,
3040 #[serde(default)]
3042 pub worker_train_copper: u64,
3043 #[serde(default)]
3045 pub output_display_name: String,
3046}
3047
3048#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3050pub struct TerrainKindNavView {
3051 pub kind: TerrainKindView,
3052 #[serde(default = "default_move_speed_mult_one")]
3053 pub move_speed_mult: f32,
3054 #[serde(default)]
3055 pub impassable: bool,
3056}
3057
3058fn default_move_speed_mult_one() -> f32 {
3059 1.0
3060}
3061
3062#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3064#[serde(rename_all = "snake_case")]
3065pub enum TerrainKindView {
3066 #[default]
3067 Grass,
3068 Dirt,
3069 Tilled,
3070 Desert,
3071 Hill,
3072 Bog,
3073 Beach,
3074 ShallowWater,
3075 DeepWater,
3076 Trail,
3077 Road,
3078 Rock,
3079}
3080
3081#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3082pub struct TerrainZoneView {
3083 pub id: String,
3084 pub x0: f32,
3085 pub y0: f32,
3086 pub x1: f32,
3087 pub y1: f32,
3088 #[serde(default)]
3089 pub kind: TerrainKindView,
3090 #[serde(default)]
3092 pub elevation: f32,
3093 #[serde(default)]
3096 pub glyph: Option<String>,
3097 #[serde(default)]
3099 pub color: Option<String>,
3100 #[serde(default)]
3102 pub tile_id: Option<String>,
3103 #[serde(default)]
3105 pub z_order: i32,
3106 #[serde(default)]
3108 pub channel_start_tick: Option<Tick>,
3109 #[serde(default)]
3110 pub channel_end_tick: Option<Tick>,
3111}
3112
3113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3115pub struct ZoneRectView {
3116 pub x0: f32,
3117 pub y0: f32,
3118 pub x1: f32,
3119 pub y1: f32,
3120}
3121
3122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3124pub struct PropertyZoneView {
3125 pub id: String,
3126 #[serde(default)]
3128 pub label: Option<String>,
3129 pub rects: Vec<ZoneRectView>,
3130 #[serde(default)]
3131 pub z_order: i32,
3132 pub crown_price_copper: u64,
3133 pub upkeep_copper_per_day: u64,
3134 #[serde(default)]
3135 pub max_area_m2: Option<f32>,
3136 #[serde(default)]
3137 pub owner_tax_discount_bps: u32,
3138}
3139
3140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3142pub struct TaxZoneView {
3143 pub id: String,
3144 #[serde(default)]
3145 pub label: Option<String>,
3146 pub rects: Vec<ZoneRectView>,
3147 #[serde(default)]
3148 pub z_order: i32,
3149 pub rate_bps: u32,
3150 #[serde(default)]
3151 pub flat_copper: u64,
3152 #[serde(default)]
3154 pub market_sales_tax_bps: u32,
3155 #[serde(default)]
3157 pub market_sales_flat_copper: u32,
3158}
3159
3160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3162pub struct BoundaryZoneView {
3163 pub id: String,
3164 #[serde(default)]
3165 pub label: Option<String>,
3166 pub rects: Vec<ZoneRectView>,
3167 #[serde(default)]
3168 pub z_order: i32,
3169 #[serde(default, skip_serializing_if = "Option::is_none")]
3170 pub jurisdiction_id: Option<String>,
3171 #[serde(default = "default_true")]
3172 pub worker_logistics: bool,
3173 #[serde(default)]
3174 pub security_tier: String,
3175 #[serde(default)]
3176 pub pvp_mode: String,
3177 #[serde(default = "default_true")]
3178 pub crime_enabled: bool,
3179 #[serde(default)]
3180 pub guard_response: bool,
3181}
3182
3183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3185pub struct EncounterZoneView {
3186 pub id: String,
3187 #[serde(default)]
3188 pub label: Option<String>,
3189 pub rects: Vec<ZoneRectView>,
3190 #[serde(default)]
3191 pub z_order: i32,
3192}
3193
3194fn default_true() -> bool {
3195 true
3196}
3197
3198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3200pub struct GrowthZoneView {
3201 pub id: String,
3202 #[serde(default)]
3203 pub label: Option<String>,
3204 pub rects: Vec<ZoneRectView>,
3205 #[serde(default)]
3206 pub z_order: i32,
3207 #[serde(default = "default_one_f32")]
3208 pub fertility: f32,
3209}
3210
3211fn default_one_f32() -> f32 {
3212 1.0
3213}
3214
3215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3217pub struct BiomeZoneView {
3218 pub id: String,
3219 #[serde(default)]
3220 pub label: Option<String>,
3221 pub rects: Vec<ZoneRectView>,
3222 #[serde(default)]
3223 pub z_order: i32,
3224 pub biome_id: String,
3225}
3226
3227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3229pub struct FarmGrantView {
3230 pub character_id: Uuid,
3231 #[serde(default)]
3233 pub character_label: String,
3234 pub tax_discount_bps: u32,
3235}
3236
3237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3239pub struct PropertyPlotView {
3240 pub plot_id: Uuid,
3241 pub property_zone_id: String,
3242 #[serde(default)]
3243 pub zone_label: Option<String>,
3244 pub deed_instance_id: Uuid,
3245 pub x0: f32,
3246 pub y0: f32,
3247 pub x1: f32,
3248 pub y1: f32,
3249 pub upkeep_copper_per_day: u64,
3250 pub arrears_days: u32,
3251 #[serde(default)]
3253 pub is_mine: bool,
3254 #[serde(default)]
3256 pub may_farm: bool,
3257 #[serde(default)]
3259 pub purchase_basis_copper: u64,
3260 #[serde(default)]
3261 pub farm_public: bool,
3262 #[serde(default)]
3263 pub public_tax_discount_bps: u32,
3264 #[serde(default)]
3265 pub farm_allow: Vec<FarmGrantView>,
3266 #[serde(default)]
3268 pub owner_character_id: Option<Uuid>,
3269 #[serde(default)]
3270 pub owner_label: Option<String>,
3271 #[serde(default)]
3273 pub building_id: Option<String>,
3274}
3275
3276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3278pub struct PropertyPlotSettingsView {
3279 pub min_plot_area_m2: f32,
3280 pub tax_premium_weight: f32,
3281 pub sellback_bps: u32,
3282}
3283
3284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3286pub struct ZPlatformView {
3287 pub id: String,
3288 pub z: f32,
3289 pub x0: f32,
3290 pub y0: f32,
3291 pub x1: f32,
3292 pub y1: f32,
3293}
3294
3295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3297pub struct ZTransitionView {
3298 pub id: String,
3299 pub z_from: f32,
3300 pub z_to: f32,
3301 pub x0: f32,
3302 pub y0: f32,
3303 pub x1: f32,
3304 pub y1: f32,
3305}
3306
3307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3308pub struct BuildingView {
3309 pub id: String,
3310 pub label: String,
3311 pub x: f32,
3312 pub y: f32,
3313 pub width_m: f32,
3314 pub depth_m: f32,
3315 #[serde(default)]
3316 pub interior_blueprint: Option<String>,
3317 #[serde(default)]
3318 pub tags: Vec<String>,
3319 #[serde(default)]
3321 pub market_boundary_zone_ids: Vec<String>,
3322 #[serde(default)]
3324 pub market_max_volume: Option<f32>,
3325 #[serde(default)]
3328 pub wall_set: Option<String>,
3329 #[serde(default)]
3331 pub roof_set: Option<String>,
3332}
3333
3334pub const DEFAULT_BUILDING_ART_SET: &str = "classic_stone";
3337
3338impl BuildingView {
3339 pub fn effective_wall_set(&self) -> &str {
3340 self.wall_set
3341 .as_deref()
3342 .filter(|s| !s.is_empty())
3343 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3344 }
3345
3346 pub fn effective_roof_set(&self) -> &str {
3347 self.roof_set
3348 .as_deref()
3349 .filter(|s| !s.is_empty())
3350 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3351 }
3352}
3353
3354#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3355pub struct DoorView {
3356 pub id: String,
3357 pub building_id: String,
3358 pub x: f32,
3359 pub y: f32,
3360 #[serde(default)]
3361 pub open: bool,
3362 #[serde(default)]
3363 pub portal: Option<String>,
3364 #[serde(default)]
3366 pub locked: bool,
3367 #[serde(default = "default_door_accessible")]
3369 pub accessible: bool,
3370 #[serde(default)]
3371 pub lock_id: Option<Uuid>,
3372}
3373
3374fn default_door_accessible() -> bool {
3375 true
3376}
3377
3378#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3380pub struct InteriorRoomEdit {
3381 pub id: String,
3382 pub label: String,
3383 pub x0: f32,
3384 pub y0: f32,
3385 pub x1: f32,
3386 pub y1: f32,
3387}
3388
3389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3390pub struct InteriorRoomDoorEdit {
3391 pub id: String,
3392 pub room_a: String,
3393 pub room_b: String,
3394 pub x: f32,
3395 pub y: f32,
3396}
3397
3398#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3399pub struct InteriorRoomView {
3400 pub id: String,
3401 pub label: String,
3402 pub floor: i32,
3403 pub x0: f32,
3404 pub y0: f32,
3405 pub x1: f32,
3406 pub y1: f32,
3407 #[serde(default)]
3408 pub floor_color: Option<String>,
3409 #[serde(default)]
3410 pub floor_glyph: Option<String>,
3411}
3412
3413#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3414pub struct InteriorDoorView {
3415 pub id: String,
3416 pub room_a: String,
3417 pub room_b: String,
3418 pub x: f32,
3419 pub y: f32,
3420 pub kind: String,
3421 #[serde(default)]
3422 pub x_a: Option<f32>,
3423 #[serde(default)]
3424 pub y_a: Option<f32>,
3425 #[serde(default)]
3426 pub x_b: Option<f32>,
3427 #[serde(default)]
3428 pub y_b: Option<f32>,
3429}
3430
3431#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3432pub struct InteriorMapView {
3433 pub building_id: String,
3434 pub blueprint_id: String,
3435 pub background_color: String,
3436 #[serde(default)]
3437 pub default_floor_color: Option<String>,
3438 #[serde(default = "default_floor_height_view")]
3439 pub floor_height_m: f32,
3440 #[serde(default)]
3442 pub z_platforms: Vec<ZPlatformView>,
3443 #[serde(default)]
3444 pub z_transitions: Vec<ZTransitionView>,
3445 pub rooms: Vec<InteriorRoomView>,
3446 #[serde(default)]
3447 pub room_doors: Vec<InteriorDoorView>,
3448}
3449
3450fn default_floor_height_view() -> f32 {
3451 3.0
3452}
3453
3454#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3455pub struct NpcView {
3456 pub id: String,
3457 pub label: String,
3458 pub role: String,
3459 pub x: f32,
3460 pub y: f32,
3461 #[serde(default)]
3463 pub building_id: Option<String>,
3464 #[serde(default)]
3466 pub entity_id: Option<EntityId>,
3467 #[serde(default)]
3468 pub life_state: Option<LifeState>,
3469 #[serde(default)]
3470 pub hp_pct: Option<f32>,
3471 #[serde(default)]
3473 pub can_trade: bool,
3474 #[serde(default)]
3476 pub tile_id: Option<String>,
3477 #[serde(default)]
3479 pub behavior_state: Option<String>,
3480 #[serde(default)]
3482 pub presentation_state: Option<String>,
3483 #[serde(default)]
3485 pub sprite_mode: Option<String>,
3486 #[serde(default)]
3488 pub paperdoll_ref: Option<String>,
3489}
3490
3491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3492pub struct UseResult {
3493 pub template_id: String,
3494 pub hunger_restored: f32,
3495 pub thirst_restored: f32,
3496 #[serde(default)]
3497 pub health_restored: f32,
3498 #[serde(default)]
3499 pub mana_restored: f32,
3500 #[serde(default)]
3501 pub cleared_dot_ids: Vec<String>,
3502}
3503
3504#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3505pub struct CraftResult {
3506 pub blueprint_id: String,
3507 pub outputs: Vec<ItemStack>,
3508 pub consumed: Vec<ItemStack>,
3509 #[serde(default = "default_one")]
3511 pub batch_index: u32,
3512 #[serde(default = "default_one")]
3514 pub batch_total: u32,
3515}
3516
3517fn default_one() -> u32 {
3518 1
3519}
3520
3521#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3522pub struct DeathNotice {
3523 pub entity_id: EntityId,
3524 pub respawn_x: f32,
3525 pub respawn_y: f32,
3526 pub message: String,
3527}
3528
3529#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3530pub struct InteractionNotice {
3531 pub target_id: String,
3532 pub message: String,
3533 #[serde(default)]
3534 pub coins_delta: i32,
3535 #[serde(default)]
3536 pub inventory_delta: Vec<ItemStack>,
3537}
3538
3539#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3540#[serde(rename_all = "snake_case")]
3541pub enum NpcTalkTrustFlag {
3542 Stranger,
3543 Acquainted,
3544 Trusted,
3545}
3546
3547#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3548#[serde(rename_all = "snake_case")]
3549pub enum NpcTalkDepth {
3550 #[default]
3551 Full,
3552 Brief,
3553 Unavailable,
3554}
3555
3556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3557pub struct NpcTalkOpened {
3558 pub npc_id: String,
3559 pub npc_label: String,
3560 pub greeting: String,
3561 pub trust_flag: NpcTalkTrustFlag,
3562 #[serde(default)]
3563 pub talk_depth: NpcTalkDepth,
3564 #[serde(default = "default_true")]
3565 pub trade_allowed: bool,
3566}
3567
3568#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3569pub struct NpcTalkPending {
3570 pub npc_id: String,
3571}
3572
3573#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3574pub struct NpcTalkReply {
3575 pub npc_id: String,
3576 pub line: String,
3577 pub trust_flag: NpcTalkTrustFlag,
3578 #[serde(default)]
3579 pub wind_down: bool,
3580 #[serde(default)]
3581 pub trade_disabled: bool,
3582}
3583
3584#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3585pub struct NpcTalkClosed {
3586 pub npc_id: String,
3587}
3588
3589#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3590pub struct NpcTalkError {
3591 pub npc_id: String,
3592 pub reason: String,
3593}
3594
3595#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3596#[serde(rename_all = "snake_case")]
3597pub enum QuestStatusView {
3598 Available,
3599 Active,
3600 Completed,
3601}
3602
3603#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3604pub struct QuestObjectiveProgress {
3605 pub label: String,
3606 pub current: u32,
3607 pub required: u32,
3608 pub done: bool,
3609}
3610
3611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3612pub struct QuestLogEntry {
3613 pub quest_id: String,
3614 pub title: String,
3615 pub description: String,
3616 pub status: QuestStatusView,
3617 #[serde(default)]
3618 pub current_step_id: Option<String>,
3619 #[serde(default)]
3620 pub current_step_title: String,
3621 #[serde(default)]
3622 pub objectives: Vec<QuestObjectiveProgress>,
3623 #[serde(default)]
3624 pub is_tracked: bool,
3625 #[serde(default)]
3626 pub can_withdraw: bool,
3627}
3628
3629#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3630pub struct InteractableView {
3631 pub id: String,
3632 pub kind: String,
3633 pub label: String,
3634 pub x: f32,
3635 pub y: f32,
3636 pub z: f32,
3637 #[serde(default)]
3638 pub board_id: Option<String>,
3639}
3640
3641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3642pub struct QuestOffer {
3643 pub quest_id: String,
3644 pub title: String,
3645 pub description: String,
3646 #[serde(default)]
3647 pub step_count: u32,
3648}
3649
3650#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3651pub struct QuestNotice {
3652 pub quest_id: String,
3653 pub title: String,
3654 pub message: String,
3655}
3656
3657#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3658#[serde(rename_all = "snake_case")]
3659pub enum ShopOfferKind {
3660 Item,
3661 Blueprint,
3662}
3663
3664#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3665pub struct ShopOffer {
3666 pub offer_id: String,
3667 pub kind: ShopOfferKind,
3668 pub label: String,
3669 #[serde(default)]
3670 pub template_id: Option<String>,
3671 #[serde(default)]
3672 pub blueprint_id: Option<String>,
3673 pub price_copper: u32,
3674 #[serde(default)]
3675 pub affordable: bool,
3676 #[serde(default)]
3677 pub already_owned: bool,
3678}
3679
3680#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3681pub struct ShopBuyLine {
3682 pub template_id: String,
3683 pub label: String,
3684 pub quantity: u32,
3685 pub price_copper: u32,
3686}
3687
3688#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3690pub struct BankPanel {
3691 pub npc_id: String,
3692 pub npc_label: String,
3693 pub bank_balance_copper: u64,
3694 pub on_person_copper: u64,
3695 #[serde(default)]
3697 pub pending_outgoing_copper: u64,
3698 #[serde(default)]
3699 pub transfer_fee_bps: u32,
3700 #[serde(default)]
3701 pub transfer_clear_ticks: u64,
3702}
3703
3704#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3706pub struct StoragePanel {
3707 pub npc_id: String,
3708 pub npc_label: String,
3709 pub building_id: String,
3710 pub building_label: String,
3711 pub used_volume: f32,
3712 pub max_volume: f32,
3713 #[serde(default)]
3714 pub contents: Vec<ItemStack>,
3715 #[serde(default)]
3717 pub ship_destinations: Vec<StorageShipDest>,
3718}
3719
3720#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3721pub struct StorageShipDest {
3722 pub building_id: String,
3723 pub label: String,
3724 pub distance_m: f32,
3725 pub fee_copper: u64,
3726 pub travel_ticks: u64,
3727}
3728
3729#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3732pub enum GoodsLocation {
3733 Person,
3735 TownStorage { building_id: String },
3738}
3739
3740#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3743pub struct MarketListingView {
3744 pub listing_id: Uuid,
3745 pub seller_character_id: Uuid,
3746 pub seller_label: String,
3748 pub hall_building_id: String,
3749 pub hall_label: String,
3750 pub template_id: String,
3751 pub display_name: String,
3752 #[serde(default)]
3754 pub category: String,
3755 pub quantity: u32,
3756 pub unit_price_copper: u64,
3757 pub line_total_copper: u64,
3759 #[serde(default)]
3761 pub npc_price: bool,
3762 pub mine: bool,
3764}
3765
3766#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3768pub struct MarketListVault {
3769 pub building_id: String,
3770 pub building_label: String,
3772 #[serde(default)]
3773 pub contents: Vec<ItemStack>,
3774}
3775
3776#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3779pub struct MarketPanel {
3780 pub npc_id: String,
3781 pub npc_label: String,
3782 pub building_id: String,
3783 pub building_label: String,
3784 pub used_volume: f32,
3786 pub max_volume: f32,
3787 #[serde(default)]
3790 pub listings: Vec<MarketListingView>,
3791 #[serde(default)]
3793 pub tax_bps: u32,
3794 #[serde(default)]
3795 pub tax_flat_copper: u32,
3796 #[serde(default)]
3798 pub list_vaults: Vec<MarketListVault>,
3799}
3800
3801#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3802pub struct ShopCatalog {
3803 pub npc_id: String,
3804 pub npc_label: String,
3805 #[serde(default)]
3806 pub sells: Vec<ShopOffer>,
3807 #[serde(default)]
3808 pub buys: Vec<ShopBuyLine>,
3809}
3810
3811#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3812pub struct HarvestResult {
3813 pub node_id: String,
3814 pub quantity: u32,
3816 pub item_template: String,
3817 #[serde(default)]
3820 pub item_instance_id: Option<Uuid>,
3821}
3822
3823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3825pub struct Envelope<T> {
3826 pub protocol_version: u16,
3827 pub payload: T,
3828}
3829
3830impl<T> Envelope<T> {
3831 pub fn new(payload: T) -> Self {
3832 Self {
3833 protocol_version: crate::PROTOCOL_VERSION,
3834 payload,
3835 }
3836 }
3837}
3838
3839#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3841pub struct Hello {
3842 pub client_name: String,
3843 pub protocol_version: u16,
3844 #[serde(default)]
3845 pub auth: AuthCredential,
3846 #[serde(default)]
3848 pub character_id: Option<Uuid>,
3849}
3850
3851#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3854#[serde(rename_all = "snake_case")]
3855pub enum AuthCredential {
3856 DevLocal,
3857 Session { token: String },
3858 ApiToken { token: String, character_id: Uuid },
3859}
3860
3861impl Default for AuthCredential {
3862 fn default() -> Self {
3863 Self::DevLocal
3864 }
3865}
3866
3867#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3868pub struct Welcome {
3869 pub session_id: SessionId,
3870 pub entity_id: EntityId,
3871 pub snapshot: Snapshot,
3872}
3873
3874#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3875pub enum ServerMessage {
3876 Welcome(Welcome),
3877 ContentUpdated(Snapshot),
3879 Tick(TickDelta),
3880 IntentAck {
3881 entity_id: EntityId,
3882 seq: Seq,
3883 tick: Tick,
3884 },
3885 Chat(ChatMessage),
3886 HarvestResult(HarvestResult),
3887 UseResult(UseResult),
3888 CraftResult(CraftResult),
3889 Death(DeathNotice),
3890 Interaction(InteractionNotice),
3891 ShopOpened(ShopCatalog),
3892 NpcTalkOpened(NpcTalkOpened),
3893 NpcTalkPending(NpcTalkPending),
3894 NpcTalkReply(NpcTalkReply),
3895 NpcTalkClosed(NpcTalkClosed),
3896 NpcTalkError(NpcTalkError),
3897 QuestOffer(QuestOffer),
3898 QuestAccepted(QuestNotice),
3899 QuestWithdrawn(QuestNotice),
3900 QuestStepCompleted(QuestNotice),
3901 QuestCompleted(QuestNotice),
3902 BankOpened(BankPanel),
3904 StorageOpened(StoragePanel),
3906 MarketOpened(MarketPanel),
3908 TradeOpened(TradePanel),
3910 TradeClosed {
3912 reason: String,
3913 },
3914 ConnectRejected {
3917 reason: String,
3918 },
3919}
3920
3921#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3923pub struct TradePanel {
3924 pub peer_entity_id: EntityId,
3925 pub peer_name: String,
3926 pub my_presented: Vec<ItemStack>,
3927 pub their_presented: Vec<ItemStack>,
3928 pub i_ready: bool,
3929 pub they_ready: bool,
3930 pub my_mass_after: f32,
3932 pub my_mass_max: f32,
3933 pub my_encumbrance_after: EncumbranceState,
3934 pub overburden_warning: bool,
3936}
3937
3938#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3939pub enum ClientMessage {
3940 Hello(Hello),
3941 Intent(Intent),
3942 Disconnect,
3943}
3944
3945#[cfg(test)]
3946mod tests {
3947 use super::*;
3948
3949 #[test]
3950 fn pristine_vitals_state_yields_full_pools() {
3951 let attrs = PrimaryAttributes::default();
3952 let vitals = StoredVitalsState::default().apply_to(attrs);
3953 assert!(vitals.health > 0.0);
3954 assert_eq!(vitals.health, vitals.health_max);
3955 assert!((vitals.mana_max - 61.0).abs() < 0.01);
3956 }
3957
3958 #[test]
3959 fn humanize_snake_id_title_cases_parts() {
3960 assert_eq!(humanize_snake_id("heal_touch"), "Heal Touch");
3961 assert_eq!(humanize_snake_id("fireball"), "Fireball");
3962 assert_eq!(humanize_snake_id("cone_frost"), "Cone Frost");
3963 }
3964
3965 #[test]
3966 fn saved_vitals_scale_when_pool_max_increases() {
3967 let mut attrs = PrimaryAttributes::default();
3968 attrs.intelligence = 140;
3969 attrs.wisdom = 140;
3970 let saved = StoredVitalsState {
3971 health: 100.0,
3972 mana: 14.0,
3973 stamina: 100.0,
3974 ..StoredVitalsState::default()
3975 };
3976 let vitals = saved.apply_to(attrs);
3977 assert!(vitals.mana_max > 55.0);
3978 assert!(
3979 (vitals.mana - vitals.mana_max).abs() < 0.01,
3980 "full legacy mana bar migrates to full new bar"
3981 );
3982 }
3983
3984 #[test]
3985 fn empty_vitals_state_is_pristine() {
3986 let pristine = StoredVitalsState {
3987 health: 0.0,
3988 mana: 0.0,
3989 stamina: 0.0,
3990 hunger: 0.0,
3991 thirst: 0.0,
3992 coins: 0,
3993 deaths: 0,
3994 life_state: LifeState::Alive,
3995 };
3996 assert!(pristine.is_pristine());
3997 let vitals = pristine.apply_to(PrimaryAttributes::default());
3998 assert!(vitals.health > 0.0);
3999 }
4000
4001 #[test]
4002 fn stored_vitals_roundtrip_preserves_partial_pools() {
4003 let attrs = PrimaryAttributes::default();
4004 let mut live = PlayerVitals::from_attributes(attrs);
4005 live.health = 25.0;
4006 live.hunger = 77.0;
4007 live.deaths = 2;
4008 let stored = StoredVitalsState::from_live(&live);
4009 let restored = stored.apply_to(attrs);
4010 assert!(
4011 (restored.health - 25.0).abs() < 0.01,
4012 "partial HP below cap stays absolute"
4013 );
4014 assert_eq!(restored.hunger, 77.0);
4015 assert_eq!(restored.deaths, 2);
4016 }
4017
4018 #[test]
4019 fn skill_tiers_start_at_zero() {
4020 let skill = SkillProgress::default();
4021 assert_eq!(skill.level, 0);
4022 assert_eq!(skill.display_tier(), 0);
4023 let trained = SkillProgress {
4024 level: 250,
4025 last_trained_tick: 1,
4026 };
4027 assert_eq!(trained.display_tier(), 2);
4028 }
4029
4030 #[test]
4031 fn quest_server_messages_roundtrip_json() {
4032 use crate::codec::{Codec, PostcardCodec};
4033
4034 let offer = ServerMessage::QuestOffer(QuestOffer {
4035 quest_id: "ada_goblin_hunt".into(),
4036 title: "Goblin Trouble".into(),
4037 description: "Help Ada".into(),
4038 step_count: 3,
4039 });
4040 let notice = ServerMessage::QuestAccepted(QuestNotice {
4041 quest_id: "ada_goblin_hunt".into(),
4042 title: "Goblin Trouble".into(),
4043 message: "Quest accepted".into(),
4044 });
4045 for msg in [offer, notice] {
4046 let bytes = PostcardCodec.encode(&msg).unwrap();
4047 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
4048 assert_eq!(decoded, msg);
4049 }
4050 }
4051
4052 #[test]
4053 fn hotbar_consumable_binding_roundtrips() {
4054 let binding = hotbar_consumable_binding("bottle_of_water");
4055 assert_eq!(binding, "item:bottle_of_water");
4056 assert!(hotbar_binding_is_consumable(&binding));
4057 assert_eq!(
4058 hotbar_consumable_template(&binding),
4059 Some("bottle_of_water")
4060 );
4061 assert!(!hotbar_binding_is_consumable("fireball"));
4062 assert_eq!(hotbar_consumable_template("fireball"), None);
4063 }
4064}