demo 1
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
//! Log cargo spawning — called when a tree is felled.
|
||||||
|
//!
|
||||||
|
//! Each trunk tile that was removed becomes a Cargo log entity placed on
|
||||||
|
//! that tile. Logs are Haulable — dorfs pick them up into their HaulSlot.
|
||||||
|
//!
|
||||||
|
//! The log sprite reuses the trunk sprite (texture ID 500004) as a
|
||||||
|
//! placeholder until a dedicated ground-log sprite exists.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::constants::PIXEL_RATIO;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Weight of a single log in kg. Enough to encumber a dorf carrying one.
|
||||||
|
pub const LOG_WEIGHT_KG: u32 = 15;
|
||||||
|
|
||||||
|
/// Spawn a Cargo log entity at `tile_pos` and register it in cargo_tiles.
|
||||||
|
///
|
||||||
|
/// If the tile is already occupied (another log landed here), finds the
|
||||||
|
/// nearest free tile via TileMap::find_nearest_free_cargo_tile.
|
||||||
|
///
|
||||||
|
/// Returns the spawned Entity, or None if no free tile found within radius.
|
||||||
|
pub fn spawn_log_cargo(
|
||||||
|
commands: &mut Commands,
|
||||||
|
tilemap: &mut TileMap,
|
||||||
|
tile_pos: IVec3,
|
||||||
|
log_sprite: Handle<Image>,
|
||||||
|
) -> Option<Entity> {
|
||||||
|
// Find a free tile — the exact trunk position may be occupied
|
||||||
|
let drop_pos = tilemap.find_nearest_free_cargo_tile(tile_pos, 4)?;
|
||||||
|
|
||||||
|
let entity = commands
|
||||||
|
.spawn((
|
||||||
|
Cargo {
|
||||||
|
tile_pos: drop_pos,
|
||||||
|
name: "log",
|
||||||
|
weight: LOG_WEIGHT_KG,
|
||||||
|
size: SIZE_LARGE,
|
||||||
|
ground_sprite: log_sprite.clone(),
|
||||||
|
},
|
||||||
|
Haulable,
|
||||||
|
Sprite {
|
||||||
|
image: log_sprite.clone(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Transform::from_translation(Vec3::new(
|
||||||
|
drop_pos.x as f32,
|
||||||
|
drop_pos.y as f32,
|
||||||
|
drop_pos.z as f32 + ITEM_Z_FIGHTING_OFFSET,
|
||||||
|
))
|
||||||
|
.with_scale(Vec3::splat(PIXEL_RATIO)),
|
||||||
|
Visibility::Visible,
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
tilemap
|
||||||
|
.place_cargo(drop_pos, entity)
|
||||||
|
.expect("place_cargo failed after find_nearest_free_cargo_tile succeeded");
|
||||||
|
|
||||||
|
Some(entity)
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
|
pub mod log;
|
||||||
pub mod systems;
|
pub mod systems;
|
||||||
|
|
||||||
pub use crate::world::tiles::tilemap::CargoPlaceError;
|
pub use crate::world::tiles::tilemap::CargoPlaceError;
|
||||||
pub use components::{Cargo, CarryVisualState, HaulSlot, Haulable};
|
pub use components::{Cargo, CarryVisualState, HaulSlot, Haulable};
|
||||||
|
pub use log::spawn_log_cargo;
|
||||||
pub use systems::{any_hauling, carry_visual_system, haul_encumbrance_system};
|
pub use systems::{any_hauling, carry_visual_system, haul_encumbrance_system};
|
||||||
|
|
||||||
pub use crate::plugins::cargo::CargoPlugin;
|
pub use crate::plugins::cargo::CargoPlugin;
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ impl Dorf {
|
|||||||
sigma_world: behaviour.idle.sigma_world,
|
sigma_world: behaviour.idle.sigma_world,
|
||||||
state: IdleState::Picking {
|
state: IdleState::Picking {
|
||||||
retry_after_tick: 0,
|
retry_after_tick: 0,
|
||||||
|
retry_count: 0,
|
||||||
},
|
},
|
||||||
}]),
|
}]),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,11 +14,18 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
/// Maximum consecutive pick failures before panic.
|
||||||
|
/// Prevents infinite retry loops when tilemap has no standable tiles.
|
||||||
|
pub const IDLE_MAX_RETRIES: u32 = 50;
|
||||||
|
|
||||||
/// Sub-state of Task::Idle.
|
/// Sub-state of Task::Idle.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum IdleState {
|
pub enum IdleState {
|
||||||
/// Pick a new target. Retry cooldown prevents CPU spike when no tiles found.
|
/// Pick a new target. Retry cooldown prevents CPU spike when no tiles found.
|
||||||
Picking { retry_after_tick: u32 },
|
Picking {
|
||||||
|
retry_after_tick: u32,
|
||||||
|
retry_count: u32,
|
||||||
|
},
|
||||||
/// Walking toward target.
|
/// Walking toward target.
|
||||||
Moving { target: IVec3 },
|
Moving { target: IVec3 },
|
||||||
/// Standing still, loitering.
|
/// Standing still, loitering.
|
||||||
@@ -32,6 +39,47 @@ pub enum IdleState {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Step state for Task::ChopTree.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ChopStep {
|
||||||
|
/// Walking to within range of the trunk.
|
||||||
|
MovingToTree,
|
||||||
|
/// Entity is in range and actively chopping.
|
||||||
|
Chopping { ticks_remaining: u32 },
|
||||||
|
/// Chopping complete.
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step state for Task::HaulCargo.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum HaulStep {
|
||||||
|
/// Walking to the cargo tile.
|
||||||
|
MovingToCargo,
|
||||||
|
/// At cargo — picking up this tick.
|
||||||
|
PickingUp,
|
||||||
|
/// Cargo in HaulSlot — walking to destination.
|
||||||
|
MovingToDest,
|
||||||
|
/// At destination — dropping this tick.
|
||||||
|
Dropping,
|
||||||
|
/// Haul complete.
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step state for Task::DropHauled.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum DropStep {
|
||||||
|
/// Walking to drop position.
|
||||||
|
Moving,
|
||||||
|
/// At drop position — dropping this tick.
|
||||||
|
Dropping,
|
||||||
|
/// Drop complete.
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default ticks to chop a tree (~2 seconds at 60 TPS).
|
||||||
|
/// Future: replaced by skill/tool calculation.
|
||||||
|
pub const CHOP_TICKS_DEFAULT: u32 = 120;
|
||||||
|
|
||||||
/// A single task an entity can execute.
|
/// A single task an entity can execute.
|
||||||
///
|
///
|
||||||
/// Tasks are self-contained: they carry all data needed for execution.
|
/// Tasks are self-contained: they carry all data needed for execution.
|
||||||
@@ -64,16 +112,32 @@ pub enum Task {
|
|||||||
threshold_tiles: i32,
|
threshold_tiles: i32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Placeholder for Stage 3 implementations.
|
/// Walk to a tree trunk and fell it. Produces Cargo log entities on completion.
|
||||||
/// Executor should match and log "unimplemented" for now.
|
|
||||||
ChopTree {
|
ChopTree {
|
||||||
target_fixture: IVec3,
|
/// World position of the lowest trunk fixture tile.
|
||||||
|
trunk_pos: IVec3,
|
||||||
|
/// Ticks required to chop. Future: derived from skill + tool.
|
||||||
|
chop_ticks: u32,
|
||||||
|
step: ChopStep,
|
||||||
},
|
},
|
||||||
HaulObject {
|
|
||||||
target_cargo_tile: IVec3,
|
/// Pick up a specific Cargo entity and haul it to dest.
|
||||||
|
/// Renamed from HaulObject for consistency with the Cargo type.
|
||||||
|
HaulCargo {
|
||||||
|
/// The Cargo entity to pick up.
|
||||||
|
cargo_entity: Entity,
|
||||||
|
/// Tile position of the cargo in the world (for pathfinding).
|
||||||
|
cargo_pos: IVec3,
|
||||||
|
/// Destination tile position.
|
||||||
|
dest: IVec3,
|
||||||
|
step: HaulStep,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Drop whatever is in HaulSlot at or near pos.
|
||||||
DropHauled {
|
DropHauled {
|
||||||
drop_tile: IVec3,
|
/// Preferred drop position. Actual drop may be nearby if occupied.
|
||||||
|
pos: IVec3,
|
||||||
|
step: DropStep,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +156,7 @@ impl Task {
|
|||||||
Task::Idle { .. } => "Idle",
|
Task::Idle { .. } => "Idle",
|
||||||
Task::GoTo { .. } => "GoTo",
|
Task::GoTo { .. } => "GoTo",
|
||||||
Task::ChopTree { .. } => "ChopTree",
|
Task::ChopTree { .. } => "ChopTree",
|
||||||
Task::HaulObject { .. } => "HaulObject",
|
Task::HaulCargo { .. } => "HaulCargo",
|
||||||
Task::DropHauled { .. } => "DropHauled",
|
Task::DropHauled { .. } => "DropHauled",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+244
-12
@@ -9,23 +9,31 @@
|
|||||||
//! Uses Changed<TaskQueue> + Changed<TaskState> to minimise queries.
|
//! Uses Changed<TaskQueue> + Changed<TaskState> to minimise queries.
|
||||||
|
|
||||||
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
|
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
|
||||||
|
use crate::entities::cargo::HaulSlot;
|
||||||
use crate::entities::shared_components::Ambulatory;
|
use crate::entities::shared_components::Ambulatory;
|
||||||
use crate::entities::tasks::components::{IdleState, Task, TaskQueue, TaskState};
|
use crate::entities::tasks::components::{
|
||||||
|
ChopStep, DropStep, HaulStep, IdleState, Task, TaskQueue, TaskState,
|
||||||
|
};
|
||||||
use crate::entities::tasks::events::{TaskClaimed, TaskCompleted, TaskFailed};
|
use crate::entities::tasks::events::{TaskClaimed, TaskCompleted, TaskFailed};
|
||||||
use crate::entities::tasks::idle::execute_idle;
|
use crate::entities::tasks::idle::execute_idle;
|
||||||
use crate::world::chunks::ChunkMap;
|
use crate::world::chunks::ChunkMap;
|
||||||
|
use crate::world::generation::forestry::{fell_tree, TreePart};
|
||||||
|
use crate::world::tiles::tile_changed::TileChangedEvent;
|
||||||
|
use crate::world::tiles::visibility::TileOcclusionEvent;
|
||||||
use crate::world::tiles::TileMap;
|
use crate::world::tiles::TileMap;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_rand::prelude::*;
|
use bevy_rand::prelude::*;
|
||||||
use rand::{RngExt, SeedableRng};
|
|
||||||
|
|
||||||
/// Main task executor. Runs in FixedUpdate.
|
/// Main task executor. Runs in FixedUpdate.
|
||||||
pub fn task_executor_system(
|
pub fn task_executor_system(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
tilemap: Res<TileMap>,
|
tilemap: Res<TileMap>,
|
||||||
|
mut tilemap_mut: ResMut<TileMap>,
|
||||||
chunk_map: Res<ChunkMap>,
|
chunk_map: Res<ChunkMap>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
||||||
mut tick: Local<u32>,
|
mut tick: Local<u32>,
|
||||||
|
tree_parts: Query<(Entity, &TreePart)>,
|
||||||
mut query: Query<(
|
mut query: Query<(
|
||||||
Entity,
|
Entity,
|
||||||
&mut TaskQueue,
|
&mut TaskQueue,
|
||||||
@@ -34,10 +42,13 @@ pub fn task_executor_system(
|
|||||||
&Transform,
|
&Transform,
|
||||||
&mut Sprite,
|
&mut Sprite,
|
||||||
&EntityType,
|
&EntityType,
|
||||||
|
Option<&mut HaulSlot>,
|
||||||
)>,
|
)>,
|
||||||
mut claimed_writer: MessageWriter<TaskClaimed>,
|
mut claimed_writer: MessageWriter<TaskClaimed>,
|
||||||
mut completed_writer: MessageWriter<TaskCompleted>,
|
mut completed_writer: MessageWriter<TaskCompleted>,
|
||||||
mut failed_writer: MessageWriter<TaskFailed>,
|
mut failed_writer: MessageWriter<TaskFailed>,
|
||||||
|
mut tile_changed: MessageWriter<TileChangedEvent>,
|
||||||
|
mut occlusion: MessageWriter<TileOcclusionEvent>,
|
||||||
) {
|
) {
|
||||||
let Ok(mut rng) = rng_q.single_mut() else {
|
let Ok(mut rng) = rng_q.single_mut() else {
|
||||||
return;
|
return;
|
||||||
@@ -46,8 +57,16 @@ pub fn task_executor_system(
|
|||||||
*tick = tick.wrapping_add(1);
|
*tick = tick.wrapping_add(1);
|
||||||
let current_tick = *tick;
|
let current_tick = *tick;
|
||||||
|
|
||||||
for (entity, mut queue, mut state, mut ambulatory, transform, mut sprite, entity_type) in
|
for (
|
||||||
query.iter_mut()
|
entity,
|
||||||
|
mut queue,
|
||||||
|
mut state,
|
||||||
|
mut ambulatory,
|
||||||
|
transform,
|
||||||
|
mut sprite,
|
||||||
|
entity_type,
|
||||||
|
mut haul_slot,
|
||||||
|
) in query.iter_mut()
|
||||||
{
|
{
|
||||||
let origin = transform.translation.as_ivec3();
|
let origin = transform.translation.as_ivec3();
|
||||||
let behaviour = EntityBehaviourRegistry::global_get(entity_type.0);
|
let behaviour = EntityBehaviourRegistry::global_get(entity_type.0);
|
||||||
@@ -58,6 +77,7 @@ pub fn task_executor_system(
|
|||||||
sigma_world: behaviour.idle.sigma_world,
|
sigma_world: behaviour.idle.sigma_world,
|
||||||
state: IdleState::Picking {
|
state: IdleState::Picking {
|
||||||
retry_after_tick: 0,
|
retry_after_tick: 0,
|
||||||
|
retry_count: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,13 +130,224 @@ pub fn task_executor_system(
|
|||||||
ambulatory.current_path = None;
|
ambulatory.current_path = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::ChopTree { .. } | Task::HaulObject { .. } | Task::DropHauled { .. } => {
|
Task::ChopTree {
|
||||||
debug!(
|
trunk_pos,
|
||||||
"Task {} unimplemented for entity {:?}",
|
chop_ticks,
|
||||||
current_task.name(),
|
step,
|
||||||
entity
|
} => match step {
|
||||||
);
|
ChopStep::MovingToTree => {
|
||||||
*state = TaskState::Failed;
|
if !tilemap_mut.fixture_tiles.contains_key(trunk_pos) {
|
||||||
|
failed_writer.write(TaskFailed {
|
||||||
|
entity,
|
||||||
|
task: current_task.clone(),
|
||||||
|
reason: "tree already gone",
|
||||||
|
});
|
||||||
|
*state = TaskState::Failed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ambulatory.target.is_none() {
|
||||||
|
ambulatory.target = Some(Vec3::new(
|
||||||
|
trunk_pos.x as f32,
|
||||||
|
trunk_pos.y as f32,
|
||||||
|
trunk_pos.z as f32 + 1.0,
|
||||||
|
));
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
}
|
||||||
|
let dx = transform.translation.x - trunk_pos.x as f32;
|
||||||
|
let dy = transform.translation.y - trunk_pos.y as f32;
|
||||||
|
let dist_sq = dx * dx + dy * dy;
|
||||||
|
let chop_range_sq = (crate::constants::TILE_SIZE * 2.5)
|
||||||
|
* (crate::constants::TILE_SIZE * 2.5);
|
||||||
|
if dist_sq <= chop_range_sq {
|
||||||
|
ambulatory.target = None;
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
*step = ChopStep::Chopping {
|
||||||
|
ticks_remaining: *chop_ticks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChopStep::Chopping { ticks_remaining } => {
|
||||||
|
if *ticks_remaining == 0 {
|
||||||
|
let trunk_positions = fell_tree(
|
||||||
|
*trunk_pos,
|
||||||
|
&tree_parts,
|
||||||
|
&mut commands,
|
||||||
|
&mut tilemap_mut,
|
||||||
|
&mut tile_changed,
|
||||||
|
&mut occlusion,
|
||||||
|
);
|
||||||
|
let log_sprite = asset_server.load("tree_trunk.png");
|
||||||
|
for &pos in trunk_positions.iter() {
|
||||||
|
crate::entities::cargo::spawn_log_cargo(
|
||||||
|
&mut commands,
|
||||||
|
&mut tilemap_mut,
|
||||||
|
pos,
|
||||||
|
log_sprite.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*step = ChopStep::Done;
|
||||||
|
} else {
|
||||||
|
*ticks_remaining -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChopStep::Done => {
|
||||||
|
*state = TaskState::Completed;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Task::HaulCargo {
|
||||||
|
cargo_entity,
|
||||||
|
cargo_pos,
|
||||||
|
dest,
|
||||||
|
step,
|
||||||
|
} => {
|
||||||
|
let Some(ref mut haul) = haul_slot else {
|
||||||
|
failed_writer.write(TaskFailed {
|
||||||
|
entity,
|
||||||
|
task: current_task.clone(),
|
||||||
|
reason: "entity has no HaulSlot",
|
||||||
|
});
|
||||||
|
*state = TaskState::Failed;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match step {
|
||||||
|
HaulStep::MovingToCargo => {
|
||||||
|
if !tilemap_mut.cargo_tiles.contains_key(cargo_pos) {
|
||||||
|
failed_writer.write(TaskFailed {
|
||||||
|
entity,
|
||||||
|
task: current_task.clone(),
|
||||||
|
reason: "cargo no longer exists",
|
||||||
|
});
|
||||||
|
*state = TaskState::Failed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ambulatory.target.is_none() {
|
||||||
|
ambulatory.target = Some(Vec3::new(
|
||||||
|
cargo_pos.x as f32,
|
||||||
|
cargo_pos.y as f32,
|
||||||
|
cargo_pos.z as f32 + 1.0,
|
||||||
|
));
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
}
|
||||||
|
let dx = transform.translation.x - cargo_pos.x as f32;
|
||||||
|
let dy = transform.translation.y - cargo_pos.y as f32;
|
||||||
|
let dist_sq = dx * dx + dy * dy;
|
||||||
|
let pickup_range_sq = (crate::constants::TILE_SIZE * 1.5)
|
||||||
|
* (crate::constants::TILE_SIZE * 1.5);
|
||||||
|
if dist_sq <= pickup_range_sq {
|
||||||
|
ambulatory.target = None;
|
||||||
|
*step = HaulStep::PickingUp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HaulStep::PickingUp => {
|
||||||
|
if haul.is_occupied() {
|
||||||
|
failed_writer.write(TaskFailed {
|
||||||
|
entity,
|
||||||
|
task: current_task.clone(),
|
||||||
|
reason: "HaulSlot already occupied",
|
||||||
|
});
|
||||||
|
*state = TaskState::Failed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(_) = tilemap_mut.remove_cargo(cargo_pos) {
|
||||||
|
haul.pick_up(*cargo_entity);
|
||||||
|
*step = HaulStep::MovingToDest;
|
||||||
|
} else {
|
||||||
|
failed_writer.write(TaskFailed {
|
||||||
|
entity,
|
||||||
|
task: current_task.clone(),
|
||||||
|
reason: "cargo vanished before pickup",
|
||||||
|
});
|
||||||
|
*state = TaskState::Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HaulStep::MovingToDest => {
|
||||||
|
if ambulatory.target.is_none() {
|
||||||
|
let drop_pos = tilemap_mut
|
||||||
|
.find_nearest_free_cargo_tile(*dest, 8)
|
||||||
|
.unwrap_or(*dest);
|
||||||
|
ambulatory.target = Some(Vec3::new(
|
||||||
|
drop_pos.x as f32,
|
||||||
|
drop_pos.y as f32,
|
||||||
|
drop_pos.z as f32 + 1.0,
|
||||||
|
));
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
}
|
||||||
|
let target_pos = ambulatory.target.unwrap_or(Vec3::ZERO);
|
||||||
|
let dx = transform.translation.x - target_pos.x;
|
||||||
|
let dy = transform.translation.y - target_pos.y;
|
||||||
|
let dist_sq = dx * dx + dy * dy;
|
||||||
|
let arrive_sq = (crate::constants::TILE_SIZE * 1.5)
|
||||||
|
* (crate::constants::TILE_SIZE * 1.5);
|
||||||
|
if dist_sq <= arrive_sq {
|
||||||
|
ambulatory.target = None;
|
||||||
|
*step = HaulStep::Dropping;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HaulStep::Dropping => {
|
||||||
|
if let Some(cargo) = haul.release() {
|
||||||
|
let drop_pos = tilemap_mut
|
||||||
|
.find_nearest_free_cargo_tile(
|
||||||
|
transform.translation.as_ivec3(),
|
||||||
|
8,
|
||||||
|
)
|
||||||
|
.unwrap_or(transform.translation.as_ivec3());
|
||||||
|
let _ = tilemap_mut.place_cargo(drop_pos, cargo);
|
||||||
|
// TODO: update Cargo.tile_pos via CargoDroppedEvent
|
||||||
|
}
|
||||||
|
*step = HaulStep::Done;
|
||||||
|
}
|
||||||
|
HaulStep::Done => {
|
||||||
|
*state = TaskState::Completed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::DropHauled { pos, step } => {
|
||||||
|
let Some(ref mut haul) = haul_slot else {
|
||||||
|
*state = TaskState::Completed;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match step {
|
||||||
|
DropStep::Moving => {
|
||||||
|
if ambulatory.target.is_none() {
|
||||||
|
let drop_pos = tilemap_mut
|
||||||
|
.find_nearest_free_cargo_tile(*pos, 8)
|
||||||
|
.unwrap_or(*pos);
|
||||||
|
ambulatory.target = Some(Vec3::new(
|
||||||
|
drop_pos.x as f32,
|
||||||
|
drop_pos.y as f32,
|
||||||
|
drop_pos.z as f32 + 1.0,
|
||||||
|
));
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
}
|
||||||
|
let target_pos = ambulatory.target.unwrap_or(Vec3::ZERO);
|
||||||
|
let dx = transform.translation.x - target_pos.x;
|
||||||
|
let dy = transform.translation.y - target_pos.y;
|
||||||
|
let dist_sq = dx * dx + dy * dy;
|
||||||
|
let arrive_sq = (crate::constants::TILE_SIZE * 1.5)
|
||||||
|
* (crate::constants::TILE_SIZE * 1.5);
|
||||||
|
if dist_sq <= arrive_sq {
|
||||||
|
ambulatory.target = None;
|
||||||
|
*step = DropStep::Dropping;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropStep::Dropping => {
|
||||||
|
if let Some(cargo) = haul.release() {
|
||||||
|
let drop_pos = tilemap_mut
|
||||||
|
.find_nearest_free_cargo_tile(
|
||||||
|
transform.translation.as_ivec3(),
|
||||||
|
8,
|
||||||
|
)
|
||||||
|
.unwrap_or(transform.translation.as_ivec3());
|
||||||
|
let _ = tilemap_mut.place_cargo(drop_pos, cargo);
|
||||||
|
}
|
||||||
|
*step = DropStep::Done;
|
||||||
|
}
|
||||||
|
DropStep::Done => {
|
||||||
|
*state = TaskState::Completed;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +365,7 @@ pub fn task_executor_system(
|
|||||||
failed_writer.write(TaskFailed {
|
failed_writer.write(TaskFailed {
|
||||||
entity,
|
entity,
|
||||||
task: completed_task.clone(),
|
task: completed_task.clone(),
|
||||||
reason: "unimplemented",
|
reason: completed_task.name(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +375,7 @@ pub fn task_executor_system(
|
|||||||
sigma_world: behaviour.idle.sigma_world,
|
sigma_world: behaviour.idle.sigma_world,
|
||||||
state: IdleState::Picking {
|
state: IdleState::Picking {
|
||||||
retry_after_tick: 0,
|
retry_after_tick: 0,
|
||||||
|
retry_count: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::constants::{ITILE_SIZE, TILE_SIZE};
|
use crate::constants::{ITILE_SIZE, TILE_SIZE};
|
||||||
use crate::entities::behaviour::IdleBehaviour;
|
use crate::entities::behaviour::IdleBehaviour;
|
||||||
use crate::entities::shared_components::Ambulatory;
|
use crate::entities::shared_components::Ambulatory;
|
||||||
use crate::entities::tasks::components::{IdleState, Task};
|
use crate::entities::tasks::components::{IdleState, Task, IDLE_MAX_RETRIES};
|
||||||
use crate::world::chunks::ChunkMap;
|
use crate::world::chunks::ChunkMap;
|
||||||
use crate::world::chunks::CHUNK_SIZE;
|
use crate::world::chunks::CHUNK_SIZE;
|
||||||
use crate::world::tiles::TileMap;
|
use crate::world::tiles::TileMap;
|
||||||
@@ -30,7 +30,10 @@ pub(super) fn execute_idle(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
IdleState::Picking { retry_after_tick } => {
|
IdleState::Picking {
|
||||||
|
retry_after_tick,
|
||||||
|
retry_count,
|
||||||
|
} => {
|
||||||
if current_tick < *retry_after_tick {
|
if current_tick < *retry_after_tick {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -47,6 +50,19 @@ pub(super) fn execute_idle(
|
|||||||
*state = IdleState::Moving { target };
|
*state = IdleState::Moving { target };
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
*retry_count += 1;
|
||||||
|
if *retry_count >= IDLE_MAX_RETRIES {
|
||||||
|
panic!(
|
||||||
|
"Entity stuck: pick_gaussian_target returned None {} times \
|
||||||
|
consecutively. origin={:?} sigma_world={:.1} \
|
||||||
|
loaded_chunks={} \
|
||||||
|
— no standable tile found. Check tilemap state.",
|
||||||
|
retry_count,
|
||||||
|
origin,
|
||||||
|
sigma_world,
|
||||||
|
chunk_map.loaded_chunks.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
*retry_after_tick = current_tick.saturating_add(30);
|
*retry_after_tick = current_tick.saturating_add(30);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,6 +106,7 @@ pub(super) fn execute_idle(
|
|||||||
} else {
|
} else {
|
||||||
*state = IdleState::Picking {
|
*state = IdleState::Picking {
|
||||||
retry_after_tick: 0,
|
retry_after_tick: 0,
|
||||||
|
retry_count: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,6 +131,7 @@ pub(super) fn execute_idle(
|
|||||||
sprite.flip_x = false;
|
sprite.flip_x = false;
|
||||||
*state = IdleState::Picking {
|
*state = IdleState::Picking {
|
||||||
retry_after_tick: 0,
|
retry_after_tick: 0,
|
||||||
|
retry_count: 0,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
*ticks_remaining -= 1;
|
*ticks_remaining -= 1;
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ pub fn fell_tree(
|
|||||||
tilemap: &mut TileMap,
|
tilemap: &mut TileMap,
|
||||||
tile_changed: &mut MessageWriter<TileChangedEvent>,
|
tile_changed: &mut MessageWriter<TileChangedEvent>,
|
||||||
occlusion: &mut MessageWriter<TileOcclusionEvent>,
|
occlusion: &mut MessageWriter<TileOcclusionEvent>,
|
||||||
) {
|
) -> SmallVec<[IVec3; 8]> {
|
||||||
let target_chunk = world_to_chunk(trunk_pos);
|
let target_chunk = world_to_chunk(trunk_pos);
|
||||||
let trunk_x = trunk_pos.x;
|
let trunk_x = trunk_pos.x;
|
||||||
let trunk_y = trunk_pos.y;
|
let trunk_y = trunk_pos.y;
|
||||||
@@ -315,7 +315,22 @@ pub fn fell_tree(
|
|||||||
// calculate_visibility traces up arbitrarily deep, so refresh the full column.
|
// calculate_visibility traces up arbitrarily deep, so refresh the full column.
|
||||||
// z_total is constant per call — hoist above the loop.
|
// z_total is constant per call — hoist above the loop.
|
||||||
let z_total = crate::world::chunks::Z_BELOW as i32 + crate::world::chunks::Z_ABOVE as i32 + 1;
|
let z_total = crate::world::chunks::Z_BELOW as i32 + crate::world::chunks::Z_ABOVE as i32 + 1;
|
||||||
|
|
||||||
|
// Collect trunk positions before despawning — we need is_trunk from TreePart
|
||||||
|
// while the entity still exists. This is why we DON'T immediately despawn
|
||||||
|
// in the to_remove mapping step.
|
||||||
|
let mut trunk_positions: SmallVec<[IVec3; 8]> = SmallVec::new();
|
||||||
|
|
||||||
for (entity, tile_pos) in to_remove.iter() {
|
for (entity, tile_pos) in to_remove.iter() {
|
||||||
|
// Check if this was a trunk (is_trunk stored on TreePart).
|
||||||
|
// We need to look up is_trunk before the entity is despawned.
|
||||||
|
// to_remove was collected from tree_parts query, so find it again:
|
||||||
|
if let Ok((_, part)) = tree_parts.get(*entity) {
|
||||||
|
if part.is_trunk {
|
||||||
|
trunk_positions.push(*tile_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tilemap.remove_fixture(tile_pos);
|
tilemap.remove_fixture(tile_pos);
|
||||||
tile_changed.write(TileChangedEvent { pos: *tile_pos });
|
tile_changed.write(TileChangedEvent { pos: *tile_pos });
|
||||||
commands.entity(*entity).despawn();
|
commands.entity(*entity).despawn();
|
||||||
@@ -333,4 +348,5 @@ pub fn fell_tree(
|
|||||||
for pos in dirty_columns {
|
for pos in dirty_columns {
|
||||||
occlusion.write(TileOcclusionEvent { tile_position: pos });
|
occlusion.write(TileOcclusionEvent { tile_position: pos });
|
||||||
}
|
}
|
||||||
|
trunk_positions
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user