inventory: linter fixes - saturating_add, SmallVec<4>, new_default, drop PIG_SLOTS

This commit is contained in:
2026-03-21 21:39:52 +00:00
parent 2f9360c8bc
commit b7b7af5560
6 changed files with 11 additions and 99 deletions
-1
View File
@@ -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
+1 -1
View File
@@ -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,
} }
+1 -1
View File
@@ -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
+6 -67
View File
@@ -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 (0255). 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;
} }
+2 -29
View File
@@ -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
} }
} }
} }
+1
View File
@@ -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 {