inv update
This commit is contained in:
@@ -1,49 +0,0 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct Container {
|
||||
pub max_weight: u32,
|
||||
pub max_items: Option<u32>,
|
||||
pub current_weight: u32,
|
||||
pub contents: Vec<Entity>, // TODO - move away from Entity
|
||||
pub is_open: bool,
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn new(max_weight: u32, max_items: Option<u32>) -> Self {
|
||||
Self {
|
||||
max_weight,
|
||||
max_items,
|
||||
current_weight: 0,
|
||||
contents: Vec::new(),
|
||||
is_open: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_fit(&self, weight: u32) -> bool {
|
||||
self.current_weight + weight <= self.max_weight
|
||||
&& self
|
||||
.max_items
|
||||
.map_or(true, |max| self.contents.len() < max as usize)
|
||||
}
|
||||
|
||||
pub fn add_item(&mut self, item_entity: Entity, weight: u32) -> bool {
|
||||
if self.can_fit(weight) {
|
||||
self.contents.push(item_entity);
|
||||
self.current_weight += weight;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_item(&mut self, item_entity: Entity, weight: u32) -> bool {
|
||||
if let Some(pos) = self.contents.iter().position(|&e| e == item_entity) {
|
||||
self.contents.remove(pos);
|
||||
self.current_weight = self.current_weight.saturating_sub(weight);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/// Baseline carry weight for an entity with strength 0 (kg, arbitrary units).
|
||||
/// Actual max = BASE_CARRY_WEIGHT + strength * STRENGTH_CARRY_MULTIPLIER.
|
||||
pub const BASE_CARRY_WEIGHT: u32 = 20;
|
||||
|
||||
/// Carry weight added per point of strength.
|
||||
pub const STRENGTH_CARRY_MULTIPLIER: u32 = 5;
|
||||
|
||||
/// Default strength value for a newly spawned entity.
|
||||
/// Tune per entity type by setting PersonalInventory.strength at spawn.
|
||||
pub const DEFAULT_STRENGTH: u8 = 5;
|
||||
|
||||
/// walk_speed multiplier applied when carry weight exceeds max carry weight.
|
||||
/// 0.5 = half speed when over-encumbered (Skyrim-style).
|
||||
pub const ENCUMBERED_SPEED_MULTIPLIER: f32 = 0.5;
|
||||
|
||||
/// Item size values. Every item has a size: u8.
|
||||
/// Containers use max_item_size: u8 to gate what they accept.
|
||||
pub const SIZE_TINY: u8 = 1; // coin, seed, ring, dice
|
||||
pub const SIZE_SMALL: u8 = 2; // apple, knife, pouch, small book
|
||||
pub const SIZE_MEDIUM: u8 = 4; // sword, boot, helmet, loaf of bread
|
||||
pub const SIZE_LARGE: u8 = 6; // shield, backpack (empty), large tool
|
||||
pub const SIZE_HUGE: u8 = 8; // chest, anvil, large crate
|
||||
|
||||
/// Personal inventory slot counts per life stage, applied at spawn.
|
||||
/// Baby: max(0, 1) unless species adult_slots == 1, then 0.
|
||||
/// Child: floor(adult_slots / 2), minimum 0.
|
||||
/// Adult: species-defined value.
|
||||
/// See PersonalInventory::slots_for_age.
|
||||
pub const DORF_ADULT_SLOTS: u8 = 8;
|
||||
pub const RABBIT_ADULT_SLOTS: u8 = 1;
|
||||
pub const PIG_ADULT_SLOTS: u8 = 0; // pigs carry nothing
|
||||
@@ -0,0 +1,143 @@
|
||||
//! Container — a carryable or world-placed storage object.
|
||||
//!
|
||||
//! # Design
|
||||
//! Containers enforce count and size limits. Weight is enforced at the carrier
|
||||
//! (PersonalInventory) level, not here — a container does not know who carries it.
|
||||
//!
|
||||
//! # carry_weight_bonus
|
||||
//! When a Container is directly held in a PersonalInventory slot (not nested inside
|
||||
//! another container), its carry_weight_bonus is added to the carrier's effective
|
||||
//! max weight. Only direct-hold containers contribute — nested containers do not.
|
||||
//! The encumbrance system queries for this during weight recalculation.
|
||||
//!
|
||||
//! # Nesting
|
||||
//! Containers can be placed inside other containers (contents: Vec<Entity> may
|
||||
//! contain entities that themselves have Container components). Nesting depth is
|
||||
//! physically bounded by size — a coin purse (SIZE_SMALL) cannot hold a chest (SIZE_HUGE).
|
||||
//!
|
||||
//! # TODO
|
||||
//! contents currently stores Entity. Once a stable non-generational ID scheme
|
||||
//! exists, migrate to that. Tracked in container.rs TODO comment.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::ops::AddResult;
|
||||
use crate::entities::item::inventory::ops::can_add;
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct Container {
|
||||
/// Maximum number of items this container can hold.
|
||||
pub max_count: u32,
|
||||
/// Maximum size of a single item this container accepts.
|
||||
/// Prevents putting a chest inside a coin purse.
|
||||
pub max_item_size: u8,
|
||||
/// This container's own size as an item (for fitting into other containers).
|
||||
pub size: u8,
|
||||
/// Bonus carry weight (kg) added to the direct carrier when this container
|
||||
/// is held in a PersonalInventory slot. Does NOT stack when nested.
|
||||
pub carry_weight_bonus: u32,
|
||||
/// Whether this container can be opened/closed. Closed containers reject adds.
|
||||
pub is_open: bool,
|
||||
/// Items held. May include entities that themselves have Container components.
|
||||
/// TODO: migrate away from Entity when stable ID scheme exists.
|
||||
pub contents: SmallVec<[Entity; 8]>,
|
||||
/// Cached total weight of contents (updated on add/remove, not per-tick).
|
||||
pub current_weight: u32,
|
||||
}
|
||||
|
||||
impl Container {
|
||||
/// Construct a new container.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `max_count`: item slot limit
|
||||
/// - `max_item_size`: largest item size accepted (use SIZE_* constants)
|
||||
/// - `size`: this container's own size as an item
|
||||
/// - `carry_weight_bonus`: kg added to direct carrier's max weight
|
||||
pub fn new(max_count: u32, max_item_size: u8, size: u8, carry_weight_bonus: u32) -> Self {
|
||||
Self {
|
||||
max_count,
|
||||
max_item_size,
|
||||
size,
|
||||
carry_weight_bonus,
|
||||
is_open: true,
|
||||
contents: SmallVec::new(),
|
||||
current_weight: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to add an item.
|
||||
///
|
||||
/// `carrier_current_weight` and `carrier_max_weight` are the top-level
|
||||
/// PersonalInventory's values — weight is enforced at the carrier level.
|
||||
/// Pass u32::MAX for both if this container is a world object (chest on floor)
|
||||
/// with no carrier weight limit.
|
||||
pub fn try_add(
|
||||
&mut self,
|
||||
item_entity: Entity,
|
||||
item_weight: u32,
|
||||
item_size: u8,
|
||||
carrier_current_weight: u32,
|
||||
carrier_max_weight: u32,
|
||||
) -> AddResult {
|
||||
let result = can_add(
|
||||
self.is_open,
|
||||
self.contents.len(),
|
||||
self.max_count as usize,
|
||||
item_size,
|
||||
self.max_item_size,
|
||||
carrier_current_weight,
|
||||
carrier_max_weight,
|
||||
item_weight,
|
||||
);
|
||||
if result.is_ok() {
|
||||
self.contents.push(item_entity);
|
||||
self.current_weight = self.current_weight.saturating_add(item_weight);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, item_entity: Entity, item_weight: u32) -> bool {
|
||||
if let Some(pos) = self.contents.iter().position(|&e| e == item_entity) {
|
||||
self.contents.swap_remove(pos);
|
||||
self.current_weight = self.current_weight.saturating_sub(item_weight);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, item_entity: Entity) -> bool {
|
||||
self.contents.contains(&item_entity)
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.contents.len()
|
||||
}
|
||||
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.contents.len() >= self.max_count as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Common container presets. Use these rather than constructing manually
|
||||
/// to keep sizes and bonuses consistent across the codebase.
|
||||
impl Container {
|
||||
/// Small coin purse — coins, seeds, dice, rings only.
|
||||
pub fn coin_purse() -> Self {
|
||||
use crate::entities::item::inventory::constants::*;
|
||||
Self::new(20, SIZE_TINY, SIZE_SMALL, 0)
|
||||
}
|
||||
|
||||
/// Leather backpack — most items fit, carried on back.
|
||||
pub fn backpack() -> Self {
|
||||
use crate::entities::item::inventory::constants::*;
|
||||
Self::new(12, SIZE_LARGE, SIZE_LARGE, 20)
|
||||
}
|
||||
|
||||
/// Wooden chest — large capacity, large items fit, significant weight bonus.
|
||||
pub fn wooden_chest() -> Self {
|
||||
use crate::entities::item::inventory::constants::*;
|
||||
Self::new(40, SIZE_HUGE, SIZE_HUGE, 40)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod constants;
|
||||
pub mod container;
|
||||
pub mod ops;
|
||||
pub mod personal;
|
||||
pub mod systems;
|
||||
|
||||
pub use constants::*;
|
||||
pub use container::Container;
|
||||
pub use ops::{best_slot, can_add, AddResult, SwapOp};
|
||||
pub use personal::{LifeStage, PersonalInventory};
|
||||
pub use systems::{update_encumbrance, InventoryChangedEvent};
|
||||
@@ -0,0 +1,103 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// Result of an attempt to add an item to an inventory slot.
|
||||
/// The entity/player sees only the variant name — no internal details exposed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AddResult {
|
||||
/// Item added successfully.
|
||||
Ok,
|
||||
/// Total weight would exceed the carrier's max. Item not added.
|
||||
TooHeavy,
|
||||
/// No free slots remain. Item not added.
|
||||
TooMany,
|
||||
/// Item's size exceeds this container's max_item_size. Item not added.
|
||||
TooBig,
|
||||
/// Container is closed. Item not added.
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl AddResult {
|
||||
pub fn is_ok(self) -> bool {
|
||||
self == AddResult::Ok
|
||||
}
|
||||
|
||||
/// Human-readable reason for UI/log use. Returns None on Ok.
|
||||
pub fn reason(self) -> Option<&'static str> {
|
||||
match self {
|
||||
AddResult::Ok => None,
|
||||
AddResult::TooHeavy => Some("too heavy to carry"),
|
||||
AddResult::TooMany => Some("not enough space"),
|
||||
AddResult::TooBig => Some("item too large to fit"),
|
||||
AddResult::Closed => Some("container is closed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether an item can be added without modifying any state.
|
||||
/// Used by both PersonalInventory and Container before committing.
|
||||
///
|
||||
/// `carrier_current_weight` — total weight currently carried by the top-level carrier.
|
||||
/// `carrier_max_weight` — max weight the carrier can hold.
|
||||
/// Both are passed in so this fn has no ECS access and is testable in isolation.
|
||||
#[inline]
|
||||
pub fn can_add(
|
||||
is_open: bool,
|
||||
current_count: usize,
|
||||
max_count: usize,
|
||||
item_size: u8,
|
||||
max_item_size: u8,
|
||||
carrier_current_weight: u32,
|
||||
carrier_max_weight: u32,
|
||||
item_weight: u32,
|
||||
) -> AddResult {
|
||||
if !is_open {
|
||||
return AddResult::Closed;
|
||||
}
|
||||
if item_size > max_item_size {
|
||||
return AddResult::TooBig;
|
||||
}
|
||||
if current_count >= max_count {
|
||||
return AddResult::TooMany;
|
||||
}
|
||||
if carrier_current_weight + item_weight > carrier_max_weight {
|
||||
return AddResult::TooHeavy;
|
||||
}
|
||||
AddResult::Ok
|
||||
}
|
||||
|
||||
/// Represents a single item movement needed during reorganisation.
|
||||
/// Used by the future reorganise task — defined now so the interface is stable.
|
||||
///
|
||||
/// The task system executes these one at a time with a per-swap delay based on
|
||||
/// the entity's intelligence stat (smarter entity = shorter delay per swap).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SwapOp {
|
||||
/// Entity being moved.
|
||||
pub item: Entity,
|
||||
/// Source container/inventory entity (None = ground).
|
||||
pub from: Option<Entity>,
|
||||
/// Destination container/inventory entity (None = ground/drop).
|
||||
pub to: Option<Entity>,
|
||||
}
|
||||
|
||||
/// Find the best destination entity (container or personal inventory) for an item
|
||||
/// given a list of candidate containers sorted by preference.
|
||||
///
|
||||
/// "Best" = smallest max_item_size that still fits the item, i.e. most specific
|
||||
/// container. A coin goes into the coin purse before the backpack.
|
||||
///
|
||||
/// Returns None if no candidate can accept the item.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `candidates`: slice of (container_entity, max_item_size, current_count,
|
||||
/// max_count, is_open) tuples, already filtered for weight.
|
||||
/// - `item_size`: size of the item to place.
|
||||
pub fn best_slot(candidates: &[(Entity, u8, usize, usize, bool)], item_size: u8) -> Option<Entity> {
|
||||
candidates
|
||||
.iter()
|
||||
.filter(|(_, max_size, count, max_count, open)| {
|
||||
*open && item_size <= *max_size && count < max_count
|
||||
})
|
||||
.min_by_key(|(_, max_size, _, _, _)| *max_size)
|
||||
.map(|(entity, _, _, _, _)| *entity)
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//! PersonalInventory — the inventory of a living entity.
|
||||
//!
|
||||
//! # 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 smallvec::SmallVec;
|
||||
|
||||
use super::constants::*;
|
||||
use super::ops::{can_add, AddResult};
|
||||
|
||||
/// Life stage of a living entity. Determines slot count at spawn.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LifeStage {
|
||||
Baby,
|
||||
Child,
|
||||
Adult,
|
||||
}
|
||||
|
||||
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 {
|
||||
match self {
|
||||
LifeStage::Adult => adult_slots,
|
||||
LifeStage::Child => adult_slots / 2, // integer floor
|
||||
LifeStage::Baby => {
|
||||
if adult_slots >= 2 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
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]>,
|
||||
|
||||
/// Maximum number of directly-held items (hands, pockets, body slots).
|
||||
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,
|
||||
|
||||
/// Cached total weight of all directly-held items + their contents.
|
||||
/// Updated on add/remove. NOT recomputed per tick.
|
||||
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,
|
||||
|
||||
/// Whether this entity is currently over their weight limit.
|
||||
/// Set by update_encumbrance system. Read by Ambulatory system.
|
||||
pub is_encumbered: bool,
|
||||
}
|
||||
|
||||
impl PersonalInventory {
|
||||
/// Construct for a specific life stage and species.
|
||||
pub fn new(adult_slots: u8, stage: LifeStage, strength: u8) -> Self {
|
||||
Self {
|
||||
slots: SmallVec::new(),
|
||||
max_slots: stage.slots_for_age(adult_slots),
|
||||
strength,
|
||||
current_weight: 0,
|
||||
held_container_bonus: 0,
|
||||
is_encumbered: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum carry weight based on strength and held container bonuses.
|
||||
pub fn max_carry_weight(&self) -> u32 {
|
||||
BASE_CARRY_WEIGHT
|
||||
+ self.strength as u32 * STRENGTH_CARRY_MULTIPLIER
|
||||
+ 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(
|
||||
&mut self,
|
||||
item_entity: Entity,
|
||||
item_weight: u32,
|
||||
item_size: u8,
|
||||
) -> AddResult {
|
||||
let result = can_add(
|
||||
true,
|
||||
self.slots.len(),
|
||||
self.max_slots as usize,
|
||||
item_size,
|
||||
u8::MAX,
|
||||
self.current_weight,
|
||||
self.max_carry_weight(),
|
||||
item_weight,
|
||||
);
|
||||
if result.is_ok() {
|
||||
self.slots.push(item_entity);
|
||||
self.current_weight = self.current_weight.saturating_add(item_weight);
|
||||
}
|
||||
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 {
|
||||
if let Some(pos) = self.slots.iter().position(|&e| e == item_entity) {
|
||||
self.slots.swap_remove(pos);
|
||||
self.current_weight = self.current_weight.saturating_sub(item_weight);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.slots.len() < self.max_slots as usize
|
||||
&& self.current_weight + item_weight <= self.max_carry_weight()
|
||||
}
|
||||
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.slots.len() >= self.max_slots as usize
|
||||
}
|
||||
|
||||
pub fn slot_count(&self) -> usize {
|
||||
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) {
|
||||
self.held_container_bonus = bonus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Encumbrance system — updates walk_speed based on carry weight.
|
||||
//!
|
||||
//! # 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 rustc_hash::FxHashSet;
|
||||
|
||||
use crate::entities::item::inventory::constants::ENCUMBERED_SPEED_MULTIPLIER;
|
||||
use crate::entities::item::inventory::personal::PersonalInventory;
|
||||
use crate::entities::item::Container;
|
||||
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)]
|
||||
pub struct InventoryChangedEvent {
|
||||
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(
|
||||
mut events: MessageReader<InventoryChangedEvent>,
|
||||
mut carriers: Query<(&mut PersonalInventory, &mut Ambulatory)>,
|
||||
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();
|
||||
if changed.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for carrier_entity in changed {
|
||||
let Ok((mut inventory, mut ambulatory)) = carriers.get_mut(carrier_entity) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Rebuild container bonus from direct slots only (not recursive)
|
||||
let bonus: u32 = inventory
|
||||
.slots
|
||||
.iter()
|
||||
.filter_map(|&e| containers.get(e).ok())
|
||||
.map(|c| c.carry_weight_bonus)
|
||||
.sum();
|
||||
inventory.rebuild_container_bonus(bonus);
|
||||
|
||||
let was_encumbered = inventory.is_encumbered;
|
||||
let now_encumbered = inventory.current_weight > inventory.max_carry_weight();
|
||||
inventory.is_encumbered = now_encumbered;
|
||||
|
||||
if now_encumbered != was_encumbered {
|
||||
ambulatory.walk_speed = if now_encumbered {
|
||||
ambulatory.base_walk_speed * ENCUMBERED_SPEED_MULTIPLIER
|
||||
} else {
|
||||
ambulatory.base_walk_speed
|
||||
};
|
||||
// TODO: additional encumbrance debuffs (combat penalty, stamina drain,
|
||||
// job speed modifier) when gameplay systems exist
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
pub mod constants;
|
||||
pub mod container;
|
||||
pub mod core;
|
||||
pub mod decorations;
|
||||
pub mod drop_table;
|
||||
pub mod inventory;
|
||||
pub mod material;
|
||||
pub mod perishable;
|
||||
pub mod prefabs;
|
||||
@@ -10,10 +10,13 @@ pub mod quality;
|
||||
pub mod systems;
|
||||
pub mod types;
|
||||
|
||||
pub use container::*;
|
||||
pub use core::*;
|
||||
pub use decorations::*;
|
||||
pub use drop_table::*;
|
||||
pub use inventory::{
|
||||
best_slot, can_add, update_encumbrance, AddResult, Container, InventoryChangedEvent, LifeStage,
|
||||
PersonalInventory, SwapOp,
|
||||
};
|
||||
pub use material::*;
|
||||
pub use perishable::*;
|
||||
pub use prefabs::*;
|
||||
|
||||
@@ -18,10 +18,13 @@ pub struct Pig {
|
||||
|
||||
impl Pig {
|
||||
pub fn new(asset_server: &Res<AssetServer>, position: Vec3) -> Self {
|
||||
let walk_speed = 25.;
|
||||
let run_speed = 6.;
|
||||
Pig {
|
||||
ambulatory: Ambulatory {
|
||||
walk_speed: 25.,
|
||||
run_speed: 6.,
|
||||
walk_speed,
|
||||
base_walk_speed: walk_speed,
|
||||
run_speed,
|
||||
target: None,
|
||||
current_path: None,
|
||||
path_index: 0,
|
||||
|
||||
@@ -20,10 +20,13 @@ pub struct Rabbit {
|
||||
|
||||
impl Rabbit {
|
||||
pub fn new(asset_server: &Res<AssetServer>, position: Vec3) -> Self {
|
||||
let walk_speed = 1.;
|
||||
let run_speed = 6.;
|
||||
Rabbit {
|
||||
ambulatory: Ambulatory {
|
||||
walk_speed: 1.,
|
||||
run_speed: 6.,
|
||||
walk_speed,
|
||||
base_walk_speed: walk_speed,
|
||||
run_speed,
|
||||
target: None,
|
||||
current_path: None,
|
||||
path_index: 0,
|
||||
|
||||
@@ -18,10 +18,13 @@ pub struct Dorf {
|
||||
|
||||
impl Dorf {
|
||||
pub fn new(asset_server: &Res<AssetServer>, position: Vec3) -> Self {
|
||||
let walk_speed = 5.;
|
||||
let run_speed = 6.;
|
||||
Dorf {
|
||||
ambulatory: Ambulatory {
|
||||
walk_speed: 5.,
|
||||
run_speed: 6.,
|
||||
walk_speed,
|
||||
base_walk_speed: walk_speed,
|
||||
run_speed,
|
||||
target: None,
|
||||
current_path: None,
|
||||
path_index: 0,
|
||||
|
||||
@@ -4,6 +4,7 @@ use bevy::tasks::Task;
|
||||
#[derive(Component)]
|
||||
pub struct Ambulatory {
|
||||
pub walk_speed: f32,
|
||||
pub base_walk_speed: f32,
|
||||
pub run_speed: f32,
|
||||
pub current_path: Option<Vec<Vec3>>,
|
||||
pub path_index: usize,
|
||||
@@ -18,6 +19,7 @@ impl Default for Ambulatory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
walk_speed: 1.0,
|
||||
base_walk_speed: 1.0,
|
||||
run_speed: 2.0,
|
||||
current_path: None,
|
||||
path_index: 0,
|
||||
|
||||
@@ -63,6 +63,7 @@ use crate::constants::{
|
||||
ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES,
|
||||
PATHFINDER_PROVISIONAL_NODE_LIMIT, PIXEL_RATIO, TILE_SIZE,
|
||||
};
|
||||
use crate::entities::item::inventory::{update_encumbrance, InventoryChangedEvent};
|
||||
use crate::entities::shared_systems::constants::{
|
||||
CONVOY_DOT_THRESHOLD, ES_DIRECTION_THRESHOLD, HEAD_ON_DOT_THRESHOLD, OCCUPANCY_CROWD_THRESHOLD,
|
||||
PATHFINDER_DIRTY_LOOKAHEAD, PATHFINDER_VALIDATE_STEPS, PATHFINDER_VALIDATION_COOLDOWN,
|
||||
@@ -218,7 +219,8 @@ pub struct PathfindingPlugin;
|
||||
|
||||
impl Plugin for PathfindingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(PathfindingBenchmark::new(100))
|
||||
app.add_message::<InventoryChangedEvent>()
|
||||
.insert_resource(PathfindingBenchmark::new(100))
|
||||
.insert_resource(crate::entities::shared_components::CompletedPaths::default())
|
||||
.insert_resource(crate::entities::shared_components::PathRequestCounter::default())
|
||||
.insert_resource(PathRequestQueue::default())
|
||||
@@ -228,6 +230,7 @@ impl Plugin for PathfindingPlugin {
|
||||
FixedUpdate,
|
||||
(
|
||||
rebuild_tile_occupancy,
|
||||
update_encumbrance,
|
||||
collect_pathfinding_dirty_chunks,
|
||||
invalidate_paths_on_tile_change,
|
||||
prepare_paths,
|
||||
|
||||
Reference in New Issue
Block a user