use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub type EntityId = u64;
pub type Tick = u64;
pub type Seq = u32;
pub type SessionId = u64;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct WorldCoord {
pub x: f32,
pub y: f32,
pub z: f32,
pub w: u32,
pub t: u32,
}
impl WorldCoord {
pub fn surface(x: f32, y: f32) -> Self {
Self {
x,
y,
z: 0.0,
w: 0,
t: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Velocity2D {
pub vx: f32,
pub vy: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Transform {
pub position: WorldCoord,
pub yaw: f32,
pub velocity: Velocity2D,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LifeState {
Alive,
Dead,
}
impl Default for LifeState {
fn default() -> Self {
Self::Alive
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TimeOfDayPhase {
Night,
Dawn,
Morning,
Midday,
Afternoon,
Evening,
}
impl TimeOfDayPhase {
pub fn label(self) -> &'static str {
match self {
Self::Night => "Night",
Self::Dawn => "Dawn",
Self::Morning => "Morning",
Self::Midday => "Midday",
Self::Afternoon => "Afternoon",
Self::Evening => "Evening",
}
}
pub fn from_name(name: &str) -> Self {
match name.to_ascii_lowercase().as_str() {
"dawn" => Self::Dawn,
"morning" => Self::Morning,
"midday" | "mid_day" | "noon" => Self::Midday,
"afternoon" => Self::Afternoon,
"evening" | "dusk" => Self::Evening,
_ => Self::Night,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct WorldClock {
pub day: u64,
pub hour: u8,
pub minute: u8,
pub phase: TimeOfDayPhase,
}
impl Default for WorldClock {
fn default() -> Self {
Self {
day: 0,
hour: 8,
minute: 0,
phase: TimeOfDayPhase::Morning,
}
}
}
impl WorldClock {
pub fn display_time(self) -> String {
format!("{:02}:{:02}", self.hour, self.minute)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrimaryAttributes {
pub strength: u16,
pub dexterity: u16,
pub intelligence: u16,
pub stamina: u16,
pub vitality: u16,
pub wisdom: u16,
pub charisma: u16,
}
impl Default for PrimaryAttributes {
fn default() -> Self {
Self {
strength: 150,
dexterity: 150,
intelligence: 150,
stamina: 150,
vitality: 150,
wisdom: 150,
charisma: 150,
}
}
}
impl PrimaryAttributes {
pub fn display(value: u16) -> u16 {
(value / 10).clamp(1, 100)
}
pub fn derived_preview(&self) -> DerivedPreview {
let str_d = Self::display(self.strength) as f32;
let dex_d = Self::display(self.dexterity) as f32;
let int_d = Self::display(self.intelligence) as f32;
let wis_d = Self::display(self.wisdom) as f32;
DerivedPreview {
attack_power: str_d * 1.2 + dex_d * 0.3,
spell_power: int_d * 1.1 + wis_d * 0.4,
evasion: dex_d * 0.8 + wis_d * 0.2,
carry_mass_max: str_d * 2.5,
sight_range_m: 12.0 + wis_d * 0.15 + dex_d * 0.05,
fov_deg: 120.0 + wis_d * 0.2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DerivedPreview {
pub attack_power: f32,
pub spell_power: f32,
pub evasion: f32,
pub carry_mass_max: f32,
pub sight_range_m: f32,
pub fov_deg: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkillProgress {
pub level: u16,
#[serde(default)]
pub last_trained_tick: u64,
}
impl Default for SkillProgress {
fn default() -> Self {
Self {
level: 0,
last_trained_tick: 0,
}
}
}
impl SkillProgress {
pub fn display_tier(&self) -> u16 {
(self.level / 100).min(10)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ProgressionXp {
pub strength: f64,
pub dexterity: f64,
pub intelligence: f64,
pub stamina: f64,
pub vitality: f64,
pub wisdom: f64,
pub charisma: f64,
pub logging: f64,
pub mining: f64,
pub evocation: f64,
pub restoration: f64,
pub swords: f64,
pub archery: f64,
pub crafting: f64,
pub alchemy: f64,
pub cartography: f64,
}
impl ProgressionXp {
pub fn bootstrap_new(baseline_display: u16, xp_base: f64, xp_growth: f64) -> Self {
let bootstrap = |display: f64| {
if display <= 1.0 {
0.0
} else {
xp_base * xp_growth.powf(display - 1.0)
}
};
let b = baseline_display as f64;
let primary = bootstrap(b);
Self {
strength: primary,
dexterity: primary,
intelligence: primary,
stamina: primary,
vitality: primary,
wisdom: primary,
charisma: primary,
..Self::default()
}
}
pub fn is_empty(&self) -> bool {
self.strength == 0.0
&& self.dexterity == 0.0
&& self.intelligence == 0.0
&& self.stamina == 0.0
&& self.vitality == 0.0
&& self.wisdom == 0.0
&& self.charisma == 0.0
&& self.logging == 0.0
&& self.mining == 0.0
&& self.evocation == 0.0
&& self.restoration == 0.0
&& self.swords == 0.0
&& self.archery == 0.0
&& self.crafting == 0.0
&& self.alchemy == 0.0
&& self.cartography == 0.0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct PlayerSkills {
pub logging: SkillProgress,
pub mining: SkillProgress,
pub evocation: SkillProgress,
#[serde(default)]
pub restoration: SkillProgress,
pub swords: SkillProgress,
#[serde(default)]
pub archery: SkillProgress,
pub crafting: SkillProgress,
#[serde(default)]
pub alchemy: SkillProgress,
pub cartography: SkillProgress,
}
impl Default for PlayerSkills {
fn default() -> Self {
Self {
logging: SkillProgress::default(),
mining: SkillProgress::default(),
evocation: SkillProgress::default(),
restoration: SkillProgress::default(),
swords: SkillProgress::default(),
archery: SkillProgress::default(),
crafting: SkillProgress::default(),
alchemy: SkillProgress::default(),
cartography: SkillProgress::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct PlayerVitals {
pub health: f32,
pub health_max: f32,
pub mana: f32,
pub mana_max: f32,
pub stamina: f32,
pub stamina_max: f32,
#[serde(default = "default_survival_pool_max")]
pub hunger: f32,
#[serde(default = "default_survival_pool_max")]
pub hunger_max: f32,
#[serde(default = "default_survival_pool_max")]
pub thirst: f32,
#[serde(default = "default_survival_pool_max")]
pub thirst_max: f32,
#[serde(default)]
pub coins: u32,
#[serde(default)]
pub deaths: u32,
#[serde(default)]
pub life_state: LifeState,
}
fn default_survival_pool_max() -> f32 {
100.0
}
impl Default for PlayerVitals {
fn default() -> Self {
Self::from_attributes(PrimaryAttributes::default())
}
}
impl PlayerVitals {
pub fn from_attributes(attrs: PrimaryAttributes) -> Self {
let vit_d = PrimaryAttributes::display(attrs.vitality) as f32;
let int_d = PrimaryAttributes::display(attrs.intelligence) as f32;
let wis_d = PrimaryAttributes::display(attrs.wisdom) as f32;
let sta_d = PrimaryAttributes::display(attrs.stamina) as f32;
let health_max = 50.0 + vit_d * 2.0;
let stamina_max = 30.0 + sta_d * 1.4;
let mana_max = 25.0 + int_d * 1.1 + wis_d * 1.3;
let hunger_max = 100.0;
let thirst_max = 100.0;
Self {
health: health_max,
health_max,
mana: mana_max,
mana_max,
stamina: stamina_max,
stamina_max,
hunger: hunger_max,
hunger_max,
thirst: thirst_max,
thirst_max,
coins: 0,
deaths: 0,
life_state: LifeState::Alive,
}
}
pub fn legacy_maxima(attrs: PrimaryAttributes) -> (f32, f32, f32) {
(
attrs.vitality as f32 / 5.0,
attrs.stamina as f32 / 5.0,
(attrs.intelligence as f32 + attrs.wisdom as f32) / 20.0,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct StoredVitalsState {
pub health: f32,
pub mana: f32,
pub stamina: f32,
pub hunger: f32,
pub thirst: f32,
pub coins: u32,
pub deaths: u32,
pub life_state: LifeState,
}
impl StoredVitalsState {
pub fn from_live(v: &PlayerVitals) -> Self {
Self {
health: v.health,
mana: v.mana,
stamina: v.stamina,
hunger: v.hunger,
thirst: v.thirst,
coins: v.coins,
deaths: v.deaths,
life_state: v.life_state,
}
}
pub fn is_pristine(&self) -> bool {
self.health == 0.0
&& self.mana == 0.0
&& self.stamina == 0.0
&& self.hunger == 0.0
&& self.thirst == 0.0
&& self.coins == 0
&& self.deaths == 0
&& self.life_state == LifeState::Alive
}
pub fn apply_to(self, attrs: PrimaryAttributes) -> PlayerVitals {
if self.is_pristine() {
return PlayerVitals::from_attributes(attrs);
}
let fresh = PlayerVitals::from_attributes(attrs);
let (legacy_hp, legacy_sta, legacy_mana) = PlayerVitals::legacy_maxima(attrs);
let scale = |current: f32, legacy_max: f32, new_max: f32| {
if legacy_max > 0.0 && new_max > legacy_max * 1.05 && current >= legacy_max * 0.95 {
let ratio = (current / legacy_max).clamp(0.0, 1.0);
(new_max * ratio).min(new_max)
} else {
current.min(new_max)
}
};
let mut v = fresh;
v.health = scale(self.health, legacy_hp, fresh.health_max);
v.mana = scale(self.mana, legacy_mana, fresh.mana_max);
v.stamina = scale(self.stamina, legacy_sta, fresh.stamina_max);
v.hunger = self.hunger.min(v.hunger_max);
v.thirst = self.thirst.min(v.thirst_max);
v.coins = self.coins;
v.deaths = self.deaths;
v.life_state = self.life_state;
v
}
}
impl Default for StoredVitalsState {
fn default() -> Self {
Self::from_live(&PlayerVitals::default())
}
}
pub fn humanize_snake_id(id: &str) -> String {
id.split('_')
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct KnownAbility {
pub ability_id: String,
#[serde(default = "default_known_permanent")]
pub permanent: bool,
#[serde(default)]
pub expires_at_tick: Option<u64>,
}
fn default_known_permanent() -> bool {
true
}
impl KnownAbility {
pub fn permanent(ability_id: impl Into<String>) -> Self {
Self {
ability_id: ability_id.into(),
permanent: true,
expires_at_tick: None,
}
}
pub fn is_active(&self, tick: u64) -> bool {
if self.permanent {
return true;
}
match self.expires_at_tick {
Some(exp) => tick < exp,
None => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RotationPreset {
pub id: String,
pub label: String,
#[serde(default)]
pub abilities: Vec<String>,
}
impl RotationPreset {
pub fn melee_default(ability_id: impl Into<String>) -> Self {
let id = ability_id.into();
Self {
id: "melee".into(),
label: "Weapon".into(),
abilities: vec![id],
}
}
pub fn is_weapon_preset(&self) -> bool {
self.id == "melee"
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct StoredTargetSlot {
pub instance_id: Option<String>,
#[serde(default)]
pub preset_id: Option<String>,
#[serde(default)]
pub rotation_index: u32,
#[serde(default)]
pub auto_enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct StoredCombatProfile {
pub combat_target_instance_id: Option<String>,
pub in_combat: bool,
pub last_combat_tick: u64,
pub last_attack_tick: u64,
pub cooldowns_until_tick: BTreeMap<String, u64>,
#[serde(default = "default_auto_attack")]
pub auto_attack_enabled: bool,
#[serde(default)]
pub mainhand_template_id: Option<String>,
#[serde(default)]
pub mainhand_instance_id: Option<Uuid>,
#[serde(default)]
pub offhand_template_id: Option<String>,
#[serde(default)]
pub offhand_instance_id: Option<Uuid>,
#[serde(default)]
pub worn: Vec<(BodySlot, ItemStack)>,
#[serde(default)]
pub rotation_presets: Vec<RotationPreset>,
#[serde(default)]
pub target_slots: Vec<StoredTargetSlot>,
#[serde(default)]
pub known_blueprint_ids: Vec<String>,
#[serde(default)]
pub keychain: Vec<ItemStack>,
#[serde(default)]
pub whisper_pouch: Vec<ItemStack>,
#[serde(default)]
pub known_abilities: Vec<KnownAbility>,
#[serde(default)]
pub hotbar: Vec<Option<String>>,
#[serde(default)]
pub abilities_schema_version: u32,
#[serde(default)]
pub bank_balance_copper: u64,
}
fn default_auto_attack() -> bool {
true
}
impl Default for StoredCombatProfile {
fn default() -> Self {
Self {
combat_target_instance_id: None,
in_combat: false,
last_combat_tick: 0,
last_attack_tick: 0,
cooldowns_until_tick: BTreeMap::new(),
auto_attack_enabled: true,
mainhand_template_id: None,
mainhand_instance_id: None,
offhand_template_id: None,
offhand_instance_id: None,
worn: Vec::new(),
rotation_presets: Vec::new(),
target_slots: Vec::new(),
known_blueprint_ids: Vec::new(),
keychain: Vec::new(),
whisper_pouch: Vec::new(),
known_abilities: Vec::new(),
hotbar: Vec::new(),
abilities_schema_version: 0,
bank_balance_copper: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EntityState {
pub id: EntityId,
pub transform: Transform,
#[serde(default)]
pub label: String,
#[serde(default)]
pub vitals: Option<PlayerVitals>,
#[serde(default)]
pub attributes: Option<PrimaryAttributes>,
#[serde(default)]
pub skills: Option<PlayerSkills>,
#[serde(default)]
pub inside_building: Option<String>,
#[serde(default)]
pub tile_id: Option<String>,
#[serde(default)]
pub paperdoll_ref: Option<String>,
#[serde(default)]
pub presentation_state: Option<String>,
#[serde(default)]
pub sprite_mode: Option<String>,
#[serde(default)]
pub progression_xp: Option<ProgressionXp>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChatChannel {
Nearby,
Direct,
Whisper,
WhisperStone,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ChatClarity {
#[default]
Clear,
Partial,
Heavy,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatMessage {
pub channel: ChatChannel,
pub from_entity: EntityId,
pub from_name: String,
pub text: String,
pub tick: Tick,
#[serde(default)]
pub to_entity: Option<EntityId>,
#[serde(default)]
pub clarity: ChatClarity,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Intent {
Move {
entity_id: EntityId,
forward: f32,
strafe: f32,
#[serde(default)]
vertical: f32,
#[serde(default)]
sprint: bool,
seq: Seq,
},
Stop {
entity_id: EntityId,
seq: Seq,
},
Harvest {
entity_id: EntityId,
node_id: String,
seq: Seq,
},
Use {
entity_id: EntityId,
template_id: String,
seq: Seq,
},
UseGrant {
entity_id: EntityId,
grant_instance_id: Uuid,
target_instance_id: Uuid,
seq: Seq,
},
Say {
entity_id: EntityId,
channel: ChatChannel,
text: String,
#[serde(default)]
to_entity: Option<EntityId>,
seq: Seq,
},
Craft {
entity_id: EntityId,
blueprint_id: String,
#[serde(default)]
count: Option<u32>,
seq: Seq,
},
Interact {
entity_id: EntityId,
target_id: String,
seq: Seq,
},
ShopBuy {
entity_id: EntityId,
npc_id: String,
offer_id: String,
#[serde(default = "default_one")]
quantity: u32,
seq: Seq,
},
ShopSell {
entity_id: EntityId,
npc_id: String,
template_id: String,
#[serde(default = "default_one")]
quantity: u32,
seq: Seq,
},
ShopClose {
entity_id: EntityId,
npc_id: String,
seq: Seq,
},
TestDamage {
entity_id: EntityId,
amount: f32,
seq: Seq,
},
SetTarget {
entity_id: EntityId,
target_id: EntityId,
seq: Seq,
},
SetTargetSlot {
entity_id: EntityId,
slot_index: u8,
target_id: EntityId,
seq: Seq,
},
ClearTarget {
entity_id: EntityId,
seq: Seq,
},
ClearTargetSlot {
entity_id: EntityId,
slot_index: u8,
seq: Seq,
},
SetAutoAttack {
entity_id: EntityId,
slot_index: u8,
enabled: bool,
seq: Seq,
},
Attack {
entity_id: EntityId,
#[serde(default)]
target_id: Option<EntityId>,
#[serde(default)]
weapon_slot: Option<u32>,
seq: Seq,
},
Pickup {
entity_id: EntityId,
#[serde(default)]
drop_id: Option<String>,
seq: Seq,
},
Cast {
entity_id: EntityId,
ability_id: String,
target_id: EntityId,
seq: Seq,
},
BindActionSlot {
entity_id: EntityId,
slot_index: u8,
ability_id: String,
#[serde(default = "default_auto_attack")]
auto_enabled: bool,
seq: Seq,
},
UseActionSlot {
entity_id: EntityId,
slot_index: u8,
seq: Seq,
},
Dodge {
entity_id: EntityId,
seq: Seq,
},
Lunge {
entity_id: EntityId,
#[serde(default)]
forward: f32,
#[serde(default)]
strafe: f32,
seq: Seq,
},
DirectionalJump {
entity_id: EntityId,
#[serde(default)]
forward: f32,
#[serde(default)]
strafe: f32,
seq: Seq,
},
Block {
entity_id: EntityId,
#[serde(default = "default_block_enabled")]
enabled: bool,
seq: Seq,
},
EquipMainhand {
entity_id: EntityId,
#[serde(default)]
template_id: Option<String>,
#[serde(default)]
instance_id: Option<Uuid>,
seq: Seq,
},
EquipOffhand {
entity_id: EntityId,
#[serde(default)]
template_id: Option<String>,
#[serde(default)]
instance_id: Option<Uuid>,
seq: Seq,
},
EquipWorn {
entity_id: EntityId,
slot: BodySlot,
#[serde(default)]
instance_id: Option<Uuid>,
seq: Seq,
},
MoveItem {
entity_id: EntityId,
item_instance_id: Uuid,
from: InventoryLocation,
to: InventoryLocation,
#[serde(default)]
to_parent_instance_id: Option<Uuid>,
#[serde(default)]
quantity: Option<u32>,
seq: Seq,
},
PlaceContainer {
entity_id: EntityId,
item_instance_id: Uuid,
seq: Seq,
},
PickupContainer {
entity_id: EntityId,
container_id: String,
seq: Seq,
},
SetContainerLocked {
entity_id: EntityId,
location: InventoryLocation,
locked: bool,
seq: Seq,
},
DropItem {
entity_id: EntityId,
item_instance_id: Uuid,
from: InventoryLocation,
seq: Seq,
},
DestroyItem {
entity_id: EntityId,
item_instance_id: Uuid,
from: InventoryLocation,
#[serde(default)]
quantity: Option<u32>,
seq: Seq,
},
RenameContainer {
entity_id: EntityId,
item_instance_id: Uuid,
location: InventoryLocation,
name: String,
seq: Seq,
},
UpsertRotationPreset {
entity_id: EntityId,
preset: RotationPreset,
seq: Seq,
},
DeleteRotationPreset {
entity_id: EntityId,
preset_id: String,
seq: Seq,
},
AssignSlotPreset {
entity_id: EntityId,
slot_index: u8,
preset_id: String,
seq: Seq,
},
SetHotbarSlot {
entity_id: EntityId,
slot: u8,
#[serde(default)]
ability_id: Option<String>,
seq: Seq,
},
AdvanceRotation {
entity_id: EntityId,
slot_index: u8,
seq: Seq,
},
NpcTalkOpen {
entity_id: EntityId,
npc_id: String,
seq: Seq,
},
NpcTalkSay {
entity_id: EntityId,
npc_id: String,
message: String,
seq: Seq,
},
NpcTalkClose {
entity_id: EntityId,
npc_id: String,
seq: Seq,
},
AcceptQuest {
entity_id: EntityId,
quest_id: String,
seq: Seq,
},
WithdrawQuest {
entity_id: EntityId,
quest_id: String,
seq: Seq,
},
TrackQuest {
entity_id: EntityId,
quest_id: String,
seq: Seq,
},
QuestGiveItem {
entity_id: EntityId,
npc_id: String,
template_id: String,
#[serde(default = "default_one")]
quantity: u32,
seq: Seq,
},
HireWorker {
entity_id: EntityId,
def_id: String,
wage_copper_per_interval: u32,
#[serde(default)]
lodging_container_id: Option<String>,
#[serde(default)]
job_yaml: Option<String>,
seq: Seq,
},
DismissWorker {
entity_id: EntityId,
worker_instance_id: String,
seq: Seq,
},
SetWorkerJob {
entity_id: EntityId,
worker_instance_id: String,
job_yaml: String,
seq: Seq,
},
AssignWorkerLodging {
entity_id: EntityId,
worker_instance_id: String,
lodging_container_id: String,
seq: Seq,
},
SetWorkerMode {
entity_id: EntityId,
worker_instance_id: String,
mode: String,
seq: Seq,
},
GiveWorkerItem {
entity_id: EntityId,
worker_instance_id: String,
item_instance_id: uuid::Uuid,
#[serde(default)]
quantity: Option<u32>,
seq: Seq,
},
TakeWorkerItem {
entity_id: EntityId,
worker_instance_id: String,
item_instance_id: uuid::Uuid,
#[serde(default)]
quantity: Option<u32>,
seq: Seq,
},
RenameHiredWorker {
entity_id: EntityId,
worker_instance_id: String,
name: String,
seq: Seq,
},
TeachWorkerBlueprint {
entity_id: EntityId,
worker_instance_id: String,
blueprint_id: String,
seq: Seq,
},
BuyProperty {
entity_id: EntityId,
zone_id: String,
seq: Seq,
},
SellProperty {
entity_id: EntityId,
zone_id: String,
seq: Seq,
},
BankDeposit {
entity_id: EntityId,
npc_id: String,
#[serde(default)]
amount_copper: u64,
seq: Seq,
},
BankWithdraw {
entity_id: EntityId,
npc_id: String,
#[serde(default)]
amount_copper: u64,
seq: Seq,
},
BankClose {
entity_id: EntityId,
npc_id: String,
seq: Seq,
},
BankTransfer {
entity_id: EntityId,
npc_id: String,
#[serde(default)]
to_character_id: Option<Uuid>,
#[serde(default)]
to_name: String,
amount_copper: u64,
seq: Seq,
},
StorageStore {
entity_id: EntityId,
npc_id: String,
item_instance_id: Uuid,
#[serde(default)]
quantity: Option<u32>,
seq: Seq,
},
StorageTake {
entity_id: EntityId,
npc_id: String,
item_instance_id: Uuid,
#[serde(default)]
quantity: Option<u32>,
seq: Seq,
},
StorageShip {
entity_id: EntityId,
npc_id: String,
dest_building_id: String,
item_instance_id: Uuid,
#[serde(default)]
quantity: Option<u32>,
seq: Seq,
},
StorageClose {
entity_id: EntityId,
npc_id: String,
seq: Seq,
},
TradeRequest {
entity_id: EntityId,
peer_entity_id: EntityId,
seq: Seq,
},
TradeRespond {
entity_id: EntityId,
peer_entity_id: EntityId,
accept: bool,
seq: Seq,
},
TradePresent {
entity_id: EntityId,
item_instance_id: Uuid,
#[serde(default)]
quantity: Option<u32>,
seq: Seq,
},
TradeUnpresent {
entity_id: EntityId,
item_instance_id: Uuid,
seq: Seq,
},
TradeSetReady {
entity_id: EntityId,
ready: bool,
seq: Seq,
},
TradeCancel {
entity_id: EntityId,
seq: Seq,
},
DestroyWhisperStone {
entity_id: EntityId,
item_instance_id: Uuid,
seq: Seq,
},
StowWhisperStone {
entity_id: EntityId,
item_instance_id: Uuid,
seq: Seq,
},
}
fn default_block_enabled() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct StatusEffectHud {
pub effect_id: String,
pub label: String,
#[serde(default)]
pub polarity: String,
#[serde(default)]
pub icon_tile_id: Option<String>,
#[serde(default)]
pub remaining_sec: Option<f32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct CombatTargetHud {
pub entity_id: EntityId,
#[serde(default)]
pub label: String,
#[serde(default)]
pub level: u32,
pub health: f32,
pub health_max: f32,
#[serde(default)]
pub life_state: LifeState,
#[serde(default)]
pub distance_m: f32,
#[serde(default)]
pub statuses: Vec<StatusEffectHud>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct CastProgressHud {
#[serde(default)]
pub ability_id: String,
#[serde(default)]
pub ability_label: String,
#[serde(default)]
pub ticks_remaining: u64,
#[serde(default)]
pub ticks_total: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct AbilityCooldownHud {
#[serde(default)]
pub ability_id: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub cd_ticks: u64,
#[serde(default)]
pub cd_total_ticks: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct CombatSlotHud {
pub slot_index: u8,
#[serde(default)]
pub target_entity_id: Option<EntityId>,
#[serde(default)]
pub target_label: Option<String>,
#[serde(default)]
pub target: Option<CombatTargetHud>,
#[serde(default)]
pub preset_id: Option<String>,
#[serde(default)]
pub preset_label: Option<String>,
#[serde(default)]
pub rotation: Vec<String>,
#[serde(default)]
pub rotation_index: u32,
#[serde(default)]
pub next_ability_id: Option<String>,
#[serde(default)]
pub auto_enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DefensePieceHud {
pub slot: BodySlot,
pub label: String,
pub template_id: String,
#[serde(default)]
pub armor_physical: f32,
#[serde(default)]
pub resists: Vec<(String, f32)>,
}
impl Default for DefensePieceHud {
fn default() -> Self {
Self {
slot: BodySlot::Head,
label: String::new(),
template_id: String::new(),
armor_physical: 0.0,
resists: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct DefenseHud {
pub armor_physical: f32,
pub vitality_contribution: f32,
pub total_mitigation_rating: f32,
pub estimated_physical_dr: f32,
#[serde(default)]
pub resists: Vec<(String, f32)>,
#[serde(default)]
pub pieces: Vec<DefensePieceHud>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct CombatHud {
pub in_combat: bool,
pub auto_attack: bool,
pub has_los: bool,
pub attack_cd_ticks: u64,
#[serde(default)]
pub ability_id: String,
#[serde(default)]
pub target_entity_id: Option<EntityId>,
#[serde(default)]
pub target_label: Option<String>,
#[serde(default)]
pub max_target_slots: u8,
#[serde(default)]
pub slots: Vec<CombatSlotHud>,
#[serde(default)]
pub rotation_presets: Vec<RotationPreset>,
#[serde(default)]
pub gcd_ticks: u64,
#[serde(default)]
pub mainhand_template_id: Option<String>,
#[serde(default)]
pub mainhand_label: Option<String>,
#[serde(default)]
pub offhand_template_id: Option<String>,
#[serde(default)]
pub offhand_label: Option<String>,
#[serde(default)]
pub mainhand_hand_slots: u8,
#[serde(default)]
pub worn: Vec<(BodySlot, ItemStack)>,
#[serde(default)]
pub defense: Option<DefenseHud>,
#[serde(default)]
pub carry_mass: f32,
#[serde(default)]
pub carry_mass_max: f32,
#[serde(default)]
pub encumbrance: EncumbranceState,
#[serde(default)]
pub keychain: Vec<ItemStack>,
#[serde(default)]
pub whisper_pouch: Vec<ItemStack>,
#[serde(default)]
pub target: Option<CombatTargetHud>,
#[serde(default)]
pub cast: Option<CastProgressHud>,
#[serde(default)]
pub ability_cooldowns: Vec<AbilityCooldownHud>,
#[serde(default)]
pub blocking_active: bool,
#[serde(default)]
pub progression_xp: Option<ProgressionXp>,
#[serde(default)]
pub progression_baseline: u16,
#[serde(default)]
pub progression_xp_base: f64,
#[serde(default)]
pub progression_xp_growth: f64,
#[serde(default)]
pub attributes: Option<PrimaryAttributes>,
#[serde(default)]
pub skills: Option<PlayerSkills>,
#[serde(default)]
pub statuses: Vec<StatusEffectHud>,
#[serde(default)]
pub known_abilities: Vec<String>,
#[serde(default)]
pub hotbar: Vec<Option<String>>,
#[serde(default)]
pub max_abilities_per_rotation: u8,
}
pub const HOTBAR_ITEM_PREFIX: &str = "item:";
pub fn hotbar_consumable_binding(template_id: &str) -> String {
format!("{HOTBAR_ITEM_PREFIX}{}", template_id.trim())
}
pub fn hotbar_consumable_template(binding: &str) -> Option<&str> {
binding
.strip_prefix(HOTBAR_ITEM_PREFIX)
.map(str::trim)
.filter(|id| !id.is_empty())
}
pub fn hotbar_binding_is_consumable(binding: &str) -> bool {
hotbar_consumable_template(binding).is_some()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CombatFxKind {
MeleeArc,
Cone,
Sphere,
Beam,
HitMarker,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CombatFxHitOutcome {
#[default]
Hit,
Blocked,
Miss,
Glance,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CombatFxHit {
pub entity_id: EntityId,
pub x: f32,
pub y: f32,
pub z: f32,
#[serde(default)]
pub outcome: CombatFxHitOutcome,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CombatFx {
pub id: u64,
pub kind: CombatFxKind,
pub ability_id: String,
pub caster_id: EntityId,
pub origin_x: f32,
pub origin_y: f32,
pub origin_z: f32,
#[serde(default)]
pub end_x: Option<f32>,
#[serde(default)]
pub end_y: Option<f32>,
#[serde(default)]
pub end_z: Option<f32>,
#[serde(default)]
pub yaw: Option<f32>,
#[serde(default)]
pub reach_m: Option<f32>,
#[serde(default)]
pub arc_deg: Option<f32>,
#[serde(default)]
pub radius_m: Option<f32>,
#[serde(default)]
pub hits: Vec<CombatFxHit>,
pub until_tick: u64,
#[serde(default)]
pub damage_type: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerModeView {
Companion,
JobLoop,
Idle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerStateView {
Idle,
Traveling,
Working,
Resting,
Waiting,
Strike,
Dismissed,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
pub struct WorkerVitalsSummary {
pub health_pct: f32,
pub stamina_pct: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum WorkerRouteKindView {
#[default]
HarvestLoop,
Ordered,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct WorkerRouteView {
#[serde(default)]
pub kind: WorkerRouteKindView,
#[serde(default)]
pub lodging_container_id: Option<String>,
#[serde(default)]
pub outbound_waypoints: Vec<WorkerRouteWaypointView>,
#[serde(default)]
pub harvest_nodes: Vec<String>,
#[serde(default = "default_route_carry_ratio")]
pub carry_return_ratio: f32,
#[serde(default)]
pub stops: Vec<WorkerRouteStopView>,
}
fn default_route_carry_ratio() -> f32 {
0.90
}
fn default_true_view() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkerWithdrawItemView {
pub template: String,
#[serde(default)]
pub qty: u32,
#[serde(default)]
pub all: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct WorkerRouteWaypointView {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerRouteStopView {
Waypoint {
x: f32,
y: f32,
#[serde(default)]
z: f32,
},
HarvestNode {
node_id: String,
},
DepositAt {
container_id: String,
#[serde(default)]
filter: Option<Vec<String>>,
},
TradeWith {
#[serde(default)]
npc_id: Option<String>,
template: String,
#[serde(default = "default_true_view")]
sell_all: bool,
},
WithdrawFrom {
container_id: String,
items: Vec<WorkerWithdrawItemView>,
},
CraftAt {
device: String,
blueprint: String,
#[serde(default)]
qty: Option<u32>,
},
RestIfNeeded,
Wait {
#[serde(default)]
wait_ticks: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LedgerCategory {
Workers,
Hire,
Train,
ShopBuy,
Taxes,
WorkerSales,
TraderSales,
BankDeposit,
BankWithdraw,
BankTransferOut,
BankTransferIn,
BankTransferFee,
StorageShipFee,
Other,
}
impl LedgerCategory {
pub fn as_str(self) -> &'static str {
match self {
Self::Workers => "workers",
Self::Hire => "hire",
Self::Train => "train",
Self::ShopBuy => "shop_buy",
Self::Taxes => "taxes",
Self::WorkerSales => "worker_sales",
Self::TraderSales => "trader_sales",
Self::BankDeposit => "bank_deposit",
Self::BankWithdraw => "bank_withdraw",
Self::BankTransferOut => "bank_transfer_out",
Self::BankTransferIn => "bank_transfer_in",
Self::BankTransferFee => "bank_transfer_fee",
Self::StorageShipFee => "storage_ship_fee",
Self::Other => "other",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LedgerEntryView {
pub id: uuid::Uuid,
pub game_day: u64,
pub signed_copper: i64,
pub category: LedgerCategory,
#[serde(default)]
pub label: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct LedgerPeriodTotals {
#[serde(default)]
pub expenses: std::collections::HashMap<String, u64>,
#[serde(default)]
pub income: std::collections::HashMap<String, u64>,
pub expense_copper: u64,
pub income_copper: u64,
pub cash_flow_copper: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct PlayerLedgerView {
pub current_game_day: u64,
#[serde(default)]
pub period_day: LedgerPeriodTotals,
#[serde(default)]
pub period_week: LedgerPeriodTotals,
#[serde(default)]
pub period_month: LedgerPeriodTotals,
#[serde(default)]
pub period_lifetime: LedgerPeriodTotals,
#[serde(default)]
pub recent: Vec<LedgerEntryView>,
#[serde(default)]
pub wealth_on_person_copper: u64,
#[serde(default)]
pub wealth_in_storage_copper: u64,
#[serde(default)]
pub wealth_in_bank_copper: u64,
#[serde(default)]
pub wealth_total_copper: u64,
#[serde(default)]
pub live_expense_per_interval_copper: u64,
#[serde(default)]
pub live_income_route_est_per_loop_copper: u64,
#[serde(default)]
pub live_income_avg_per_interval_copper: u64,
#[serde(default)]
pub live_income_avg_window_intervals: u32,
#[serde(default)]
pub live_net_avg_per_interval_copper: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnalyticsMetric {
NpcKill,
WildlifeKill,
Harvest,
QuestComplete,
QuestAccept,
QuestAbandon,
PlayerDeath,
Craft,
WorkerHire,
WorkerDismiss,
WorkerTeach,
NpcTalk,
ShopBuy,
ShopSell,
PlaceContainer,
PickupContainer,
PickupDrop,
ConsumableUse,
AbilityUse,
DistanceWalkedM,
DoorUse,
BuildingEnter,
}
impl AnalyticsMetric {
pub fn as_str(self) -> &'static str {
match self {
Self::NpcKill => "npc_kill",
Self::WildlifeKill => "wildlife_kill",
Self::Harvest => "harvest",
Self::QuestComplete => "quest_complete",
Self::QuestAccept => "quest_accept",
Self::QuestAbandon => "quest_abandon",
Self::PlayerDeath => "player_death",
Self::Craft => "craft",
Self::WorkerHire => "worker_hire",
Self::WorkerDismiss => "worker_dismiss",
Self::WorkerTeach => "worker_teach",
Self::NpcTalk => "npc_talk",
Self::ShopBuy => "shop_buy",
Self::ShopSell => "shop_sell",
Self::PlaceContainer => "place_container",
Self::PickupContainer => "pickup_container",
Self::PickupDrop => "pickup_drop",
Self::ConsumableUse => "consumable_use",
Self::AbilityUse => "ability_use",
Self::DistanceWalkedM => "distance_walked_m",
Self::DoorUse => "door_use",
Self::BuildingEnter => "building_enter",
}
}
pub fn from_str_key(s: &str) -> Option<Self> {
Some(match s {
"npc_kill" => Self::NpcKill,
"wildlife_kill" => Self::WildlifeKill,
"harvest" => Self::Harvest,
"quest_complete" => Self::QuestComplete,
"quest_accept" => Self::QuestAccept,
"quest_abandon" => Self::QuestAbandon,
"player_death" => Self::PlayerDeath,
"craft" => Self::Craft,
"worker_hire" => Self::WorkerHire,
"worker_dismiss" => Self::WorkerDismiss,
"worker_teach" => Self::WorkerTeach,
"npc_talk" => Self::NpcTalk,
"shop_buy" => Self::ShopBuy,
"shop_sell" => Self::ShopSell,
"place_container" => Self::PlaceContainer,
"pickup_container" => Self::PickupContainer,
"pickup_drop" => Self::PickupDrop,
"consumable_use" => Self::ConsumableUse,
"ability_use" => Self::AbilityUse,
"distance_walked_m" => Self::DistanceWalkedM,
"door_use" => Self::DoorUse,
"building_enter" => Self::BuildingEnter,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CareerMetricRow {
pub subject_id: String,
pub amount: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct PlayerCareerView {
pub current_game_day: u64,
#[serde(default)]
pub kills: Vec<CareerMetricRow>,
#[serde(default)]
pub harvests: Vec<CareerMetricRow>,
pub quests_completed: u64,
#[serde(default)]
pub crafts: Vec<CareerMetricRow>,
pub deaths: u64,
pub npc_talks: u64,
pub shop_buys: u64,
pub shop_sells: u64,
pub distance_m: u64,
#[serde(default)]
pub other: Vec<CareerMetricRow>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HiredWorkerView {
pub instance_id: String,
pub entity_id: EntityId,
pub def_id: String,
pub label: String,
pub x: f32,
pub y: f32,
pub z: f32,
pub mode: WorkerModeView,
pub state: WorkerStateView,
#[serde(default)]
pub step_label: String,
pub vitals: WorkerVitalsSummary,
#[serde(default)]
pub carry_pct: f32,
#[serde(default)]
pub last_error: Option<String>,
pub wage_copper_per_interval: u32,
#[serde(default)]
pub effective_wage_copper: u32,
#[serde(default)]
pub wage_meters_walked: f32,
#[serde(default)]
pub lodging_container_id: Option<String>,
#[serde(default)]
pub route: Option<WorkerRouteView>,
#[serde(default)]
pub route_stop_index: Option<u32>,
#[serde(default)]
pub known_blueprint_ids: Vec<String>,
#[serde(default = "default_worker_view_level")]
pub level: u32,
#[serde(default)]
pub worker_xp: f64,
#[serde(default)]
pub inventory: Vec<ItemStack>,
}
fn default_worker_view_level() -> u32 {
1
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TickDelta {
pub tick: Tick,
pub entities: Vec<EntityState>,
#[serde(default)]
pub resource_nodes: Vec<ResourceNodeView>,
#[serde(default)]
pub buildings: Vec<BuildingView>,
#[serde(default)]
pub doors: Vec<DoorView>,
#[serde(default)]
pub npcs: Vec<NpcView>,
#[serde(default)]
pub inventory: Vec<ItemStack>,
#[serde(default)]
pub blueprints: Vec<BlueprintView>,
#[serde(default)]
pub world_clock: WorldClock,
#[serde(default)]
pub ground_drops: Vec<GroundDropView>,
#[serde(default)]
pub placed_containers: Vec<PlacedContainerView>,
#[serde(default)]
pub combat: Option<CombatHud>,
#[serde(default)]
pub interior_map: Option<InteriorMapView>,
#[serde(default)]
pub quest_log: Vec<QuestLogEntry>,
#[serde(default)]
pub hired_workers: Vec<HiredWorkerView>,
#[serde(default)]
pub interactables: Vec<InteractableView>,
#[serde(default)]
pub ledger: Option<PlayerLedgerView>,
#[serde(default)]
pub career: Option<PlayerCareerView>,
#[serde(default)]
pub combat_fx: Vec<CombatFx>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GroundDropView {
pub id: String,
pub template_id: String,
pub quantity: u32,
pub x: f32,
pub y: f32,
pub z: f32,
#[serde(default)]
pub tile_id: Option<String>,
#[serde(default)]
pub paperdoll_ref: Option<String>,
#[serde(default)]
pub yaw: f32,
#[serde(default)]
pub pitch: f32,
#[serde(default)]
pub roll: f32,
#[serde(default = "default_draw_scale")]
pub draw_scale: f32,
}
fn default_draw_scale() -> f32 {
1.0
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Snapshot {
pub tick: Tick,
pub chunk_rev: u64,
#[serde(default)]
pub content_rev: u64,
#[serde(default)]
pub publish_rev: u64,
pub entities: Vec<EntityState>,
#[serde(default)]
pub resource_nodes: Vec<ResourceNodeView>,
#[serde(default)]
pub world_width_m: f32,
#[serde(default)]
pub world_height_m: f32,
#[serde(default)]
pub buildings: Vec<BuildingView>,
#[serde(default)]
pub doors: Vec<DoorView>,
#[serde(default)]
pub npcs: Vec<NpcView>,
#[serde(default)]
pub inventory: Vec<ItemStack>,
#[serde(default)]
pub blueprints: Vec<BlueprintView>,
#[serde(default)]
pub world_clock: WorldClock,
#[serde(default)]
pub terrain_zones: Vec<TerrainZoneView>,
#[serde(default)]
pub z_platforms: Vec<ZPlatformView>,
#[serde(default)]
pub z_transitions: Vec<ZTransitionView>,
#[serde(default)]
pub ground_drops: Vec<GroundDropView>,
#[serde(default)]
pub placed_containers: Vec<PlacedContainerView>,
#[serde(default)]
pub combat: Option<CombatHud>,
#[serde(default)]
pub interior_map: Option<InteriorMapView>,
#[serde(default)]
pub quest_log: Vec<QuestLogEntry>,
#[serde(default)]
pub hired_workers: Vec<HiredWorkerView>,
#[serde(default)]
pub interactables: Vec<InteractableView>,
#[serde(default)]
pub ledger: Option<PlayerLedgerView>,
#[serde(default)]
pub career: Option<PlayerCareerView>,
#[serde(default)]
pub combat_fx: Vec<CombatFx>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResourceNodeView {
pub id: String,
pub label: String,
pub x: f32,
pub y: f32,
pub z: f32,
pub item_template: String,
#[serde(default = "default_node_state")]
pub state: ResourceNodeState,
#[serde(default = "default_blocking_view")]
pub blocking: bool,
#[serde(default = "default_blocking_radius_view")]
pub blocking_radius_m: f32,
#[serde(default)]
pub tile_id: Option<String>,
#[serde(default)]
pub paperdoll_ref: Option<String>,
#[serde(default)]
pub yaw: f32,
#[serde(default)]
pub pitch: f32,
#[serde(default)]
pub roll: f32,
#[serde(default = "default_draw_scale")]
pub draw_scale: f32,
#[serde(default)]
pub sprite_mode: Option<String>,
#[serde(default)]
pub presentation_state: Option<String>,
}
fn default_blocking_radius_view() -> f32 {
0.8
}
fn default_blocking_view() -> bool {
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResourceNodeState {
Available,
Harvesting,
Cooldown,
}
fn default_node_state() -> ResourceNodeState {
ResourceNodeState::Available
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ItemStatusBindingMode {
OnHit,
WhileEquipped,
}
impl Default for ItemStatusBindingMode {
fn default() -> Self {
Self::OnHit
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ItemStatusBinding {
pub effect_id: String,
#[serde(default)]
pub mode: ItemStatusBindingMode,
#[serde(default)]
pub source: String,
#[serde(default)]
pub applied_at_tick: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at_tick: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ItemStack {
pub template_id: String,
pub quantity: u32,
#[serde(default)]
pub item_instance_id: Option<Uuid>,
#[serde(default)]
pub props: BTreeMap<String, String>,
#[serde(default)]
pub status_bindings: Vec<ItemStatusBinding>,
#[serde(default)]
pub contents: Vec<ItemStack>,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub base_mass: Option<f32>,
#[serde(default)]
pub base_volume: Option<f32>,
#[serde(default)]
pub capacity_volume: Option<f32>,
#[serde(default)]
pub stackable: Option<bool>,
#[serde(default)]
pub world_placeable: Option<bool>,
#[serde(default)]
pub worker_lodging_capacity: Option<u32>,
#[serde(default)]
pub equip_slot: Option<BodySlot>,
#[serde(default)]
pub armor_physical: Option<f32>,
#[serde(default)]
pub resists: Vec<(String, f32)>,
#[serde(default)]
pub hand_slots: Option<u8>,
}
impl ItemStack {
pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
Self {
template_id: template_id.into(),
quantity,
..Default::default()
}
}
}
impl Default for ItemStack {
fn default() -> Self {
Self {
template_id: String::new(),
quantity: 0,
item_instance_id: None,
props: BTreeMap::new(),
status_bindings: Vec::new(),
contents: Vec::new(),
display_name: None,
category: None,
base_mass: None,
base_volume: None,
capacity_volume: None,
stackable: None,
world_placeable: None,
worker_lodging_capacity: None,
equip_slot: None,
armor_physical: None,
resists: Vec::new(),
hand_slots: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EncumbranceState {
#[default]
Light,
Heavy,
Over,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BodySlot {
Head,
#[serde(alias = "body")]
Chest,
#[serde(alias = "arms")]
Forearms,
Legs,
Feet,
Cloak,
Back,
Waist,
Earrings,
Necklace,
Eyeglasses,
#[serde(rename = "ring_left_1", alias = "ring_left1")]
RingLeft1,
#[serde(rename = "ring_left_2", alias = "ring_left2")]
RingLeft2,
#[serde(rename = "ring_right_1", alias = "ring_right1")]
RingRight1,
#[serde(rename = "ring_right_2", alias = "ring_right2")]
RingRight2,
}
impl BodySlot {
pub const ALL: [BodySlot; 15] = [
BodySlot::Head,
BodySlot::Chest,
BodySlot::Forearms,
BodySlot::Legs,
BodySlot::Feet,
BodySlot::Cloak,
BodySlot::Back,
BodySlot::Waist,
BodySlot::Earrings,
BodySlot::Necklace,
BodySlot::Eyeglasses,
BodySlot::RingLeft1,
BodySlot::RingLeft2,
BodySlot::RingRight1,
BodySlot::RingRight2,
];
pub fn as_str(self) -> &'static str {
match self {
BodySlot::Head => "head",
BodySlot::Chest => "chest",
BodySlot::Forearms => "forearms",
BodySlot::Legs => "legs",
BodySlot::Feet => "feet",
BodySlot::Cloak => "cloak",
BodySlot::Back => "back",
BodySlot::Waist => "waist",
BodySlot::Earrings => "earrings",
BodySlot::Necklace => "necklace",
BodySlot::Eyeglasses => "eyeglasses",
BodySlot::RingLeft1 => "ring_left_1",
BodySlot::RingLeft2 => "ring_left_2",
BodySlot::RingRight1 => "ring_right_1",
BodySlot::RingRight2 => "ring_right_2",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InventoryLocation {
Root,
Worn { slot: BodySlot },
Placed { container_id: String },
Keychain,
WhisperPouch,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlacedContainerView {
pub id: String,
pub template_id: String,
pub display_name: String,
pub x: f32,
pub y: f32,
pub z: f32,
pub locked: bool,
#[serde(default)]
pub accessible: bool,
#[serde(default)]
pub owner_character_id: Option<Uuid>,
#[serde(default)]
pub contents: Vec<ItemStack>,
#[serde(default)]
pub lock_id: Option<String>,
#[serde(default)]
pub capacity_volume: Option<f32>,
#[serde(default)]
pub item_instance_id: Option<Uuid>,
#[serde(default)]
pub tile_id: Option<String>,
#[serde(default)]
pub paperdoll_ref: Option<String>,
#[serde(default)]
pub worker_lodging_capacity: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlueprintIngredientView {
pub template_id: String,
pub quantity: u32,
pub consumed: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolRequirementView {
pub item: String,
pub consumed: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SkillRequirementView {
pub skill: String,
pub level: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlueprintView {
pub id: String,
pub label: String,
pub output: String,
pub output_qty: u32,
pub craft_ticks: u32,
pub inputs: Vec<BlueprintIngredientView>,
#[serde(default)]
pub station: Option<String>,
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub required_tools: Vec<ToolRequirementView>,
#[serde(default)]
pub skill: Option<SkillRequirementView>,
#[serde(default)]
pub failure_chance: f32,
#[serde(default)]
pub worker_train_copper: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TerrainKindView {
#[default]
Grass,
Dirt,
Tilled,
Desert,
Hill,
Bog,
ShallowWater,
DeepWater,
Trail,
Road,
Rock,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TerrainZoneView {
pub id: String,
pub x0: f32,
pub y0: f32,
pub x1: f32,
pub y1: f32,
#[serde(default)]
pub kind: TerrainKindView,
#[serde(default)]
pub elevation: f32,
#[serde(default)]
pub glyph: Option<String>,
#[serde(default)]
pub color: Option<String>,
#[serde(default)]
pub tile_id: Option<String>,
#[serde(default)]
pub z_order: i32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ZPlatformView {
pub id: String,
pub z: f32,
pub x0: f32,
pub y0: f32,
pub x1: f32,
pub y1: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ZTransitionView {
pub id: String,
pub z_from: f32,
pub z_to: f32,
pub x0: f32,
pub y0: f32,
pub x1: f32,
pub y1: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BuildingView {
pub id: String,
pub label: String,
pub x: f32,
pub y: f32,
pub width_m: f32,
pub depth_m: f32,
#[serde(default)]
pub interior_blueprint: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DoorView {
pub id: String,
pub building_id: String,
pub x: f32,
pub y: f32,
#[serde(default)]
pub open: bool,
#[serde(default)]
pub portal: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InteriorRoomView {
pub id: String,
pub label: String,
pub floor: i32,
pub x0: f32,
pub y0: f32,
pub x1: f32,
pub y1: f32,
#[serde(default)]
pub floor_color: Option<String>,
#[serde(default)]
pub floor_glyph: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InteriorDoorView {
pub id: String,
pub room_a: String,
pub room_b: String,
pub x: f32,
pub y: f32,
pub kind: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InteriorMapView {
pub building_id: String,
pub blueprint_id: String,
pub background_color: String,
#[serde(default)]
pub default_floor_color: Option<String>,
#[serde(default = "default_floor_height_view")]
pub floor_height_m: f32,
pub rooms: Vec<InteriorRoomView>,
#[serde(default)]
pub room_doors: Vec<InteriorDoorView>,
}
fn default_floor_height_view() -> f32 {
3.0
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NpcView {
pub id: String,
pub label: String,
pub role: String,
pub x: f32,
pub y: f32,
#[serde(default)]
pub building_id: Option<String>,
#[serde(default)]
pub entity_id: Option<EntityId>,
#[serde(default)]
pub life_state: Option<LifeState>,
#[serde(default)]
pub hp_pct: Option<f32>,
#[serde(default)]
pub can_trade: bool,
#[serde(default)]
pub tile_id: Option<String>,
#[serde(default)]
pub behavior_state: Option<String>,
#[serde(default)]
pub presentation_state: Option<String>,
#[serde(default)]
pub sprite_mode: Option<String>,
#[serde(default)]
pub paperdoll_ref: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UseResult {
pub template_id: String,
pub hunger_restored: f32,
pub thirst_restored: f32,
#[serde(default)]
pub health_restored: f32,
#[serde(default)]
pub mana_restored: f32,
#[serde(default)]
pub cleared_dot_ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CraftResult {
pub blueprint_id: String,
pub outputs: Vec<ItemStack>,
pub consumed: Vec<ItemStack>,
#[serde(default = "default_one")]
pub batch_index: u32,
#[serde(default = "default_one")]
pub batch_total: u32,
}
fn default_one() -> u32 {
1
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeathNotice {
pub entity_id: EntityId,
pub respawn_x: f32,
pub respawn_y: f32,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InteractionNotice {
pub target_id: String,
pub message: String,
#[serde(default)]
pub coins_delta: i32,
#[serde(default)]
pub inventory_delta: Vec<ItemStack>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NpcTalkTrustFlag {
Stranger,
Acquainted,
Trusted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NpcTalkDepth {
#[default]
Full,
Brief,
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NpcTalkOpened {
pub npc_id: String,
pub npc_label: String,
pub greeting: String,
pub trust_flag: NpcTalkTrustFlag,
#[serde(default)]
pub talk_depth: NpcTalkDepth,
#[serde(default = "default_true")]
pub trade_allowed: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NpcTalkPending {
pub npc_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NpcTalkReply {
pub npc_id: String,
pub line: String,
pub trust_flag: NpcTalkTrustFlag,
#[serde(default)]
pub wind_down: bool,
#[serde(default)]
pub trade_disabled: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NpcTalkClosed {
pub npc_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NpcTalkError {
pub npc_id: String,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuestStatusView {
Available,
Active,
Completed,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuestObjectiveProgress {
pub label: String,
pub current: u32,
pub required: u32,
pub done: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuestLogEntry {
pub quest_id: String,
pub title: String,
pub description: String,
pub status: QuestStatusView,
#[serde(default)]
pub current_step_id: Option<String>,
#[serde(default)]
pub current_step_title: String,
#[serde(default)]
pub objectives: Vec<QuestObjectiveProgress>,
#[serde(default)]
pub is_tracked: bool,
#[serde(default)]
pub can_withdraw: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InteractableView {
pub id: String,
pub kind: String,
pub label: String,
pub x: f32,
pub y: f32,
pub z: f32,
#[serde(default)]
pub board_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuestOffer {
pub quest_id: String,
pub title: String,
pub description: String,
#[serde(default)]
pub step_count: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuestNotice {
pub quest_id: String,
pub title: String,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShopOfferKind {
Item,
Blueprint,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ShopOffer {
pub offer_id: String,
pub kind: ShopOfferKind,
pub label: String,
#[serde(default)]
pub template_id: Option<String>,
#[serde(default)]
pub blueprint_id: Option<String>,
pub price_copper: u32,
#[serde(default)]
pub affordable: bool,
#[serde(default)]
pub already_owned: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ShopBuyLine {
pub template_id: String,
pub label: String,
pub quantity: u32,
pub price_copper: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BankPanel {
pub npc_id: String,
pub npc_label: String,
pub bank_balance_copper: u64,
pub on_person_copper: u64,
#[serde(default)]
pub pending_outgoing_copper: u64,
#[serde(default)]
pub transfer_fee_bps: u32,
#[serde(default)]
pub transfer_clear_ticks: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StoragePanel {
pub npc_id: String,
pub npc_label: String,
pub building_id: String,
pub building_label: String,
pub used_volume: f32,
pub max_volume: f32,
#[serde(default)]
pub contents: Vec<ItemStack>,
#[serde(default)]
pub ship_destinations: Vec<StorageShipDest>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StorageShipDest {
pub building_id: String,
pub label: String,
pub distance_m: f32,
pub fee_copper: u64,
pub travel_ticks: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ShopCatalog {
pub npc_id: String,
pub npc_label: String,
#[serde(default)]
pub sells: Vec<ShopOffer>,
#[serde(default)]
pub buys: Vec<ShopBuyLine>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HarvestResult {
pub node_id: String,
pub quantity: u32,
pub item_template: String,
#[serde(default)]
pub item_instance_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Envelope<T> {
pub protocol_version: u16,
pub payload: T,
}
impl<T> Envelope<T> {
pub fn new(payload: T) -> Self {
Self {
protocol_version: crate::PROTOCOL_VERSION,
payload,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Hello {
pub client_name: String,
pub protocol_version: u16,
#[serde(default)]
pub auth: AuthCredential,
#[serde(default)]
pub character_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthCredential {
DevLocal,
Session { token: String },
ApiToken { token: String, character_id: Uuid },
}
impl Default for AuthCredential {
fn default() -> Self {
Self::DevLocal
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Welcome {
pub session_id: SessionId,
pub entity_id: EntityId,
pub snapshot: Snapshot,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ServerMessage {
Welcome(Welcome),
ContentUpdated(Snapshot),
Tick(TickDelta),
IntentAck {
entity_id: EntityId,
seq: Seq,
tick: Tick,
},
Chat(ChatMessage),
HarvestResult(HarvestResult),
UseResult(UseResult),
CraftResult(CraftResult),
Death(DeathNotice),
Interaction(InteractionNotice),
ShopOpened(ShopCatalog),
NpcTalkOpened(NpcTalkOpened),
NpcTalkPending(NpcTalkPending),
NpcTalkReply(NpcTalkReply),
NpcTalkClosed(NpcTalkClosed),
NpcTalkError(NpcTalkError),
QuestOffer(QuestOffer),
QuestAccepted(QuestNotice),
QuestWithdrawn(QuestNotice),
QuestStepCompleted(QuestNotice),
QuestCompleted(QuestNotice),
BankOpened(BankPanel),
StorageOpened(StoragePanel),
TradeOpened(TradePanel),
TradeClosed {
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TradePanel {
pub peer_entity_id: EntityId,
pub peer_name: String,
pub my_presented: Vec<ItemStack>,
pub their_presented: Vec<ItemStack>,
pub i_ready: bool,
pub they_ready: bool,
pub my_mass_after: f32,
pub my_mass_max: f32,
pub my_encumbrance_after: EncumbranceState,
pub overburden_warning: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ClientMessage {
Hello(Hello),
Intent(Intent),
Disconnect,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pristine_vitals_state_yields_full_pools() {
let attrs = PrimaryAttributes::default();
let vitals = StoredVitalsState::default().apply_to(attrs);
assert!(vitals.health > 0.0);
assert_eq!(vitals.health, vitals.health_max);
assert!((vitals.mana_max - 61.0).abs() < 0.01);
}
#[test]
fn humanize_snake_id_title_cases_parts() {
assert_eq!(humanize_snake_id("heal_touch"), "Heal Touch");
assert_eq!(humanize_snake_id("fireball"), "Fireball");
assert_eq!(humanize_snake_id("cone_frost"), "Cone Frost");
}
#[test]
fn saved_vitals_scale_when_pool_max_increases() {
let mut attrs = PrimaryAttributes::default();
attrs.intelligence = 140;
attrs.wisdom = 140;
let saved = StoredVitalsState {
health: 100.0,
mana: 14.0,
stamina: 100.0,
..StoredVitalsState::default()
};
let vitals = saved.apply_to(attrs);
assert!(vitals.mana_max > 55.0);
assert!(
(vitals.mana - vitals.mana_max).abs() < 0.01,
"full legacy mana bar migrates to full new bar"
);
}
#[test]
fn empty_vitals_state_is_pristine() {
let pristine = StoredVitalsState {
health: 0.0,
mana: 0.0,
stamina: 0.0,
hunger: 0.0,
thirst: 0.0,
coins: 0,
deaths: 0,
life_state: LifeState::Alive,
};
assert!(pristine.is_pristine());
let vitals = pristine.apply_to(PrimaryAttributes::default());
assert!(vitals.health > 0.0);
}
#[test]
fn stored_vitals_roundtrip_preserves_partial_pools() {
let attrs = PrimaryAttributes::default();
let mut live = PlayerVitals::from_attributes(attrs);
live.health = 25.0;
live.hunger = 77.0;
live.deaths = 2;
let stored = StoredVitalsState::from_live(&live);
let restored = stored.apply_to(attrs);
assert!(
(restored.health - 25.0).abs() < 0.01,
"partial HP below cap stays absolute"
);
assert_eq!(restored.hunger, 77.0);
assert_eq!(restored.deaths, 2);
}
#[test]
fn skill_tiers_start_at_zero() {
let skill = SkillProgress::default();
assert_eq!(skill.level, 0);
assert_eq!(skill.display_tier(), 0);
let trained = SkillProgress {
level: 250,
last_trained_tick: 1,
};
assert_eq!(trained.display_tier(), 2);
}
#[test]
fn quest_server_messages_roundtrip_json() {
use crate::codec::{Codec, PostcardCodec};
let offer = ServerMessage::QuestOffer(QuestOffer {
quest_id: "ada_goblin_hunt".into(),
title: "Goblin Trouble".into(),
description: "Help Ada".into(),
step_count: 3,
});
let notice = ServerMessage::QuestAccepted(QuestNotice {
quest_id: "ada_goblin_hunt".into(),
title: "Goblin Trouble".into(),
message: "Quest accepted".into(),
});
for msg in [offer, notice] {
let bytes = PostcardCodec.encode(&msg).unwrap();
let decoded: ServerMessage = PostcardCodec.decode(&bytes).unwrap();
assert_eq!(decoded, msg);
}
}
#[test]
fn hotbar_consumable_binding_roundtrips() {
let binding = hotbar_consumable_binding("bottle_of_water");
assert_eq!(binding, "item:bottle_of_water");
assert!(hotbar_binding_is_consumable(&binding));
assert_eq!(
hotbar_consumable_template(&binding),
Some("bottle_of_water")
);
assert!(!hotbar_binding_is_consumable("fireball"));
assert_eq!(hotbar_consumable_template("fireball"), None);
}
}