haulage system WIP
This commit is contained in:
@@ -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<IVec3, Entity>.
|
||||
//! 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<Image>,
|
||||
}
|
||||
|
||||
/// 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<Entity>,
|
||||
}
|
||||
|
||||
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<Entity> {
|
||||
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<Image>,
|
||||
/// The entity's original sprite, restored when not hauling.
|
||||
pub normal_sprite: Handle<Image>,
|
||||
}
|
||||
|
||||
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<Image>, carry: Handle<Image>) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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<Time>,
|
||||
mut query: Query<(&HaulSlot, &mut CarryVisualState, &mut Sprite)>,
|
||||
) {
|
||||
for (haul_slot, mut visual, mut sprite) in query.iter_mut() {
|
||||
if haul_slot.is_empty() {
|
||||
if visual.showing_cargo {
|
||||
sprite.image = visual.normal_sprite.clone();
|
||||
visual.showing_cargo = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
visual.timer.tick(time.delta());
|
||||
if visual.timer.just_finished() {
|
||||
visual.showing_cargo = !visual.showing_cargo;
|
||||
sprite.image = if visual.showing_cargo {
|
||||
visual.carry_sprite.clone()
|
||||
} else {
|
||||
visual.normal_sprite.clone()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply encumbrance when HaulSlot is occupied, remove when empty.
|
||||
/// Fires InventoryChangedEvent so update_encumbrance recalculates walk_speed.
|
||||
///
|
||||
/// Uses Changed<HaulSlot> so this only runs for entities whose slot changed this frame.
|
||||
pub fn haul_encumbrance_system(
|
||||
mut query: Query<(Entity, &HaulSlot, &mut Ambulatory), Changed<HaulSlot>>,
|
||||
mut events: MessageWriter<InventoryChangedEvent>,
|
||||
) {
|
||||
for (entity, haul_slot, mut ambulatory) in query.iter_mut() {
|
||||
if haul_slot.is_occupied() {
|
||||
ambulatory.walk_speed = ambulatory.base_walk_speed * ENCUMBERED_SPEED_MULTIPLIER;
|
||||
} else {
|
||||
ambulatory.walk_speed = ambulatory.base_walk_speed;
|
||||
}
|
||||
events.write(InventoryChangedEvent { carrier: entity });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user