Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ members = [
"src/game_systems",
"src/game_systems/src/background",
"src/game_systems/src/mobs",
"src/game_systems/src/pathfinding",
"src/game_systems/src/interactions",
"src/game_systems/src/packets",
"src/game_systems/src/physics",
Expand Down
120 changes: 120 additions & 0 deletions src/core/src/block_properties.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::sync::LazyLock;

use crate::block_data::BlockData;
use crate::block_state_id::{BlockStateId, ID2BLOCK};

/// Precomputed solidity for all block states.
/// A block is solid if it has a full collision box that entities cannot walk through.
/// Indexed by `BlockStateId::raw()`.
static SOLID_BLOCKS: LazyLock<Vec<bool>> =
Comment thread
Tonguechaude marked this conversation as resolved.
LazyLock::new(|| ID2BLOCK.iter().map(compute_solid).collect());

/// Returns whether a block is solid (has a full collision box).
///
/// This is the single source of truth for solidity, used by both the collision
/// system and the pathfinding system.
#[inline]
pub fn is_solid(id: BlockStateId) -> bool {
SOLID_BLOCKS[id.raw() as usize]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blockstateids aren't checked to be valid, can we return false instead of panicking

}

/// Determine whether a block data entry represents a solid block.
fn compute_solid(data: &BlockData) -> bool {
let name = data.name.trim_start_matches("minecraft:");

// Air variants are never solid
if name.ends_with("air") {
return false;
}

// Liquids
if matches!(name, "water" | "lava" | "bubble_column") {
return false;
}

// Fire
if matches!(name, "fire" | "soul_fire") {
return false;
}
if name.ends_with("_campfire") {
// Campfires are solid blocks you can stand on, but lit ones deal damage
return true;
}

// Doors, fence gates, trapdoors: solid only when closed
if name.ends_with("_door") || name.ends_with("_fence_gate") || name.ends_with("_trapdoor") {
let open = data
.properties
.as_ref()
.and_then(|p| p.get("open"))
.is_some_and(|v| v == "true");
return !open;
}

// Non-solid vegetation and decorations
if is_non_solid_decoration(name) {
return false;
}

// Default: solid
true
}

/// Returns true for blocks that have no collision box (decorative, vegetation, etc.)
fn is_non_solid_decoration(name: &str) -> bool {
if matches!(
name,
"grass"
| "short_grass"
| "tall_grass"
| "fern"
| "large_fern"
| "dead_bush"
| "snow"
| "string"
| "nether_portal"
| "spore_blossom"
| "glow_lichen"
| "dandelion"
| "poppy"
| "blue_orchid"
| "allium"
| "azure_bluet"
| "oxeye_daisy"
| "cornflower"
| "lily_of_the_valley"
| "wither_rose"
| "sunflower"
| "lilac"
| "rose_bush"
| "peony"
| "torchflower"
| "pitcher_plant"
| "pitcher_pod"
| "sweet_berry_bush"
| "cobweb"
| "powder_snow"
| "redstone_wire"
| "rail"
| "powered_rail"
| "detector_rail"
| "activator_rail"
| "tripwire"
| "tripwire_hook"
| "structure_void"
) {
return true;
}

name.ends_with("_button")
|| name.ends_with("_pressure_plate")
|| name.ends_with("_sign")
|| name.ends_with("_banner")
|| name.ends_with("_carpet")
|| name.ends_with("_torch")
|| name.ends_with("_sapling")
|| name.ends_with("_mushroom")
|| name.ends_with("_flower")
|| name.ends_with("_vine")
|| name.ends_with("_roots")
}
1 change: 1 addition & 0 deletions src/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod errors;

// Core structs/types. Usually used in ECS Components.
pub mod block_data;
pub mod block_properties;
pub mod block_state_id;
pub mod color;
pub mod dimension;
Expand Down
3 changes: 2 additions & 1 deletion src/game_systems/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ version.workspace = true
[dependencies]
background = { path = "./src/background" }
mobs = { path = "./src/mobs" }
pathfinding = { path = "./src/pathfinding" }
packets = { path = "./src/packets" }
physics = { path = "./src/physics" }
player = { path = "./src/player" }
shutdown = { path = "./src/shutdown" }
world = { path = "./src/world" }
interactions = { path = "./src/interactions" }

tempfile = { workspace = true }
bevy_ecs = { workspace = true }

[lints]
Expand Down
9 changes: 8 additions & 1 deletion src/game_systems/src/mobs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,12 @@ edition = "2024"

[dependencies]
bevy_ecs = { workspace = true }
bevy_math = { workspace = true }
pathfinding = { path = "../pathfinding" }
temper-components = { workspace = true }
temper-entities = { workspace = true }
temper-core = { workspace = true }
temper-entities = { workspace = true }
temper-state = { workspace = true }
temper-messages = { workspace = true }
temper-particles = { workspace = true }
temper-utils = { workspace = true }
6 changes: 5 additions & 1 deletion src/game_systems/src/mobs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use bevy_ecs::prelude::Schedule;
use bevy_ecs::schedule::IntoScheduleConfigs;

mod pig;

pub fn register_mob_systems(schedule: &mut Schedule) {
schedule.add_systems(pig::tick_pig);
schedule.add_systems(pathfinding::tick_pathfinder);
schedule.add_systems(pig::tick_pig.after(pathfinding::tick_pathfinder));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we its weird, but this would actually run the pathfinder twice cos the .after() method also adds it's argument to the ecs. could we use ().chain() instead to make it clearer and more extendable?

schedule.add_systems(pig::tick_pig_particles);
}
133 changes: 130 additions & 3 deletions src/game_systems/src/mobs/src/pig.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,138 @@
use bevy_ecs::prelude::{Query, With};
use bevy_ecs::prelude::*;
use bevy_math::Vec3A;
use pathfinding::{Pathfinder, pos_to_block};
use temper_components::player::grounded::OnGround;
use temper_components::player::player_identity::PlayerIdentity;
use temper_components::player::position::Position;
use temper_components::player::velocity::Velocity;
use temper_entities::markers::entity_types::Pig;
use temper_messages::particle::SendParticle;
use temper_particles::ParticleType;

/// Pig walk speed in blocks per tick.
const PIG_WALK_SPEED: f32 = 0.1;

/// Jump impulse matching Minecraft's standard jump velocity (blocks/tick).
const JUMP_IMPULSE: f32 = 0.42;

/// How often to update the pathfinding target (ticks).
const REPATH_INTERVAL: u32 = 40;

/// Per-pig AI state.
#[derive(Component, Default)]
pub struct PigAI {
repath_cooldown: u32,
}

type PigQuery<'a> = (
Entity,
&'a Position,
&'a mut Velocity,
&'a OnGround,
Option<&'a mut PigAI>,
Option<&'a mut Pathfinder>,
);

#[expect(unused_variables)]
pub fn tick_pig(
Comment thread
Tonguechaude marked this conversation as resolved.
query: Query<&Position, With<Pig>>,
mut commands: Commands,
mut pigs: Query<PigQuery, With<Pig>>,
players: Query<&Position, With<PlayerIdentity>>,
) {
for (entity, pig_pos, mut velocity, grounded, ai_opt, pf_opt) in pigs.iter_mut() {
let (Some(mut ai), Some(mut pathfinder)) = (ai_opt, pf_opt) else {
commands
.entity(entity)
.insert((PigAI::default(), Pathfinder::default()));
continue;
};

ai.repath_cooldown = ai.repath_cooldown.saturating_sub(1);

// Repath when the cooldown expires OR when the pig has followed a path
// to its end (but not when pathfinding simply failed to find a route).
let path_reached_end =
!pathfinder.has_path() && !pathfinder.path.is_empty() && !pathfinder.is_searching();

if ai.repath_cooldown == 0 || path_reached_end {
pathfinder.target = players
.iter()
.min_by(|a, b| {
pig_pos
.coords
.distance_squared(a.coords)
.total_cmp(&pig_pos.coords.distance_squared(b.coords))
})
.map(pos_to_block);
pathfinder.request_repath();
ai.repath_cooldown = REPATH_INTERVAL;
}

let current_block = pos_to_block(pig_pos);

// Advance waypoint when the pig reaches it (same X/Z block).
if let Some(wp) = pathfinder.current_waypoint()
&& wp.pos.x == current_block.pos.x
&& wp.pos.z == current_block.pos.z
{
pathfinder.advance_waypoint();
}

let Some(next) = pathfinder.current_waypoint() else {
stop(&mut velocity);
continue;
};

// Jump if the next waypoint is 1 block above and the pig is on the ground.
if next.pos.y > current_block.pos.y && grounded.0 {
velocity.vec.y = JUMP_IMPULSE;
}

// Steer horizontally toward the center of the next waypoint block.
let dx = (next.pos.x as f64 + 0.5 - pig_pos.x) as f32;
let dz = (next.pos.z as f64 + 0.5 - pig_pos.z) as f32;
let len = (dx * dx + dz * dz).sqrt();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if len > 0.1 {
velocity.vec.x = (dx / len) * PIG_WALK_SPEED;
velocity.vec.z = (dz / len) * PIG_WALK_SPEED;
} else {
velocity.vec.x = 0.0;
velocity.vec.z = 0.0;
}
}
}

pub fn tick_pig_particles(
pigs: Query<(Entity, &Position), With<Pig>>,
players: Query<&Position, With<PlayerIdentity>>,
mut msgs: MessageWriter<SendParticle>,
) {
for pos in pigs.iter() {
for player_pos in players.iter() {
let distance_sq = player_pos.as_vec3a().distance_squared(pos.1.as_vec3a());
if distance_sq > 16.0 * 256.0 {
continue;
}
let steps = temper_utils::maths::step::step_between(
pos.1.as_vec3a(),
player_pos.coords.as_vec3a(),
0.5,
);
for step_pos in steps.iter().take(32) {
let particle_message = SendParticle {
particle_type: ParticleType::EndRod,
position: *step_pos,
offset: Vec3A::new(0.0, 0.0, 0.0),
speed: 0.0,
count: 1,
};
msgs.write(particle_message);
}
}
}
}

fn stop(velocity: &mut Velocity) {
velocity.vec.x = 0.0;
velocity.vec.z = 0.0;
}
23 changes: 23 additions & 0 deletions src/game_systems/src/pathfinding/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "pathfinding"
version = "0.1.0"
edition = "2024"

[dependencies]
temper-core = { workspace = true }
temper-world = { workspace = true }
temper-components = { workspace = true }
bevy_ecs = { workspace = true }
temper-entities = { workspace = true }
temper-state = { workspace = true }
rustc-hash = "2"
arrayvec = "0.7"

[dev-dependencies]
temper-state = { workspace = true }
temper-macros = { workspace = true }
temper-data = { workspace = true }
tempfile = { workspace = true }

[lints]
workspace = true
Loading
Loading