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 DeconstructItem {
1615 entity_id: EntityId,
1616 item_instance_id: Uuid,
1617 from: InventoryLocation,
1618 #[serde(default)]
1620 quantity: Option<u32>,
1621 seq: Seq,
1622 },
1623}
1624
1625fn default_block_enabled() -> bool {
1626 true
1627}
1628
1629#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1631pub struct StatusEffectHud {
1632 pub effect_id: String,
1633 pub label: String,
1634 #[serde(default)]
1635 pub polarity: String,
1636 #[serde(default)]
1637 pub icon_tile_id: Option<String>,
1638 #[serde(default)]
1640 pub dot_color: Option<String>,
1641 #[serde(default)]
1643 pub remaining_sec: Option<f32>,
1644 #[serde(default = "default_stack_count")]
1646 pub stack_count: u8,
1647}
1648
1649fn default_stack_count() -> u8 {
1650 1
1651}
1652
1653#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1655pub struct CombatTargetHud {
1656 pub entity_id: EntityId,
1657 #[serde(default)]
1658 pub label: String,
1659 #[serde(default)]
1660 pub level: u32,
1661 pub health: f32,
1662 pub health_max: f32,
1663 #[serde(default)]
1664 pub life_state: LifeState,
1665 #[serde(default)]
1666 pub distance_m: f32,
1667 #[serde(default)]
1668 pub statuses: Vec<StatusEffectHud>,
1669}
1670
1671#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1673#[serde(rename_all = "snake_case")]
1674pub enum TimedChannelKind {
1675 #[default]
1676 Cultivate,
1677 Plant,
1678 Harvest,
1679 Build,
1681 Craft,
1683}
1684
1685#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1687pub struct TimedChannelHud {
1688 #[serde(default)]
1689 pub label: String,
1690 #[serde(default)]
1691 pub channel: TimedChannelKind,
1692 #[serde(default)]
1693 pub cell_x: i32,
1694 #[serde(default)]
1695 pub cell_y: i32,
1696 #[serde(default)]
1698 pub x0: f32,
1699 #[serde(default)]
1700 pub y0: f32,
1701 #[serde(default)]
1702 pub x1: f32,
1703 #[serde(default)]
1704 pub y1: f32,
1705 #[serde(default)]
1706 pub ticks_remaining: u64,
1707 #[serde(default)]
1708 pub ticks_total: u64,
1709}
1710
1711impl TimedChannelHud {
1712 pub fn has_footprint(&self) -> bool {
1714 self.x1 > self.x0 && self.y1 > self.y0
1715 }
1716}
1717
1718#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1720#[serde(rename_all = "snake_case")]
1721pub enum PlotBuildMaterialSource {
1722 #[default]
1723 None,
1724 TownStorage,
1725 NearbyContainer,
1726}
1727
1728#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1730pub struct BuildingMaterialView {
1731 pub id: String,
1732 pub display_name: String,
1733 #[serde(default)]
1734 pub can_wall: bool,
1735 #[serde(default)]
1736 pub can_roof: bool,
1737 #[serde(default)]
1738 pub wall_set: String,
1739 #[serde(default)]
1740 pub roof_set: String,
1741 #[serde(default = "default_material_tick_mult")]
1742 pub tick_mult: f32,
1743 #[serde(default)]
1744 pub wall_bom: Vec<BuildingBomLineView>,
1745 #[serde(default)]
1746 pub roof_bom: Vec<BuildingBomLineView>,
1747}
1748
1749fn default_material_tick_mult() -> f32 {
1750 1.0
1751}
1752
1753#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1754pub struct BuildingBomLineView {
1755 pub template_id: String,
1756 #[serde(default)]
1757 pub display_name: String,
1758 pub per_m2: f32,
1759}
1760
1761#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1763pub struct PlotBuildStockView {
1764 pub template_id: String,
1765 #[serde(default)]
1766 pub display_name: String,
1767 pub quantity: u32,
1768}
1769
1770#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1772pub struct PlotBuildOfferHud {
1773 pub plot_id: Uuid,
1774 #[serde(default)]
1775 pub pad_width_m: f32,
1776 #[serde(default)]
1777 pub pad_depth_m: f32,
1778 #[serde(default)]
1779 pub pad_ok: bool,
1780 #[serde(default)]
1781 pub pad_error: String,
1782 #[serde(default)]
1783 pub source: PlotBuildMaterialSource,
1784 #[serde(default)]
1785 pub source_label: String,
1786 #[serde(default)]
1787 pub available: Vec<PlotBuildStockView>,
1788 #[serde(default)]
1789 pub base_ticks: u32,
1790 #[serde(default)]
1791 pub tick_per_m2: u32,
1792}
1793
1794#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1796pub struct CastProgressHud {
1797 #[serde(default)]
1798 pub ability_id: String,
1799 #[serde(default)]
1800 pub ability_label: String,
1801 #[serde(default)]
1802 pub ticks_remaining: u64,
1803 #[serde(default)]
1804 pub ticks_total: u64,
1805}
1806
1807#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1809pub struct AbilityCooldownHud {
1810 #[serde(default)]
1811 pub ability_id: String,
1812 #[serde(default)]
1813 pub label: String,
1814 #[serde(default)]
1815 pub cd_ticks: u64,
1816 #[serde(default)]
1817 pub cd_total_ticks: u64,
1818}
1819
1820#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1822pub struct CombatSlotHud {
1823 pub slot_index: u8,
1824 #[serde(default)]
1825 pub target_entity_id: Option<EntityId>,
1826 #[serde(default)]
1827 pub target_label: Option<String>,
1828 #[serde(default)]
1829 pub target: Option<CombatTargetHud>,
1830 #[serde(default)]
1831 pub preset_id: Option<String>,
1832 #[serde(default)]
1833 pub preset_label: Option<String>,
1834 #[serde(default)]
1835 pub rotation: Vec<String>,
1836 #[serde(default)]
1837 pub rotation_index: u32,
1838 #[serde(default)]
1839 pub next_ability_id: Option<String>,
1840 #[serde(default)]
1841 pub auto_enabled: bool,
1842}
1843
1844#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1846pub struct DefensePieceHud {
1847 pub slot: BodySlot,
1848 pub label: String,
1849 pub template_id: String,
1850 #[serde(default)]
1851 pub armor_physical: f32,
1852 #[serde(default)]
1853 pub resists: Vec<(String, f32)>,
1854}
1855
1856impl Default for DefensePieceHud {
1857 fn default() -> Self {
1858 Self {
1859 slot: BodySlot::Head,
1860 label: String::new(),
1861 template_id: String::new(),
1862 armor_physical: 0.0,
1863 resists: Vec::new(),
1864 }
1865 }
1866}
1867
1868#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1870pub struct DefenseHud {
1871 pub armor_physical: f32,
1872 pub vitality_contribution: f32,
1873 pub total_mitigation_rating: f32,
1874 pub estimated_physical_dr: f32,
1876 #[serde(default)]
1877 pub resists: Vec<(String, f32)>,
1878 #[serde(default)]
1879 pub pieces: Vec<DefensePieceHud>,
1880}
1881
1882#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1884pub struct CombatHud {
1885 pub in_combat: bool,
1886 pub auto_attack: bool,
1888 pub has_los: bool,
1889 pub attack_cd_ticks: u64,
1890 #[serde(default)]
1891 pub ability_id: String,
1892 #[serde(default)]
1893 pub target_entity_id: Option<EntityId>,
1894 #[serde(default)]
1895 pub target_label: Option<String>,
1896 #[serde(default)]
1897 pub max_target_slots: u8,
1898 #[serde(default)]
1899 pub slots: Vec<CombatSlotHud>,
1900 #[serde(default)]
1901 pub rotation_presets: Vec<RotationPreset>,
1902 #[serde(default)]
1903 pub gcd_ticks: u64,
1904 #[serde(default)]
1905 pub mainhand_template_id: Option<String>,
1906 #[serde(default)]
1907 pub mainhand_label: Option<String>,
1908 #[serde(default)]
1910 pub mainhand_instance_id: Option<Uuid>,
1911 #[serde(default)]
1912 pub offhand_template_id: Option<String>,
1913 #[serde(default)]
1914 pub offhand_label: Option<String>,
1915 #[serde(default)]
1917 pub offhand_instance_id: Option<Uuid>,
1918 #[serde(default)]
1920 pub mainhand_hand_slots: u8,
1921 #[serde(default)]
1923 pub worn: Vec<(BodySlot, ItemStack)>,
1924 #[serde(default)]
1926 pub defense: Option<DefenseHud>,
1927 #[serde(default)]
1928 pub carry_mass: f32,
1929 #[serde(default)]
1930 pub carry_mass_max: f32,
1931 #[serde(default)]
1932 pub encumbrance: EncumbranceState,
1933 #[serde(default)]
1935 pub keychain: Vec<ItemStack>,
1936 #[serde(default)]
1938 pub whisper_pouch: Vec<ItemStack>,
1939 #[serde(default)]
1940 pub target: Option<CombatTargetHud>,
1941 #[serde(default)]
1942 pub cast: Option<CastProgressHud>,
1943 #[serde(default)]
1945 pub timed_channel: Option<TimedChannelHud>,
1946 #[serde(default)]
1948 pub plot_build: Option<PlotBuildOfferHud>,
1949 #[serde(default)]
1950 pub ability_cooldowns: Vec<AbilityCooldownHud>,
1951 #[serde(default)]
1952 pub blocking_active: bool,
1953 #[serde(default)]
1955 pub progression_xp: Option<ProgressionXp>,
1956 #[serde(default)]
1957 pub progression_baseline: u16,
1958 #[serde(default)]
1959 pub progression_xp_base: f64,
1960 #[serde(default)]
1961 pub progression_xp_growth: f64,
1962 #[serde(default)]
1963 pub attributes: Option<PrimaryAttributes>,
1964 #[serde(default)]
1965 pub skills: Option<PlayerSkills>,
1966 #[serde(default)]
1968 pub statuses: Vec<StatusEffectHud>,
1969 #[serde(default)]
1971 pub known_abilities: Vec<String>,
1972 #[serde(default)]
1974 pub ability_meta: Vec<AbilityMetaHud>,
1975 #[serde(default)]
1977 pub ability_mastery: Vec<AbilityMasteryHud>,
1978 #[serde(default)]
1981 pub hotbar: Vec<Option<String>>,
1982 #[serde(default)]
1984 pub max_abilities_per_rotation: u8,
1985 #[serde(default)]
1987 pub move_speed_mps: f32,
1988 #[serde(default)]
1990 pub move_speed_mult: f32,
1991}
1992
1993#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1995pub struct AbilityMetaHud {
1996 pub id: String,
1997 #[serde(default = "default_aim_mode_entity")]
1999 pub aim_mode: String,
2000 #[serde(default)]
2001 pub blast_radius_m: f32,
2002 #[serde(default)]
2003 pub allows_self: bool,
2004 #[serde(default)]
2005 pub is_heal: bool,
2006 #[serde(default = "default_auto_rotation_eligible")]
2009 pub auto_rotation_eligible: bool,
2010}
2011
2012fn default_auto_rotation_eligible() -> bool {
2013 true
2014}
2015
2016fn default_aim_mode_entity() -> String {
2017 "entity".into()
2018}
2019
2020pub const HOTBAR_ITEM_PREFIX: &str = "item:";
2022
2023pub fn hotbar_consumable_binding(template_id: &str) -> String {
2025 format!("{HOTBAR_ITEM_PREFIX}{}", template_id.trim())
2026}
2027
2028pub fn hotbar_consumable_template(binding: &str) -> Option<&str> {
2030 binding
2031 .strip_prefix(HOTBAR_ITEM_PREFIX)
2032 .map(str::trim)
2033 .filter(|id| !id.is_empty())
2034}
2035
2036pub fn hotbar_binding_is_consumable(binding: &str) -> bool {
2038 hotbar_consumable_template(binding).is_some()
2039}
2040
2041#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2043#[serde(rename_all = "snake_case")]
2044pub enum CombatFxKind {
2045 MeleeArc,
2046 Cone,
2047 Sphere,
2048 Beam,
2049 HitMarker,
2050}
2051
2052#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2054#[serde(rename_all = "snake_case")]
2055pub enum CombatFxHitOutcome {
2056 #[default]
2057 Hit,
2058 Blocked,
2059 Miss,
2060 Glance,
2061}
2062
2063#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2065pub struct CombatFxHit {
2066 pub entity_id: EntityId,
2067 pub x: f32,
2068 pub y: f32,
2069 pub z: f32,
2070 #[serde(default)]
2071 pub outcome: CombatFxHitOutcome,
2072}
2073
2074#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2076pub struct CombatFx {
2077 pub id: u64,
2078 pub kind: CombatFxKind,
2079 pub ability_id: String,
2080 pub caster_id: EntityId,
2081 pub origin_x: f32,
2082 pub origin_y: f32,
2083 pub origin_z: f32,
2084 #[serde(default)]
2085 pub end_x: Option<f32>,
2086 #[serde(default)]
2087 pub end_y: Option<f32>,
2088 #[serde(default)]
2089 pub end_z: Option<f32>,
2090 #[serde(default)]
2091 pub yaw: Option<f32>,
2092 #[serde(default)]
2093 pub reach_m: Option<f32>,
2094 #[serde(default)]
2095 pub arc_deg: Option<f32>,
2096 #[serde(default)]
2097 pub radius_m: Option<f32>,
2098 #[serde(default)]
2099 pub hits: Vec<CombatFxHit>,
2100 pub until_tick: u64,
2102 #[serde(default)]
2103 pub damage_type: String,
2104}
2105
2106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2108pub struct GroundHazardView {
2109 pub x: f32,
2110 pub y: f32,
2111 pub z: f32,
2112 pub radius_m: f32,
2113 pub expires_at_tick: u64,
2114 #[serde(default)]
2115 pub damage_type: String,
2116}
2117
2118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2120#[serde(rename_all = "snake_case")]
2121pub enum WorkerModeView {
2122 Companion,
2123 Defender,
2124 JobLoop,
2125 Idle,
2128}
2129
2130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2132#[serde(rename_all = "snake_case")]
2133pub enum WorkerStateView {
2134 Idle,
2135 Traveling,
2136 Working,
2137 Resting,
2138 Waiting,
2139 Strike,
2140 Dismissed,
2141}
2142
2143#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
2145pub struct WorkerVitalsSummary {
2146 pub health_pct: f32,
2147 pub stamina_pct: f32,
2148 #[serde(default)]
2149 pub mana_pct: f32,
2150 #[serde(default)]
2151 pub hunger_pct: f32,
2152 #[serde(default)]
2153 pub thirst_pct: f32,
2154}
2155
2156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
2158#[serde(rename_all = "snake_case")]
2159pub enum WorkerRouteKindView {
2160 #[default]
2161 HarvestLoop,
2162 Ordered,
2163}
2164
2165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2167pub struct WorkerRouteView {
2168 #[serde(default)]
2169 pub kind: WorkerRouteKindView,
2170 #[serde(default)]
2171 pub lodging_container_id: Option<String>,
2172 #[serde(default)]
2174 pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
2175 #[serde(default)]
2177 pub harvest_nodes: Vec<String>,
2178 #[serde(default = "default_route_carry_ratio")]
2179 pub carry_return_ratio: f32,
2180 #[serde(default)]
2182 pub stops: Vec<WorkerRouteStopView>,
2183}
2184
2185fn default_route_carry_ratio() -> f32 {
2186 0.90
2187}
2188
2189fn default_true_view() -> bool {
2190 true
2191}
2192
2193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2195pub struct WorkerWithdrawItemView {
2196 pub template: String,
2197 #[serde(default)]
2199 pub qty: u32,
2200 #[serde(default)]
2202 pub all: bool,
2203}
2204
2205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2206pub struct WorkerRouteWaypointView {
2207 pub x: f32,
2208 pub y: f32,
2209 pub z: f32,
2210}
2211
2212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2221#[serde(rename_all = "snake_case")]
2222pub enum WorkerRouteStopView {
2223 Waypoint {
2224 x: f32,
2225 y: f32,
2226 #[serde(default)]
2227 z: f32,
2228 },
2229 HarvestNode {
2230 node_id: String,
2231 },
2232 DepositAt {
2233 container_id: String,
2234 #[serde(default)]
2235 filter: Option<Vec<String>>,
2236 },
2237 TradeWith {
2238 #[serde(default)]
2239 npc_id: Option<String>,
2240 template: String,
2241 #[serde(default = "default_true_view")]
2242 sell_all: bool,
2243 },
2244 ListOnMarket {
2246 template: String,
2247 #[serde(default = "default_true_view")]
2248 list_all: bool,
2249 #[serde(default)]
2250 hall_id: Option<String>,
2251 },
2252 WithdrawFrom {
2253 container_id: String,
2254 items: Vec<WorkerWithdrawItemView>,
2255 },
2256 CraftAt {
2257 device: String,
2258 blueprint: String,
2259 #[serde(default)]
2260 qty: Option<u32>,
2261 },
2262 CultivatePlot {
2263 plot_id: uuid::Uuid,
2264 },
2265 PlantPlot {
2266 plot_id: uuid::Uuid,
2267 seed_template: String,
2268 },
2269 HarvestPlot {
2270 plot_id: uuid::Uuid,
2271 },
2272 RestIfNeeded,
2273 Wait {
2274 #[serde(default)]
2275 wait_ticks: u64,
2276 },
2277}
2278
2279#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2281#[serde(rename_all = "snake_case")]
2282pub enum LedgerCategory {
2283 Workers,
2284 Hire,
2285 Train,
2286 ShopBuy,
2287 Taxes,
2288 WorkerSales,
2289 TraderSales,
2290 BankDeposit,
2291 BankWithdraw,
2292 BankTransferOut,
2293 BankTransferIn,
2294 BankTransferFee,
2295 StorageShipFee,
2296 PropertyBuy,
2298 PropertySell,
2300 TaxShare,
2302 MarketBuy,
2304 MarketSell,
2306 Other,
2307}
2308
2309impl LedgerCategory {
2310 pub fn as_str(self) -> &'static str {
2311 match self {
2312 Self::Workers => "workers",
2313 Self::Hire => "hire",
2314 Self::Train => "train",
2315 Self::ShopBuy => "shop_buy",
2316 Self::Taxes => "taxes",
2317 Self::WorkerSales => "worker_sales",
2318 Self::TraderSales => "trader_sales",
2319 Self::BankDeposit => "bank_deposit",
2320 Self::BankWithdraw => "bank_withdraw",
2321 Self::BankTransferOut => "bank_transfer_out",
2322 Self::BankTransferIn => "bank_transfer_in",
2323 Self::BankTransferFee => "bank_transfer_fee",
2324 Self::StorageShipFee => "storage_ship_fee",
2325 Self::PropertyBuy => "property_buy",
2326 Self::PropertySell => "property_sell",
2327 Self::TaxShare => "tax_share",
2328 Self::MarketBuy => "market_buy",
2329 Self::MarketSell => "market_sell",
2330 Self::Other => "other",
2331 }
2332 }
2333}
2334
2335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2336pub struct LedgerEntryView {
2337 pub id: uuid::Uuid,
2338 pub game_day: u64,
2339 pub signed_copper: i64,
2340 pub category: LedgerCategory,
2341 #[serde(default)]
2342 pub label: String,
2343}
2344
2345#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2346pub struct LedgerPeriodTotals {
2347 #[serde(default)]
2349 pub expenses: std::collections::HashMap<String, u64>,
2350 #[serde(default)]
2352 pub income: std::collections::HashMap<String, u64>,
2353 pub expense_copper: u64,
2354 pub income_copper: u64,
2355 pub cash_flow_copper: i64,
2357}
2358
2359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2360pub struct PlayerLedgerView {
2361 pub current_game_day: u64,
2362 #[serde(default)]
2363 pub period_day: LedgerPeriodTotals,
2364 #[serde(default)]
2365 pub period_week: LedgerPeriodTotals,
2366 #[serde(default)]
2367 pub period_month: LedgerPeriodTotals,
2368 #[serde(default)]
2369 pub period_lifetime: LedgerPeriodTotals,
2370 #[serde(default)]
2371 pub recent: Vec<LedgerEntryView>,
2372 #[serde(default)]
2374 pub wealth_on_person_copper: u64,
2375 #[serde(default)]
2377 pub wealth_in_storage_copper: u64,
2378 #[serde(default)]
2380 pub wealth_in_bank_copper: u64,
2381 #[serde(default)]
2383 pub wealth_total_copper: u64,
2384 #[serde(default)]
2386 pub wealth_in_property_copper: u64,
2387 #[serde(default)]
2389 pub wealth_net_worth_copper: u64,
2390 #[serde(default)]
2392 pub property_assets: Vec<PropertyAssetView>,
2393 #[serde(default)]
2395 pub property_market_nearby: Vec<PropertyMarketCompView>,
2396 #[serde(default)]
2398 pub live_expense_per_interval_copper: u64,
2399 #[serde(default)]
2401 pub live_income_route_est_per_loop_copper: u64,
2402 #[serde(default)]
2404 pub live_income_avg_per_interval_copper: u64,
2405 #[serde(default)]
2407 pub live_income_avg_window_intervals: u32,
2408 #[serde(default)]
2410 pub live_net_avg_per_interval_copper: i64,
2411}
2412
2413#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2415pub struct PropertyAssetView {
2416 pub plot_id: Uuid,
2417 pub label: String,
2419 pub zone_id: String,
2420 #[serde(default)]
2421 pub zone_label: Option<String>,
2422 pub area_m2: f32,
2423 pub purchase_basis_copper: u64,
2425 pub upkeep_copper_per_day: u64,
2426}
2427
2428#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2430pub struct PropertyMarketCompView {
2431 pub day: u64,
2432 pub zone_id: String,
2433 #[serde(default)]
2434 pub zone_label: Option<String>,
2435 pub area_m2: f32,
2436 pub price_copper: u64,
2437 pub price_per_m2_copper: u64,
2439 pub kind: String,
2441 pub distance_m: f32,
2443}
2444
2445#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2447#[serde(rename_all = "snake_case")]
2448pub enum AnalyticsMetric {
2449 NpcKill,
2450 WildlifeKill,
2451 Harvest,
2452 QuestComplete,
2453 QuestAccept,
2454 QuestAbandon,
2455 PlayerDeath,
2456 Craft,
2457 WorkerHire,
2458 WorkerDismiss,
2459 WorkerTeach,
2460 NpcTalk,
2461 ShopBuy,
2462 ShopSell,
2463 PlaceContainer,
2464 PickupContainer,
2465 PickupDrop,
2466 ConsumableUse,
2467 AbilityUse,
2468 DistanceWalkedM,
2469 DoorUse,
2470 BuildingEnter,
2471}
2472
2473impl AnalyticsMetric {
2474 pub fn as_str(self) -> &'static str {
2475 match self {
2476 Self::NpcKill => "npc_kill",
2477 Self::WildlifeKill => "wildlife_kill",
2478 Self::Harvest => "harvest",
2479 Self::QuestComplete => "quest_complete",
2480 Self::QuestAccept => "quest_accept",
2481 Self::QuestAbandon => "quest_abandon",
2482 Self::PlayerDeath => "player_death",
2483 Self::Craft => "craft",
2484 Self::WorkerHire => "worker_hire",
2485 Self::WorkerDismiss => "worker_dismiss",
2486 Self::WorkerTeach => "worker_teach",
2487 Self::NpcTalk => "npc_talk",
2488 Self::ShopBuy => "shop_buy",
2489 Self::ShopSell => "shop_sell",
2490 Self::PlaceContainer => "place_container",
2491 Self::PickupContainer => "pickup_container",
2492 Self::PickupDrop => "pickup_drop",
2493 Self::ConsumableUse => "consumable_use",
2494 Self::AbilityUse => "ability_use",
2495 Self::DistanceWalkedM => "distance_walked_m",
2496 Self::DoorUse => "door_use",
2497 Self::BuildingEnter => "building_enter",
2498 }
2499 }
2500
2501 pub fn from_str_key(s: &str) -> Option<Self> {
2502 Some(match s {
2503 "npc_kill" => Self::NpcKill,
2504 "wildlife_kill" => Self::WildlifeKill,
2505 "harvest" => Self::Harvest,
2506 "quest_complete" => Self::QuestComplete,
2507 "quest_accept" => Self::QuestAccept,
2508 "quest_abandon" => Self::QuestAbandon,
2509 "player_death" => Self::PlayerDeath,
2510 "craft" => Self::Craft,
2511 "worker_hire" => Self::WorkerHire,
2512 "worker_dismiss" => Self::WorkerDismiss,
2513 "worker_teach" => Self::WorkerTeach,
2514 "npc_talk" => Self::NpcTalk,
2515 "shop_buy" => Self::ShopBuy,
2516 "shop_sell" => Self::ShopSell,
2517 "place_container" => Self::PlaceContainer,
2518 "pickup_container" => Self::PickupContainer,
2519 "pickup_drop" => Self::PickupDrop,
2520 "consumable_use" => Self::ConsumableUse,
2521 "ability_use" => Self::AbilityUse,
2522 "distance_walked_m" => Self::DistanceWalkedM,
2523 "door_use" => Self::DoorUse,
2524 "building_enter" => Self::BuildingEnter,
2525 _ => return None,
2526 })
2527 }
2528}
2529
2530#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2531pub struct CareerMetricRow {
2532 pub subject_id: String,
2533 pub amount: u64,
2534}
2535
2536#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2538pub struct PlayerCareerView {
2539 pub current_game_day: u64,
2540 #[serde(default)]
2541 pub kills: Vec<CareerMetricRow>,
2542 #[serde(default)]
2543 pub harvests: Vec<CareerMetricRow>,
2544 pub quests_completed: u64,
2545 #[serde(default)]
2546 pub crafts: Vec<CareerMetricRow>,
2547 pub deaths: u64,
2548 pub npc_talks: u64,
2549 pub shop_buys: u64,
2550 pub shop_sells: u64,
2551 pub distance_m: u64,
2552 #[serde(default)]
2553 pub other: Vec<CareerMetricRow>,
2554}
2555
2556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
2561pub struct WorkerEquipmentView {
2562 #[serde(default)]
2563 pub mainhand: Option<ItemStack>,
2564 #[serde(default)]
2565 pub offhand: Option<ItemStack>,
2566 #[serde(default)]
2567 pub worn: Vec<(BodySlot, ItemStack)>,
2568}
2569
2570#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2572pub struct HiredWorkerView {
2573 pub instance_id: String,
2574 pub entity_id: EntityId,
2575 pub def_id: String,
2576 pub label: String,
2578 pub x: f32,
2579 pub y: f32,
2580 pub z: f32,
2581 pub mode: WorkerModeView,
2582 pub state: WorkerStateView,
2583 #[serde(default)]
2584 pub step_label: String,
2585 pub vitals: WorkerVitalsSummary,
2586 #[serde(default)]
2587 pub carry_pct: f32,
2588 #[serde(default)]
2589 pub last_error: Option<String>,
2590 pub wage_copper_per_interval: u32,
2591 #[serde(default)]
2593 pub effective_wage_copper: u32,
2594 #[serde(default)]
2596 pub wage_meters_walked: f32,
2597 #[serde(default)]
2599 pub lodging_container_id: Option<String>,
2600 #[serde(default)]
2602 pub route: Option<WorkerRouteView>,
2603 #[serde(default)]
2606 pub route_stop_index: Option<u32>,
2607 #[serde(default)]
2609 pub known_blueprint_ids: Vec<String>,
2610 #[serde(default = "default_worker_view_level")]
2612 pub level: u32,
2613 #[serde(default)]
2615 pub worker_xp: f64,
2616 #[serde(default)]
2618 pub inventory: Vec<ItemStack>,
2619 #[serde(default)]
2621 pub equipment: WorkerEquipmentView,
2622 #[serde(default)]
2625 pub issue_hint: Option<String>,
2626 #[serde(default)]
2629 pub has_blocking_issue: bool,
2630 #[serde(default)]
2632 pub harvest_node_issues: Vec<WorkerHarvestNodeIssueView>,
2633}
2634
2635#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2637pub struct WorkerHarvestNodeIssueView {
2638 pub node_id: String,
2639 pub issue: String,
2640}
2641
2642fn default_worker_view_level() -> u32 {
2643 1
2644}
2645
2646#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2648pub struct TickDelta {
2649 pub tick: Tick,
2650 pub entities: Vec<EntityState>,
2651 #[serde(default)]
2652 pub resource_nodes: Vec<ResourceNodeView>,
2653 #[serde(default)]
2654 pub buildings: Vec<BuildingView>,
2655 #[serde(default)]
2656 pub doors: Vec<DoorView>,
2657 #[serde(default)]
2658 pub npcs: Vec<NpcView>,
2659 #[serde(default)]
2661 pub inventory: Vec<ItemStack>,
2662 #[serde(default)]
2663 pub blueprints: Vec<BlueprintView>,
2664 #[serde(default)]
2666 pub building_materials: Vec<BuildingMaterialView>,
2667 #[serde(default)]
2668 pub world_clock: WorldClock,
2669 #[serde(default)]
2670 pub ground_drops: Vec<GroundDropView>,
2671 #[serde(default)]
2672 pub placed_containers: Vec<PlacedContainerView>,
2673 #[serde(default)]
2674 pub combat: Option<CombatHud>,
2675 #[serde(default)]
2676 pub interior_map: Option<InteriorMapView>,
2677 #[serde(default)]
2678 pub quest_log: Vec<QuestLogEntry>,
2679 #[serde(default)]
2680 pub hired_workers: Vec<HiredWorkerView>,
2681 #[serde(default)]
2682 pub interactables: Vec<InteractableView>,
2683 #[serde(default)]
2684 pub ledger: Option<PlayerLedgerView>,
2685 #[serde(default)]
2686 pub career: Option<PlayerCareerView>,
2687 #[serde(default)]
2689 pub combat_fx: Vec<CombatFx>,
2690 #[serde(default)]
2692 pub ground_hazards: Vec<GroundHazardView>,
2693 #[serde(default)]
2695 pub property_plots: Vec<PropertyPlotView>,
2696 #[serde(default)]
2698 pub terrain_overlays: Vec<TerrainZoneView>,
2699}
2700#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2701pub struct GroundDropView {
2702 pub id: String,
2703 pub template_id: String,
2704 pub quantity: u32,
2705 pub x: f32,
2706 pub y: f32,
2707 pub z: f32,
2708 #[serde(default)]
2710 pub tile_id: Option<String>,
2711 #[serde(default)]
2713 pub display_name: Option<String>,
2714 #[serde(default)]
2716 pub yaw: f32,
2717 #[serde(default)]
2719 pub pitch: f32,
2720 #[serde(default)]
2722 pub roll: f32,
2723 #[serde(default = "default_draw_scale")]
2725 pub draw_scale: f32,
2726 #[serde(default)]
2728 pub item_instance_id: Option<Uuid>,
2729 #[serde(default)]
2731 pub props: std::collections::BTreeMap<String, String>,
2732 #[serde(default)]
2734 pub status_bindings: Vec<ItemStatusBinding>,
2735}
2736
2737fn default_draw_scale() -> f32 {
2738 1.0
2739}
2740
2741#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2743pub struct Snapshot {
2744 pub tick: Tick,
2745 pub chunk_rev: u64,
2746 #[serde(default)]
2748 pub content_rev: u64,
2749 #[serde(default)]
2751 pub publish_rev: u64,
2752 pub entities: Vec<EntityState>,
2753 #[serde(default)]
2754 pub resource_nodes: Vec<ResourceNodeView>,
2755 #[serde(default)]
2757 pub world_x0: f32,
2758 #[serde(default)]
2759 pub world_y0: f32,
2760 #[serde(default)]
2762 pub world_width_m: f32,
2763 #[serde(default)]
2764 pub world_height_m: f32,
2765 #[serde(default)]
2766 pub buildings: Vec<BuildingView>,
2767 #[serde(default)]
2768 pub doors: Vec<DoorView>,
2769 #[serde(default)]
2770 pub npcs: Vec<NpcView>,
2771 #[serde(default)]
2772 pub inventory: Vec<ItemStack>,
2773 #[serde(default)]
2774 pub blueprints: Vec<BlueprintView>,
2775 #[serde(default)]
2777 pub building_materials: Vec<BuildingMaterialView>,
2778 #[serde(default)]
2779 pub world_clock: WorldClock,
2780 #[serde(default)]
2781 pub terrain_zones: Vec<TerrainZoneView>,
2782 #[serde(default)]
2783 pub z_platforms: Vec<ZPlatformView>,
2784 #[serde(default)]
2785 pub z_transitions: Vec<ZTransitionView>,
2786 #[serde(default)]
2787 pub ground_drops: Vec<GroundDropView>,
2788 #[serde(default)]
2789 pub placed_containers: Vec<PlacedContainerView>,
2790 #[serde(default)]
2791 pub combat: Option<CombatHud>,
2792 #[serde(default)]
2793 pub interior_map: Option<InteriorMapView>,
2794 #[serde(default)]
2795 pub quest_log: Vec<QuestLogEntry>,
2796 #[serde(default)]
2797 pub hired_workers: Vec<HiredWorkerView>,
2798 #[serde(default)]
2799 pub interactables: Vec<InteractableView>,
2800 #[serde(default)]
2801 pub ledger: Option<PlayerLedgerView>,
2802 #[serde(default)]
2803 pub career: Option<PlayerCareerView>,
2804 #[serde(default)]
2806 pub combat_fx: Vec<CombatFx>,
2807 #[serde(default)]
2809 pub ground_hazards: Vec<GroundHazardView>,
2810 #[serde(default)]
2812 pub property_zones: Vec<PropertyZoneView>,
2813 #[serde(default)]
2815 pub tax_zones: Vec<TaxZoneView>,
2816 #[serde(default)]
2818 pub boundary_zones: Vec<BoundaryZoneView>,
2819 #[serde(default)]
2821 pub encounter_zones: Vec<EncounterZoneView>,
2822 #[serde(default)]
2824 pub growth_zones: Vec<GrowthZoneView>,
2825 #[serde(default)]
2827 pub biome_zones: Vec<BiomeZoneView>,
2828 #[serde(default)]
2830 pub terrain_kind_nav: Vec<TerrainKindNavView>,
2831 #[serde(default)]
2833 pub property_plots: Vec<PropertyPlotView>,
2834 #[serde(default)]
2836 pub property_plot_settings: Option<PropertyPlotSettingsView>,
2837 #[serde(default)]
2840 pub item_catalog: Vec<ItemCatalogEntryView>,
2841}
2842
2843#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2845pub struct ItemCatalogEntryView {
2846 pub template_id: String,
2847 #[serde(default)]
2848 pub display_name: String,
2849 #[serde(default)]
2850 pub category: String,
2851 #[serde(default)]
2853 pub seed_for: Option<String>,
2854}
2855
2856impl ItemCatalogEntryView {
2857 pub fn is_harvest_node(&self) -> bool {
2858 self.category == "harvest_node"
2859 }
2860
2861 pub fn is_depositable_stack(&self) -> bool {
2863 !self.is_harvest_node()
2864 }
2865
2866 pub fn is_farm_seed(&self) -> bool {
2867 self.seed_for
2868 .as_deref()
2869 .is_some_and(|s| !s.trim().is_empty())
2870 || self.category == "seed"
2871 }
2872}
2873
2874#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2876pub struct ResourceNodeView {
2877 pub id: String,
2878 pub label: String,
2879 pub x: f32,
2880 pub y: f32,
2881 pub z: f32,
2882 pub item_template: String,
2883 #[serde(default = "default_node_state")]
2884 pub state: ResourceNodeState,
2885 #[serde(default = "default_blocking_view")]
2887 pub blocking: bool,
2888 #[serde(default = "default_blocking_radius_view")]
2890 pub blocking_radius_m: f32,
2891 #[serde(default)]
2893 pub harvest_off: bool,
2894 #[serde(default)]
2896 pub tile_id: Option<String>,
2897 #[serde(default)]
2899 pub yaw: f32,
2900 #[serde(default)]
2902 pub pitch: f32,
2903 #[serde(default)]
2905 pub roll: f32,
2906 #[serde(default = "default_draw_scale")]
2908 pub draw_scale: f32,
2909 #[serde(default)]
2911 pub sprite_mode: Option<String>,
2912 #[serde(default)]
2914 pub presentation_state: Option<String>,
2915 #[serde(default)]
2918 pub growth_progress: Option<f32>,
2919 #[serde(default)]
2921 pub channel_start_tick: Option<Tick>,
2922 #[serde(default)]
2923 pub channel_end_tick: Option<Tick>,
2924 #[serde(default)]
2926 pub harvest_drop_templates: Vec<String>,
2927}
2928
2929fn default_blocking_radius_view() -> f32 {
2930 0.8
2931}
2932
2933fn default_blocking_view() -> bool {
2934 true
2935}
2936
2937#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2938#[serde(rename_all = "snake_case")]
2939pub enum ResourceNodeState {
2940 Available,
2941 Harvesting,
2942 Cooldown,
2943}
2944fn default_node_state() -> ResourceNodeState {
2945 ResourceNodeState::Available
2946}
2947
2948#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2950#[serde(rename_all = "snake_case")]
2951pub enum ItemSpawnStateView {
2952 Spawned,
2953 PickedUp { respawn_at_tick: u64 },
2954 Consumed,
2955}
2956
2957#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2959pub struct ItemSpawnView {
2960 pub id: String,
2961 pub label: String,
2962 pub item_template: String,
2963 pub quantity: u32,
2964 pub x: f32,
2965 pub y: f32,
2966 pub z: f32,
2967 pub respawn_ticks: u32,
2968 #[serde(default)]
2969 pub building_id: Option<String>,
2970 pub state: ItemSpawnStateView,
2971 #[serde(default)]
2973 pub once_per_character: bool,
2974 #[serde(default)]
2976 pub collected_count: u32,
2977}
2978
2979#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2980#[serde(rename_all = "snake_case")]
2981pub enum ItemStatusBindingMode {
2982 OnHit,
2983 WhileEquipped,
2984}
2985
2986impl Default for ItemStatusBindingMode {
2987 fn default() -> Self {
2988 Self::OnHit
2989 }
2990}
2991
2992#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2994pub struct ItemStatusBinding {
2995 pub effect_id: String,
2996 #[serde(default)]
2997 pub mode: ItemStatusBindingMode,
2998 #[serde(default)]
3000 pub source: String,
3001 #[serde(default)]
3002 pub applied_at_tick: u64,
3003 #[serde(default)]
3006 pub expires_at_tick: Option<u64>,
3007}
3008
3009#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3010pub struct ItemStack {
3011 pub template_id: String,
3012 pub quantity: u32,
3013 #[serde(default)]
3015 pub item_instance_id: Option<Uuid>,
3016 #[serde(default)]
3018 pub props: BTreeMap<String, String>,
3019 #[serde(default)]
3021 pub status_bindings: Vec<ItemStatusBinding>,
3022 #[serde(default)]
3024 pub contents: Vec<ItemStack>,
3025 #[serde(default)]
3027 pub display_name: Option<String>,
3028 #[serde(default)]
3030 pub category: Option<String>,
3031 #[serde(default)]
3033 pub base_mass: Option<f32>,
3034 #[serde(default)]
3036 pub base_volume: Option<f32>,
3037 #[serde(default)]
3039 pub capacity_volume: Option<f32>,
3040 #[serde(default)]
3042 pub stackable: Option<bool>,
3043 #[serde(default)]
3045 pub world_placeable: Option<bool>,
3046 #[serde(default)]
3048 pub worker_lodging_capacity: Option<u32>,
3049 #[serde(default)]
3051 pub equip_slot: Option<BodySlot>,
3052 #[serde(default)]
3054 pub armor_physical: Option<f32>,
3055 #[serde(default)]
3057 pub resists: Vec<(String, f32)>,
3058 #[serde(default)]
3060 pub hand_slots: Option<u8>,
3061 #[serde(default)]
3063 pub listable: Option<bool>,
3064 #[serde(default)]
3066 pub base_value_copper: Option<u32>,
3067}
3068
3069impl ItemStack {
3070 pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
3071 Self {
3072 template_id: template_id.into(),
3073 quantity,
3074 ..Default::default()
3075 }
3076 }
3077}
3078
3079impl Default for ItemStack {
3080 fn default() -> Self {
3081 Self {
3082 template_id: String::new(),
3083 quantity: 0,
3084 item_instance_id: None,
3085 props: BTreeMap::new(),
3086 status_bindings: Vec::new(),
3087 contents: Vec::new(),
3088 display_name: None,
3089 category: None,
3090 base_mass: None,
3091 base_volume: None,
3092 capacity_volume: None,
3093 stackable: None,
3094 world_placeable: None,
3095 worker_lodging_capacity: None,
3096 equip_slot: None,
3097 armor_physical: None,
3098 resists: Vec::new(),
3099 hand_slots: None,
3100 listable: None,
3101 base_value_copper: None,
3102 }
3103 }
3104}
3105
3106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3108#[serde(rename_all = "snake_case")]
3109pub enum EncumbranceState {
3110 #[default]
3111 Light,
3112 Heavy,
3113 Orange,
3114 Over,
3115}
3116
3117impl EncumbranceState {
3118 pub fn label(self) -> &'static str {
3120 match self {
3121 Self::Light => "Light",
3122 Self::Heavy => "Heavy",
3123 Self::Orange => "Overloaded",
3124 Self::Over => "Over",
3125 }
3126 }
3127
3128 pub fn allows_sprint(self) -> bool {
3130 !matches!(self, Self::Over)
3131 }
3132}
3133
3134#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
3138#[serde(rename_all = "snake_case")]
3139pub enum BodySlot {
3140 Head,
3141 #[serde(alias = "body")]
3143 Chest,
3144 #[serde(alias = "arms")]
3146 Forearms,
3147 Legs,
3148 Feet,
3149 Cloak,
3150 Back,
3151 Waist,
3152 Earrings,
3153 Necklace,
3154 Eyeglasses,
3155 #[serde(rename = "ring_left_1", alias = "ring_left1")]
3157 RingLeft1,
3158 #[serde(rename = "ring_left_2", alias = "ring_left2")]
3159 RingLeft2,
3160 #[serde(rename = "ring_right_1", alias = "ring_right1")]
3161 RingRight1,
3162 #[serde(rename = "ring_right_2", alias = "ring_right2")]
3163 RingRight2,
3164}
3165
3166impl BodySlot {
3167 pub const ALL: [BodySlot; 15] = [
3169 BodySlot::Head,
3170 BodySlot::Chest,
3171 BodySlot::Forearms,
3172 BodySlot::Legs,
3173 BodySlot::Feet,
3174 BodySlot::Cloak,
3175 BodySlot::Back,
3176 BodySlot::Waist,
3177 BodySlot::Earrings,
3178 BodySlot::Necklace,
3179 BodySlot::Eyeglasses,
3180 BodySlot::RingLeft1,
3181 BodySlot::RingLeft2,
3182 BodySlot::RingRight1,
3183 BodySlot::RingRight2,
3184 ];
3185
3186 pub fn as_str(self) -> &'static str {
3187 match self {
3188 BodySlot::Head => "head",
3189 BodySlot::Chest => "chest",
3190 BodySlot::Forearms => "forearms",
3191 BodySlot::Legs => "legs",
3192 BodySlot::Feet => "feet",
3193 BodySlot::Cloak => "cloak",
3194 BodySlot::Back => "back",
3195 BodySlot::Waist => "waist",
3196 BodySlot::Earrings => "earrings",
3197 BodySlot::Necklace => "necklace",
3198 BodySlot::Eyeglasses => "eyeglasses",
3199 BodySlot::RingLeft1 => "ring_left_1",
3200 BodySlot::RingLeft2 => "ring_left_2",
3201 BodySlot::RingRight1 => "ring_right_1",
3202 BodySlot::RingRight2 => "ring_right_2",
3203 }
3204 }
3205}
3206
3207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3209#[serde(rename_all = "snake_case")]
3210pub enum InventoryLocation {
3211 Root,
3213 Worn { slot: BodySlot },
3215 Placed { container_id: String },
3217 Keychain,
3219 WhisperPouch,
3221}
3222
3223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3225pub struct PlacedContainerView {
3226 pub id: String,
3227 pub template_id: String,
3228 pub display_name: String,
3229 pub x: f32,
3230 pub y: f32,
3231 pub z: f32,
3232 pub locked: bool,
3233 #[serde(default)]
3235 pub accessible: bool,
3236 #[serde(default)]
3237 pub owner_character_id: Option<Uuid>,
3238 #[serde(default)]
3240 pub contents: Vec<ItemStack>,
3241 #[serde(default)]
3243 pub lock_id: Option<String>,
3244 #[serde(default)]
3246 pub capacity_volume: Option<f32>,
3247 #[serde(default)]
3249 pub item_instance_id: Option<Uuid>,
3250 #[serde(default)]
3252 pub tile_id: Option<String>,
3253 #[serde(default)]
3255 pub worker_lodging_capacity: Option<u32>,
3256 #[serde(default)]
3258 pub blocking: bool,
3259 #[serde(default)]
3261 pub blocking_radius_m: f32,
3262 #[serde(default)]
3265 pub building_id: Option<String>,
3266}
3267
3268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3269pub struct BlueprintIngredientView {
3270 pub template_id: String,
3271 pub quantity: u32,
3272 pub consumed: bool,
3274 #[serde(default)]
3276 pub display_name: String,
3277}
3278
3279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3280pub struct ToolRequirementView {
3281 pub item: String,
3282 pub consumed: bool,
3284 #[serde(default)]
3286 pub display_name: String,
3287}
3288
3289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3290pub struct SkillRequirementView {
3291 pub skill: String,
3292 pub level: u32,
3293}
3294
3295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3296pub struct BlueprintView {
3297 pub id: String,
3298 pub label: String,
3299 pub output: String,
3300 pub output_qty: u32,
3301 pub craft_ticks: u32,
3302 pub inputs: Vec<BlueprintIngredientView>,
3303 #[serde(default)]
3305 pub station: Option<String>,
3306 #[serde(default)]
3307 pub category: Option<String>,
3308 #[serde(default)]
3309 pub required_tools: Vec<ToolRequirementView>,
3310 #[serde(default)]
3311 pub skill: Option<SkillRequirementView>,
3312 #[serde(default)]
3313 pub failure_chance: f32,
3314 #[serde(default)]
3316 pub worker_train_copper: u64,
3317 #[serde(default)]
3319 pub output_display_name: String,
3320 #[serde(default)]
3322 pub craft_tier: u32,
3323}
3324
3325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3327pub struct TerrainKindNavView {
3328 pub kind: TerrainKindView,
3329 #[serde(default = "default_move_speed_mult_one")]
3330 pub move_speed_mult: f32,
3331 #[serde(default)]
3332 pub impassable: bool,
3333}
3334
3335fn default_move_speed_mult_one() -> f32 {
3336 1.0
3337}
3338
3339#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3341#[serde(rename_all = "snake_case")]
3342pub enum TerrainKindView {
3343 #[default]
3344 Grass,
3345 Dirt,
3346 Tilled,
3347 Desert,
3348 Hill,
3349 Bog,
3350 Beach,
3351 ShallowWater,
3352 DeepWater,
3353 Trail,
3354 Road,
3355 Rock,
3356}
3357
3358#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3359pub struct TerrainZoneView {
3360 pub id: String,
3361 pub x0: f32,
3362 pub y0: f32,
3363 pub x1: f32,
3364 pub y1: f32,
3365 #[serde(default)]
3366 pub kind: TerrainKindView,
3367 #[serde(default)]
3369 pub elevation: f32,
3370 #[serde(default)]
3373 pub glyph: Option<String>,
3374 #[serde(default)]
3376 pub color: Option<String>,
3377 #[serde(default)]
3379 pub tile_id: Option<String>,
3380 #[serde(default)]
3382 pub z_order: i32,
3383 #[serde(default)]
3385 pub channel_start_tick: Option<Tick>,
3386 #[serde(default)]
3387 pub channel_end_tick: Option<Tick>,
3388}
3389
3390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3392pub struct ZoneRectView {
3393 pub x0: f32,
3394 pub y0: f32,
3395 pub x1: f32,
3396 pub y1: f32,
3397}
3398
3399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3401pub struct PropertyZoneView {
3402 pub id: String,
3403 #[serde(default)]
3405 pub label: Option<String>,
3406 pub rects: Vec<ZoneRectView>,
3407 #[serde(default)]
3408 pub z_order: i32,
3409 pub crown_price_copper: u64,
3410 pub upkeep_copper_per_day: u64,
3411 #[serde(default)]
3412 pub max_area_m2: Option<f32>,
3413 #[serde(default)]
3414 pub owner_tax_discount_bps: u32,
3415}
3416
3417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3419pub struct TaxZoneView {
3420 pub id: String,
3421 #[serde(default)]
3422 pub label: Option<String>,
3423 pub rects: Vec<ZoneRectView>,
3424 #[serde(default)]
3425 pub z_order: i32,
3426 pub rate_bps: u32,
3427 #[serde(default)]
3428 pub flat_copper: u64,
3429 #[serde(default)]
3431 pub market_sales_tax_bps: u32,
3432 #[serde(default)]
3434 pub market_sales_flat_copper: u32,
3435}
3436
3437#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3439pub struct BoundaryZoneView {
3440 pub id: String,
3441 #[serde(default)]
3442 pub label: Option<String>,
3443 pub rects: Vec<ZoneRectView>,
3444 #[serde(default)]
3445 pub z_order: i32,
3446 #[serde(default, skip_serializing_if = "Option::is_none")]
3447 pub jurisdiction_id: Option<String>,
3448 #[serde(default = "default_true")]
3449 pub worker_logistics: bool,
3450 #[serde(default)]
3451 pub security_tier: String,
3452 #[serde(default)]
3453 pub pvp_mode: String,
3454 #[serde(default = "default_true")]
3455 pub crime_enabled: bool,
3456 #[serde(default)]
3457 pub guard_response: bool,
3458 #[serde(default)]
3460 pub pass_through_props: bool,
3461 #[serde(default)]
3463 pub presence_mode: String,
3464}
3465
3466#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3468pub struct EncounterZoneView {
3469 pub id: String,
3470 #[serde(default)]
3471 pub label: Option<String>,
3472 pub rects: Vec<ZoneRectView>,
3473 #[serde(default)]
3474 pub z_order: i32,
3475}
3476
3477fn default_true() -> bool {
3478 true
3479}
3480
3481#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3483pub struct GrowthZoneView {
3484 pub id: String,
3485 #[serde(default)]
3486 pub label: Option<String>,
3487 pub rects: Vec<ZoneRectView>,
3488 #[serde(default)]
3489 pub z_order: i32,
3490 #[serde(default = "default_one_f32")]
3491 pub fertility: f32,
3492}
3493
3494fn default_one_f32() -> f32 {
3495 1.0
3496}
3497
3498#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3500pub struct BiomeZoneView {
3501 pub id: String,
3502 #[serde(default)]
3503 pub label: Option<String>,
3504 pub rects: Vec<ZoneRectView>,
3505 #[serde(default)]
3506 pub z_order: i32,
3507 pub biome_id: String,
3508}
3509
3510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3512pub struct FarmGrantView {
3513 pub character_id: Uuid,
3514 #[serde(default)]
3516 pub character_label: String,
3517 pub tax_discount_bps: u32,
3518}
3519
3520#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3522pub struct PropertyPlotView {
3523 pub plot_id: Uuid,
3524 pub property_zone_id: String,
3525 #[serde(default)]
3526 pub zone_label: Option<String>,
3527 pub deed_instance_id: Uuid,
3528 pub x0: f32,
3529 pub y0: f32,
3530 pub x1: f32,
3531 pub y1: f32,
3532 pub upkeep_copper_per_day: u64,
3533 pub arrears_days: u32,
3534 #[serde(default)]
3536 pub is_mine: bool,
3537 #[serde(default)]
3539 pub may_farm: bool,
3540 #[serde(default)]
3542 pub purchase_basis_copper: u64,
3543 #[serde(default)]
3544 pub farm_public: bool,
3545 #[serde(default)]
3546 pub public_tax_discount_bps: u32,
3547 #[serde(default)]
3548 pub farm_allow: Vec<FarmGrantView>,
3549 #[serde(default)]
3551 pub owner_character_id: Option<Uuid>,
3552 #[serde(default)]
3553 pub owner_label: Option<String>,
3554 #[serde(default)]
3556 pub building_id: Option<String>,
3557 #[serde(default)]
3559 pub plot_code: String,
3560 #[serde(default)]
3562 pub label: String,
3563}
3564
3565#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3567pub struct PropertyPlotSettingsView {
3568 pub min_plot_area_m2: f32,
3569 pub tax_premium_weight: f32,
3570 pub sellback_bps: u32,
3571}
3572
3573#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3575pub struct ZPlatformView {
3576 pub id: String,
3577 pub z: f32,
3578 pub x0: f32,
3579 pub y0: f32,
3580 pub x1: f32,
3581 pub y1: f32,
3582}
3583
3584#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3586pub struct ZTransitionView {
3587 pub id: String,
3588 pub z_from: f32,
3589 pub z_to: f32,
3590 pub x0: f32,
3591 pub y0: f32,
3592 pub x1: f32,
3593 pub y1: f32,
3594}
3595
3596#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3597pub struct BuildingView {
3598 pub id: String,
3599 pub label: String,
3600 pub x: f32,
3601 pub y: f32,
3602 pub width_m: f32,
3603 pub depth_m: f32,
3604 #[serde(default)]
3605 pub interior_blueprint: Option<String>,
3606 #[serde(default)]
3607 pub tags: Vec<String>,
3608 #[serde(default)]
3610 pub market_boundary_zone_ids: Vec<String>,
3611 #[serde(default)]
3613 pub market_max_volume: Option<f32>,
3614 #[serde(default)]
3617 pub wall_set: Option<String>,
3618 #[serde(default)]
3620 pub roof_set: Option<String>,
3621}
3622
3623pub const DEFAULT_BUILDING_ART_SET: &str = "classic_stone";
3626
3627impl BuildingView {
3628 pub fn effective_wall_set(&self) -> &str {
3629 self.wall_set
3630 .as_deref()
3631 .filter(|s| !s.is_empty())
3632 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3633 }
3634
3635 pub fn effective_roof_set(&self) -> &str {
3636 self.roof_set
3637 .as_deref()
3638 .filter(|s| !s.is_empty())
3639 .unwrap_or(DEFAULT_BUILDING_ART_SET)
3640 }
3641}
3642
3643#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3644pub struct DoorView {
3645 pub id: String,
3646 pub building_id: String,
3647 pub x: f32,
3648 pub y: f32,
3649 #[serde(default)]
3650 pub open: bool,
3651 #[serde(default)]
3652 pub portal: Option<String>,
3653 #[serde(default)]
3656 pub locked: bool,
3657 #[serde(default = "default_door_accessible")]
3659 pub accessible: bool,
3660 #[serde(default)]
3661 pub lock_id: Option<Uuid>,
3662}
3663
3664fn default_door_accessible() -> bool {
3665 true
3666}
3667
3668#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3670pub struct InteriorRoomEdit {
3671 pub id: String,
3672 pub label: String,
3673 pub x0: f32,
3674 pub y0: f32,
3675 pub x1: f32,
3676 pub y1: f32,
3677}
3678
3679#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3680pub struct InteriorRoomDoorEdit {
3681 pub id: String,
3682 pub room_a: String,
3683 pub room_b: String,
3684 pub x: f32,
3685 pub y: f32,
3686}
3687
3688#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3689pub struct InteriorRoomView {
3690 pub id: String,
3691 pub label: String,
3692 pub floor: i32,
3693 pub x0: f32,
3694 pub y0: f32,
3695 pub x1: f32,
3696 pub y1: f32,
3697 #[serde(default)]
3698 pub floor_color: Option<String>,
3699 #[serde(default)]
3700 pub floor_glyph: Option<String>,
3701}
3702
3703#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3704pub struct InteriorDoorView {
3705 pub id: String,
3706 pub room_a: String,
3707 pub room_b: String,
3708 pub x: f32,
3709 pub y: f32,
3710 pub kind: String,
3711 #[serde(default)]
3712 pub x_a: Option<f32>,
3713 #[serde(default)]
3714 pub y_a: Option<f32>,
3715 #[serde(default)]
3716 pub x_b: Option<f32>,
3717 #[serde(default)]
3718 pub y_b: Option<f32>,
3719}
3720
3721#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3722pub struct InteriorMapView {
3723 pub building_id: String,
3724 pub blueprint_id: String,
3725 pub background_color: String,
3726 #[serde(default)]
3727 pub default_floor_color: Option<String>,
3728 #[serde(default = "default_floor_height_view")]
3729 pub floor_height_m: f32,
3730 #[serde(default)]
3732 pub z_platforms: Vec<ZPlatformView>,
3733 #[serde(default)]
3734 pub z_transitions: Vec<ZTransitionView>,
3735 pub rooms: Vec<InteriorRoomView>,
3736 #[serde(default)]
3737 pub room_doors: Vec<InteriorDoorView>,
3738}
3739
3740fn default_floor_height_view() -> f32 {
3741 3.0
3742}
3743
3744#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3745pub struct NpcView {
3746 pub id: String,
3747 pub label: String,
3748 pub role: String,
3749 pub x: f32,
3750 pub y: f32,
3751 #[serde(default)]
3753 pub building_id: Option<String>,
3754 #[serde(default)]
3756 pub entity_id: Option<EntityId>,
3757 #[serde(default)]
3758 pub life_state: Option<LifeState>,
3759 #[serde(default)]
3760 pub hp_pct: Option<f32>,
3761 #[serde(default)]
3763 pub can_trade: bool,
3764 #[serde(default)]
3766 pub buy_templates: Vec<String>,
3767 #[serde(default)]
3769 pub tile_id: Option<String>,
3770 #[serde(default)]
3772 pub behavior_state: Option<String>,
3773 #[serde(default)]
3775 pub presentation_state: Option<String>,
3776 #[serde(default)]
3778 pub sprite_mode: Option<String>,
3779 #[serde(default)]
3781 pub paperdoll_ref: Option<String>,
3782 #[serde(default = "default_draw_scale")]
3784 pub draw_scale: f32,
3785 #[serde(default)]
3787 pub yaw: Option<f32>,
3788 #[serde(default)]
3790 pub perception_fov_deg: Option<f32>,
3791 #[serde(default)]
3793 pub perception_sight_m: Option<f32>,
3794 #[serde(default)]
3796 pub perception_hear_m: Option<f32>,
3797 #[serde(default)]
3799 pub quest_verbs: Vec<NpcQuestVerb>,
3800}
3801
3802#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3804pub struct NpcQuestVerb {
3805 pub quest_id: String,
3806 pub label: String,
3808 pub kind: String,
3809}
3810
3811impl NpcQuestVerb {
3812 pub const KIND_OFFER: &'static str = "offer";
3813 pub const KIND_TALK: &'static str = "talk";
3814 pub const KIND_GIVE: &'static str = "give";
3815}
3816
3817#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3818pub struct UseResult {
3819 pub template_id: String,
3820 pub hunger_restored: f32,
3821 pub thirst_restored: f32,
3822 #[serde(default)]
3823 pub health_restored: f32,
3824 #[serde(default)]
3825 pub mana_restored: f32,
3826 #[serde(default)]
3827 pub cleared_dot_ids: Vec<String>,
3828}
3829
3830#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3831pub struct CraftResult {
3832 pub blueprint_id: String,
3833 pub outputs: Vec<ItemStack>,
3834 pub consumed: Vec<ItemStack>,
3835 #[serde(default = "default_one")]
3837 pub batch_index: u32,
3838 #[serde(default = "default_one")]
3840 pub batch_total: u32,
3841}
3842
3843fn default_one() -> u32 {
3844 1
3845}
3846
3847#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3848pub struct DeathNotice {
3849 pub entity_id: EntityId,
3850 pub respawn_x: f32,
3851 pub respawn_y: f32,
3852 pub message: String,
3853}
3854
3855#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3856pub struct InteractionNotice {
3857 pub target_id: String,
3858 pub message: String,
3859 #[serde(default)]
3860 pub coins_delta: i32,
3861 #[serde(default)]
3862 pub inventory_delta: Vec<ItemStack>,
3863}
3864
3865#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3866#[serde(rename_all = "snake_case")]
3867pub enum NpcTalkTrustFlag {
3868 Stranger,
3869 Acquainted,
3870 Trusted,
3871}
3872
3873#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3874#[serde(rename_all = "snake_case")]
3875pub enum NpcTalkDepth {
3876 #[default]
3877 Full,
3878 Brief,
3879 Unavailable,
3880}
3881
3882#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3883pub struct NpcTalkOpened {
3884 pub npc_id: String,
3885 pub npc_label: String,
3886 pub greeting: String,
3887 pub trust_flag: NpcTalkTrustFlag,
3888 #[serde(default)]
3889 pub talk_depth: NpcTalkDepth,
3890 #[serde(default = "default_true")]
3891 pub trade_allowed: bool,
3892 #[serde(default)]
3894 pub suggested_topics: Vec<String>,
3895}
3896
3897#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3898pub struct NpcTalkPending {
3899 pub npc_id: String,
3900}
3901
3902#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3903pub struct NpcTalkReply {
3904 pub npc_id: String,
3905 pub line: String,
3906 pub trust_flag: NpcTalkTrustFlag,
3907 #[serde(default)]
3908 pub wind_down: bool,
3909 #[serde(default)]
3910 pub trade_disabled: bool,
3911}
3912
3913#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3914pub struct NpcTalkClosed {
3915 pub npc_id: String,
3916}
3917
3918#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3919pub struct NpcTalkError {
3920 pub npc_id: String,
3921 pub reason: String,
3922}
3923
3924#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3925#[serde(rename_all = "snake_case")]
3926pub enum QuestStatusView {
3927 Available,
3928 Active,
3929 Completed,
3930}
3931
3932#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3933pub struct QuestObjectiveProgress {
3934 pub label: String,
3935 pub current: u32,
3936 pub required: u32,
3937 pub done: bool,
3938 #[serde(default)]
3942 pub kind: String,
3943 #[serde(default)]
3944 pub npc_ref: Option<String>,
3945 #[serde(default)]
3946 pub item_template: Option<String>,
3947 #[serde(default)]
3948 pub blueprint_id: Option<String>,
3949 #[serde(default)]
3950 pub building_id: Option<String>,
3951}
3952
3953#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3955pub struct QuestRewardItemView {
3956 pub template_id: String,
3957 #[serde(default)]
3959 pub display_name: String,
3960 pub quantity: u32,
3961}
3962
3963#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
3965pub struct QuestRewardView {
3966 #[serde(default)]
3967 pub coins: u32,
3968 #[serde(default)]
3969 pub items: Vec<QuestRewardItemView>,
3970}
3971
3972impl QuestRewardView {
3973 pub fn is_empty(&self) -> bool {
3974 self.coins == 0 && self.items.is_empty()
3975 }
3976}
3977
3978#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
3979#[serde(rename_all = "snake_case")]
3980pub enum QuestStepStatusView {
3981 #[default]
3982 Pending,
3983 Current,
3984 Done,
3985}
3986
3987#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3989pub struct QuestStepView {
3990 pub id: String,
3991 pub title: String,
3992 #[serde(default)]
3993 pub status: QuestStepStatusView,
3994 #[serde(default)]
3995 pub reward: QuestRewardView,
3996}
3997
3998#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3999pub struct QuestLogEntry {
4000 pub quest_id: String,
4001 pub title: String,
4002 pub description: String,
4003 pub status: QuestStatusView,
4004 #[serde(default)]
4005 pub current_step_id: Option<String>,
4006 #[serde(default)]
4007 pub current_step_title: String,
4008 #[serde(default)]
4009 pub current_step_index: u32,
4010 #[serde(default)]
4011 pub objectives: Vec<QuestObjectiveProgress>,
4012 #[serde(default)]
4014 pub current_step_reward: QuestRewardView,
4015 #[serde(default)]
4017 pub completion_reward: QuestRewardView,
4018 #[serde(default)]
4020 pub steps: Vec<QuestStepView>,
4021 #[serde(default)]
4022 pub is_tracked: bool,
4023 #[serde(default)]
4024 pub can_withdraw: bool,
4025}
4026
4027#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4028pub struct InteractableView {
4029 pub id: String,
4030 pub kind: String,
4031 pub label: String,
4032 pub x: f32,
4033 pub y: f32,
4034 pub z: f32,
4035 #[serde(default)]
4036 pub board_id: Option<String>,
4037}
4038
4039#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4040pub struct QuestOffer {
4041 pub quest_id: String,
4042 pub title: String,
4043 pub description: String,
4044 #[serde(default)]
4045 pub step_count: u32,
4046}
4047
4048#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4049pub struct QuestCatalogEntry {
4050 pub quest_id: String,
4051 pub title: String,
4052 pub description: String,
4053 pub step_count: u32,
4054 #[serde(default)]
4055 pub board_ids: Vec<String>,
4056}
4057
4058#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4059pub struct QuestCatalogUpdated {
4060 pub revision: u64,
4061 pub game_day: String,
4062 #[serde(default)]
4063 pub accepted: Vec<QuestCatalogEntry>,
4064 #[serde(default)]
4065 pub retired: Vec<String>,
4066}
4067
4068#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4069pub struct QuestNotice {
4070 pub quest_id: String,
4071 pub title: String,
4072 pub message: String,
4073}
4074
4075#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4076#[serde(rename_all = "snake_case")]
4077pub enum ShopOfferKind {
4078 Item,
4079 Blueprint,
4080}
4081
4082#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4083pub struct ShopOffer {
4084 pub offer_id: String,
4085 pub kind: ShopOfferKind,
4086 pub label: String,
4087 #[serde(default)]
4088 pub template_id: Option<String>,
4089 #[serde(default)]
4090 pub blueprint_id: Option<String>,
4091 pub price_copper: u32,
4092 #[serde(default)]
4093 pub affordable: bool,
4094 #[serde(default)]
4095 pub already_owned: bool,
4096}
4097
4098#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4099pub struct ShopBuyLine {
4100 pub template_id: String,
4101 pub label: String,
4102 pub quantity: u32,
4103 pub price_copper: u32,
4104}
4105
4106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4108pub struct BankPanel {
4109 pub npc_id: String,
4110 pub npc_label: String,
4111 pub bank_balance_copper: u64,
4112 pub on_person_copper: u64,
4113 #[serde(default)]
4115 pub pending_outgoing_copper: u64,
4116 #[serde(default)]
4117 pub transfer_fee_bps: u32,
4118 #[serde(default)]
4119 pub transfer_clear_ticks: u64,
4120}
4121
4122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4124pub struct StoragePanel {
4125 pub npc_id: String,
4126 pub npc_label: String,
4127 pub building_id: String,
4128 pub building_label: String,
4129 pub used_volume: f32,
4130 pub max_volume: f32,
4131 #[serde(default)]
4132 pub contents: Vec<ItemStack>,
4133 #[serde(default)]
4135 pub ship_destinations: Vec<StorageShipDest>,
4136}
4137
4138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4139pub struct StorageShipDest {
4140 pub building_id: String,
4141 pub label: String,
4142 pub distance_m: f32,
4143 pub fee_copper: u64,
4144 pub travel_ticks: u64,
4145}
4146
4147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4150pub enum GoodsLocation {
4151 Person,
4153 TownStorage { building_id: String },
4156}
4157
4158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4161pub struct MarketListingView {
4162 pub listing_id: Uuid,
4163 pub seller_character_id: Uuid,
4164 pub seller_label: String,
4166 pub hall_building_id: String,
4167 pub hall_label: String,
4168 pub template_id: String,
4169 pub display_name: String,
4170 #[serde(default)]
4172 pub category: String,
4173 pub quantity: u32,
4174 pub unit_price_copper: u64,
4175 pub line_total_copper: u64,
4177 #[serde(default)]
4179 pub npc_price: bool,
4180 #[serde(default)]
4183 pub npc_dump_unit_copper: Option<u32>,
4184 pub mine: bool,
4186}
4187
4188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4190pub struct MarketListVault {
4191 pub building_id: String,
4192 pub building_label: String,
4194 #[serde(default)]
4195 pub contents: Vec<ItemStack>,
4196}
4197
4198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4201pub struct MarketPanel {
4202 pub npc_id: String,
4203 pub npc_label: String,
4204 pub building_id: String,
4205 pub building_label: String,
4206 pub used_volume: f32,
4208 pub max_volume: f32,
4209 #[serde(default)]
4212 pub listings: Vec<MarketListingView>,
4213 #[serde(default)]
4215 pub tax_bps: u32,
4216 #[serde(default)]
4217 pub tax_flat_copper: u32,
4218 #[serde(default)]
4220 pub list_vaults: Vec<MarketListVault>,
4221}
4222
4223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4224pub struct ShopCatalog {
4225 pub npc_id: String,
4226 pub npc_label: String,
4227 #[serde(default)]
4228 pub sells: Vec<ShopOffer>,
4229 #[serde(default)]
4230 pub buys: Vec<ShopBuyLine>,
4231}
4232
4233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4234pub struct HarvestResult {
4235 pub node_id: String,
4236 pub quantity: u32,
4238 pub item_template: String,
4239 #[serde(default)]
4242 pub item_instance_id: Option<Uuid>,
4243}
4244
4245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4247pub struct Envelope<T> {
4248 pub protocol_version: u16,
4249 pub payload: T,
4250}
4251
4252impl<T> Envelope<T> {
4253 pub fn new(payload: T) -> Self {
4254 Self {
4255 protocol_version: crate::PROTOCOL_VERSION,
4256 payload,
4257 }
4258 }
4259}
4260
4261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4263pub struct Hello {
4264 pub client_name: String,
4265 pub protocol_version: u16,
4266 #[serde(default)]
4267 pub auth: AuthCredential,
4268 #[serde(default)]
4270 pub character_id: Option<Uuid>,
4271}
4272
4273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4276#[serde(rename_all = "snake_case")]
4277pub enum AuthCredential {
4278 DevLocal,
4279 Session { token: String },
4280 ApiToken { token: String, character_id: Uuid },
4281}
4282
4283impl Default for AuthCredential {
4284 fn default() -> Self {
4285 Self::DevLocal
4286 }
4287}
4288
4289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4290pub struct Welcome {
4291 pub session_id: SessionId,
4292 pub entity_id: EntityId,
4293 pub snapshot: Snapshot,
4294}
4295
4296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4297pub enum ServerMessage {
4298 Welcome(Welcome),
4299 ContentUpdated(Snapshot),
4301 Tick(TickDelta),
4302 IntentAck {
4303 entity_id: EntityId,
4304 seq: Seq,
4305 tick: Tick,
4306 },
4307 Chat(ChatMessage),
4308 HarvestResult(HarvestResult),
4309 UseResult(UseResult),
4310 CraftResult(CraftResult),
4311 Death(DeathNotice),
4312 Interaction(InteractionNotice),
4313 ShopOpened(ShopCatalog),
4314 NpcTalkOpened(NpcTalkOpened),
4315 NpcTalkPending(NpcTalkPending),
4316 NpcTalkReply(NpcTalkReply),
4317 NpcTalkClosed(NpcTalkClosed),
4318 NpcTalkError(NpcTalkError),
4319 QuestOffer(QuestOffer),
4320 QuestAccepted(QuestNotice),
4321 QuestWithdrawn(QuestNotice),
4322 QuestStepCompleted(QuestNotice),
4323 QuestCompleted(QuestNotice),
4324 QuestCatalogUpdated(QuestCatalogUpdated),
4325 BankOpened(BankPanel),
4327 StorageOpened(StoragePanel),
4329 MarketOpened(MarketPanel),
4331 TradeOpened(TradePanel),
4333 TradeClosed {
4335 reason: String,
4336 },
4337 ConnectRejected {
4340 reason: String,
4341 },
4342}
4343
4344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4346pub struct TradePanel {
4347 pub peer_entity_id: EntityId,
4348 pub peer_name: String,
4349 pub my_presented: Vec<ItemStack>,
4350 pub their_presented: Vec<ItemStack>,
4351 pub i_ready: bool,
4352 pub they_ready: bool,
4353 pub my_mass_after: f32,
4355 pub my_mass_max: f32,
4356 pub my_encumbrance_after: EncumbranceState,
4357 pub overburden_warning: bool,
4359}
4360
4361#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4362pub enum ClientMessage {
4363 Hello(Hello),
4364 Intent(Intent),
4365 Disconnect,
4366}
4367
4368#[cfg(test)]
4369mod tests {
4370 use super::*;
4371
4372 #[test]
4373 fn pristine_vitals_state_yields_full_pools() {
4374 let attrs = PrimaryAttributes::default();
4375 let vitals = StoredVitalsState::default().apply_to(attrs);
4376 assert!(vitals.health > 0.0);
4377 assert_eq!(vitals.health, vitals.health_max);
4378 assert!((vitals.mana_max - 61.0).abs() < 0.01);
4379 }
4380
4381 #[test]
4382 fn humanize_snake_id_title_cases_parts() {
4383 assert_eq!(humanize_snake_id("heal_touch"), "Heal Touch");
4384 assert_eq!(humanize_snake_id("fireball"), "Fireball");
4385 assert_eq!(humanize_snake_id("cone_frost"), "Cone Frost");
4386 }
4387
4388 #[test]
4389 fn saved_vitals_scale_when_pool_max_increases() {
4390 let mut attrs = PrimaryAttributes::default();
4391 attrs.intelligence = 140;
4392 attrs.wisdom = 140;
4393 let saved = StoredVitalsState {
4394 health: 100.0,
4395 mana: 14.0,
4396 stamina: 100.0,
4397 ..StoredVitalsState::default()
4398 };
4399 let vitals = saved.apply_to(attrs);
4400 assert!(vitals.mana_max > 55.0);
4401 assert!(
4402 (vitals.mana - vitals.mana_max).abs() < 0.01,
4403 "full legacy mana bar migrates to full new bar"
4404 );
4405 }
4406
4407 #[test]
4408 fn empty_vitals_state_is_pristine() {
4409 let pristine = StoredVitalsState {
4410 health: 0.0,
4411 mana: 0.0,
4412 stamina: 0.0,
4413 hunger: 0.0,
4414 thirst: 0.0,
4415 coins: 0,
4416 deaths: 0,
4417 life_state: LifeState::Alive,
4418 winded: false,
4419 };
4420 assert!(pristine.is_pristine());
4421 let vitals = pristine.apply_to(PrimaryAttributes::default());
4422 assert!(vitals.health > 0.0);
4423 }
4424
4425 #[test]
4426 fn stored_vitals_roundtrip_preserves_partial_pools() {
4427 let attrs = PrimaryAttributes::default();
4428 let mut live = PlayerVitals::from_attributes(attrs);
4429 live.health = 25.0;
4430 live.hunger = 77.0;
4431 live.deaths = 2;
4432 live.winded = true;
4433 let stored = StoredVitalsState::from_live(&live);
4434 let restored = stored.apply_to(attrs);
4435 assert!(
4436 (restored.health - 25.0).abs() < 0.01,
4437 "partial HP below cap stays absolute"
4438 );
4439 assert_eq!(restored.hunger, 77.0);
4440 assert_eq!(restored.deaths, 2);
4441 assert!(restored.winded);
4442 }
4443
4444 #[test]
4445 fn skill_tiers_start_at_zero() {
4446 let skill = SkillProgress::default();
4447 assert_eq!(skill.level, 0);
4448 assert_eq!(skill.display_tier(), 0);
4449 let trained = SkillProgress {
4450 level: 250,
4451 last_trained_tick: 1,
4452 };
4453 assert_eq!(trained.display_tier(), 2);
4454 }
4455
4456 #[test]
4457 fn quest_server_messages_roundtrip_json() {
4458 use crate::codec::{Codec, PostcardCodec};
4459
4460 let offer = ServerMessage::QuestOffer(QuestOffer {
4461 quest_id: "ada_goblin_hunt".into(),
4462 title: "Goblin Trouble".into(),
4463 description: "Help Ada".into(),
4464 step_count: 3,
4465 });
4466 let notice = ServerMessage::QuestAccepted(QuestNotice {
4467 quest_id: "ada_goblin_hunt".into(),
4468 title: "Goblin Trouble".into(),
4469 message: "Quest accepted".into(),
4470 });
4471 for msg in [offer, notice] {
4472 let bytes = PostcardCodec.encode(&msg).unwrap();
4473 let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
4474 assert_eq!(decoded, msg);
4475 }
4476 }
4477
4478 #[test]
4479 fn hotbar_consumable_binding_roundtrips() {
4480 let binding = hotbar_consumable_binding("vegetable_soup");
4481 assert_eq!(binding, "item:vegetable_soup");
4482 assert!(hotbar_binding_is_consumable(&binding));
4483 assert_eq!(hotbar_consumable_template(&binding), Some("vegetable_soup"));
4484 assert!(!hotbar_binding_is_consumable("fireball"));
4485 assert_eq!(hotbar_consumable_template("fireball"), None);
4486 }
4487}