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 hearing_range_m: 6.0
183 + wis_d * 0.08
184 + (PrimaryAttributes::display(self.stamina) as f32) * 0.04,
185 }
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq)]
191pub struct DerivedPreview {
192 pub attack_power: f32,
193 pub spell_power: f32,
194 pub evasion: f32,
195 pub carry_mass_max: f32,
196 pub sight_range_m: f32,
197 pub fov_deg: f32,
198 pub hearing_range_m: f32,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203pub struct SkillProgress {
204 pub level: u16,
205 #[serde(default)]
206 pub last_trained_tick: u64,
207}
208
209impl Default for SkillProgress {
210 fn default() -> Self {
211 Self {
212 level: 0,
213 last_trained_tick: 0,
214 }
215 }
216}
217
218impl SkillProgress {
219 pub fn display_tier(&self) -> u16 {
221 (self.level / 100).min(10)
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
227#[serde(default)]
228pub struct ProgressionXp {
229 pub strength: f64,
230 pub dexterity: f64,
231 pub intelligence: f64,
232 pub stamina: f64,
233 pub vitality: f64,
234 pub wisdom: f64,
235 pub charisma: f64,
236 pub logging: f64,
237 pub mining: f64,
238 pub evocation: f64,
239 pub restoration: f64,
240 pub swords: f64,
241 pub archery: f64,
242 pub crafting: f64,
243 pub alchemy: f64,
244 pub cartography: f64,
245 #[serde(default)]
247 pub ability: std::collections::BTreeMap<String, f64>,
248}
249
250impl ProgressionXp {
251 pub fn bootstrap_new(baseline_display: u16, xp_base: f64, xp_growth: f64) -> Self {
253 let bootstrap = |display: f64| {
254 if display <= 1.0 {
255 0.0
256 } else {
257 xp_base * xp_growth.powf(display - 1.0)
258 }
259 };
260 let b = baseline_display as f64;
261 let primary = bootstrap(b);
262 Self {
263 strength: primary,
264 dexterity: primary,
265 intelligence: primary,
266 stamina: primary,
267 vitality: primary,
268 wisdom: primary,
269 charisma: primary,
270 ..Self::default()
271 }
272 }
273
274 pub fn is_empty(&self) -> bool {
275 self.strength == 0.0
276 && self.dexterity == 0.0
277 && self.intelligence == 0.0
278 && self.stamina == 0.0
279 && self.vitality == 0.0
280 && self.wisdom == 0.0
281 && self.charisma == 0.0
282 && self.logging == 0.0
283 && self.mining == 0.0
284 && self.evocation == 0.0
285 && self.restoration == 0.0
286 && self.swords == 0.0
287 && self.archery == 0.0
288 && self.crafting == 0.0
289 && self.alchemy == 0.0
290 && self.cartography == 0.0
291 && self.ability.is_empty()
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct AbilityMasteryHud {
298 pub ability_id: String,
299 pub tier: u16,
301 pub level: u16,
303 pub xp: f64,
305 pub xp_to_next: f64,
307}
308
309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311#[serde(default)]
312pub struct PlayerSkills {
313 pub logging: SkillProgress,
314 pub mining: SkillProgress,
315 pub evocation: SkillProgress,
316 #[serde(default)]
317 pub restoration: SkillProgress,
318 pub swords: SkillProgress,
319 #[serde(default)]
320 pub archery: SkillProgress,
321 pub crafting: SkillProgress,
322 #[serde(default)]
323 pub alchemy: SkillProgress,
324 pub cartography: SkillProgress,
325}
326
327impl Default for PlayerSkills {
328 fn default() -> Self {
329 Self {
330 logging: SkillProgress::default(),
331 mining: SkillProgress::default(),
332 evocation: SkillProgress::default(),
333 restoration: SkillProgress::default(),
334 swords: SkillProgress::default(),
335 archery: SkillProgress::default(),
336 crafting: SkillProgress::default(),
337 alchemy: SkillProgress::default(),
338 cartography: SkillProgress::default(),
339 }
340 }
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
345pub struct PlayerVitals {
346 pub health: f32,
347 pub health_max: f32,
348 pub mana: f32,
349 pub mana_max: f32,
350 pub stamina: f32,
351 pub stamina_max: f32,
352 #[serde(default = "default_survival_pool_max")]
353 pub hunger: f32,
354 #[serde(default = "default_survival_pool_max")]
355 pub hunger_max: f32,
356 #[serde(default = "default_survival_pool_max")]
357 pub thirst: f32,
358 #[serde(default = "default_survival_pool_max")]
359 pub thirst_max: f32,
360 #[serde(default)]
361 pub coins: u32,
362 #[serde(default)]
363 pub deaths: u32,
364 #[serde(default)]
365 pub life_state: LifeState,
366 #[serde(default)]
368 pub winded: bool,
369}
370
371fn default_survival_pool_max() -> f32 {
372 100.0
373}
374
375impl Default for PlayerVitals {
376 fn default() -> Self {
377 Self::from_attributes(PrimaryAttributes::default())
378 }
379}
380
381impl PlayerVitals {
382 pub fn from_attributes(attrs: PrimaryAttributes) -> Self {
389 let vit_d = PrimaryAttributes::display(attrs.vitality) as f32;
390 let int_d = PrimaryAttributes::display(attrs.intelligence) as f32;
391 let wis_d = PrimaryAttributes::display(attrs.wisdom) as f32;
392 let sta_d = PrimaryAttributes::display(attrs.stamina) as f32;
393
394 let health_max = 50.0 + vit_d * 2.0;
395 let stamina_max = 30.0 + sta_d * 1.4;
396 let mana_max = 25.0 + int_d * 1.1 + wis_d * 1.3;
397 let hunger_max = 100.0;
398 let thirst_max = 100.0;
399 Self {
400 health: health_max,
401 health_max,
402 mana: mana_max,
403 mana_max,
404 stamina: stamina_max,
405 stamina_max,
406 hunger: hunger_max,
407 hunger_max,
408 thirst: thirst_max,
409 thirst_max,
410 coins: 0,
411 deaths: 0,
412 life_state: LifeState::Alive,
413 winded: false,
414 }
415 }
416
417 pub fn legacy_maxima(attrs: PrimaryAttributes) -> (f32, f32, f32) {
419 (
420 attrs.vitality as f32 / 5.0,
421 attrs.stamina as f32 / 5.0,
422 (attrs.intelligence as f32 + attrs.wisdom as f32) / 20.0,
423 )
424 }
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
429#[serde(default)]
430pub struct StoredVitalsState {
431 pub health: f32,
432 pub mana: f32,
433 pub stamina: f32,
434 pub hunger: f32,
435 pub thirst: f32,
436 pub coins: u32,
437 pub deaths: u32,
438 pub life_state: LifeState,
439 #[serde(default)]
440 pub winded: bool,
441}
442
443impl StoredVitalsState {
444 pub fn from_live(v: &PlayerVitals) -> Self {
445 Self {
446 health: v.health,
447 mana: v.mana,
448 stamina: v.stamina,
449 hunger: v.hunger,
450 thirst: v.thirst,
451 coins: v.coins,
452 deaths: v.deaths,
453 life_state: v.life_state,
454 winded: v.winded,
455 }
456 }
457
458 pub fn is_pristine(&self) -> bool {
460 self.health == 0.0
461 && self.mana == 0.0
462 && self.stamina == 0.0
463 && self.hunger == 0.0
464 && self.thirst == 0.0
465 && self.coins == 0
466 && self.deaths == 0
467 && self.life_state == LifeState::Alive
468 }
469
470 pub fn apply_to(self, attrs: PrimaryAttributes) -> PlayerVitals {
471 if self.is_pristine() {
472 return PlayerVitals::from_attributes(attrs);
473 }
474 let fresh = PlayerVitals::from_attributes(attrs);
475 let (legacy_hp, legacy_sta, legacy_mana) = PlayerVitals::legacy_maxima(attrs);
476
477 let scale = |current: f32, legacy_max: f32, new_max: f32| {
478 if legacy_max > 0.0 && new_max > legacy_max * 1.05 && current >= legacy_max * 0.95 {
479 let ratio = (current / legacy_max).clamp(0.0, 1.0);
480 (new_max * ratio).min(new_max)
481 } else {
482 current.min(new_max)
483 }
484 };
485
486 let mut v = fresh;
487 v.health = scale(self.health, legacy_hp, fresh.health_max);
488 v.mana = scale(self.mana, legacy_mana, fresh.mana_max);
489 v.stamina = scale(self.stamina, legacy_sta, fresh.stamina_max);
490 v.hunger = self.hunger.min(v.hunger_max);
491 v.thirst = self.thirst.min(v.thirst_max);
492 v.coins = self.coins;
493 v.deaths = self.deaths;
494 v.life_state = self.life_state;
495 v.winded = self.winded;
496 v
497 }
498}
499
500impl Default for StoredVitalsState {
501 fn default() -> Self {
502 Self::from_live(&PlayerVitals::default())
503 }
504}
505
506pub fn humanize_snake_id(id: &str) -> String {
510 id.split('_')
511 .filter(|part| !part.is_empty())
512 .map(|part| {
513 let mut chars = part.chars();
514 match chars.next() {
515 None => String::new(),
516 Some(first) => first.to_uppercase().chain(chars).collect(),
517 }
518 })
519 .collect::<Vec<_>>()
520 .join(" ")
521}
522
523#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
525pub struct KnownAbility {
526 pub ability_id: String,
527 #[serde(default = "default_known_permanent")]
529 pub permanent: bool,
530 #[serde(default)]
532 pub expires_at_tick: Option<u64>,
533}
534
535fn default_known_permanent() -> bool {
536 true
537}
538
539impl KnownAbility {
540 pub fn permanent(ability_id: impl Into<String>) -> Self {
541 Self {
542 ability_id: ability_id.into(),
543 permanent: true,
544 expires_at_tick: None,
545 }
546 }
547
548 pub fn is_active(&self, tick: u64) -> bool {
549 if self.permanent {
550 return true;
551 }
552 match self.expires_at_tick {
553 Some(exp) => tick < exp,
554 None => false,
555 }
556 }
557}
558
559#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
561pub struct RotationPreset {
562 pub id: String,
563 pub label: String,
564 #[serde(default)]
565 pub abilities: Vec<String>,
566}
567
568impl RotationPreset {
569 pub fn melee_default(ability_id: impl Into<String>) -> Self {
570 let id = ability_id.into();
571 Self {
572 id: "melee".into(),
573 label: "Weapon".into(),
575 abilities: vec![id],
576 }
577 }
578
579 pub fn is_weapon_preset(&self) -> bool {
580 self.id == "melee"
581 }
582}
583
584#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
586pub struct StoredTargetSlot {
587 pub instance_id: Option<String>,
588 #[serde(default)]
589 pub preset_id: Option<String>,
590 #[serde(default)]
591 pub rotation_index: u32,
592 #[serde(default)]
593 pub auto_enabled: bool,
594}
595
596#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
597#[serde(default)]
598pub struct StoredCombatProfile {
599 pub combat_target_instance_id: Option<String>,
601 pub in_combat: bool,
602 pub last_combat_tick: u64,
603 pub last_attack_tick: u64,
604 pub cooldowns_until_tick: BTreeMap<String, u64>,
605 #[serde(default = "default_auto_attack")]
606 pub auto_attack_enabled: bool,
607 #[serde(default)]
609 pub mainhand_template_id: Option<String>,
610 #[serde(default)]
612 pub mainhand_instance_id: Option<Uuid>,
613 #[serde(default)]
615 pub offhand_template_id: Option<String>,
616 #[serde(default)]
618 pub offhand_instance_id: Option<Uuid>,
619 #[serde(default)]
622 pub worn: Vec<(BodySlot, ItemStack)>,
623 #[serde(default)]
625 pub rotation_presets: Vec<RotationPreset>,
626 #[serde(default)]
628 pub target_slots: Vec<StoredTargetSlot>,
629 #[serde(default)]
631 pub known_blueprint_ids: Vec<String>,
632 #[serde(default)]
634 pub keychain: Vec<ItemStack>,
635 #[serde(default)]
637 pub whisper_pouch: Vec<ItemStack>,
638 #[serde(default)]
640 pub known_abilities: Vec<KnownAbility>,
641 #[serde(default)]
643 pub hotbar: Vec<Option<String>>,
644 #[serde(default)]
646 pub abilities_schema_version: u32,
647 #[serde(default)]
649 pub bank_balance_copper: u64,
650}
651
652fn default_auto_attack() -> bool {
653 true
654}
655
656impl Default for StoredCombatProfile {
657 fn default() -> Self {
658 Self {
659 combat_target_instance_id: None,
660 in_combat: false,
661 last_combat_tick: 0,
662 last_attack_tick: 0,
663 cooldowns_until_tick: BTreeMap::new(),
664 auto_attack_enabled: true,
665 mainhand_template_id: None,
666 mainhand_instance_id: None,
667 offhand_template_id: None,
668 offhand_instance_id: None,
669 worn: Vec::new(),
670 rotation_presets: Vec::new(),
671 target_slots: Vec::new(),
672 known_blueprint_ids: Vec::new(),
673 keychain: Vec::new(),
674 whisper_pouch: Vec::new(),
675 known_abilities: Vec::new(),
676 hotbar: Vec::new(),
677 abilities_schema_version: 0,
678 bank_balance_copper: 0,
679 }
680 }
681}
682
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
685#[serde(rename_all = "snake_case")]
686pub enum CombatCueKind {
687 Dodge,
688 Block,
689 AttackTelegraph,
690}
691
692#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
693pub struct CombatCueView {
694 pub kind: CombatCueKind,
695 pub until_tick: Tick,
697 #[serde(default)]
699 pub start_tick: Tick,
700 #[serde(default)]
702 pub ability_id: Option<String>,
703 #[serde(default)]
705 pub telegraph_kind: Option<CombatFxKind>,
706 #[serde(default)]
707 pub origin_x: Option<f32>,
708 #[serde(default)]
709 pub origin_y: Option<f32>,
710 #[serde(default)]
711 pub origin_z: Option<f32>,
712 #[serde(default)]
713 pub end_x: Option<f32>,
714 #[serde(default)]
715 pub end_y: Option<f32>,
716 #[serde(default)]
717 pub end_z: Option<f32>,
718 #[serde(default)]
719 pub yaw: Option<f32>,
720 #[serde(default)]
721 pub reach_m: Option<f32>,
722 #[serde(default)]
723 pub arc_deg: Option<f32>,
724 #[serde(default)]
725 pub radius_m: Option<f32>,
726}
727
728impl CombatCueView {
729 pub fn timing(kind: CombatCueKind, until_tick: Tick, start_tick: Tick) -> Self {
731 Self {
732 kind,
733 until_tick,
734 start_tick,
735 ability_id: None,
736 telegraph_kind: None,
737 origin_x: None,
738 origin_y: None,
739 origin_z: None,
740 end_x: None,
741 end_y: None,
742 end_z: None,
743 yaw: None,
744 reach_m: None,
745 arc_deg: None,
746 radius_m: None,
747 }
748 }
749}
750
751#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
752pub struct EntityState {
753 pub id: EntityId,
754 pub transform: Transform,
755 #[serde(default)]
757 pub label: String,
758 #[serde(default)]
759 pub vitals: Option<PlayerVitals>,
760 #[serde(default)]
762 pub attributes: Option<PrimaryAttributes>,
763 #[serde(default)]
764 pub skills: Option<PlayerSkills>,
765 #[serde(default)]
767 pub inside_building: Option<String>,
768 #[serde(default)]
770 pub tile_id: Option<String>,
771 #[serde(default)]
773 pub paperdoll_ref: Option<String>,
774 #[serde(default = "default_draw_scale")]
776 pub draw_scale: f32,
777 #[serde(default)]
779 pub presentation_state: Option<String>,
780 #[serde(default)]
782 pub sprite_mode: Option<String>,
783 #[serde(default)]
785 pub progression_xp: Option<ProgressionXp>,
786 #[serde(default)]
788 pub combat_cues: Vec<CombatCueView>,
789 #[serde(default)]
791 pub statuses: Vec<StatusEffectHud>,
792}
793
794#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
796#[serde(rename_all = "snake_case")]
797pub enum ChatChannel {
798 Nearby,
800 Direct,
802 Whisper,
804 WhisperStone,
806}
807
808#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
810#[serde(rename_all = "snake_case")]
811pub enum ChatClarity {
812 #[default]
813 Clear,
814 Partial,
815 Heavy,
816}
817
818#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
819pub struct ChatMessage {
820 pub channel: ChatChannel,
821 pub from_entity: EntityId,
822 pub from_name: String,
823 pub text: String,
825 pub tick: Tick,
826 #[serde(default)]
828 pub to_entity: Option<EntityId>,
829 #[serde(default)]
830 pub clarity: ChatClarity,
831}
832
833#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
835pub enum Intent {
836 Move {
837 entity_id: EntityId,
838 forward: f32,
839 strafe: f32,
840 #[serde(default)]
842 vertical: f32,
843 #[serde(default)]
845 sprint: bool,
846 #[serde(default)]
848 sneak: bool,
849 seq: Seq,
850 },
851 Stop {
852 entity_id: EntityId,
853 seq: Seq,
854 },
855 Harvest {
856 entity_id: EntityId,
857 node_id: String,
858 seq: Seq,
859 },
860 Use {
861 entity_id: EntityId,
862 template_id: String,
863 seq: Seq,
864 },
865 UseGrant {
867 entity_id: EntityId,
868 grant_instance_id: Uuid,
869 target_instance_id: Uuid,
870 seq: Seq,
871 },
872 Say {
873 entity_id: EntityId,
874 channel: ChatChannel,
875 text: String,
876 #[serde(default)]
878 to_entity: Option<EntityId>,
879 seq: Seq,
880 },
881 Craft {
883 entity_id: EntityId,
884 blueprint_id: String,
885 #[serde(default)]
887 count: Option<u32>,
888 seq: Seq,
889 },
890 Interact {
892 entity_id: EntityId,
893 target_id: String,
894 seq: Seq,
895 },
896 ShopBuy {
898 entity_id: EntityId,
899 npc_id: String,
900 offer_id: String,
901 #[serde(default = "default_one")]
902 quantity: u32,
903 seq: Seq,
904 },
905 ShopSell {
907 entity_id: EntityId,
908 npc_id: String,
909 template_id: String,
910 #[serde(default = "default_one")]
911 quantity: u32,
912 seq: Seq,
913 },
914 ShopClose {
916 entity_id: EntityId,
917 npc_id: String,
918 seq: Seq,
919 },
920 TestDamage {
922 entity_id: EntityId,
923 amount: f32,
924 seq: Seq,
925 },
926 SetTarget {
928 entity_id: EntityId,
929 target_id: EntityId,
930 seq: Seq,
931 },
932 SetTargetSlot {
934 entity_id: EntityId,
935 slot_index: u8,
936 target_id: EntityId,
937 seq: Seq,
938 },
939 ClearTarget {
940 entity_id: EntityId,
941 seq: Seq,
942 },
943 ClearTargetSlot {
944 entity_id: EntityId,
945 slot_index: u8,
946 seq: Seq,
947 },
948 SetAutoAttack {
950 entity_id: EntityId,
951 slot_index: u8,
952 enabled: bool,
953 seq: Seq,
954 },
955 Attack {
957 entity_id: EntityId,
958 #[serde(default)]
959 target_id: Option<EntityId>,
960 #[serde(default)]
961 weapon_slot: Option<u32>,
962 seq: Seq,
963 },
964 Pickup {
966 entity_id: EntityId,
967 #[serde(default)]
968 drop_id: Option<String>,
969 seq: Seq,
970 },
971 Cast {
974 entity_id: EntityId,
975 ability_id: String,
976 target_id: EntityId,
977 #[serde(default)]
978 target_point: Option<AimPoint>,
979 seq: Seq,
980 },
981 BindActionSlot {
983 entity_id: EntityId,
984 slot_index: u8,
985 ability_id: String,
986 #[serde(default = "default_auto_attack")]
987 auto_enabled: bool,
988 seq: Seq,
989 },
990 UseActionSlot {
992 entity_id: EntityId,
993 slot_index: u8,
994 seq: Seq,
995 },
996 Dodge {
1000 entity_id: EntityId,
1001 #[serde(default)]
1003 forward: f32,
1004 #[serde(default)]
1006 strafe: f32,
1007 seq: Seq,
1008 },
1009 Lunge {
1011 entity_id: EntityId,
1012 #[serde(default)]
1014 forward: f32,
1015 #[serde(default)]
1017 strafe: f32,
1018 seq: Seq,
1019 },
1020 DirectionalJump {
1022 entity_id: EntityId,
1023 #[serde(default)]
1025 forward: f32,
1026 #[serde(default)]
1028 strafe: f32,
1029 seq: Seq,
1030 },
1031 Block {
1033 entity_id: EntityId,
1034 #[serde(default = "default_block_enabled")]
1035 enabled: bool,
1036 seq: Seq,
1037 },
1038 EquipMainhand {
1042 entity_id: EntityId,
1043 #[serde(default)]
1044 template_id: Option<String>,
1045 #[serde(default)]
1046 instance_id: Option<Uuid>,
1047 seq: Seq,
1048 },
1049 EquipOffhand {
1051 entity_id: EntityId,
1052 #[serde(default)]
1053 template_id: Option<String>,
1054 #[serde(default)]
1055 instance_id: Option<Uuid>,
1056 seq: Seq,
1057 },
1058 EquipWorn {
1061 entity_id: EntityId,
1062 slot: BodySlot,
1063 #[serde(default)]
1064 instance_id: Option<Uuid>,
1065 seq: Seq,
1066 },
1067 MoveItem {
1069 entity_id: EntityId,
1070 item_instance_id: Uuid,
1071 from: InventoryLocation,
1072 to: InventoryLocation,
1073 #[serde(default)]
1075 to_parent_instance_id: Option<Uuid>,
1076 #[serde(default)]
1078 quantity: Option<u32>,
1079 seq: Seq,
1080 },
1081 PlaceContainer {
1083 entity_id: EntityId,
1084 item_instance_id: Uuid,
1085 seq: Seq,
1086 },
1087 PickupContainer {
1089 entity_id: EntityId,
1090 container_id: String,
1091 seq: Seq,
1092 },
1093 MovePlacedContainer {
1095 entity_id: EntityId,
1096 container_id: String,
1097 x: f32,
1098 y: f32,
1099 seq: Seq,
1100 },
1101 SetContainerLocked {
1103 entity_id: EntityId,
1104 location: InventoryLocation,
1106 locked: bool,
1107 seq: Seq,
1108 },
1109 DropItem {
1111 entity_id: EntityId,
1112 item_instance_id: Uuid,
1113 from: InventoryLocation,
1114 seq: Seq,
1115 },
1116 DestroyItem {
1118 entity_id: EntityId,
1119 item_instance_id: Uuid,
1120 from: InventoryLocation,
1121 #[serde(default)]
1123 quantity: Option<u32>,
1124 seq: Seq,
1125 },
1126 RenameContainer {
1128 entity_id: EntityId,
1129 item_instance_id: Uuid,
1130 location: InventoryLocation,
1131 name: String,
1132 seq: Seq,
1133 },
1134 UpsertRotationPreset {
1136 entity_id: EntityId,
1137 preset: RotationPreset,
1138 seq: Seq,
1139 },
1140 DeleteRotationPreset {
1142 entity_id: EntityId,
1143 preset_id: String,
1144 seq: Seq,
1145 },
1146 AssignSlotPreset {
1148 entity_id: EntityId,
1149 slot_index: u8,
1150 preset_id: String,
1151 seq: Seq,
1152 },
1153 SetHotbarSlot {
1156 entity_id: EntityId,
1157 slot: u8,
1159 #[serde(default)]
1161 ability_id: Option<String>,
1162 seq: Seq,
1163 },
1164 AdvanceRotation {
1166 entity_id: EntityId,
1167 slot_index: u8,
1168 seq: Seq,
1169 },
1170 NpcTalkOpen {
1172 entity_id: EntityId,
1173 npc_id: String,
1174 #[serde(default)]
1176 quest_id: Option<String>,
1177 seq: Seq,
1178 },
1179 NpcTalkSay {
1181 entity_id: EntityId,
1182 npc_id: String,
1183 message: String,
1184 seq: Seq,
1185 },
1186 NpcTalkClose {
1188 entity_id: EntityId,
1189 npc_id: String,
1190 seq: Seq,
1191 },
1192 AcceptQuest {
1194 entity_id: EntityId,
1195 quest_id: String,
1196 seq: Seq,
1197 },
1198 WithdrawQuest {
1200 entity_id: EntityId,
1201 quest_id: String,
1202 seq: Seq,
1203 },
1204 TrackQuest {
1206 entity_id: EntityId,
1207 quest_id: String,
1208 seq: Seq,
1209 },
1210 QuestGiveItem {
1212 entity_id: EntityId,
1213 npc_id: String,
1214 template_id: String,
1215 #[serde(default = "default_one")]
1216 quantity: u32,
1217 seq: Seq,
1218 },
1219 HireWorker {
1221 entity_id: EntityId,
1222 def_id: String,
1223 wage_copper_per_interval: u32,
1224 #[serde(default)]
1225 lodging_container_id: Option<String>,
1226 #[serde(default)]
1227 job_yaml: Option<String>,
1228 seq: Seq,
1229 },
1230 DismissWorker {
1232 entity_id: EntityId,
1233 worker_instance_id: String,
1234 seq: Seq,
1235 },
1236 SetWorkerJob {
1238 entity_id: EntityId,
1239 worker_instance_id: String,
1240 job_yaml: String,
1241 seq: Seq,
1242 },
1243 AssignWorkerLodging {
1245 entity_id: EntityId,
1246 worker_instance_id: String,
1247 lodging_container_id: String,
1248 seq: Seq,
1249 },
1250 SetWorkerMode {
1252 entity_id: EntityId,
1253 worker_instance_id: String,
1254 mode: String,
1255 seq: Seq,
1256 },
1257 EquipWorkerItem {
1263 entity_id: EntityId,
1264 worker_instance_id: String,
1265 item_instance_id: uuid::Uuid,
1266 slot: String,
1267 seq: Seq,
1268 },
1269 GiveWorkerItem {
1272 entity_id: EntityId,
1273 worker_instance_id: String,
1274 item_instance_id: uuid::Uuid,
1275 #[serde(default)]
1276 quantity: Option<u32>,
1277 seq: Seq,
1278 },
1279 TakeWorkerItem {
1281 entity_id: EntityId,
1282 worker_instance_id: String,
1283 item_instance_id: uuid::Uuid,
1284 #[serde(default)]
1285 quantity: Option<u32>,
1286 seq: Seq,
1287 },
1288 RenameHiredWorker {
1290 entity_id: EntityId,
1291 worker_instance_id: String,
1292 name: String,
1293 seq: Seq,
1294 },
1295 RenamePropertyPlot {
1297 entity_id: EntityId,
1298 plot_id: Uuid,
1299 label: String,
1300 seq: Seq,
1301 },
1302 TeachWorkerBlueprint {
1304 entity_id: EntityId,
1305 worker_instance_id: String,
1306 blueprint_id: String,
1307 seq: Seq,
1308 },
1309 AttendHiredWorker {
1311 entity_id: EntityId,
1312 worker_instance_id: String,
1313 attending: bool,
1314 seq: Seq,
1315 },
1316 BuyPlot {
1318 entity_id: EntityId,
1319 zone_id: String,
1320 x0: f32,
1321 y0: f32,
1322 x1: f32,
1323 y1: f32,
1324 seq: Seq,
1325 },
1326 BuyPlotAllFree {
1328 entity_id: EntityId,
1329 zone_id: String,
1330 seq: Seq,
1331 },
1332 SellPlotToCrown {
1334 entity_id: EntityId,
1335 plot_id: Uuid,
1336 seq: Seq,
1337 },
1338 Cultivate {
1340 entity_id: EntityId,
1341 x: f32,
1343 y: f32,
1344 seq: Seq,
1345 },
1346 PlantSeeds {
1348 entity_id: EntityId,
1349 seed_template_id: String,
1350 quantity: u32,
1351 seq: Seq,
1352 },
1353 SetPlotFarmPublic {
1355 entity_id: EntityId,
1356 plot_id: Uuid,
1357 public: bool,
1358 #[serde(default)]
1359 public_tax_discount_bps: u32,
1360 seq: Seq,
1361 },
1362 PlotFarmAllowUpsert {
1364 entity_id: EntityId,
1365 plot_id: Uuid,
1366 #[serde(default)]
1368 character_id: Option<Uuid>,
1369 #[serde(default)]
1371 character_name: String,
1372 #[serde(default)]
1373 tax_discount_bps: u32,
1374 seq: Seq,
1375 },
1376 PlotFarmAllowRemove {
1378 entity_id: EntityId,
1379 plot_id: Uuid,
1380 character_id: Uuid,
1381 seq: Seq,
1382 },
1383 StartPlotBuild {
1386 entity_id: EntityId,
1387 plot_id: Uuid,
1388 wall_material_id: String,
1389 roof_material_id: String,
1390 seq: Seq,
1391 },
1392 CancelPlotBuild {
1393 entity_id: EntityId,
1394 seq: Seq,
1395 },
1396 SetDoorLocked {
1399 entity_id: EntityId,
1400 door_id: String,
1401 locked: bool,
1402 seq: Seq,
1403 },
1404 EnterBuildingDoor {
1407 entity_id: EntityId,
1408 door_id: String,
1409 seq: Seq,
1410 },
1411 ExitBuildingDoor {
1414 entity_id: EntityId,
1415 door_id: String,
1416 seq: Seq,
1417 },
1418 ConfirmInteriorEdit {
1420 entity_id: EntityId,
1421 building_id: String,
1422 rooms: Vec<InteriorRoomEdit>,
1423 room_doors: Vec<InteriorRoomDoorEdit>,
1424 seq: Seq,
1425 },
1426 CancelInteriorEdit {
1427 entity_id: EntityId,
1428 building_id: String,
1429 seq: Seq,
1430 },
1431 BankDeposit {
1433 entity_id: EntityId,
1434 npc_id: String,
1435 #[serde(default)]
1437 amount_copper: u64,
1438 seq: Seq,
1439 },
1440 BankWithdraw {
1442 entity_id: EntityId,
1443 npc_id: String,
1444 #[serde(default)]
1446 amount_copper: u64,
1447 seq: Seq,
1448 },
1449 BankClose {
1451 entity_id: EntityId,
1452 npc_id: String,
1453 seq: Seq,
1454 },
1455 BankTransfer {
1457 entity_id: EntityId,
1458 npc_id: String,
1459 #[serde(default)]
1461 to_character_id: Option<Uuid>,
1462 #[serde(default)]
1464 to_name: String,
1465 amount_copper: u64,
1467 seq: Seq,
1468 },
1469 StorageStore {
1471 entity_id: EntityId,
1472 npc_id: String,
1473 item_instance_id: Uuid,
1474 #[serde(default)]
1475 quantity: Option<u32>,
1476 seq: Seq,
1477 },
1478 StorageTake {
1480 entity_id: EntityId,
1481 npc_id: String,
1482 item_instance_id: Uuid,
1483 #[serde(default)]
1484 quantity: Option<u32>,
1485 seq: Seq,
1486 },
1487 StorageShip {
1489 entity_id: EntityId,
1490 npc_id: String,
1491 dest_building_id: String,
1492 item_instance_id: Uuid,
1493 #[serde(default)]
1494 quantity: Option<u32>,
1495 seq: Seq,
1496 },
1497 StorageClose {
1499 entity_id: EntityId,
1500 npc_id: String,
1501 seq: Seq,
1502 },
1503 MarketList {
1506 entity_id: EntityId,
1507 npc_id: String,
1508 source: GoodsLocation,
1509 item_instance_id: Uuid,
1510 #[serde(default)]
1511 quantity: Option<u32>,
1512 unit_price_copper: u64,
1513 #[serde(default)]
1515 npc_price: bool,
1516 seq: Seq,
1517 },
1518 MarketReprice {
1520 entity_id: EntityId,
1521 npc_id: String,
1522 listing_id: Uuid,
1523 unit_price_copper: u64,
1524 seq: Seq,
1525 },
1526 MarketDelist {
1528 entity_id: EntityId,
1529 npc_id: String,
1530 listing_id: Uuid,
1531 dest: GoodsLocation,
1532 seq: Seq,
1533 },
1534 MarketBuy {
1536 entity_id: EntityId,
1537 npc_id: String,
1538 listing_id: Uuid,
1539 #[serde(default = "default_one")]
1540 quantity: u32,
1541 dest: GoodsLocation,
1542 seq: Seq,
1543 },
1544 MarketClose {
1546 entity_id: EntityId,
1547 npc_id: String,
1548 seq: Seq,
1549 },
1550 TradeRequest {
1552 entity_id: EntityId,
1553 peer_entity_id: EntityId,
1554 seq: Seq,
1555 },
1556 TradeRespond {
1558 entity_id: EntityId,
1559 peer_entity_id: EntityId,
1560 accept: bool,
1561 seq: Seq,
1562 },
1563 TradePresent {
1565 entity_id: EntityId,
1566 item_instance_id: Uuid,
1567 #[serde(default)]
1568 quantity: Option<u32>,
1569 seq: Seq,
1570 },
1571 TradeUnpresent {
1573 entity_id: EntityId,
1574 item_instance_id: Uuid,
1575 seq: Seq,
1576 },
1577 TradeSetReady {
1579 entity_id: EntityId,
1580 ready: bool,
1581 seq: Seq,
1582 },
1583 TradeCancel {
1585 entity_id: EntityId,
1586 seq: Seq,
1587 },
1588 DestroyWhisperStone {
1590 entity_id: EntityId,
1591 item_instance_id: Uuid,
1592 seq: Seq,
1593 },
1594 StowWhisperStone {
1596 entity_id: EntityId,
1597 item_instance_id: Uuid,
1598 seq: Seq,
1599 },
1600 DeliverWorkerToNearestStorage {
1603 entity_id: EntityId,
1604 worker_instance_id: String,
1605 seq: Seq,
1606 },
1607 CancelWorkerDelivery {
1609 entity_id: EntityId,
1610 worker_instance_id: String,
1611 seq: Seq,
1612 },
1613}
1614
1615fn default_block_enabled() -> bool {
1616 true
1617}
1618
1619#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1621pub struct StatusEffectHud {
1622 pub effect_id: String,
1623 pub label: String,
1624 #[serde(default)]
1625 pub polarity: String,
1626 #[serde(default)]
1627 pub icon_tile_id: Option<String>,
1628 #[serde(default)]
1630 pub dot_color: Option<String>,
1631 #[serde(default)]
1633 pub remaining_sec: Option<f32>,
1634 #[serde(default = "default_stack_count")]
1636 pub stack_count: u8,
1637}
1638
1639fn default_stack_count() -> u8 {
1640 1
1641}
1642
1643#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1645pub struct CombatTargetHud {
1646 pub entity_id: EntityId,
1647 #[serde(default)]
1648 pub label: String,
1649 #[serde(default)]
1650 pub level: u32,
1651 pub health: f32,
1652 pub health_max: f32,
1653 #[serde(default)]
1654 pub life_state: LifeState,
1655 #[serde(default)]
1656 pub distance_m: f32,
1657 #[serde(default)]
1658 pub statuses: Vec<StatusEffectHud>,
1659}
1660
1661#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1663#[serde(rename_all = "snake_case")]
1664pub enum TimedChannelKind {
1665 #[default]
1666 Cultivate,
1667 Plant,
1668 Harvest,
1669 Build,
1671 Craft,
1673}
1674
1675#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1677pub struct TimedChannelHud {
1678 #[serde(default)]
1679 pub label: String,
1680 #[serde(default)]
1681 pub channel: TimedChannelKind,
1682 #[serde(default)]
1683 pub cell_x: i32,
1684 #[serde(default)]
1685 pub cell_y: i32,
1686 #[serde(default)]
1688 pub x0: f32,
1689 #[serde(default)]
1690 pub y0: f32,
1691 #[serde(default)]
1692 pub x1: f32,
1693 #[serde(default)]
1694 pub y1: f32,
1695 #[serde(default)]
1696 pub ticks_remaining: u64,
1697 #[serde(default)]
1698 pub ticks_total: u64,
1699}
1700
1701impl TimedChannelHud {
1702 pub fn has_footprint(&self) -> bool {
1704 self.x1 > self.x0 && self.y1 > self.y0
1705 }
1706}
1707
1708#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1710#[serde(rename_all = "snake_case")]
1711pub enum PlotBuildMaterialSource {
1712 #[default]
1713 None,
1714 TownStorage,
1715 NearbyContainer,
1716}
1717
1718#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1720pub struct BuildingMaterialView {
1721 pub id: String,
1722 pub display_name: String,
1723 #[serde(default)]
1724 pub can_wall: bool,
1725 #[serde(default)]
1726 pub can_roof: bool,
1727 #[serde(default)]
1728 pub wall_set: String,
1729 #[serde(default)]
1730 pub roof_set: String,
1731 #[serde(default = "default_material_tick_mult")]
1732 pub tick_mult: f32,
1733 #[serde(default)]
1734 pub wall_bom: Vec<BuildingBomLineView>,
1735 #[serde(default)]
1736 pub roof_bom: Vec<BuildingBomLineView>,
1737}
1738
1739fn default_material_tick_mult() -> f32 {
1740 1.0
1741}
1742
1743#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1744pub struct BuildingBomLineView {
1745 pub template_id: String,
1746 #[serde(default)]
1747 pub display_name: String,
1748 pub per_m2: f32,
1749}
1750
1751#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1753pub struct PlotBuildStockView {
1754 pub template_id: String,
1755 #[serde(default)]
1756 pub display_name: String,
1757 pub quantity: u32,
1758}
1759
1760#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1762pub struct PlotBuildOfferHud {
1763 pub plot_id: Uuid,
1764 #[serde(default)]
1765 pub pad_width_m: f32,
1766 #[serde(default)]
1767 pub pad_depth_m: f32,
1768 #[serde(default)]
1769 pub pad_ok: bool,
1770 #[serde(default)]
1771 pub pad_error: String,
1772 #[serde(default)]
1773 pub source: PlotBuildMaterialSource,
1774 #[serde(default)]
1775 pub source_label: String,
1776 #[serde(default)]
1777 pub available: Vec<PlotBuildStockView>,
1778 #[serde(default)]
1779 pub base_ticks: u32,
1780 #[serde(default)]
1781 pub tick_per_m2: u32,
1782}
1783
1784#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1786pub struct CastProgressHud {
1787 #[serde(default)]
1788 pub ability_id: String,
1789 #[serde(default)]
1790 pub ability_label: String,
1791 #[serde(default)]
1792 pub ticks_remaining: u64,
1793 #[serde(default)]
1794 pub ticks_total: u64,
1795}
1796
1797#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1799pub struct AbilityCooldownHud {
1800 #[serde(default)]
1801 pub ability_id: String,
1802 #[serde(default)]
1803 pub label: String,
1804 #[serde(default)]
1805 pub cd_ticks: u64,
1806 #[serde(default)]
1807 pub cd_total_ticks: u64,
1808}
1809
1810#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1812pub struct CombatSlotHud {
1813 pub slot_index: u8,
1814 #[serde(default)]
1815 pub target_entity_id: Option<EntityId>,
1816 #[serde(default)]
1817 pub target_label: Option<String>,
1818 #[serde(default)]
1819 pub target: Option<CombatTargetHud>,
1820 #[serde(default)]
1821 pub preset_id: Option<String>,
1822 #[serde(default)]
1823 pub preset_label: Option<String>,
1824 #[serde(default)]
1825 pub rotation: Vec<String>,
1826 #[serde(default)]
1827 pub rotation_index: u32,
1828 #[serde(default)]
1829 pub next_ability_id: Option<String>,
1830 #[serde(default)]
1831 pub auto_enabled: bool,
1832}
1833
1834#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1836pub struct DefensePieceHud {
1837 pub slot: BodySlot,
1838 pub label: String,
1839 pub template_id: String,
1840 #[serde(default)]
1841 pub armor_physical: f32,
1842 #[serde(default)]
1843 pub resists: Vec<(String, f32)>,
1844}
1845
1846impl Default for DefensePieceHud {
1847 fn default() -> Self {
1848 Self {
1849 slot: BodySlot::Head,
1850 label: String::new(),
1851 template_id: String::new(),
1852 armor_physical: 0.0,
1853 resists: Vec::new(),
1854 }
1855 }
1856}
1857
1858#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1860pub struct DefenseHud {
1861 pub armor_physical: f32,
1862 pub vitality_contribution: f32,
1863 pub total_mitigation_rating: f32,
1864 pub estimated_physical_dr: f32,
1866 #[serde(default)]
1867 pub resists: Vec<(String, f32)>,
1868 #[serde(default)]
1869 pub pieces: Vec<DefensePieceHud>,
1870}
1871
1872#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1874pub struct CombatHud {
1875 pub in_combat: bool,
1876 pub auto_attack: bool,
1878 pub has_los: bool,
1879 pub attack_cd_ticks: u64,
1880 #[serde(default)]
1881 pub ability_id: String,
1882 #[serde(default)]
1883 pub target_entity_id: Option<EntityId>,
1884 #[serde(default)]
1885 pub target_label: Option<String>,
1886 #[serde(default)]
1887 pub max_target_slots: u8,
1888 #[serde(default)]
1889 pub slots: Vec<CombatSlotHud>,
1890 #[serde(default)]
1891 pub rotation_presets: Vec<RotationPreset>,
1892 #[serde(default)]
1893 pub gcd_ticks: u64,
1894 #[serde(default)]
1895 pub mainhand_template_id: Option<String>,
1896 #[serde(default)]
1897 pub mainhand_label: Option<String>,
1898 #[serde(default)]
1900 pub mainhand_instance_id: Option<Uuid>,
1901 #[serde(default)]
1902 pub offhand_template_id: Option<String>,
1903 #[serde(default)]
1904 pub offhand_label: Option<String>,
1905 #[serde(default)]
1907 pub offhand_instance_id: Option<Uuid>,
1908 #[serde(default)]
1910 pub mainhand_hand_slots: u8,
1911 #[serde(default)]
1913 pub worn: Vec<(BodySlot, ItemStack)>,
1914 #[serde(default)]
1916 pub defense: Option<DefenseHud>,
1917 #[serde(default)]
1918 pub carry_mass: f32,
1919 #[serde(default)]
1920 pub carry_mass_max: f32,
1921 #[serde(default)]
1922 pub encumbrance: EncumbranceState,
1923 #[serde(default)]
1925 pub keychain: Vec<ItemStack>,
1926 #[serde(default)]
1928 pub whisper_pouch: Vec<ItemStack>,
1929 #[serde(default)]
1930 pub target: Option<CombatTargetHud>,
1931 #[serde(default)]
1932 pub cast: Option<CastProgressHud>,
1933 #[serde(default)]
1935 pub timed_channel: Option<TimedChannelHud>,
1936 #[serde(default)]
1938 pub plot_build: Option<PlotBuildOfferHud>,
1939 #[serde(default)]
1940 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1941 #[serde(default)]
1942 pub blocking_active: bool,
1943 #[serde(default)]
1945 pub progression_xp: Option<ProgressionXp>,
1946 #[serde(default)]
1947 pub progression_baseline: u16,
1948 #[serde(default)]
1949 pub progression_xp_base: f64,
1950 #[serde(default)]
1951 pub progression_xp_growth: f64,
1952 #[serde(default)]
1953 pub attributes: Option<PrimaryAttributes>,
1954 #[serde(default)]
1955 pub skills: Option<PlayerSkills>,
1956 #[serde(default)]
1958 pub statuses: Vec<StatusEffectHud>,
1959 #[serde(default)]
1961 pub known_abilities: Vec<String>,
1962 #[serde(default)]
1964 pub ability_meta: Vec<AbilityMetaHud>,
1965 #[serde(default)]
1967 pub ability_mastery: Vec<AbilityMasteryHud>,
1968 #[serde(default)]
1971 pub hotbar: Vec<Option<String>>,
1972 #[serde(default)]
1974 pub max_abilities_per_rotation: u8,
1975 #[serde(default)]
1977 pub move_speed_mps: f32,
1978 #[serde(default)]
1980 pub move_speed_mult: f32,
1981}
1982
1983#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1985pub struct AbilityMetaHud {
1986 pub id: String,
1987 #[serde(default = "default_aim_mode_entity")]
1989 pub aim_mode: String,
1990 #[serde(default)]
1991 pub blast_radius_m: f32,
1992 #[serde(default)]
1993 pub allows_self: bool,
1994 #[serde(default)]
1995 pub is_heal: bool,
1996 #[serde(default = "default_auto_rotation_eligible")]
1999 pub auto_rotation_eligible: bool,
2000}
2001
2002fn default_auto_rotation_eligible() -> bool {
2003 true
2004}
2005
2006fn default_aim_mode_entity() -> String {
2007 "entity".into()
2008}
2009
2010pub const HOTBAR_ITEM_PREFIX: &str = "item:";
2012
2013pub fn hotbar_consumable_binding(template_id: &str) -> String {
2015 format!("{HOTBAR_ITEM_PREFIX}{}", template_id.trim())
2016}
2017
2018pub fn hotbar_consumable_template(binding: &str) -> Option<&str> {
2020 binding
2021 .strip_prefix(HOTBAR_ITEM_PREFIX)
2022 .map(str::trim)
2023 .filter(|id| !id.is_empty())
2024}
2025
2026pub fn hotbar_binding_is_consumable(binding: &str) -> bool {
2028 hotbar_consumable_template(binding).is_some()
2029}
2030
2031#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2033#[serde(rename_all = "snake_case")]
2034pub enum CombatFxKind {
2035 MeleeArc,
2036 Cone,
2037 Sphere,
2038 Beam,
2039 HitMarker,
2040}
2041
2042#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2044#[serde(rename_all = "snake_case")]
2045pub enum CombatFxHitOutcome {
2046 #[default]
2047 Hit,
2048 Blocked,
2049 Miss,
2050 Glance,
2051}
2052
2053#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2055pub struct CombatFxHit {
2056 pub entity_id: EntityId,
2057 pub x: f32,
2058 pub y: f32,
2059 pub z: f32,
2060 #[serde(default)]
2061 pub outcome: CombatFxHitOutcome,
2062}
2063
2064#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2066pub struct CombatFx {
2067 pub id: u64,
2068 pub kind: CombatFxKind,
2069 pub ability_id: String,
2070 pub caster_id: EntityId,
2071 pub origin_x: f32,
2072 pub origin_y: f32,
2073 pub origin_z: f32,
2074 #[serde(default)]
2075 pub end_x: Option<f32>,
2076 #[serde(default)]
2077 pub end_y: Option<f32>,
2078 #[serde(default)]
2079 pub end_z: Option<f32>,
2080 #[serde(default)]
2081 pub yaw: Option<f32>,
2082 #[serde(default)]
2083 pub reach_m: Option<f32>,
2084 #[serde(default)]
2085 pub arc_deg: Option<f32>,
2086 #[serde(default)]
2087 pub radius_m: Option<f32>,
2088 #[serde(default)]
2089 pub hits: Vec<CombatFxHit>,
2090 pub until_tick: u64,
2092 #[serde(default)]
2093 pub damage_type: String,
2094}
2095
2096#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2098pub struct GroundHazardView {
2099 pub x: f32,
2100 pub y: f32,
2101 pub z: f32,
2102 pub radius_m: f32,
2103 pub expires_at_tick: u64,
2104 #[serde(default)]
2105 pub damage_type: String,
2106}
2107
2108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2110#[serde(rename_all = "snake_case")]
2111pub enum WorkerModeView {
2112 Companion,
2113 Defender,
2114 JobLoop,
2115 Idle,
2118}
2119
2120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2122#[serde(rename_all = "snake_case")]
2123pub enum WorkerStateView {
2124 Idle,
2125 Traveling,
2126 Working,
2127 Resting,
2128 Waiting,
2129 Strike,
2130 Dismissed,
2131}
2132
2133#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
2135pub struct WorkerVitalsSummary {
2136 pub health_pct: f32,
2137 pub stamina_pct: f32,
2138 #[serde(default)]
2139 pub mana_pct: f32,
2140 #[serde(default)]
2141 pub hunger_pct: f32,
2142 #[serde(default)]
2143 pub thirst_pct: f32,
2144}
2145
2146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2148#[serde(rename_all = "snake_case")]
2149pub enum WorkerRouteKindView {
2150 #[default]
2151 HarvestLoop,
2152 Ordered,
2153}
2154
2155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2157pub struct WorkerRouteView {
2158 #[serde(default)]
2159 pub kind: WorkerRouteKindView,
2160 #[serde(default)]
2161 pub lodging_container_id: Option<String>,
2162 #[serde(default)]
2164 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
2165 #[serde(default)]
2167 pub harvest_nodes: Vec<String>,
2168 #[serde(default = "default_route_carry_ratio")]
2169 pub carry_return_ratio: f32,
2170 #[serde(default)]
2172 pub stops: Vec<WorkerRouteStopView>,
2173}
2174
2175fn default_route_carry_ratio() -> f32 {
2176 0.90
2177}
2178
2179fn default_true_view() -> bool {
2180 true
2181}
2182
2183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2185pub struct WorkerWithdrawItemView {
2186 pub template: String,
2187 #[serde(default)]
2189 pub qty: u32,
2190 #[serde(default)]
2192 pub all: bool,
2193}
2194
2195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2196pub struct WorkerRouteWaypointView {
2197 pub x: f32,
2198 pub y: f32,
2199 pub z: f32,
2200}
2201
2202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2211#[serde(rename_all = "snake_case")]
2212pub enum WorkerRouteStopView {
2213 Waypoint {
2214 x: f32,
2215 y: f32,
2216 #[serde(default)]
2217 z: f32,
2218 },
2219 HarvestNode {
2220 node_id: String,
2221 },
2222 DepositAt {
2223 container_id: String,
2224 #[serde(default)]
2225 filter: Option<Vec<String>>,
2226 },
2227 TradeWith {
2228 #[serde(default)]
2229 npc_id: Option<String>,
2230 template: String,
2231 #[serde(default = "default_true_view")]
2232 sell_all: bool,
2233 },
2234 ListOnMarket {
2236 template: String,
2237 #[serde(default = "default_true_view")]
2238 list_all: bool,
2239 #[serde(default)]
2240 hall_id: Option<String>,
2241 },
2242 WithdrawFrom {
2243 container_id: String,
2244 items: Vec<WorkerWithdrawItemView>,
2245 },
2246 CraftAt {
2247 device: String,
2248 blueprint: String,
2249 #[serde(default)]
2250 qty: Option<u32>,
2251 },
2252 CultivatePlot {
2253 plot_id: uuid::Uuid,
2254 },
2255 PlantPlot {
2256 plot_id: uuid::Uuid,
2257 seed_template: String,
2258 },
2259 HarvestPlot {
2260 plot_id: uuid::Uuid,
2261 },
2262 RestIfNeeded,
2263 Wait {
2264 #[serde(default)]
2265 wait_ticks: u64,
2266 },
2267}
2268
2269#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2271#[serde(rename_all = "snake_case")]
2272pub enum LedgerCategory {
2273 Workers,
2274 Hire,
2275 Train,
2276 ShopBuy,
2277 Taxes,
2278 WorkerSales,
2279 TraderSales,
2280 BankDeposit,
2281 BankWithdraw,
2282 BankTransferOut,
2283 BankTransferIn,
2284 BankTransferFee,
2285 StorageShipFee,
2286 PropertyBuy,
2288 PropertySell,
2290 TaxShare,
2292 MarketBuy,
2294 MarketSell,
2296 Other,
2297}
2298
2299impl LedgerCategory {
2300 pub fn as_str(self) -> &'static str {
2301 match self {
2302 Self::Workers => "workers",
2303 Self::Hire => "hire",
2304 Self::Train => "train",
2305 Self::ShopBuy => "shop_buy",
2306 Self::Taxes => "taxes",
2307 Self::WorkerSales => "worker_sales",
2308 Self::TraderSales => "trader_sales",
2309 Self::BankDeposit => "bank_deposit",
2310 Self::BankWithdraw => "bank_withdraw",
2311 Self::BankTransferOut => "bank_transfer_out",
2312 Self::BankTransferIn => "bank_transfer_in",
2313 Self::BankTransferFee => "bank_transfer_fee",
2314 Self::StorageShipFee => "storage_ship_fee",
2315 Self::PropertyBuy => "property_buy",
2316 Self::PropertySell => "property_sell",
2317 Self::TaxShare => "tax_share",
2318 Self::MarketBuy => "market_buy",
2319 Self::MarketSell => "market_sell",
2320 Self::Other => "other",
2321 }
2322 }
2323}
2324
2325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2326pub struct LedgerEntryView {
2327 pub id: uuid::Uuid,
2328 pub game_day: u64,
2329 pub signed_copper: i64,
2330 pub category: LedgerCategory,
2331 #[serde(default)]
2332 pub label: String,
2333}
2334
2335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2336pub struct LedgerPeriodTotals {
2337 #[serde(default)]
2339 pub expenses: std::collections::HashMap<String, u64>,
2340 #[serde(default)]
2342 pub income: std::collections::HashMap<String, u64>,
2343 pub expense_copper: u64,
2344 pub income_copper: u64,
2345 pub cash_flow_copper: i64,
2347}
2348
2349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2350pub struct PlayerLedgerView {
2351 pub current_game_day: u64,
2352 #[serde(default)]
2353 pub period_day: LedgerPeriodTotals,
2354 #[serde(default)]
2355 pub period_week: LedgerPeriodTotals,
2356 #[serde(default)]
2357 pub period_month: LedgerPeriodTotals,
2358 #[serde(default)]
2359 pub period_lifetime: LedgerPeriodTotals,
2360 #[serde(default)]
2361 pub recent: Vec<LedgerEntryView>,
2362 #[serde(default)]
2364 pub wealth_on_person_copper: u64,
2365 #[serde(default)]
2367 pub wealth_in_storage_copper: u64,
2368 #[serde(default)]
2370 pub wealth_in_bank_copper: u64,
2371 #[serde(default)]
2373 pub wealth_total_copper: u64,
2374 #[serde(default)]
2376 pub wealth_in_property_copper: u64,
2377 #[serde(default)]
2379 pub wealth_net_worth_copper: u64,
2380 #[serde(default)]
2382 pub property_assets: Vec<PropertyAssetView>,
2383 #[serde(default)]
2385 pub property_market_nearby: Vec<PropertyMarketCompView>,
2386 #[serde(default)]
2388 pub live_expense_per_interval_copper: u64,
2389 #[serde(default)]
2391 pub live_income_route_est_per_loop_copper: u64,
2392 #[serde(default)]
2394 pub live_income_avg_per_interval_copper: u64,
2395 #[serde(default)]
2397 pub live_income_avg_window_intervals: u32,
2398 #[serde(default)]
2400 pub live_net_avg_per_interval_copper: i64,
2401}
2402
2403#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2405pub struct PropertyAssetView {
2406 pub plot_id: Uuid,
2407 pub label: String,
2409 pub zone_id: String,
2410 #[serde(default)]
2411 pub zone_label: Option<String>,
2412 pub area_m2: f32,
2413 pub purchase_basis_copper: u64,
2415 pub upkeep_copper_per_day: u64,
2416}
2417
2418#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2420pub struct PropertyMarketCompView {
2421 pub day: u64,
2422 pub zone_id: String,
2423 #[serde(default)]
2424 pub zone_label: Option<String>,
2425 pub area_m2: f32,
2426 pub price_copper: u64,
2427 pub price_per_m2_copper: u64,
2429 pub kind: String,
2431 pub distance_m: f32,
2433}
2434
2435#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2437#[serde(rename_all = "snake_case")]
2438pub enum AnalyticsMetric {
2439 NpcKill,
2440 WildlifeKill,
2441 Harvest,
2442 QuestComplete,
2443 QuestAccept,
2444 QuestAbandon,
2445 PlayerDeath,
2446 Craft,
2447 WorkerHire,
2448 WorkerDismiss,
2449 WorkerTeach,
2450 NpcTalk,
2451 ShopBuy,
2452 ShopSell,
2453 PlaceContainer,
2454 PickupContainer,
2455 PickupDrop,
2456 ConsumableUse,
2457 AbilityUse,
2458 DistanceWalkedM,
2459 DoorUse,
2460 BuildingEnter,
2461}
2462
2463impl AnalyticsMetric {
2464 pub fn as_str(self) -> &'static str {
2465 match self {
2466 Self::NpcKill => "npc_kill",
2467 Self::WildlifeKill => "wildlife_kill",
2468 Self::Harvest => "harvest",
2469 Self::QuestComplete => "quest_complete",
2470 Self::QuestAccept => "quest_accept",
2471 Self::QuestAbandon => "quest_abandon",
2472 Self::PlayerDeath => "player_death",
2473 Self::Craft => "craft",
2474 Self::WorkerHire => "worker_hire",
2475 Self::WorkerDismiss => "worker_dismiss",
2476 Self::WorkerTeach => "worker_teach",
2477 Self::NpcTalk => "npc_talk",
2478 Self::ShopBuy => "shop_buy",
2479 Self::ShopSell => "shop_sell",
2480 Self::PlaceContainer => "place_container",
2481 Self::PickupContainer => "pickup_container",
2482 Self::PickupDrop => "pickup_drop",
2483 Self::ConsumableUse => "consumable_use",
2484 Self::AbilityUse => "ability_use",
2485 Self::DistanceWalkedM => "distance_walked_m",
2486 Self::DoorUse => "door_use",
2487 Self::BuildingEnter => "building_enter",
2488 }
2489 }
2490
2491 pub fn from_str_key(s: &str) -> Option<Self> {
2492 Some(match s {
2493 "npc_kill" => Self::NpcKill,
2494 "wildlife_kill" => Self::WildlifeKill,
2495 "harvest" => Self::Harvest,
2496 "quest_complete" => Self::QuestComplete,
2497 "quest_accept" => Self::QuestAccept,
2498 "quest_abandon" => Self::QuestAbandon,
2499 "player_death" => Self::PlayerDeath,
2500 "craft" => Self::Craft,
2501 "worker_hire" => Self::WorkerHire,
2502 "worker_dismiss" => Self::WorkerDismiss,
2503 "worker_teach" => Self::WorkerTeach,
2504 "npc_talk" => Self::NpcTalk,
2505 "shop_buy" => Self::ShopBuy,
2506 "shop_sell" => Self::ShopSell,
2507 "place_container" => Self::PlaceContainer,
2508 "pickup_container" => Self::PickupContainer,
2509 "pickup_drop" => Self::PickupDrop,
2510 "consumable_use" => Self::ConsumableUse,
2511 "ability_use" => Self::AbilityUse,
2512 "distance_walked_m" => Self::DistanceWalkedM,
2513 "door_use" => Self::DoorUse,
2514 "building_enter" => Self::BuildingEnter,
2515 _ => return None,
2516 })
2517 }
2518}
2519
2520#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2521pub struct CareerMetricRow {
2522 pub subject_id: String,
2523 pub amount: u64,
2524}
2525
2526#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2528pub struct PlayerCareerView {
2529 pub current_game_day: u64,
2530 #[serde(default)]
2531 pub kills: Vec<CareerMetricRow>,
2532 #[serde(default)]
2533 pub harvests: Vec<CareerMetricRow>,
2534 pub quests_completed: u64,
2535 #[serde(default)]
2536 pub crafts: Vec<CareerMetricRow>,
2537 pub deaths: u64,
2538 pub npc_talks: u64,
2539 pub shop_buys: u64,
2540 pub shop_sells: u64,
2541 pub distance_m: u64,
2542 #[serde(default)]
2543 pub other: Vec<CareerMetricRow>,
2544}
2545
2546#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2551pub struct WorkerEquipmentView {
2552 #[serde(default)]
2553 pub mainhand: Option<ItemStack>,
2554 #[serde(default)]
2555 pub offhand: Option<ItemStack>,
2556 #[serde(default)]
2557 pub worn: Vec<(BodySlot, ItemStack)>,
2558}
2559
2560#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2562pub struct HiredWorkerView {
2563 pub instance_id: String,
2564 pub entity_id: EntityId,
2565 pub def_id: String,
2566 pub label: String,
2568 pub x: f32,
2569 pub y: f32,
2570 pub z: f32,
2571 pub mode: WorkerModeView,
2572 pub state: WorkerStateView,
2573 #[serde(default)]
2574 pub step_label: String,
2575 pub vitals: WorkerVitalsSummary,
2576 #[serde(default)]
2577 pub carry_pct: f32,
2578 #[serde(default)]
2579 pub last_error: Option<String>,
2580 pub wage_copper_per_interval: u32,
2581 #[serde(default)]
2583 pub effective_wage_copper: u32,
2584 #[serde(default)]
2586 pub wage_meters_walked: f32,
2587 #[serde(default)]
2589 pub lodging_container_id: Option<String>,
2590 #[serde(default)]
2592 pub route: Option<WorkerRouteView>,
2593 #[serde(default)]
2596 pub route_stop_index: Option<u32>,
2597 #[serde(default)]
2599 pub known_blueprint_ids: Vec<String>,
2600 #[serde(default = "default_worker_view_level")]
2602 pub level: u32,
2603 #[serde(default)]
2605 pub worker_xp: f64,
2606 #[serde(default)]
2608 pub inventory: Vec<ItemStack>,
2609 #[serde(default)]
2611 pub equipment: WorkerEquipmentView,
2612 #[serde(default)]
2615 pub issue_hint: Option<String>,
2616}
2617
2618fn default_worker_view_level() -> u32 {
2619 1
2620}
2621
2622#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2624pub struct TickDelta {
2625 pub tick: Tick,
2626 pub entities: Vec<EntityState>,
2627 #[serde(default)]
2628 pub resource_nodes: Vec<ResourceNodeView>,
2629 #[serde(default)]
2630 pub buildings: Vec<BuildingView>,
2631 #[serde(default)]
2632 pub doors: Vec<DoorView>,
2633 #[serde(default)]
2634 pub npcs: Vec<NpcView>,
2635 #[serde(default)]
2637 pub inventory: Vec<ItemStack>,
2638 #[serde(default)]
2639 pub blueprints: Vec<BlueprintView>,
2640 #[serde(default)]
2642 pub building_materials: Vec<BuildingMaterialView>,
2643 #[serde(default)]
2644 pub world_clock: WorldClock,
2645 #[serde(default)]
2646 pub ground_drops: Vec<GroundDropView>,
2647 #[serde(default)]
2648 pub placed_containers: Vec<PlacedContainerView>,
2649 #[serde(default)]
2650 pub combat: Option<CombatHud>,
2651 #[serde(default)]
2652 pub interior_map: Option<InteriorMapView>,
2653 #[serde(default)]
2654 pub quest_log: Vec<QuestLogEntry>,
2655 #[serde(default)]
2656 pub hired_workers: Vec<HiredWorkerView>,
2657 #[serde(default)]
2658 pub interactables: Vec<InteractableView>,
2659 #[serde(default)]
2660 pub ledger: Option<PlayerLedgerView>,
2661 #[serde(default)]
2662 pub career: Option<PlayerCareerView>,
2663 #[serde(default)]
2665 pub combat_fx: Vec<CombatFx>,
2666 #[serde(default)]
2668 pub ground_hazards: Vec<GroundHazardView>,
2669 #[serde(default)]
2671 pub property_plots: Vec<PropertyPlotView>,
2672 #[serde(default)]
2674 pub terrain_overlays: Vec<TerrainZoneView>,
2675}
2676#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2677pub struct GroundDropView {
2678 pub id: String,
2679 pub template_id: String,
2680 pub quantity: u32,
2681 pub x: f32,
2682 pub y: f32,
2683 pub z: f32,
2684 #[serde(default)]
2686 pub tile_id: Option<String>,
2687 #[serde(default)]
2689 pub display_name: Option<String>,
2690 #[serde(default)]
2692 pub yaw: f32,
2693 #[serde(default)]
2695 pub pitch: f32,
2696 #[serde(default)]
2698 pub roll: f32,
2699 #[serde(default = "default_draw_scale")]
2701 pub draw_scale: f32,
2702 #[serde(default)]
2704 pub item_instance_id: Option<Uuid>,
2705 #[serde(default)]
2707 pub props: std::collections::BTreeMap<String, String>,
2708 #[serde(default)]
2710 pub status_bindings: Vec<ItemStatusBinding>,
2711}
2712
2713fn default_draw_scale() -> f32 {
2714 1.0
2715}
2716
2717#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2719pub struct Snapshot {
2720 pub tick: Tick,
2721 pub chunk_rev: u64,
2722 #[serde(default)]
2724 pub content_rev: u64,
2725 #[serde(default)]
2727 pub publish_rev: u64,
2728 pub entities: Vec<EntityState>,
2729 #[serde(default)]
2730 pub resource_nodes: Vec<ResourceNodeView>,
2731 #[serde(default)]
2733 pub world_x0: f32,
2734 #[serde(default)]
2735 pub world_y0: f32,
2736 #[serde(default)]
2738 pub world_width_m: f32,
2739 #[serde(default)]
2740 pub world_height_m: f32,
2741 #[serde(default)]
2742 pub buildings: Vec<BuildingView>,
2743 #[serde(default)]
2744 pub doors: Vec<DoorView>,
2745 #[serde(default)]
2746 pub npcs: Vec<NpcView>,
2747 #[serde(default)]
2748 pub inventory: Vec<ItemStack>,
2749 #[serde(default)]
2750 pub blueprints: Vec<BlueprintView>,
2751 #[serde(default)]
2753 pub building_materials: Vec<BuildingMaterialView>,
2754 #[serde(default)]
2755 pub world_clock: WorldClock,
2756 #[serde(default)]
2757 pub terrain_zones: Vec<TerrainZoneView>,
2758 #[serde(default)]
2759 pub z_platforms: Vec<ZPlatformView>,
2760 #[serde(default)]
2761 pub z_transitions: Vec<ZTransitionView>,
2762 #[serde(default)]
2763 pub ground_drops: Vec<GroundDropView>,
2764 #[serde(default)]
2765 pub placed_containers: Vec<PlacedContainerView>,
2766 #[serde(default)]
2767 pub combat: Option<CombatHud>,
2768 #[serde(default)]
2769 pub interior_map: Option<InteriorMapView>,
2770 #[serde(default)]
2771 pub quest_log: Vec<QuestLogEntry>,
2772 #[serde(default)]
2773 pub hired_workers: Vec<HiredWorkerView>,
2774 #[serde(default)]
2775 pub interactables: Vec<InteractableView>,
2776 #[serde(default)]
2777 pub ledger: Option<PlayerLedgerView>,
2778 #[serde(default)]
2779 pub career: Option<PlayerCareerView>,
2780 #[serde(default)]
2782 pub combat_fx: Vec<CombatFx>,
2783 #[serde(default)]
2785 pub ground_hazards: Vec<GroundHazardView>,
2786 #[serde(default)]
2788 pub property_zones: Vec<PropertyZoneView>,
2789 #[serde(default)]
2791 pub tax_zones: Vec<TaxZoneView>,
2792 #[serde(default)]
2794 pub boundary_zones: Vec<BoundaryZoneView>,
2795 #[serde(default)]
2797 pub encounter_zones: Vec<EncounterZoneView>,
2798 #[serde(default)]
2800 pub growth_zones: Vec<GrowthZoneView>,
2801 #[serde(default)]
2803 pub biome_zones: Vec<BiomeZoneView>,
2804 #[serde(default)]
2806 pub terrain_kind_nav: Vec<TerrainKindNavView>,
2807 #[serde(default)]
2809 pub property_plots: Vec<PropertyPlotView>,
2810 #[serde(default)]
2812 pub property_plot_settings: Option<PropertyPlotSettingsView>,
2813 #[serde(default)]
2816 pub item_catalog: Vec<ItemCatalogEntryView>,
2817}
2818
2819#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2821pub struct ItemCatalogEntryView {
2822 pub template_id: String,
2823 #[serde(default)]
2824 pub display_name: String,
2825 #[serde(default)]
2826 pub category: String,
2827 #[serde(default)]
2829 pub seed_for: Option<String>,
2830}
2831
2832impl ItemCatalogEntryView {
2833 pub fn is_harvest_node(&self) -> bool {
2834 self.category == "harvest_node"
2835 }
2836
2837 pub fn is_depositable_stack(&self) -> bool {
2839 !self.is_harvest_node()
2840 }
2841
2842 pub fn is_farm_seed(&self) -> bool {
2843 self.seed_for
2844 .as_deref()
2845 .is_some_and(|s| !s.trim().is_empty())
2846 || self.category == "seed"
2847 }
2848}
2849
2850#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2852pub struct ResourceNodeView {
2853 pub id: String,
2854 pub label: String,
2855 pub x: f32,
2856 pub y: f32,
2857 pub z: f32,
2858 pub item_template: String,
2859 #[serde(default = "default_node_state")]
2860 pub state: ResourceNodeState,
2861 #[serde(default = "default_blocking_view")]
2863 pub blocking: bool,
2864 #[serde(default = "default_blocking_radius_view")]
2866 pub blocking_radius_m: f32,
2867 #[serde(default)]
2869 pub harvest_off: bool,
2870 #[serde(default)]
2872 pub tile_id: Option<String>,
2873 #[serde(default)]
2875 pub yaw: f32,
2876 #[serde(default)]
2878 pub pitch: f32,
2879 #[serde(default)]
2881 pub roll: f32,
2882 #[serde(default = "default_draw_scale")]
2884 pub draw_scale: f32,
2885 #[serde(default)]
2887 pub sprite_mode: Option<String>,
2888 #[serde(default)]
2890 pub presentation_state: Option<String>,
2891 #[serde(default)]
2894 pub growth_progress: Option<f32>,
2895 #[serde(default)]
2897 pub channel_start_tick: Option<Tick>,
2898 #[serde(default)]
2899 pub channel_end_tick: Option<Tick>,
2900 #[serde(default)]
2902 pub harvest_drop_templates: Vec<String>,
2903}
2904
2905fn default_blocking_radius_view() -> f32 {
2906 0.8
2907}
2908
2909fn default_blocking_view() -> bool {
2910 true
2911}
2912
2913#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2914#[serde(rename_all = "snake_case")]
2915pub enum ResourceNodeState {
2916 Available,
2917 Harvesting,
2918 Cooldown,
2919}
2920fn default_node_state() -> ResourceNodeState {
2921 ResourceNodeState::Available
2922}
2923
2924#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2926#[serde(rename_all = "snake_case")]
2927pub enum ItemSpawnStateView {
2928 Spawned,
2929 PickedUp { respawn_at_tick: u64 },
2930 Consumed,
2931}
2932
2933#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2935pub struct ItemSpawnView {
2936 pub id: String,
2937 pub label: String,
2938 pub item_template: String,
2939 pub quantity: u32,
2940 pub x: f32,
2941 pub y: f32,
2942 pub z: f32,
2943 pub respawn_ticks: u32,
2944 #[serde(default)]
2945 pub building_id: Option<String>,
2946 pub state: ItemSpawnStateView,
2947 #[serde(default)]
2949 pub once_per_character: bool,
2950 #[serde(default)]
2952 pub collected_count: u32,
2953}
2954
2955#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2956#[serde(rename_all = "snake_case")]
2957pub enum ItemStatusBindingMode {
2958 OnHit,
2959 WhileEquipped,
2960}
2961
2962impl Default for ItemStatusBindingMode {
2963 fn default() -> Self {
2964 Self::OnHit
2965 }
2966}
2967
2968#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2970pub struct ItemStatusBinding {
2971 pub effect_id: String,
2972 #[serde(default)]
2973 pub mode: ItemStatusBindingMode,
2974 #[serde(default)]
2976 pub source: String,
2977 #[serde(default)]
2978 pub applied_at_tick: u64,
2979 #[serde(default)]
2982 pub expires_at_tick: Option<u64>,
2983}
2984
2985#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2986pub struct ItemStack {
2987 pub template_id: String,
2988 pub quantity: u32,
2989 #[serde(default)]
2991 pub item_instance_id: Option<Uuid>,
2992 #[serde(default)]
2994 pub props: BTreeMap<String, String>,
2995 #[serde(default)]
2997 pub status_bindings: Vec<ItemStatusBinding>,
2998 #[serde(default)]
3000 pub contents: Vec<ItemStack>,
3001 #[serde(default)]
3003 pub display_name: Option<String>,
3004 #[serde(default)]
3006 pub category: Option<String>,
3007 #[serde(default)]
3009 pub base_mass: Option<f32>,
3010 #[serde(default)]
3012 pub base_volume: Option<f32>,
3013 #[serde(default)]
3015 pub capacity_volume: Option<f32>,
3016 #[serde(default)]
3018 pub stackable: Option<bool>,
3019 #[serde(default)]
3021 pub world_placeable: Option<bool>,
3022 #[serde(default)]
3024 pub worker_lodging_capacity: Option<u32>,
3025 #[serde(default)]
3027 pub equip_slot: Option<BodySlot>,
3028 #[serde(default)]
3030 pub armor_physical: Option<f32>,
3031 #[serde(default)]
3033 pub resists: Vec<(String, f32)>,
3034 #[serde(default)]
3036 pub hand_slots: Option<u8>,
3037 #[serde(default)]
3039 pub listable: Option<bool>,
3040 #[serde(default)]
3042 pub base_value_copper: Option<u32>,
3043}
3044
3045impl ItemStack {
3046 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
3047 Self {
3048 template_id: template_id.into(),
3049 quantity,
3050 ..Default::default()
3051 }
3052 }
3053}
3054
3055impl Default for ItemStack {
3056 fn default() -> Self {
3057 Self {
3058 template_id: String::new(),
3059 quantity: 0,
3060 item_instance_id: None,
3061 props: BTreeMap::new(),
3062 status_bindings: Vec::new(),
3063 contents: Vec::new(),
3064 display_name: None,
3065 category: None,
3066 base_mass: None,
3067 base_volume: None,
3068 capacity_volume: None,
3069 stackable: None,
3070 world_placeable: None,
3071 worker_lodging_capacity: None,
3072 equip_slot: None,
3073 armor_physical: None,
3074 resists: Vec::new(),
3075 hand_slots: None,
3076 listable: None,
3077 base_value_copper: None,
3078 }
3079 }
3080}
3081
3082#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3084#[serde(rename_all = "snake_case")]
3085pub enum EncumbranceState {
3086 #[default]
3087 Light,
3088 Heavy,
3089 Orange,
3090 Over,
3091}
3092
3093impl EncumbranceState {
3094 pub fn label(self) -> &'static str {
3096 match self {
3097 Self::Light => "Light",
3098 Self::Heavy => "Heavy",
3099 Self::Orange => "Overloaded",
3100 Self::Over => "Over",
3101 }
3102 }
3103
3104 pub fn allows_sprint(self) -> bool {
3106 !matches!(self, Self::Over)
3107 }
3108}
3109
3110#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
3114#[serde(rename_all = "snake_case")]
3115pub enum BodySlot {
3116 Head,
3117 #[serde(alias = "body")]
3119 Chest,
3120 #[serde(alias = "arms")]
3122 Forearms,
3123 Legs,
3124 Feet,
3125 Cloak,
3126 Back,
3127 Waist,
3128 Earrings,
3129 Necklace,
3130 Eyeglasses,
3131 #[serde(rename = "ring_left_1", alias = "ring_left1")]
3133 RingLeft1,
3134 #[serde(rename = "ring_left_2", alias = "ring_left2")]
3135 RingLeft2,
3136 #[serde(rename = "ring_right_1", alias = "ring_right1")]
3137 RingRight1,
3138 #[serde(rename = "ring_right_2", alias = "ring_right2")]
3139 RingRight2,
3140}
3141
3142impl BodySlot {
3143 pub const ALL: [BodySlot; 15] = [
3145 BodySlot::Head,
3146 BodySlot::Chest,
3147 BodySlot::Forearms,
3148 BodySlot::Legs,
3149 BodySlot::Feet,
3150 BodySlot::Cloak,
3151 BodySlot::Back,
3152 BodySlot::Waist,
3153 BodySlot::Earrings,
3154 BodySlot::Necklace,
3155 BodySlot::Eyeglasses,
3156 BodySlot::RingLeft1,
3157 BodySlot::RingLeft2,
3158 BodySlot::RingRight1,
3159 BodySlot::RingRight2,
3160 ];
3161
3162 pub fn as_str(self) -> &'static str {
3163 match self {
3164 BodySlot::Head => "head",
3165 BodySlot::Chest => "chest",
3166 BodySlot::Forearms => "forearms",
3167 BodySlot::Legs => "legs",
3168 BodySlot::Feet => "feet",
3169 BodySlot::Cloak => "cloak",
3170 BodySlot::Back => "back",
3171 BodySlot::Waist => "waist",
3172 BodySlot::Earrings => "earrings",
3173 BodySlot::Necklace => "necklace",
3174 BodySlot::Eyeglasses => "eyeglasses",
3175 BodySlot::RingLeft1 => "ring_left_1",
3176 BodySlot::RingLeft2 => "ring_left_2",
3177 BodySlot::RingRight1 => "ring_right_1",
3178 BodySlot::RingRight2 => "ring_right_2",
3179 }
3180 }
3181}
3182
3183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3185#[serde(rename_all = "snake_case")]
3186pub enum InventoryLocation {
3187 Root,
3189 Worn { slot: BodySlot },
3191 Placed { container_id: String },
3193 Keychain,
3195 WhisperPouch,
3197}
3198
3199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3201pub struct PlacedContainerView {
3202 pub id: String,
3203 pub template_id: String,
3204 pub display_name: String,
3205 pub x: f32,
3206 pub y: f32,
3207 pub z: f32,
3208 pub locked: bool,
3209 #[serde(default)]
3211 pub accessible: bool,
3212 #[serde(default)]
3213 pub owner_character_id: Option<Uuid>,
3214 #[serde(default)]
3216 pub contents: Vec<ItemStack>,
3217 #[serde(default)]
3219 pub lock_id: Option<String>,
3220 #[serde(default)]
3222 pub capacity_volume: Option<f32>,
3223 #[serde(default)]
3225 pub item_instance_id: Option<Uuid>,
3226 #[serde(default)]
3228 pub tile_id: Option<String>,
3229 #[serde(default)]
3231 pub worker_lodging_capacity: Option<u32>,
3232 #[serde(default)]
3234 pub blocking: bool,
3235 #[serde(default)]
3237 pub blocking_radius_m: f32,
3238 #[serde(default)]
3241 pub building_id: Option<String>,
3242}
3243
3244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3245pub struct BlueprintIngredientView {
3246 pub template_id: String,
3247 pub quantity: u32,
3248 pub consumed: bool,
3250 #[serde(default)]
3252 pub display_name: String,
3253}
3254
3255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3256pub struct ToolRequirementView {
3257 pub item: String,
3258 pub consumed: bool,
3260 #[serde(default)]
3262 pub display_name: String,
3263}
3264
3265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3266pub struct SkillRequirementView {
3267 pub skill: String,
3268 pub level: u32,
3269}
3270
3271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3272pub struct BlueprintView {
3273 pub id: String,
3274 pub label: String,
3275 pub output: String,
3276 pub output_qty: u32,
3277 pub craft_ticks: u32,
3278 pub inputs: Vec<BlueprintIngredientView>,
3279 #[serde(default)]
3281 pub station: Option<String>,
3282 #[serde(default)]
3283 pub category: Option<String>,
3284 #[serde(default)]
3285 pub required_tools: Vec<ToolRequirementView>,
3286 #[serde(default)]
3287 pub skill: Option<SkillRequirementView>,
3288 #[serde(default)]
3289 pub failure_chance: f32,
3290 #[serde(default)]
3292 pub worker_train_copper: u64,
3293 #[serde(default)]
3295 pub output_display_name: String,
3296 #[serde(default)]
3298 pub craft_tier: u32,
3299}
3300
3301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3303pub struct TerrainKindNavView {
3304 pub kind: TerrainKindView,
3305 #[serde(default = "default_move_speed_mult_one")]
3306 pub move_speed_mult: f32,
3307 #[serde(default)]
3308 pub impassable: bool,
3309}
3310
3311fn default_move_speed_mult_one() -> f32 {
3312 1.0
3313}
3314
3315#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3317#[serde(rename_all = "snake_case")]
3318pub enum TerrainKindView {
3319 #[default]
3320 Grass,
3321 Dirt,
3322 Tilled,
3323 Desert,
3324 Hill,
3325 Bog,
3326 Beach,
3327 ShallowWater,
3328 DeepWater,
3329 Trail,
3330 Road,
3331 Rock,
3332}
3333
3334#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3335pub struct TerrainZoneView {
3336 pub id: String,
3337 pub x0: f32,
3338 pub y0: f32,
3339 pub x1: f32,
3340 pub y1: f32,
3341 #[serde(default)]
3342 pub kind: TerrainKindView,
3343 #[serde(default)]
3345 pub elevation: f32,
3346 #[serde(default)]
3349 pub glyph: Option<String>,
3350 #[serde(default)]
3352 pub color: Option<String>,
3353 #[serde(default)]
3355 pub tile_id: Option<String>,
3356 #[serde(default)]
3358 pub z_order: i32,
3359 #[serde(default)]
3361 pub channel_start_tick: Option<Tick>,
3362 #[serde(default)]
3363 pub channel_end_tick: Option<Tick>,
3364}
3365
3366#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3368pub struct ZoneRectView {
3369 pub x0: f32,
3370 pub y0: f32,
3371 pub x1: f32,
3372 pub y1: f32,
3373}
3374
3375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3377pub struct PropertyZoneView {
3378 pub id: String,
3379 #[serde(default)]
3381 pub label: Option<String>,
3382 pub rects: Vec<ZoneRectView>,
3383 #[serde(default)]
3384 pub z_order: i32,
3385 pub crown_price_copper: u64,
3386 pub upkeep_copper_per_day: u64,
3387 #[serde(default)]
3388 pub max_area_m2: Option<f32>,
3389 #[serde(default)]
3390 pub owner_tax_discount_bps: u32,
3391}
3392
3393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3395pub struct TaxZoneView {
3396 pub id: String,
3397 #[serde(default)]
3398 pub label: Option<String>,
3399 pub rects: Vec<ZoneRectView>,
3400 #[serde(default)]
3401 pub z_order: i32,
3402 pub rate_bps: u32,
3403 #[serde(default)]
3404 pub flat_copper: u64,
3405 #[serde(default)]
3407 pub market_sales_tax_bps: u32,
3408 #[serde(default)]
3410 pub market_sales_flat_copper: u32,
3411}
3412
3413#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3415pub struct BoundaryZoneView {
3416 pub id: String,
3417 #[serde(default)]
3418 pub label: Option<String>,
3419 pub rects: Vec<ZoneRectView>,
3420 #[serde(default)]
3421 pub z_order: i32,
3422 #[serde(default, skip_serializing_if = "Option::is_none")]
3423 pub jurisdiction_id: Option<String>,
3424 #[serde(default = "default_true")]
3425 pub worker_logistics: bool,
3426 #[serde(default)]
3427 pub security_tier: String,
3428 #[serde(default)]
3429 pub pvp_mode: String,
3430 #[serde(default = "default_true")]
3431 pub crime_enabled: bool,
3432 #[serde(default)]
3433 pub guard_response: bool,
3434 #[serde(default)]
3436 pub pass_through_props: bool,
3437 #[serde(default)]
3439 pub presence_mode: String,
3440}
3441
3442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3444pub struct EncounterZoneView {
3445 pub id: String,
3446 #[serde(default)]
3447 pub label: Option<String>,
3448 pub rects: Vec<ZoneRectView>,
3449 #[serde(default)]
3450 pub z_order: i32,
3451}
3452
3453fn default_true() -> bool {
3454 true
3455}
3456
3457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3459pub struct GrowthZoneView {
3460 pub id: String,
3461 #[serde(default)]
3462 pub label: Option<String>,
3463 pub rects: Vec<ZoneRectView>,
3464 #[serde(default)]
3465 pub z_order: i32,
3466 #[serde(default = "default_one_f32")]
3467 pub fertility: f32,
3468}
3469
3470fn default_one_f32() -> f32 {
3471 1.0
3472}
3473
3474#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3476pub struct BiomeZoneView {
3477 pub id: String,
3478 #[serde(default)]
3479 pub label: Option<String>,
3480 pub rects: Vec<ZoneRectView>,
3481 #[serde(default)]
3482 pub z_order: i32,
3483 pub biome_id: String,
3484}
3485
3486#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3488pub struct FarmGrantView {
3489 pub character_id: Uuid,
3490 #[serde(default)]
3492 pub character_label: String,
3493 pub tax_discount_bps: u32,
3494}
3495
3496#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3498pub struct PropertyPlotView {
3499 pub plot_id: Uuid,
3500 pub property_zone_id: String,
3501 #[serde(default)]
3502 pub zone_label: Option<String>,
3503 pub deed_instance_id: Uuid,
3504 pub x0: f32,
3505 pub y0: f32,
3506 pub x1: f32,
3507 pub y1: f32,
3508 pub upkeep_copper_per_day: u64,
3509 pub arrears_days: u32,
3510 #[serde(default)]
3512 pub is_mine: bool,
3513 #[serde(default)]
3515 pub may_farm: bool,
3516 #[serde(default)]
3518 pub purchase_basis_copper: u64,
3519 #[serde(default)]
3520 pub farm_public: bool,
3521 #[serde(default)]
3522 pub public_tax_discount_bps: u32,
3523 #[serde(default)]
3524 pub farm_allow: Vec<FarmGrantView>,
3525 #[serde(default)]
3527 pub owner_character_id: Option<Uuid>,
3528 #[serde(default)]
3529 pub owner_label: Option<String>,
3530 #[serde(default)]
3532 pub building_id: Option<String>,
3533 #[serde(default)]
3535 pub plot_code: String,
3536 #[serde(default)]
3538 pub label: String,
3539}
3540
3541#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3543pub struct PropertyPlotSettingsView {
3544 pub min_plot_area_m2: f32,
3545 pub tax_premium_weight: f32,
3546 pub sellback_bps: u32,
3547}
3548
3549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3551pub struct ZPlatformView {
3552 pub id: String,
3553 pub z: f32,
3554 pub x0: f32,
3555 pub y0: f32,
3556 pub x1: f32,
3557 pub y1: f32,
3558}
3559
3560#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3562pub struct ZTransitionView {
3563 pub id: String,
3564 pub z_from: f32,
3565 pub z_to: f32,
3566 pub x0: f32,
3567 pub y0: f32,
3568 pub x1: f32,
3569 pub y1: f32,
3570}
3571
3572#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3573pub struct BuildingView {
3574 pub id: String,
3575 pub label: String,
3576 pub x: f32,
3577 pub y: f32,
3578 pub width_m: f32,
3579 pub depth_m: f32,
3580 #[serde(default)]
3581 pub interior_blueprint: Option<String>,
3582 #[serde(default)]
3583 pub tags: Vec<String>,
3584 #[serde(default)]
3586 pub market_boundary_zone_ids: Vec<String>,
3587 #[serde(default)]
3589 pub market_max_volume: Option<f32>,
3590 #[serde(default)]
3593 pub wall_set: Option<String>,
3594 #[serde(default)]
3596 pub roof_set: Option<String>,
3597}
3598
3599pub const DEFAULT_BUILDING_ART_SET: &str = "classic_stone";
3602
3603impl BuildingView {
3604 pub fn effective_wall_set(&self) -> &str {
3605 self.wall_set
3606 .as_deref()
3607 .filter(|s| !s.is_empty())
3608 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3609 }
3610
3611 pub fn effective_roof_set(&self) -> &str {
3612 self.roof_set
3613 .as_deref()
3614 .filter(|s| !s.is_empty())
3615 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3616 }
3617}
3618
3619#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3620pub struct DoorView {
3621 pub id: String,
3622 pub building_id: String,
3623 pub x: f32,
3624 pub y: f32,
3625 #[serde(default)]
3626 pub open: bool,
3627 #[serde(default)]
3628 pub portal: Option<String>,
3629 #[serde(default)]
3632 pub locked: bool,
3633 #[serde(default = "default_door_accessible")]
3635 pub accessible: bool,
3636 #[serde(default)]
3637 pub lock_id: Option<Uuid>,
3638}
3639
3640fn default_door_accessible() -> bool {
3641 true
3642}
3643
3644#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3646pub struct InteriorRoomEdit {
3647 pub id: String,
3648 pub label: String,
3649 pub x0: f32,
3650 pub y0: f32,
3651 pub x1: f32,
3652 pub y1: f32,
3653}
3654
3655#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3656pub struct InteriorRoomDoorEdit {
3657 pub id: String,
3658 pub room_a: String,
3659 pub room_b: String,
3660 pub x: f32,
3661 pub y: f32,
3662}
3663
3664#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3665pub struct InteriorRoomView {
3666 pub id: String,
3667 pub label: String,
3668 pub floor: i32,
3669 pub x0: f32,
3670 pub y0: f32,
3671 pub x1: f32,
3672 pub y1: f32,
3673 #[serde(default)]
3674 pub floor_color: Option<String>,
3675 #[serde(default)]
3676 pub floor_glyph: Option<String>,
3677}
3678
3679#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3680pub struct InteriorDoorView {
3681 pub id: String,
3682 pub room_a: String,
3683 pub room_b: String,
3684 pub x: f32,
3685 pub y: f32,
3686 pub kind: String,
3687 #[serde(default)]
3688 pub x_a: Option<f32>,
3689 #[serde(default)]
3690 pub y_a: Option<f32>,
3691 #[serde(default)]
3692 pub x_b: Option<f32>,
3693 #[serde(default)]
3694 pub y_b: Option<f32>,
3695}
3696
3697#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3698pub struct InteriorMapView {
3699 pub building_id: String,
3700 pub blueprint_id: String,
3701 pub background_color: String,
3702 #[serde(default)]
3703 pub default_floor_color: Option<String>,
3704 #[serde(default = "default_floor_height_view")]
3705 pub floor_height_m: f32,
3706 #[serde(default)]
3708 pub z_platforms: Vec<ZPlatformView>,
3709 #[serde(default)]
3710 pub z_transitions: Vec<ZTransitionView>,
3711 pub rooms: Vec<InteriorRoomView>,
3712 #[serde(default)]
3713 pub room_doors: Vec<InteriorDoorView>,
3714}
3715
3716fn default_floor_height_view() -> f32 {
3717 3.0
3718}
3719
3720#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3721pub struct NpcView {
3722 pub id: String,
3723 pub label: String,
3724 pub role: String,
3725 pub x: f32,
3726 pub y: f32,
3727 #[serde(default)]
3729 pub building_id: Option<String>,
3730 #[serde(default)]
3732 pub entity_id: Option<EntityId>,
3733 #[serde(default)]
3734 pub life_state: Option<LifeState>,
3735 #[serde(default)]
3736 pub hp_pct: Option<f32>,
3737 #[serde(default)]
3739 pub can_trade: bool,
3740 #[serde(default)]
3742 pub buy_templates: Vec<String>,
3743 #[serde(default)]
3745 pub tile_id: Option<String>,
3746 #[serde(default)]
3748 pub behavior_state: Option<String>,
3749 #[serde(default)]
3751 pub presentation_state: Option<String>,
3752 #[serde(default)]
3754 pub sprite_mode: Option<String>,
3755 #[serde(default)]
3757 pub paperdoll_ref: Option<String>,
3758 #[serde(default = "default_draw_scale")]
3760 pub draw_scale: f32,
3761 #[serde(default)]
3763 pub yaw: Option<f32>,
3764 #[serde(default)]
3766 pub perception_fov_deg: Option<f32>,
3767 #[serde(default)]
3769 pub perception_sight_m: Option<f32>,
3770 #[serde(default)]
3772 pub perception_hear_m: Option<f32>,
3773 #[serde(default)]
3775 pub quest_verbs: Vec<NpcQuestVerb>,
3776}
3777
3778#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3780pub struct NpcQuestVerb {
3781 pub quest_id: String,
3782 pub label: String,
3784 pub kind: String,
3785}
3786
3787impl NpcQuestVerb {
3788 pub const KIND_OFFER: &'static str = "offer";
3789 pub const KIND_TALK: &'static str = "talk";
3790 pub const KIND_GIVE: &'static str = "give";
3791}
3792
3793#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3794pub struct UseResult {
3795 pub template_id: String,
3796 pub hunger_restored: f32,
3797 pub thirst_restored: f32,
3798 #[serde(default)]
3799 pub health_restored: f32,
3800 #[serde(default)]
3801 pub mana_restored: f32,
3802 #[serde(default)]
3803 pub cleared_dot_ids: Vec<String>,
3804}
3805
3806#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3807pub struct CraftResult {
3808 pub blueprint_id: String,
3809 pub outputs: Vec<ItemStack>,
3810 pub consumed: Vec<ItemStack>,
3811 #[serde(default = "default_one")]
3813 pub batch_index: u32,
3814 #[serde(default = "default_one")]
3816 pub batch_total: u32,
3817}
3818
3819fn default_one() -> u32 {
3820 1
3821}
3822
3823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3824pub struct DeathNotice {
3825 pub entity_id: EntityId,
3826 pub respawn_x: f32,
3827 pub respawn_y: f32,
3828 pub message: String,
3829}
3830
3831#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3832pub struct InteractionNotice {
3833 pub target_id: String,
3834 pub message: String,
3835 #[serde(default)]
3836 pub coins_delta: i32,
3837 #[serde(default)]
3838 pub inventory_delta: Vec<ItemStack>,
3839}
3840
3841#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3842#[serde(rename_all = "snake_case")]
3843pub enum NpcTalkTrustFlag {
3844 Stranger,
3845 Acquainted,
3846 Trusted,
3847}
3848
3849#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3850#[serde(rename_all = "snake_case")]
3851pub enum NpcTalkDepth {
3852 #[default]
3853 Full,
3854 Brief,
3855 Unavailable,
3856}
3857
3858#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3859pub struct NpcTalkOpened {
3860 pub npc_id: String,
3861 pub npc_label: String,
3862 pub greeting: String,
3863 pub trust_flag: NpcTalkTrustFlag,
3864 #[serde(default)]
3865 pub talk_depth: NpcTalkDepth,
3866 #[serde(default = "default_true")]
3867 pub trade_allowed: bool,
3868 #[serde(default)]
3870 pub suggested_topics: Vec<String>,
3871}
3872
3873#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3874pub struct NpcTalkPending {
3875 pub npc_id: String,
3876}
3877
3878#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3879pub struct NpcTalkReply {
3880 pub npc_id: String,
3881 pub line: String,
3882 pub trust_flag: NpcTalkTrustFlag,
3883 #[serde(default)]
3884 pub wind_down: bool,
3885 #[serde(default)]
3886 pub trade_disabled: bool,
3887}
3888
3889#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3890pub struct NpcTalkClosed {
3891 pub npc_id: String,
3892}
3893
3894#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3895pub struct NpcTalkError {
3896 pub npc_id: String,
3897 pub reason: String,
3898}
3899
3900#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3901#[serde(rename_all = "snake_case")]
3902pub enum QuestStatusView {
3903 Available,
3904 Active,
3905 Completed,
3906}
3907
3908#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3909pub struct QuestObjectiveProgress {
3910 pub label: String,
3911 pub current: u32,
3912 pub required: u32,
3913 pub done: bool,
3914 #[serde(default)]
3918 pub kind: String,
3919 #[serde(default)]
3920 pub npc_ref: Option<String>,
3921 #[serde(default)]
3922 pub item_template: Option<String>,
3923 #[serde(default)]
3924 pub blueprint_id: Option<String>,
3925 #[serde(default)]
3926 pub building_id: Option<String>,
3927}
3928
3929#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3931pub struct QuestRewardItemView {
3932 pub template_id: String,
3933 #[serde(default)]
3935 pub display_name: String,
3936 pub quantity: u32,
3937}
3938
3939#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3941pub struct QuestRewardView {
3942 #[serde(default)]
3943 pub coins: u32,
3944 #[serde(default)]
3945 pub items: Vec<QuestRewardItemView>,
3946}
3947
3948impl QuestRewardView {
3949 pub fn is_empty(&self) -> bool {
3950 self.coins == 0 && self.items.is_empty()
3951 }
3952}
3953
3954#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3955#[serde(rename_all = "snake_case")]
3956pub enum QuestStepStatusView {
3957 #[default]
3958 Pending,
3959 Current,
3960 Done,
3961}
3962
3963#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3965pub struct QuestStepView {
3966 pub id: String,
3967 pub title: String,
3968 #[serde(default)]
3969 pub status: QuestStepStatusView,
3970 #[serde(default)]
3971 pub reward: QuestRewardView,
3972}
3973
3974#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3975pub struct QuestLogEntry {
3976 pub quest_id: String,
3977 pub title: String,
3978 pub description: String,
3979 pub status: QuestStatusView,
3980 #[serde(default)]
3981 pub current_step_id: Option<String>,
3982 #[serde(default)]
3983 pub current_step_title: String,
3984 #[serde(default)]
3985 pub current_step_index: u32,
3986 #[serde(default)]
3987 pub objectives: Vec<QuestObjectiveProgress>,
3988 #[serde(default)]
3990 pub current_step_reward: QuestRewardView,
3991 #[serde(default)]
3993 pub completion_reward: QuestRewardView,
3994 #[serde(default)]
3996 pub steps: Vec<QuestStepView>,
3997 #[serde(default)]
3998 pub is_tracked: bool,
3999 #[serde(default)]
4000 pub can_withdraw: bool,
4001}
4002
4003#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4004pub struct InteractableView {
4005 pub id: String,
4006 pub kind: String,
4007 pub label: String,
4008 pub x: f32,
4009 pub y: f32,
4010 pub z: f32,
4011 #[serde(default)]
4012 pub board_id: Option<String>,
4013}
4014
4015#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4016pub struct QuestOffer {
4017 pub quest_id: String,
4018 pub title: String,
4019 pub description: String,
4020 #[serde(default)]
4021 pub step_count: u32,
4022}
4023
4024#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4025pub struct QuestCatalogEntry {
4026 pub quest_id: String,
4027 pub title: String,
4028 pub description: String,
4029 pub step_count: u32,
4030 #[serde(default)]
4031 pub board_ids: Vec<String>,
4032}
4033
4034#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4035pub struct QuestCatalogUpdated {
4036 pub revision: u64,
4037 pub game_day: String,
4038 #[serde(default)]
4039 pub accepted: Vec<QuestCatalogEntry>,
4040 #[serde(default)]
4041 pub retired: Vec<String>,
4042}
4043
4044#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4045pub struct QuestNotice {
4046 pub quest_id: String,
4047 pub title: String,
4048 pub message: String,
4049}
4050
4051#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4052#[serde(rename_all = "snake_case")]
4053pub enum ShopOfferKind {
4054 Item,
4055 Blueprint,
4056}
4057
4058#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4059pub struct ShopOffer {
4060 pub offer_id: String,
4061 pub kind: ShopOfferKind,
4062 pub label: String,
4063 #[serde(default)]
4064 pub template_id: Option<String>,
4065 #[serde(default)]
4066 pub blueprint_id: Option<String>,
4067 pub price_copper: u32,
4068 #[serde(default)]
4069 pub affordable: bool,
4070 #[serde(default)]
4071 pub already_owned: bool,
4072}
4073
4074#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4075pub struct ShopBuyLine {
4076 pub template_id: String,
4077 pub label: String,
4078 pub quantity: u32,
4079 pub price_copper: u32,
4080}
4081
4082#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4084pub struct BankPanel {
4085 pub npc_id: String,
4086 pub npc_label: String,
4087 pub bank_balance_copper: u64,
4088 pub on_person_copper: u64,
4089 #[serde(default)]
4091 pub pending_outgoing_copper: u64,
4092 #[serde(default)]
4093 pub transfer_fee_bps: u32,
4094 #[serde(default)]
4095 pub transfer_clear_ticks: u64,
4096}
4097
4098#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4100pub struct StoragePanel {
4101 pub npc_id: String,
4102 pub npc_label: String,
4103 pub building_id: String,
4104 pub building_label: String,
4105 pub used_volume: f32,
4106 pub max_volume: f32,
4107 #[serde(default)]
4108 pub contents: Vec<ItemStack>,
4109 #[serde(default)]
4111 pub ship_destinations: Vec<StorageShipDest>,
4112}
4113
4114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4115pub struct StorageShipDest {
4116 pub building_id: String,
4117 pub label: String,
4118 pub distance_m: f32,
4119 pub fee_copper: u64,
4120 pub travel_ticks: u64,
4121}
4122
4123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4126pub enum GoodsLocation {
4127 Person,
4129 TownStorage { building_id: String },
4132}
4133
4134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4137pub struct MarketListingView {
4138 pub listing_id: Uuid,
4139 pub seller_character_id: Uuid,
4140 pub seller_label: String,
4142 pub hall_building_id: String,
4143 pub hall_label: String,
4144 pub template_id: String,
4145 pub display_name: String,
4146 #[serde(default)]
4148 pub category: String,
4149 pub quantity: u32,
4150 pub unit_price_copper: u64,
4151 pub line_total_copper: u64,
4153 #[serde(default)]
4155 pub npc_price: bool,
4156 #[serde(default)]
4159 pub npc_dump_unit_copper: Option<u32>,
4160 pub mine: bool,
4162}
4163
4164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4166pub struct MarketListVault {
4167 pub building_id: String,
4168 pub building_label: String,
4170 #[serde(default)]
4171 pub contents: Vec<ItemStack>,
4172}
4173
4174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4177pub struct MarketPanel {
4178 pub npc_id: String,
4179 pub npc_label: String,
4180 pub building_id: String,
4181 pub building_label: String,
4182 pub used_volume: f32,
4184 pub max_volume: f32,
4185 #[serde(default)]
4188 pub listings: Vec<MarketListingView>,
4189 #[serde(default)]
4191 pub tax_bps: u32,
4192 #[serde(default)]
4193 pub tax_flat_copper: u32,
4194 #[serde(default)]
4196 pub list_vaults: Vec<MarketListVault>,
4197}
4198
4199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4200pub struct ShopCatalog {
4201 pub npc_id: String,
4202 pub npc_label: String,
4203 #[serde(default)]
4204 pub sells: Vec<ShopOffer>,
4205 #[serde(default)]
4206 pub buys: Vec<ShopBuyLine>,
4207}
4208
4209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4210pub struct HarvestResult {
4211 pub node_id: String,
4212 pub quantity: u32,
4214 pub item_template: String,
4215 #[serde(default)]
4218 pub item_instance_id: Option<Uuid>,
4219}
4220
4221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4223pub struct Envelope<T> {
4224 pub protocol_version: u16,
4225 pub payload: T,
4226}
4227
4228impl<T> Envelope<T> {
4229 pub fn new(payload: T) -> Self {
4230 Self {
4231 protocol_version: crate::PROTOCOL_VERSION,
4232 payload,
4233 }
4234 }
4235}
4236
4237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4239pub struct Hello {
4240 pub client_name: String,
4241 pub protocol_version: u16,
4242 #[serde(default)]
4243 pub auth: AuthCredential,
4244 #[serde(default)]
4246 pub character_id: Option<Uuid>,
4247}
4248
4249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4252#[serde(rename_all = "snake_case")]
4253pub enum AuthCredential {
4254 DevLocal,
4255 Session { token: String },
4256 ApiToken { token: String, character_id: Uuid },
4257}
4258
4259impl Default for AuthCredential {
4260 fn default() -> Self {
4261 Self::DevLocal
4262 }
4263}
4264
4265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4266pub struct Welcome {
4267 pub session_id: SessionId,
4268 pub entity_id: EntityId,
4269 pub snapshot: Snapshot,
4270}
4271
4272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4273pub enum ServerMessage {
4274 Welcome(Welcome),
4275 ContentUpdated(Snapshot),
4277 Tick(TickDelta),
4278 IntentAck {
4279 entity_id: EntityId,
4280 seq: Seq,
4281 tick: Tick,
4282 },
4283 Chat(ChatMessage),
4284 HarvestResult(HarvestResult),
4285 UseResult(UseResult),
4286 CraftResult(CraftResult),
4287 Death(DeathNotice),
4288 Interaction(InteractionNotice),
4289 ShopOpened(ShopCatalog),
4290 NpcTalkOpened(NpcTalkOpened),
4291 NpcTalkPending(NpcTalkPending),
4292 NpcTalkReply(NpcTalkReply),
4293 NpcTalkClosed(NpcTalkClosed),
4294 NpcTalkError(NpcTalkError),
4295 QuestOffer(QuestOffer),
4296 QuestAccepted(QuestNotice),
4297 QuestWithdrawn(QuestNotice),
4298 QuestStepCompleted(QuestNotice),
4299 QuestCompleted(QuestNotice),
4300 QuestCatalogUpdated(QuestCatalogUpdated),
4301 BankOpened(BankPanel),
4303 StorageOpened(StoragePanel),
4305 MarketOpened(MarketPanel),
4307 TradeOpened(TradePanel),
4309 TradeClosed {
4311 reason: String,
4312 },
4313 ConnectRejected {
4316 reason: String,
4317 },
4318}
4319
4320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4322pub struct TradePanel {
4323 pub peer_entity_id: EntityId,
4324 pub peer_name: String,
4325 pub my_presented: Vec<ItemStack>,
4326 pub their_presented: Vec<ItemStack>,
4327 pub i_ready: bool,
4328 pub they_ready: bool,
4329 pub my_mass_after: f32,
4331 pub my_mass_max: f32,
4332 pub my_encumbrance_after: EncumbranceState,
4333 pub overburden_warning: bool,
4335}
4336
4337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4338pub enum ClientMessage {
4339 Hello(Hello),
4340 Intent(Intent),
4341 Disconnect,
4342}
4343
4344#[cfg(test)]
4345mod tests {
4346 use super::*;
4347
4348 #[test]
4349 fn pristine_vitals_state_yields_full_pools() {
4350 let attrs = PrimaryAttributes::default();
4351 let vitals = StoredVitalsState::default().apply_to(attrs);
4352 assert!(vitals.health > 0.0);
4353 assert_eq!(vitals.health, vitals.health_max);
4354 assert!((vitals.mana_max - 61.0).abs() < 0.01);
4355 }
4356
4357 #[test]
4358 fn humanize_snake_id_title_cases_parts() {
4359 assert_eq!(humanize_snake_id("heal_touch"), "Heal Touch");
4360 assert_eq!(humanize_snake_id("fireball"), "Fireball");
4361 assert_eq!(humanize_snake_id("cone_frost"), "Cone Frost");
4362 }
4363
4364 #[test]
4365 fn saved_vitals_scale_when_pool_max_increases() {
4366 let mut attrs = PrimaryAttributes::default();
4367 attrs.intelligence = 140;
4368 attrs.wisdom = 140;
4369 let saved = StoredVitalsState {
4370 health: 100.0,
4371 mana: 14.0,
4372 stamina: 100.0,
4373 ..StoredVitalsState::default()
4374 };
4375 let vitals = saved.apply_to(attrs);
4376 assert!(vitals.mana_max > 55.0);
4377 assert!(
4378 (vitals.mana - vitals.mana_max).abs() < 0.01,
4379 "full legacy mana bar migrates to full new bar"
4380 );
4381 }
4382
4383 #[test]
4384 fn empty_vitals_state_is_pristine() {
4385 let pristine = StoredVitalsState {
4386 health: 0.0,
4387 mana: 0.0,
4388 stamina: 0.0,
4389 hunger: 0.0,
4390 thirst: 0.0,
4391 coins: 0,
4392 deaths: 0,
4393 life_state: LifeState::Alive,
4394 winded: false,
4395 };
4396 assert!(pristine.is_pristine());
4397 let vitals = pristine.apply_to(PrimaryAttributes::default());
4398 assert!(vitals.health > 0.0);
4399 }
4400
4401 #[test]
4402 fn stored_vitals_roundtrip_preserves_partial_pools() {
4403 let attrs = PrimaryAttributes::default();
4404 let mut live = PlayerVitals::from_attributes(attrs);
4405 live.health = 25.0;
4406 live.hunger = 77.0;
4407 live.deaths = 2;
4408 live.winded = true;
4409 let stored = StoredVitalsState::from_live(&live);
4410 let restored = stored.apply_to(attrs);
4411 assert!(
4412 (restored.health - 25.0).abs() < 0.01,
4413 "partial HP below cap stays absolute"
4414 );
4415 assert_eq!(restored.hunger, 77.0);
4416 assert_eq!(restored.deaths, 2);
4417 assert!(restored.winded);
4418 }
4419
4420 #[test]
4421 fn skill_tiers_start_at_zero() {
4422 let skill = SkillProgress::default();
4423 assert_eq!(skill.level, 0);
4424 assert_eq!(skill.display_tier(), 0);
4425 let trained = SkillProgress {
4426 level: 250,
4427 last_trained_tick: 1,
4428 };
4429 assert_eq!(trained.display_tier(), 2);
4430 }
4431
4432 #[test]
4433 fn quest_server_messages_roundtrip_json() {
4434 use crate::codec::{Codec, PostcardCodec};
4435
4436 let offer = ServerMessage::QuestOffer(QuestOffer {
4437 quest_id: "ada_goblin_hunt".into(),
4438 title: "Goblin Trouble".into(),
4439 description: "Help Ada".into(),
4440 step_count: 3,
4441 });
4442 let notice = ServerMessage::QuestAccepted(QuestNotice {
4443 quest_id: "ada_goblin_hunt".into(),
4444 title: "Goblin Trouble".into(),
4445 message: "Quest accepted".into(),
4446 });
4447 for msg in [offer, notice] {
4448 let bytes = PostcardCodec.encode(&msg).unwrap();
4449 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
4450 assert_eq!(decoded, msg);
4451 }
4452 }
4453
4454 #[test]
4455 fn hotbar_consumable_binding_roundtrips() {
4456 let binding = hotbar_consumable_binding("vegetable_soup");
4457 assert_eq!(binding, "item:vegetable_soup");
4458 assert!(hotbar_binding_is_consumable(&binding));
4459 assert_eq!(
4460 hotbar_consumable_template(&binding),
4461 Some("vegetable_soup")
4462 );
4463 assert!(!hotbar_binding_is_consumable("fireball"));
4464 assert_eq!(hotbar_consumable_template("fireball"), None);
4465 }
4466}