first pass at tree interaction
This commit is contained in:
@@ -13,3 +13,13 @@ drops = [
|
|||||||
|
|
||||||
[air]
|
[air]
|
||||||
drops = []
|
drops = []
|
||||||
|
|
||||||
|
# Tree fixtures — activated by fell_tree.
|
||||||
|
# Add WoodLog prefab + uncomment when log items exist.
|
||||||
|
# [tree_trunk]
|
||||||
|
# drops = [
|
||||||
|
# { prefab = "WoodLog", chance_pct = 100, min_count = 1, max_count = 1 },
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# [tree_leaf]
|
||||||
|
# drops = []
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::constants::ITILE_SIZE;
|
use crate::constants::ITILE_SIZE;
|
||||||
use crate::entities::item::drop_table::dig_rng;
|
use crate::entities::item::drop_table::dig_rng;
|
||||||
use crate::entities::item::prefabs::misc::misc_prefabs::spawn_prefab;
|
use crate::entities::item::prefabs::misc::misc_prefabs::spawn_prefab;
|
||||||
use crate::world::chunks::{Z_ABOVE, Z_BELOW};
|
use crate::world::chunks::Z_BELOW;
|
||||||
|
use crate::world::generation::forestry::{fell_tree, TreePart};
|
||||||
use crate::world::tiles::tile_changed::TileChangedEvent;
|
use crate::world::tiles::tile_changed::TileChangedEvent;
|
||||||
use crate::world::tiles::visibility::TileOcclusionEvent;
|
use crate::world::tiles::visibility::TileOcclusionEvent;
|
||||||
use crate::world::tiles::TileMap;
|
use crate::world::tiles::TileMap;
|
||||||
@@ -27,6 +28,7 @@ pub fn dig_system(
|
|||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
mut query: Query<(&Transform, &mut Digger)>,
|
mut query: Query<(&Transform, &mut Digger)>,
|
||||||
|
tree_parts: Query<(Entity, &TreePart)>,
|
||||||
mut tilemap: ResMut<TileMap>,
|
mut tilemap: ResMut<TileMap>,
|
||||||
mut tile_changed: MessageWriter<TileChangedEvent>,
|
mut tile_changed: MessageWriter<TileChangedEvent>,
|
||||||
mut occlusion: MessageWriter<TileOcclusionEvent>,
|
mut occlusion: MessageWriter<TileOcclusionEvent>,
|
||||||
@@ -90,6 +92,27 @@ pub fn dig_system(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there's a blocking fixture directly above the dug tile, it's a tree trunk —
|
||||||
|
// fell the whole tree. Temporary wiring until a designation + task system
|
||||||
|
// triggers fell_tree explicitly.
|
||||||
|
let above_pos = IVec3::new(below_pos.x, below_pos.y, below_pos.z + ITILE_SIZE);
|
||||||
|
let has_trunk_above = tilemap
|
||||||
|
.fixture_tiles
|
||||||
|
.get(&above_pos)
|
||||||
|
.map(|f| !f.can_stand_in())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if has_trunk_above {
|
||||||
|
fell_tree(
|
||||||
|
above_pos,
|
||||||
|
&tree_parts,
|
||||||
|
&mut commands,
|
||||||
|
&mut tilemap,
|
||||||
|
&mut tile_changed,
|
||||||
|
&mut occlusion,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,35 @@ use rand::{RngExt, SeedableRng};
|
|||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
use crate::entities::item::drop_table::DropTable;
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::{SEED, TILE_SIZE},
|
constants::{ITILE_SIZE, SEED, TILE_SIZE},
|
||||||
world::generation::forestry::constants::{
|
world::generation::forestry::constants::{
|
||||||
TREE_LEAF_BASE_RADIUS, TREE_LEAF_RADIUS_OFFSET, TREE_LEAF_RADIUS_VARIATION,
|
TREE_LEAF_BASE_RADIUS, TREE_LEAF_RADIUS_OFFSET, TREE_LEAF_RADIUS_VARIATION,
|
||||||
TREE_MIN_DISTANCE_TILES, TREE_SPAWN_CHANCE, TREE_TRUNK_EXTRA_HEIGHT, TREE_TRUNK_MIN_HEIGHT,
|
TREE_MIN_DISTANCE_TILES, TREE_SPAWN_CHANCE, TREE_TRUNK_EXTRA_HEIGHT, TREE_TRUNK_MIN_HEIGHT,
|
||||||
},
|
},
|
||||||
world::{
|
world::{
|
||||||
tiles::{FixtureTileData, TileMap},
|
tiles::{
|
||||||
|
tile_changed::TileChangedEvent, visibility::TileOcclusionEvent, FixtureTileData,
|
||||||
|
TileMap,
|
||||||
|
},
|
||||||
ChunkForrestryEvent, ChunkMap, ChunkOwner, TextureIDs, Textures, VisibleGameEntity,
|
ChunkForrestryEvent, ChunkMap, ChunkOwner, TextureIDs, Textures, VisibleGameEntity,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Marks an entity as part of a tree. Enables targeted removal via fell_tree.
|
||||||
|
///
|
||||||
|
/// `tile_pos` is the grid-aligned world position of the fixture tile this entity
|
||||||
|
/// represents. Used to call remove_fixture without scanning fixture_tiles.
|
||||||
|
///
|
||||||
|
/// `is_trunk` distinguishes trunk segments (can_stand_in=false, drops wood) from
|
||||||
|
/// leaf canopy (can_stand_in=true, drops nothing or leaves).
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct TreePart {
|
||||||
|
pub tile_pos: IVec3,
|
||||||
|
pub is_trunk: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_chunk_forrestry(
|
pub fn generate_chunk_forrestry(
|
||||||
commands: ParallelCommands<'_, '_>,
|
commands: ParallelCommands<'_, '_>,
|
||||||
mut events: MessageReader<ChunkForrestryEvent>,
|
mut events: MessageReader<ChunkForrestryEvent>,
|
||||||
@@ -84,9 +101,13 @@ pub fn generate_chunk_forrestry(
|
|||||||
))
|
))
|
||||||
.id();
|
.id();
|
||||||
tree_positions.push(trunk_pos);
|
tree_positions.push(trunk_pos);
|
||||||
commands
|
commands.entity(trunk_entity).insert((
|
||||||
.entity(trunk_entity)
|
ChunkOwner(event.chunk_position),
|
||||||
.insert(ChunkOwner(event.chunk_position));
|
TreePart {
|
||||||
|
tile_pos: trunk_ivec,
|
||||||
|
is_trunk: true,
|
||||||
|
},
|
||||||
|
));
|
||||||
collected_entities
|
collected_entities
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -109,7 +130,13 @@ pub fn generate_chunk_forrestry(
|
|||||||
|
|
||||||
collected_tilemap_updates.lock().unwrap().push((
|
collected_tilemap_updates.lock().unwrap().push((
|
||||||
trunk_ivec,
|
trunk_ivec,
|
||||||
FixtureTileData::new(1, false, true, [0; 8]),
|
FixtureTileData::new(
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
[0; 8],
|
||||||
|
DropTable::default(),
|
||||||
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
log_positions.insert(trunk_ivec);
|
log_positions.insert(trunk_ivec);
|
||||||
@@ -168,6 +195,10 @@ pub fn generate_chunk_forrestry(
|
|||||||
commands.entity(leaf).insert((
|
commands.entity(leaf).insert((
|
||||||
VisibleGameEntity,
|
VisibleGameEntity,
|
||||||
ChunkOwner(event.chunk_position),
|
ChunkOwner(event.chunk_position),
|
||||||
|
TreePart {
|
||||||
|
tile_pos: ivec,
|
||||||
|
is_trunk: false,
|
||||||
|
},
|
||||||
));
|
));
|
||||||
collected_entities
|
collected_entities
|
||||||
.lock()
|
.lock()
|
||||||
@@ -177,7 +208,11 @@ pub fn generate_chunk_forrestry(
|
|||||||
(
|
(
|
||||||
ivec,
|
ivec,
|
||||||
FixtureTileData::new(
|
FixtureTileData::new(
|
||||||
5, true, false, [0; 8],
|
5,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
[0; 8],
|
||||||
|
DropTable::default(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -217,3 +252,65 @@ pub fn generate_chunk_forrestry(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove all fixture tiles and entities belonging to the tree rooted at `root_pos`.
|
||||||
|
///
|
||||||
|
/// # How trees are identified
|
||||||
|
/// Finds all TreePart entities whose XY is within canopy radius of the trunk column.
|
||||||
|
/// This avoids storing a tree ID — the spatial structure IS the identity.
|
||||||
|
///
|
||||||
|
/// # What this does
|
||||||
|
/// For each TreePart entity found:
|
||||||
|
/// - Calls `tilemap.remove_fixture(&tree_part.tile_pos)` (clears ChunkData + HashMap)
|
||||||
|
/// - Despawns the Bevy entity
|
||||||
|
/// - Fires `TileChangedEvent` for path invalidation
|
||||||
|
/// - Fires `TileOcclusionEvent` for the column at each removed position
|
||||||
|
///
|
||||||
|
/// # Performance
|
||||||
|
/// O(tree_size) entity lookups via `Query<(Entity, &TreePart)>`. For a typical tree
|
||||||
|
/// (4-8 trunk + ~60 leaf tiles) this is ~68 iterations. Acceptable for an infrequent
|
||||||
|
/// action.
|
||||||
|
pub fn fell_tree(
|
||||||
|
root_pos: IVec3,
|
||||||
|
tree_parts: &Query<(Entity, &TreePart)>,
|
||||||
|
commands: &mut Commands,
|
||||||
|
tilemap: &mut TileMap,
|
||||||
|
tile_changed: &mut MessageWriter<TileChangedEvent>,
|
||||||
|
occlusion: &mut MessageWriter<TileOcclusionEvent>,
|
||||||
|
) {
|
||||||
|
let trunk_x = root_pos.x;
|
||||||
|
let trunk_y = root_pos.y;
|
||||||
|
let canopy_radius_world = (TREE_LEAF_BASE_RADIUS * TILE_SIZE) as i32 + ITILE_SIZE;
|
||||||
|
|
||||||
|
let to_remove: Vec<(Entity, IVec3)> = tree_parts
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, part)| {
|
||||||
|
let dx = (part.tile_pos.x - trunk_x).abs();
|
||||||
|
let dy = (part.tile_pos.y - trunk_y).abs();
|
||||||
|
dx <= canopy_radius_world && dy <= canopy_radius_world
|
||||||
|
})
|
||||||
|
.map(|(entity, part)| (entity, part.tile_pos))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (entity, tile_pos) in to_remove {
|
||||||
|
tilemap.remove_fixture(&tile_pos);
|
||||||
|
tile_changed.write(TileChangedEvent { pos: tile_pos });
|
||||||
|
|
||||||
|
// Full column occlusion refresh for each removed tile.
|
||||||
|
// calculate_visibility traces up arbitrarily deep, so refresh the full column.
|
||||||
|
let z_depth =
|
||||||
|
crate::world::chunks::Z_BELOW as i32 + crate::world::chunks::Z_ABOVE as i32 + 1;
|
||||||
|
for dz in 0..=z_depth {
|
||||||
|
for dy in -1..=1i32 {
|
||||||
|
for dx in -1..=1i32 {
|
||||||
|
occlusion.write(TileOcclusionEvent {
|
||||||
|
tile_position: tile_pos
|
||||||
|
- IVec3::new(dx * ITILE_SIZE, dy * ITILE_SIZE, dz * ITILE_SIZE),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,12 +131,16 @@ impl FloorTileData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Packed fixture tile data. ~18 bytes vs 48 bytes tuple.
|
/// Packed fixture tile data. ~18 bytes vs 48 bytes tuple.
|
||||||
#[derive(Clone, Copy, Debug)]
|
/// NOTE: Clone+Debug only — adding DropTable removes Copy.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct FixtureTileData {
|
pub struct FixtureTileData {
|
||||||
pub id: u8,
|
pub id: u8,
|
||||||
/// bit0=can_stand_in, bit1=can_stand_on
|
/// bit0=can_stand_in, bit1=can_stand_on
|
||||||
pub flags: u8,
|
pub flags: u8,
|
||||||
pub visible_range: [u32; 8],
|
pub visible_range: [u32; 8],
|
||||||
|
/// Items dropped when this fixture is removed. Empty = nothing drops.
|
||||||
|
/// Trunk tiles will yield wood logs here once log items exist.
|
||||||
|
pub drop_table: DropTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FixtureTileData {
|
impl Default for FixtureTileData {
|
||||||
@@ -145,12 +149,19 @@ impl Default for FixtureTileData {
|
|||||||
id: 0,
|
id: 0,
|
||||||
flags: 0,
|
flags: 0,
|
||||||
visible_range: [0; 8],
|
visible_range: [0; 8],
|
||||||
|
drop_table: DropTable::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FixtureTileData {
|
impl FixtureTileData {
|
||||||
pub fn new(id: u8, can_stand_in: bool, can_stand_on: bool, visible_range: [u32; 8]) -> Self {
|
pub fn new(
|
||||||
|
id: u8,
|
||||||
|
can_stand_in: bool,
|
||||||
|
can_stand_on: bool,
|
||||||
|
visible_range: [u32; 8],
|
||||||
|
drop_table: DropTable,
|
||||||
|
) -> Self {
|
||||||
let mut flags = 0u8;
|
let mut flags = 0u8;
|
||||||
if can_stand_in {
|
if can_stand_in {
|
||||||
flags |= 0b001;
|
flags |= 0b001;
|
||||||
@@ -162,6 +173,7 @@ impl FixtureTileData {
|
|||||||
id,
|
id,
|
||||||
flags,
|
flags,
|
||||||
visible_range,
|
visible_range,
|
||||||
|
drop_table,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,10 +312,23 @@ impl TileMap {
|
|||||||
.unwrap_or(ASTAR_DEFAULT_WEIGHT)
|
.unwrap_or(ASTAR_DEFAULT_WEIGHT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a fixture tile, clearing both the HashMap entry and ChunkData bitsets.
|
/// Remove a fixture tile, clearing the HashMap entry, ChunkData bitsets, and tile_ids.
|
||||||
/// BOTH must be cleared — leaving ChunkData stale causes is_standable bugs.
|
/// All three must be cleared — leaving ChunkData stale causes is_standable bugs,
|
||||||
|
/// and leaving tile_ids non-zero causes the renderer to keep drawing the fixture.
|
||||||
|
///
|
||||||
|
/// Also restores the floor tile's stand_in bit, which insert_fixture clears for
|
||||||
|
/// blocking fixtures (can_stand_in=false) to prevent air floor tiles from overriding
|
||||||
|
/// the block via the OR check in is_standable.
|
||||||
pub fn remove_fixture(&mut self, pos: &IVec3) -> Option<FixtureTileData> {
|
pub fn remove_fixture(&mut self, pos: &IVec3) -> Option<FixtureTileData> {
|
||||||
let removed = self.fixture_tiles.remove(pos);
|
let removed = self.fixture_tiles.remove(pos);
|
||||||
|
|
||||||
|
// Read floor standability before taking mutable borrow on chunks.
|
||||||
|
let floor_can_stand_in = self
|
||||||
|
.floor_tiles
|
||||||
|
.get(pos)
|
||||||
|
.map(|f| f.can_stand_in())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let chunk_pos = world_to_chunk(*pos);
|
let chunk_pos = world_to_chunk(*pos);
|
||||||
if let Some(chunk) = self.chunks.get_mut(&chunk_pos) {
|
if let Some(chunk) = self.chunks.get_mut(&chunk_pos) {
|
||||||
let (lx, ly, z) = ChunkData::world_to_local(*pos);
|
let (lx, ly, z) = ChunkData::world_to_local(*pos);
|
||||||
@@ -326,9 +351,14 @@ impl TileMap {
|
|||||||
);
|
);
|
||||||
let idx = ChunkData::pos_to_index(lx, ly, z);
|
let idx = ChunkData::pos_to_index(lx, ly, z);
|
||||||
let word = idx / 32;
|
let word = idx / 32;
|
||||||
let clear_mask = !(1u32 << (idx % 32));
|
let bit = 1u32 << (idx % 32);
|
||||||
chunk.stand_in_fixture[word] &= clear_mask;
|
chunk.stand_in_fixture[word] &= !bit;
|
||||||
chunk.stand_on_fixture[word] &= clear_mask;
|
chunk.stand_on_fixture[word] &= !bit;
|
||||||
|
chunk.tile_ids[idx] = 0;
|
||||||
|
// Restore floor stand_in bit cleared by insert_fixture for blocking fixtures.
|
||||||
|
if floor_can_stand_in {
|
||||||
|
chunk.stand_in_floor[word] |= bit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
removed
|
removed
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user