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: 500,
dexterity: 500,
intelligence: 500,
stamina: 500,
vitality: 500,
wisdom: 500,
charisma: 500,
}
}
}
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)]
#[serde(default)]
pub struct PlayerSkills {
pub logging: SkillProgress,
pub mining: SkillProgress,
pub evocation: SkillProgress,
pub swords: SkillProgress,
pub crafting: SkillProgress,
pub cartography: SkillProgress,
}
impl Default for PlayerSkills {
fn default() -> Self {
Self {
logging: SkillProgress::default(),
mining: SkillProgress::default(),
evocation: SkillProgress::default(),
swords: SkillProgress::default(),
crafting: 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())
}
}
#[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: "Melee".into(),
abilities: vec![id],
}
}
}
#[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 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>,
}
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,
worn: Vec::new(),
rotation_presets: Vec::new(),
target_slots: Vec::new(),
known_blueprint_ids: Vec::new(),
}
}
}
#[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>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChatChannel {
Nearby,
WhisperStone,
}
#[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,
}
#[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,
},
Say {
entity_id: EntityId,
channel: ChatChannel,
text: String,
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,
},
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,
},
Block {
entity_id: EntityId,
#[serde(default = "default_block_enabled")]
enabled: bool,
seq: Seq,
},
EquipMainhand {
entity_id: EntityId,
#[serde(default)]
template_id: Option<String>,
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,
},
AdvanceRotation {
entity_id: EntityId,
slot_index: u8,
seq: Seq,
},
}
fn default_block_enabled() -> bool {
true
}
#[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,
}
#[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, 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 worn: Vec<(BodySlot, ItemStack)>,
#[serde(default)]
pub carry_mass: f32,
#[serde(default)]
pub carry_mass_max: f32,
#[serde(default)]
pub encumbrance: EncumbranceState,
#[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,
}
#[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>,
}
#[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,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Snapshot {
pub tick: Tick,
pub chunk_rev: u64,
#[serde(default)]
pub content_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 ground_drops: Vec<GroundDropView>,
#[serde(default)]
pub placed_containers: Vec<PlacedContainerView>,
#[serde(default)]
pub combat: Option<CombatHud>,
}
#[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,
}
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, 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 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>,
}
impl ItemStack {
pub fn simple(template_id: impl Into<String>, quantity: u32) -> Self {
Self {
template_id: template_id.into(),
quantity,
item_instance_id: None,
props: BTreeMap::new(),
contents: Vec::new(),
display_name: None,
category: None,
base_mass: None,
base_volume: None,
capacity_volume: None,
stackable: 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,
Body,
Arms,
Legs,
Feet,
Back,
Waist,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InventoryLocation {
Root,
Worn {
slot: BodySlot,
},
Placed {
container_id: String,
},
}
#[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>,
}
#[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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TerrainKindView {
#[default]
Grass,
Hill,
Bog,
ShallowWater,
DeepWater,
}
#[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,
}
#[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_origin_x: f32,
#[serde(default)]
pub interior_origin_y: f32,
#[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 is_exit: bool,
}
#[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>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UseResult {
pub template_id: String,
pub hunger_restored: f32,
pub thirst_restored: f32,
}
#[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 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 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),
}
#[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 - 145.0).abs() < 0.01);
}
#[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 = 42.0;
live.hunger = 77.0;
live.deaths = 2;
let stored = StoredVitalsState::from_live(&live);
let restored = stored.apply_to(attrs);
assert!((restored.health - 42.0).abs() < 0.01, "partial HP below old 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);
}
}