inventory: linter fixes - saturating_add, SmallVec<4>, new_default, drop PIG_SLOTS
This commit is contained in:
@@ -28,4 +28,3 @@ pub const SIZE_HUGE: u8 = 8; // chest, anvil, large crate
|
|||||||
/// See PersonalInventory::slots_for_age.
|
/// See PersonalInventory::slots_for_age.
|
||||||
pub const DORF_ADULT_SLOTS: u8 = 8;
|
pub const DORF_ADULT_SLOTS: u8 = 8;
|
||||||
pub const RABBIT_ADULT_SLOTS: u8 = 1;
|
pub const RABBIT_ADULT_SLOTS: u8 = 1;
|
||||||
pub const PIG_ADULT_SLOTS: u8 = 0; // pigs carry nothing
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub struct Container {
|
|||||||
pub is_open: bool,
|
pub is_open: bool,
|
||||||
/// Items held. May include entities that themselves have Container components.
|
/// Items held. May include entities that themselves have Container components.
|
||||||
/// TODO: migrate away from Entity when stable ID scheme exists.
|
/// TODO: migrate away from Entity when stable ID scheme exists.
|
||||||
pub contents: SmallVec<[Entity; 8]>,
|
pub contents: SmallVec<[Entity; 4]>,
|
||||||
/// Cached total weight of contents (updated on add/remove, not per-tick).
|
/// Cached total weight of contents (updated on add/remove, not per-tick).
|
||||||
pub current_weight: u32,
|
pub current_weight: u32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ pub fn can_add(
|
|||||||
if current_count >= max_count {
|
if current_count >= max_count {
|
||||||
return AddResult::TooMany;
|
return AddResult::TooMany;
|
||||||
}
|
}
|
||||||
if carrier_current_weight + item_weight > carrier_max_weight {
|
if carrier_current_weight.saturating_add(item_weight) > carrier_max_weight {
|
||||||
return AddResult::TooHeavy;
|
return AddResult::TooHeavy;
|
||||||
}
|
}
|
||||||
AddResult::Ok
|
AddResult::Ok
|
||||||
|
|||||||
@@ -1,32 +1,4 @@
|
|||||||
//! PersonalInventory — the inventory of a living entity.
|
//! PersonalInventory — slots, strength-based carry weight, encumbrance tracking.
|
||||||
//!
|
|
||||||
//! # Design
|
|
||||||
//! Every entity that can carry items gets a PersonalInventory. Slot count is
|
|
||||||
//! species- and age-defined. Weight limit is strength-based.
|
|
||||||
//!
|
|
||||||
//! # Weight limit
|
|
||||||
//! max_carry_weight = BASE_CARRY_WEIGHT + strength * STRENGTH_CARRY_MULTIPLIER
|
|
||||||
//! + sum(carry_weight_bonus for each Container directly in slots)
|
|
||||||
//!
|
|
||||||
//! The bonus from held containers is re-evaluated whenever the slot contents
|
|
||||||
//! change. It is NOT recursive — containers inside containers do not contribute
|
|
||||||
//! their bonus to the carrier.
|
|
||||||
//!
|
|
||||||
//! # Encumbrance
|
|
||||||
//! When current_weight > max_carry_weight, the entity is encumbered.
|
|
||||||
//! The update_encumbrance system sets Ambulatory.walk_speed to
|
|
||||||
//! base_walk_speed * ENCUMBERED_SPEED_MULTIPLIER when over limit.
|
|
||||||
//!
|
|
||||||
//! # Item placement
|
|
||||||
//! Items are always placed in the best available slot at time of pickup —
|
|
||||||
//! the smallest container whose max_item_size >= item.size with a free slot.
|
|
||||||
//! No automatic reorganisation occurs. Reorganisation is an explicit task.
|
|
||||||
//!
|
|
||||||
//! # Life stage slot counts
|
|
||||||
//! Adult: species value (e.g. DORF_ADULT_SLOTS = 8)
|
|
||||||
//! Child: floor(adult / 2), minimum 0
|
|
||||||
//! Baby: 1 if adult >= 2, else 0
|
|
||||||
//! See slots_for_age().
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
@@ -34,7 +6,6 @@ use smallvec::SmallVec;
|
|||||||
use super::constants::*;
|
use super::constants::*;
|
||||||
use super::ops::{can_add, AddResult};
|
use super::ops::{can_add, AddResult};
|
||||||
|
|
||||||
/// Life stage of a living entity. Determines slot count at spawn.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum LifeStage {
|
pub enum LifeStage {
|
||||||
Baby,
|
Baby,
|
||||||
@@ -43,15 +14,10 @@ pub enum LifeStage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LifeStage {
|
impl LifeStage {
|
||||||
/// Compute slot count for this life stage given the species adult slot count.
|
|
||||||
///
|
|
||||||
/// Baby: 1 if adult_slots >= 2, else 0
|
|
||||||
/// Child: floor(adult_slots / 2), minimum 0
|
|
||||||
/// Adult: adult_slots
|
|
||||||
pub fn slots_for_age(self, adult_slots: u8) -> u8 {
|
pub fn slots_for_age(self, adult_slots: u8) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
LifeStage::Adult => adult_slots,
|
LifeStage::Adult => adult_slots,
|
||||||
LifeStage::Child => adult_slots / 2, // integer floor
|
LifeStage::Child => adult_slots / 2,
|
||||||
LifeStage::Baby => {
|
LifeStage::Baby => {
|
||||||
if adult_slots >= 2 {
|
if adult_slots >= 2 {
|
||||||
1
|
1
|
||||||
@@ -65,32 +31,15 @@ impl LifeStage {
|
|||||||
|
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct PersonalInventory {
|
pub struct PersonalInventory {
|
||||||
/// Items directly held (body slots). May include Container entities.
|
|
||||||
/// SmallVec<[Entity; 8]> — zero heap allocation for typical dorf (8 slots).
|
|
||||||
pub slots: SmallVec<[Entity; 8]>,
|
pub slots: SmallVec<[Entity; 8]>,
|
||||||
|
|
||||||
/// Maximum number of directly-held items (hands, pockets, body slots).
|
|
||||||
pub max_slots: u8,
|
pub max_slots: u8,
|
||||||
|
|
||||||
/// Strength stat (0–255). Determines carry weight capacity.
|
|
||||||
/// Set at spawn per species/individual. Increase when stats system arrives.
|
|
||||||
pub strength: u8,
|
pub strength: u8,
|
||||||
|
|
||||||
/// Cached total weight of all directly-held items + their contents.
|
|
||||||
/// Updated on add/remove. NOT recomputed per tick.
|
|
||||||
pub current_weight: u32,
|
pub current_weight: u32,
|
||||||
|
|
||||||
/// Cached carry weight bonus from Container items in direct slots.
|
|
||||||
/// Updated whenever slots change. NOT recursive.
|
|
||||||
pub held_container_bonus: u32,
|
pub held_container_bonus: u32,
|
||||||
|
|
||||||
/// Whether this entity is currently over their weight limit.
|
|
||||||
/// Set by update_encumbrance system. Read by Ambulatory system.
|
|
||||||
pub is_encumbered: bool,
|
pub is_encumbered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersonalInventory {
|
impl PersonalInventory {
|
||||||
/// Construct for a specific life stage and species.
|
|
||||||
pub fn new(adult_slots: u8, stage: LifeStage, strength: u8) -> Self {
|
pub fn new(adult_slots: u8, stage: LifeStage, strength: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
slots: SmallVec::new(),
|
slots: SmallVec::new(),
|
||||||
@@ -102,15 +51,16 @@ impl PersonalInventory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximum carry weight based on strength and held container bonuses.
|
pub fn new_default(adult_slots: u8, stage: LifeStage) -> Self {
|
||||||
|
Self::new(adult_slots, stage, DEFAULT_STRENGTH)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn max_carry_weight(&self) -> u32 {
|
pub fn max_carry_weight(&self) -> u32 {
|
||||||
BASE_CARRY_WEIGHT
|
BASE_CARRY_WEIGHT
|
||||||
+ self.strength as u32 * STRENGTH_CARRY_MULTIPLIER
|
+ self.strength as u32 * STRENGTH_CARRY_MULTIPLIER
|
||||||
+ self.held_container_bonus
|
+ self.held_container_bonus
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to add an item directly to a personal slot.
|
|
||||||
/// Use this when no container is available or preferred.
|
|
||||||
pub fn try_add_direct(
|
pub fn try_add_direct(
|
||||||
&mut self,
|
&mut self,
|
||||||
item_entity: Entity,
|
item_entity: Entity,
|
||||||
@@ -134,8 +84,6 @@ impl PersonalInventory {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove an item from direct slots. Does NOT recurse into containers.
|
|
||||||
/// Returns true if found and removed.
|
|
||||||
pub fn remove_direct(&mut self, item_entity: Entity, item_weight: u32) -> bool {
|
pub fn remove_direct(&mut self, item_entity: Entity, item_weight: u32) -> bool {
|
||||||
if let Some(pos) = self.slots.iter().position(|&e| e == item_entity) {
|
if let Some(pos) = self.slots.iter().position(|&e| e == item_entity) {
|
||||||
self.slots.swap_remove(pos);
|
self.slots.swap_remove(pos);
|
||||||
@@ -146,8 +94,6 @@ impl PersonalInventory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the entity can accept an item at all (ignoring containers).
|
|
||||||
/// Quick check before attempting full placement.
|
|
||||||
pub fn can_accept(&self, item_weight: u32) -> bool {
|
pub fn can_accept(&self, item_weight: u32) -> bool {
|
||||||
self.slots.len() < self.max_slots as usize
|
self.slots.len() < self.max_slots as usize
|
||||||
&& self.current_weight + item_weight <= self.max_carry_weight()
|
&& self.current_weight + item_weight <= self.max_carry_weight()
|
||||||
@@ -161,13 +107,6 @@ impl PersonalInventory {
|
|||||||
self.slots.len()
|
self.slots.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rebuild held_container_bonus from current direct slots.
|
|
||||||
/// Call after any slot change that might add/remove a Container.
|
|
||||||
/// Caller is responsible for triggering this — not automatic.
|
|
||||||
///
|
|
||||||
/// Requires a query to check which slot entities have Container components.
|
|
||||||
/// Called by the encumbrance system, not inline in add/remove, to avoid
|
|
||||||
/// needing World/query access at the point of inventory mutation.
|
|
||||||
pub fn rebuild_container_bonus(&mut self, bonus: u32) {
|
pub fn rebuild_container_bonus(&mut self, bonus: u32) {
|
||||||
self.held_container_bonus = bonus;
|
self.held_container_bonus = bonus;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,5 @@
|
|||||||
//! Encumbrance system — updates walk_speed based on carry weight.
|
//! Encumbrance system. Fires `InventoryChangedEvent` after any inventory mutation to
|
||||||
//!
|
//! trigger walk_speed recalculation without per-tick iteration over all carriers.
|
||||||
//! # Event-driven design
|
|
||||||
//! Rather than iterating all carriers every tick, encumbrance recalculates only
|
|
||||||
//! for entities that fired an InventoryChangedEvent. At 5000 entities with rare
|
|
||||||
//! inventory changes, this reduces per-tick work from O(entities) to O(changes).
|
|
||||||
//!
|
|
||||||
//! Any code that mutates a PersonalInventory or a Container held by an entity
|
|
||||||
//! MUST fire InventoryChangedEvent { carrier } after the mutation. The carrier
|
|
||||||
//! is always the top-level entity with PersonalInventory — not a nested container.
|
|
||||||
//!
|
|
||||||
//! # base_walk_speed
|
|
||||||
//! Ambulatory gains base_walk_speed: f32 — the unmodified speed. Encumbrance
|
|
||||||
//! writes walk_speed = base_walk_speed * ENCUMBERED_SPEED_MULTIPLIER.
|
|
||||||
//! Restoring writes walk_speed = base_walk_speed. base_walk_speed is never
|
|
||||||
//! modified by encumbrance.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
@@ -23,26 +9,16 @@ use crate::entities::item::inventory::personal::PersonalInventory;
|
|||||||
use crate::entities::item::Container;
|
use crate::entities::item::Container;
|
||||||
use crate::entities::shared_components::Ambulatory;
|
use crate::entities::shared_components::Ambulatory;
|
||||||
|
|
||||||
/// Fire this event whenever a PersonalInventory or a directly-held Container
|
|
||||||
/// changes contents or weight. Always use the top-level carrier entity.
|
|
||||||
///
|
|
||||||
/// Multiple events for the same carrier in one frame are deduplicated —
|
|
||||||
/// only one encumbrance recalculation occurs per carrier per frame.
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct InventoryChangedEvent {
|
pub struct InventoryChangedEvent {
|
||||||
pub carrier: Entity,
|
pub carrier: Entity,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recalculate encumbrance only for carriers that had inventory changes this frame.
|
|
||||||
///
|
|
||||||
/// Deduplicates multiple events for the same entity via FxHashSet before querying.
|
|
||||||
/// Cost: O(unique_changed_carriers) instead of O(all_carriers).
|
|
||||||
pub fn update_encumbrance(
|
pub fn update_encumbrance(
|
||||||
mut events: MessageReader<InventoryChangedEvent>,
|
mut events: MessageReader<InventoryChangedEvent>,
|
||||||
mut carriers: Query<(&mut PersonalInventory, &mut Ambulatory)>,
|
mut carriers: Query<(&mut PersonalInventory, &mut Ambulatory)>,
|
||||||
containers: Query<&Container>,
|
containers: Query<&Container>,
|
||||||
) {
|
) {
|
||||||
// Deduplicate — multiple adds in one frame should only trigger one recalc
|
|
||||||
let changed: FxHashSet<Entity> = events.read().map(|e| e.carrier).collect();
|
let changed: FxHashSet<Entity> = events.read().map(|e| e.carrier).collect();
|
||||||
if changed.is_empty() {
|
if changed.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -53,7 +29,6 @@ pub fn update_encumbrance(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rebuild container bonus from direct slots only (not recursive)
|
|
||||||
let bonus: u32 = inventory
|
let bonus: u32 = inventory
|
||||||
.slots
|
.slots
|
||||||
.iter()
|
.iter()
|
||||||
@@ -72,8 +47,6 @@ pub fn update_encumbrance(
|
|||||||
} else {
|
} else {
|
||||||
ambulatory.base_walk_speed
|
ambulatory.base_walk_speed
|
||||||
};
|
};
|
||||||
// TODO: additional encumbrance debuffs (combat penalty, stamina drain,
|
|
||||||
// job speed modifier) when gameplay systems exist
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub struct Pig {
|
|||||||
sprite: Sprite,
|
sprite: Sprite,
|
||||||
transform: Transform,
|
transform: Transform,
|
||||||
visibility: Visibility,
|
visibility: Visibility,
|
||||||
|
// Pigs have no inventory — PersonalInventory is not attached to this entity.
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pig {
|
impl Pig {
|
||||||
|
|||||||
Reference in New Issue
Block a user