diff --git a/src/entities/cargo/log.rs b/src/entities/cargo/log.rs index 1d3abb6..124ce02 100644 --- a/src/entities/cargo/log.rs +++ b/src/entities/cargo/log.rs @@ -15,10 +15,26 @@ use crate::entities::cargo::{Cargo, Haulable}; use crate::entities::item::constants::ITEM_Z_FIGHTING_OFFSET; use crate::entities::item::inventory::constants::SIZE_LARGE; use crate::world::tiles::TileMap; +use crate::world::VisibleGameEntity; /// Weight of a single log in kg. Enough to encumber a dorf carrying one. pub const LOG_WEIGHT_KG: u32 = 15; +/// Find the standable surface tile at world XY. Returns the tile one above +/// the highest floor tile where an entity can stand, or None if not found. +fn find_surface_at(world_x: i32, world_y: i32, tilemap: &TileMap) -> Option { + for z in -3i32..=4i32 { + let floor_pos = IVec3::new(world_x, world_y, z * ITILE_SIZE); + if tilemap.floor_tiles.contains_key(&floor_pos) { + let above = IVec3::new(world_x, world_y, floor_pos.z + ITILE_SIZE); + if tilemap.is_standable(above) { + return Some(above); + } + } + } + None +} + /// Spawn a Cargo log entity with a directional fall bias. /// /// `tile_pos` — world position of the trunk tile this log came from. @@ -50,14 +66,18 @@ pub fn spawn_log_cargo( let jitter_tiles = rng.random_range(0u32..=2) as f32 - 1.0; // -1, 0, or +1 let offset = fall_direction * fall_tiles + perp * jitter_tiles; - let biased_pos = IVec3::new( - tile_pos.x + (offset.x * ITILE_SIZE as f32).round() as i32, - tile_pos.y + (offset.y * ITILE_SIZE as f32).round() as i32, - tile_pos.z, - ); + let biased_xy_x = tile_pos.x + (offset.x * ITILE_SIZE as f32).round() as i32; + let biased_xy_y = tile_pos.y + (offset.y * ITILE_SIZE as f32).round() as i32; - // Find nearest free tile from biased position - let drop_pos = tilemap.find_nearest_free_cargo_tile(biased_pos, 4, &[])?; + // Find the actual standable surface at this XY — logs land on the floor + let biased_pos = find_surface_at(biased_xy_x, biased_xy_y, tilemap).unwrap_or(IVec3::new( + biased_xy_x, + biased_xy_y, + tile_pos.z, + )); + + // Find nearest free tile from biased position (increased radius for scattered logs) + let drop_pos = tilemap.find_nearest_free_cargo_tile(biased_pos, 8, &[])?; // TODO: OuchEvent — if a living entity occupies drop_pos, they take impact damage. // Check tilemap occupancy here when the combat/injury system exists. @@ -87,6 +107,7 @@ pub fn spawn_log_cargo( )) .with_scale(Vec3::splat(PIXEL_RATIO)), Visibility::Visible, + VisibleGameEntity, )) .id(); diff --git a/src/entities/tasks/demo.rs b/src/entities/tasks/demo.rs index 034d67e..069bf34 100644 --- a/src/entities/tasks/demo.rs +++ b/src/entities/tasks/demo.rs @@ -26,7 +26,7 @@ use smallvec::SmallVec; use std::collections::VecDeque; use crate::constants::ITILE_SIZE; -use crate::entities::cargo::{Cargo, Haulable}; +use crate::entities::cargo::{Cargo, HaulSlot, Haulable}; use crate::entities::tasks::components::{ ChopStep, HaulStep, Task, TaskQueue, TaskState, CHOP_TICKS_DEFAULT, }; @@ -53,17 +53,14 @@ pub enum DemoState { /// Tree has been felled. Working through the haul queue. /// /// `unassigned` — logs not yet claimed, ordered nearest-to-origin first. - /// `in_progress` — log entities currently assigned to a dorf. - /// - /// Each tick: assign idle dorfs from unassigned. When a log entity - /// disappears from cargo_tiles it has been dropped at destination — - /// remove it from in_progress. When both are empty, go Idle. + /// `in_progress` — dorf entities currently executing a HaulCargo task for this tree. + /// A dorf is removed when they return to idle (log dropped, task complete). Hauling { felled_trunk_pos: IVec3, /// Queue of (log_entity, log_tile_pos) not yet assigned. /// Front = highest priority (nearest to origin). unassigned: VecDeque<(Entity, IVec3)>, - /// Log entities currently being hauled by a dorf. + /// Dorf entities currently hauling a log for this tree. in_progress: FxHashSet, }, } @@ -83,6 +80,7 @@ pub fn demo_system( chunk_map: Res, tree_parts: Query<(Entity, &TreePart)>, cargo_query: Query<(Entity, &Cargo), With>, + haul_slot_query: Query<&HaulSlot>, mut dorf_query: Query<(Entity, &mut TaskQueue, &mut TaskState, &Transform)>, ) { match &mut *demo_state { @@ -139,11 +137,20 @@ pub fn demo_system( .unwrap_or(trunk_pos); // Find one idle dorf — prefer closest to the tree. + // Don't interrupt a dorf still carrying cargo. let mut best_dorf: Option<(Entity, i32)> = None; for (entity, queue, state, transform) in dorf_query.iter() { if !is_idle_dorf(&queue, &state) { continue; } + // Skip dorfs still carrying cargo + if haul_slot_query + .get(entity) + .map(|h| h.is_occupied()) + .unwrap_or(false) + { + continue; + } let pos = transform.translation.as_ivec3(); let dx = (pos.x - lowest_trunk.x).abs() / ITILE_SIZE; let dy = (pos.y - lowest_trunk.y).abs() / ITILE_SIZE; @@ -252,14 +259,12 @@ pub fn demo_system( } => { let felled_trunk_pos = *felled_trunk_pos; - // Remove completed hauls from in_progress — - // a log is done when it's no longer in cargo_tiles (picked up by dorf). - // This is correct: once picked up, the haul is committed from demo's perspective. - in_progress.retain(|&log_entity| { - cargo_query - .get(log_entity) - .map(|(_, cargo)| tilemap.cargo_tiles.contains_key(&cargo.tile_pos)) - .unwrap_or(false) + // Remove dorfs that have returned to idle — their haul is complete + in_progress.retain(|&dorf_entity| { + dorf_query + .get(dorf_entity) + .map(|(_, queue, state, _)| !is_idle_dorf(queue, state)) + .unwrap_or(false) // entity gone = treat as done }); // Assign idle dorfs to unassigned logs @@ -318,7 +323,7 @@ pub fn demo_system( step: HaulStep::MovingToCargo, }); *state = TaskState::Pending; - in_progress.insert(log_entity); + in_progress.insert(dorf_entity); } else { // Couldn't assign — put log back at front of queue unassigned.push_front((log_entity, log_pos)); diff --git a/src/entities/tasks/executor.rs b/src/entities/tasks/executor.rs index 7540dfb..b29bd03 100644 --- a/src/entities/tasks/executor.rs +++ b/src/entities/tasks/executor.rs @@ -185,7 +185,7 @@ pub fn task_executor_system( if fall_dir == Vec2::ZERO { fall_dir = Vec2::new(1.0, 0.0); // default: fall east } - let log_sprite = asset_server.load("tree_trunk.png"); + let log_sprite: Handle = asset_server.load("log_cargo.png"); for &pos in trunk_positions.iter() { crate::entities::cargo::spawn_log_cargo( &mut commands,