From c776b91a44205b110206c9045066e228616cea94 Mon Sep 17 00:00:00 2001 From: popertots Date: Sat, 21 Mar 2026 23:39:58 +0000 Subject: [PATCH] haulage system WIP --- src/entities/cargo/components.rs | 133 +++++++++++++++++++++ src/entities/cargo/mod.rs | 5 + src/entities/cargo/systems.rs | 55 +++++++++ src/entities/mod.rs | 1 + src/entities/sentient/dorf.rs | 13 +- src/entities/shared_systems/pathfinding.rs | 2 + src/main.rs | 2 + src/world/tiles/tilemap.rs | 67 +++++++++++ 8 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/entities/cargo/components.rs create mode 100644 src/entities/cargo/mod.rs create mode 100644 src/entities/cargo/systems.rs diff --git a/src/entities/cargo/components.rs b/src/entities/cargo/components.rs new file mode 100644 index 0000000..b1fcdac --- /dev/null +++ b/src/entities/cargo/components.rs @@ -0,0 +1,133 @@ +//! Cargo — loose world objects that exist on tiles and can be hauled by entities. +//! +//! # Cargo vs Fixture vs Item +//! +//! | Type | Storage | Standability | Haulable | Examples | +//! |---------|------------------------|--------------|----------|-----------------------| +//! | Fixture | fixture_tiles HashMap | Affects | No | Tree trunk, wall, door | +//! | Cargo | cargo_tiles HashMap | Unaffected | Yes* | Felled log, mined rock | +//! | Item | PersonalInventory | Unaffected | No | Coin, food, tool | +//! +//! *All Cargo is Haulable by default. The Haulable component marks this explicitly +//! and will allow future per-cargo override (e.g. a pinned ritual object). +//! +//! # One per tile +//! TileMap enforces one Cargo entity per tile position via cargo_tiles: FxHashMap. +//! Placing Cargo on an occupied tile returns Err(CargoPlaceError::TileOccupied). +//! The caller must find a free tile first — use TileMap::find_nearest_free_cargo_tile. +//! +//! # HaulSlot +//! Entities that can haul Cargo get a HaulSlot component. Occupying the slot sets +//! Ambulatory.walk_speed to base_walk_speed * ENCUMBERED_SPEED_MULTIPLIER unconditionally, +//! regardless of weight. Emptying it restores walk_speed via InventoryChangedEvent. +//! +//! # CarryVisualState +//! While hauling, the carrier cycles between its normal sprite and the cargo sprite +//! using a fast timer. This mimics ItemRotationState but is carrier-side, not tile-side. + +use bevy::prelude::*; + +/// Marks an entity as a loose world object sitting on a tile. +/// Cargo entities are non-solid — they do not affect is_standable. +/// Registered in TileMap::cargo_tiles for O(1) tile lookup. +/// +/// Cargo is created when fixtures are destroyed (felling a tree creates log Cargo) +/// or when items are dropped as world objects. It ceases to exist when: +/// - Picked up into a HaulSlot (entity still exists, removed from cargo_tiles) +/// - Placed as a new fixture (entity despawned, fixture inserted) +/// - Consumed by a crafting task (entity despawned) +#[derive(Component, Debug)] +pub struct Cargo { + /// World position this Cargo occupies. Kept in sync with Transform. + /// Redundant with Transform but avoids a query when looking up tile position. + pub tile_pos: IVec3, + /// Display name for UI and log messages. + pub name: &'static str, + /// Weight in kg, used when determining if a carrier can haul this. + pub weight: u32, + /// Size (use SIZE_* constants from inventory). Determines what can hold this. + pub size: u8, + /// Sprite handle for rendering when on the ground. + pub ground_sprite: Handle, +} + +/// Marks a Cargo entity as haulable by entities with a HaulSlot. +/// All Cargo is Haulable by default. Remove this component to pin an object +/// in place (future: ritual objects, planted crops treated as Cargo). +#[derive(Component, Debug, Default)] +pub struct Haulable; + +/// The haul slot of a carrying entity. At most one Cargo can be hauled at a time. +/// Occupying this slot sets the entity encumbered unconditionally — two hands full +/// means movement penalty regardless of cargo weight. +/// +/// Entities that can haul must have this component. Add it to Dorf bundles. +/// Do NOT add to Rabbit or Pig — they use PersonalInventory's single slot instead. +#[derive(Component, Debug, Default)] +pub struct HaulSlot { + /// The Cargo entity currently being hauled. None = empty, hands free. + pub contents: Option, +} + +impl HaulSlot { + #[inline] + pub fn is_empty(&self) -> bool { + self.contents.is_none() + } + + #[inline] + pub fn is_occupied(&self) -> bool { + self.contents.is_some() + } + + /// Pick up a Cargo entity into this slot. Returns false if slot is occupied. + #[inline] + pub fn pick_up(&mut self, cargo: Entity) -> bool { + if self.contents.is_some() { + return false; + } + self.contents = Some(cargo); + true + } + + /// Release the hauled Cargo. Returns the entity that was held, or None. + #[inline] + pub fn release(&mut self) -> Option { + self.contents.take() + } +} + +/// Controls the carry visual on the hauling entity. +/// While HaulSlot is occupied, cycles between the entity's normal sprite +/// and the cargo sprite on a fast timer, giving a visual "carrying" impression. +/// +/// Add to entities that have HaulSlot. The system reads HaulSlot to decide +/// whether to cycle or show only the normal sprite. +#[derive(Component, Debug)] +pub struct CarryVisualState { + /// Timer controlling the sprite cycle rate while hauling. + pub timer: Timer, + /// Whether the cargo sprite is currently showing (vs entity normal sprite). + pub showing_cargo: bool, + /// Sprite handle for the "carrying" visual (e.g. log over shoulder). + /// Swapped in/out of the entity's Sprite component while hauling. + pub carry_sprite: Handle, + /// The entity's original sprite, restored when not hauling. + pub normal_sprite: Handle, +} + +impl CarryVisualState { + /// Creates a new CarryVisualState. + /// + /// `normal` — the entity's default sprite handle. + /// `carry` — the sprite handle shown while carrying (may be same as normal for testing). + pub fn new(normal: Handle, carry: Handle) -> Self { + Self { + // Cycle every 0.4s — fast enough to be noticeable, slow enough to read. + timer: Timer::from_seconds(0.4, TimerMode::Repeating), + showing_cargo: false, + carry_sprite: carry, + normal_sprite: normal, + } + } +} diff --git a/src/entities/cargo/mod.rs b/src/entities/cargo/mod.rs new file mode 100644 index 0000000..c83af3b --- /dev/null +++ b/src/entities/cargo/mod.rs @@ -0,0 +1,5 @@ +pub mod components; +pub mod systems; + +pub use components::{CarryVisualState, HaulSlot, Haulable}; +pub use systems::{carry_visual_system, haul_encumbrance_system}; diff --git a/src/entities/cargo/systems.rs b/src/entities/cargo/systems.rs new file mode 100644 index 0000000..3373f49 --- /dev/null +++ b/src/entities/cargo/systems.rs @@ -0,0 +1,55 @@ +//! Cargo systems: carry visual cycling and haul encumbrance. + +use bevy::prelude::*; + +use crate::entities::cargo::components::{CarryVisualState, HaulSlot}; +use crate::entities::item::inventory::constants::ENCUMBERED_SPEED_MULTIPLIER; +use crate::entities::item::inventory::InventoryChangedEvent; +use crate::entities::shared_components::Ambulatory; + +/// Cycle the carry visual sprite while the entity's HaulSlot is occupied. +/// When empty, ensure the normal sprite is restored. +/// +/// Runs in Update (visual only, no game state change). +pub fn carry_visual_system( + time: Res