Compare commits
No commits in common. "master" and "feat/player-physics" have entirely different histories.
master
...
feat/playe
|
|
@ -2084,7 +2084,6 @@ dependencies = [
|
|||
"bevy-inspector-egui",
|
||||
"bevy_prototype_debug_lines",
|
||||
"bevy_rapier2d",
|
||||
"fastrand",
|
||||
"lazy_static",
|
||||
"noise",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,13 +10,12 @@ bevy = { version = "0.9.0", features = ["dynamic"] }
|
|||
bevy-inspector-egui = "0.14.0"
|
||||
bevy_prototype_debug_lines = "0.9.0"
|
||||
bevy_rapier2d = "0.19.0"
|
||||
fastrand = "1.8.0"
|
||||
lazy_static = "1.4.0"
|
||||
noise = "0.8.2"
|
||||
|
||||
# Enable a small amount of optimization in debug mode
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
opt-level = 0
|
||||
|
||||
# Enable high optimizations for dependencies (incl. Bevy), but not for our code:
|
||||
[profile.dev.package."*"]
|
||||
|
|
|
|||
36
src/game.rs
36
src/game.rs
|
|
@ -1,46 +1,42 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_inspector_egui::*;
|
||||
use bevy_prototype_debug_lines::DebugLinesPlugin;
|
||||
use bevy_rapier2d::prelude::*;
|
||||
|
||||
use crate::{
|
||||
terrain2d::*,
|
||||
util::{frame_counter::FrameCounterPlugin, Vector2I},
|
||||
terrain2d::{Chunk2D, Terrain2D, Terrain2DPlugin, TerrainGen2D},
|
||||
util::Vector2I,
|
||||
};
|
||||
|
||||
use self::{
|
||||
camera::GameCameraPlugin, debug::DebugPlugin, kinematic::KinematicPlugin, player::PlayerPlugin,
|
||||
camera::{GameCameraPlugin, WORLD_WIDTH},
|
||||
kinematic::KinematicPlugin,
|
||||
player::PlayerPlugin,
|
||||
};
|
||||
|
||||
pub mod camera;
|
||||
pub mod debug;
|
||||
pub mod kinematic;
|
||||
pub mod player;
|
||||
|
||||
pub fn init() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugin(FrameCounterPlugin)
|
||||
.add_plugin(DebugLinesPlugin::default())
|
||||
.add_plugin(RapierPhysicsPlugin::<NoUserData>::default())
|
||||
.add_plugin(Terrain2DPlugin)
|
||||
.add_plugin(DebugPlugin)
|
||||
.add_plugin(RapierDebugRenderPlugin::default())
|
||||
.add_plugin(WorldInspectorPlugin::new())
|
||||
.add_plugin(KinematicPlugin)
|
||||
.add_plugin(GameCameraPlugin)
|
||||
.add_plugin(Terrain2DPlugin)
|
||||
.add_plugin(PlayerPlugin)
|
||||
.add_startup_system(setup_terrain)
|
||||
.add_startup_system(setup_window)
|
||||
.add_startup_system(setup_debug_terrain)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup_window(mut windows: ResMut<Windows>) {
|
||||
if let Some(window) = windows.get_primary_mut() {
|
||||
window.set_resolution(1280.0, 720.0);
|
||||
window.set_title("Kuilu".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_terrain(mut commands: Commands, mut terrain: ResMut<Terrain2D>) {
|
||||
fn setup_debug_terrain(mut commands: Commands, mut terrain: ResMut<Terrain2D>) {
|
||||
let terrain_gen = TerrainGen2D::new(432678);
|
||||
for y in 0..(Terrain2D::WORLD_HEIGHT / Chunk2D::SIZE_Y as i32) {
|
||||
for x in 0..(Terrain2D::WORLD_WIDTH / Chunk2D::SIZE_X as i32) {
|
||||
for y in 0..(WORLD_WIDTH / Chunk2D::SIZE_Y as i32) {
|
||||
for x in 0..(WORLD_WIDTH / Chunk2D::SIZE_X as i32) {
|
||||
let position = Vector2I { x, y };
|
||||
terrain.add_chunk(position, terrain_gen.gen_chunk(&position));
|
||||
}
|
||||
|
|
@ -57,6 +53,6 @@ fn setup_terrain(mut commands: Commands, mut terrain: ResMut<Terrain2D>) {
|
|||
.spawn(Name::new("Right wall"))
|
||||
.insert(Collider::halfspace(Vec2::NEG_X).unwrap())
|
||||
.insert(TransformBundle::from_transform(
|
||||
Transform::from_translation(Vec3::new(Terrain2D::WORLD_WIDTH as f32, 0.0, 0.0)),
|
||||
Transform::from_translation(Vec3::new(WORLD_WIDTH as f32, 0.0, 0.0)),
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ use bevy::{
|
|||
};
|
||||
use bevy_inspector_egui::{Inspectable, RegisterInspectable};
|
||||
|
||||
use crate::{
|
||||
terrain2d::Terrain2D,
|
||||
util::{move_towards_vec3, vec3_lerp},
|
||||
};
|
||||
use crate::util::{move_towards_vec3, vec3_lerp};
|
||||
|
||||
pub const WORLD_WIDTH: i32 = 512;
|
||||
|
||||
pub struct GameCameraPlugin;
|
||||
|
||||
|
|
@ -49,7 +48,7 @@ fn camera_setup(mut commands: Commands) {
|
|||
Name::new("Camera"),
|
||||
Camera2dBundle {
|
||||
projection: OrthographicProjection {
|
||||
scaling_mode: ScalingMode::FixedHorizontal(Terrain2D::WORLD_WIDTH as f32),
|
||||
scaling_mode: ScalingMode::FixedHorizontal(WORLD_WIDTH as f32),
|
||||
window_origin: WindowOrigin::Center,
|
||||
scale: 1.0 / 2.0,
|
||||
..default()
|
||||
|
|
@ -78,9 +77,10 @@ fn camera_system(
|
|||
None => return,
|
||||
};
|
||||
|
||||
// let offset = Vec3::new(WORLD_WIDTH as f32 / 2.0, 0.0, 999.9);
|
||||
for (mut camera_transform, projection) in camera_query.iter_mut() {
|
||||
let left_limit = 0.0;
|
||||
let right_limit = Terrain2D::WORLD_WIDTH as f32;
|
||||
let right_limit = WORLD_WIDTH as f32;
|
||||
let offset = Vec3::new(0.0, 0.0, 999.9);
|
||||
match follow.movement {
|
||||
FollowMovement::Instant => {
|
||||
|
|
@ -101,7 +101,6 @@ fn camera_system(
|
|||
);
|
||||
}
|
||||
}
|
||||
// horizontal boundaries
|
||||
let camera_x = camera_transform.translation.x;
|
||||
camera_transform.translation += Vec3::new(
|
||||
(left_limit - (projection.left * projection.scale + camera_x)).max(0.0),
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_prototype_debug_lines::DebugLinesPlugin;
|
||||
|
||||
pub mod terrain;
|
||||
|
||||
use terrain::TerrainDebugPlugin;
|
||||
|
||||
pub struct DebugPlugin;
|
||||
|
||||
impl Plugin for DebugPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugin(DebugLinesPlugin::default())
|
||||
// .add_plugin(bevy_rapier2d::prelude::RapierDebugRenderPlugin::default())
|
||||
// .add_plugin(bevy_inspector_egui::WorldInspectorPlugin::new())
|
||||
.add_plugin(TerrainDebugPlugin);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::{game::camera::GameCamera, terrain2d::*, util::Vector2I};
|
||||
use bevy::{input::mouse::MouseWheel, prelude::*, render::camera::RenderTarget};
|
||||
use bevy_prototype_debug_lines::DebugLines;
|
||||
|
||||
pub struct TerrainDebugPlugin;
|
||||
|
||||
impl Plugin for TerrainDebugPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(TerrainBrush2D::default())
|
||||
// .add_system_to_stage(TerrainStages::EventHandler, dirty_rect_visualizer)
|
||||
// .add_system_to_stage(CoreStage::Last, chunk_debugger)
|
||||
.add_system(debug_painter);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct TerrainBrush2D {
|
||||
pub radius: i32,
|
||||
pub tile: TexelID,
|
||||
}
|
||||
|
||||
impl Default for TerrainBrush2D {
|
||||
fn default() -> Self {
|
||||
TerrainBrush2D {
|
||||
radius: 40,
|
||||
tile: 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// REM: Dirty and hopefully temporary
|
||||
fn debug_painter(
|
||||
mut terrain: ResMut<Terrain2D>,
|
||||
mut debug_draw: ResMut<DebugLines>,
|
||||
mut brush: ResMut<TerrainBrush2D>,
|
||||
windows: Res<Windows>,
|
||||
mouse_input: Res<Input<MouseButton>>,
|
||||
key_input: Res<Input<KeyCode>>,
|
||||
mut mouse_wheel: EventReader<MouseWheel>,
|
||||
camera_query: Query<(&Camera, &GlobalTransform), With<GameCamera>>,
|
||||
) {
|
||||
// let allow_painting = key_input.pressed(KeyCode::LControl);
|
||||
let allow_painting = true;
|
||||
|
||||
// Change brush
|
||||
for event in mouse_wheel.iter() {
|
||||
if allow_painting {
|
||||
brush.radius = (brush.radius + event.y.round() as i32).clamp(1, 128);
|
||||
}
|
||||
}
|
||||
|
||||
if !allow_painting {
|
||||
return;
|
||||
}
|
||||
|
||||
// https://bevy-cheatbook.github.io/cookbook/cursor2world.html#2d-games
|
||||
// get the camera info and transform
|
||||
// assuming there is exactly one main camera entity, so query::single() is OK
|
||||
let (camera, camera_transform) = camera_query.single();
|
||||
|
||||
// get the window that the camera is displaying to (or the primary window)
|
||||
let window = if let RenderTarget::Window(id) = camera.target {
|
||||
windows.get(id).unwrap()
|
||||
} else {
|
||||
windows.get_primary().unwrap()
|
||||
};
|
||||
|
||||
// check if the cursor is inside the window and get its position
|
||||
let world_pos = if let Some(screen_pos) = window.cursor_position() {
|
||||
// get the size of the window
|
||||
let window_size = Vec2::new(window.width() as f32, window.height() as f32);
|
||||
|
||||
// convert screen position [0..resolution] to ndc [-1..1] (gpu coordinates)
|
||||
let ndc = (screen_pos / window_size) * 2.0 - Vec2::ONE;
|
||||
|
||||
// matrix for undoing the projection and camera transform
|
||||
let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix().inverse();
|
||||
|
||||
// use it to convert ndc to world-space coordinates
|
||||
let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0));
|
||||
|
||||
// reduce it to a 2D value
|
||||
world_pos.truncate()
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (index, key) in vec![
|
||||
KeyCode::Key1,
|
||||
KeyCode::Key2,
|
||||
KeyCode::Key3,
|
||||
KeyCode::Key4,
|
||||
KeyCode::Key5,
|
||||
KeyCode::Key6,
|
||||
KeyCode::Key7,
|
||||
KeyCode::Key8,
|
||||
KeyCode::Key9,
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
if key_input.just_pressed(*key) {
|
||||
brush.tile = index as u8 + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let origin = Vector2I::from(world_pos);
|
||||
let radius = brush.radius;
|
||||
let id = match (
|
||||
mouse_input.pressed(MouseButton::Left),
|
||||
mouse_input.pressed(MouseButton::Right),
|
||||
) {
|
||||
(true, false) => brush.tile,
|
||||
(_, _) => 0,
|
||||
};
|
||||
let color = TexelBehaviour2D::from_id(&brush.tile)
|
||||
.map_or(Color::rgba(0.0, 0.0, 0.0, 0.0), |tb| tb.color);
|
||||
|
||||
for y in origin.y - (radius - 1)..origin.y + radius {
|
||||
for x in origin.x - (radius - 1)..origin.x + radius {
|
||||
let dx = (x - origin.x).abs();
|
||||
let dy = (y - origin.y).abs();
|
||||
let draw = dx * dx + dy * dy <= (radius - 1) * (radius - 1);
|
||||
|
||||
if draw {
|
||||
let pos: Vector2I = Vector2I { x, y };
|
||||
debug_draw.line_colored(
|
||||
Vec3::from(pos) + Vec3::new(0.45, 0.45, 1.0),
|
||||
Vec3::from(pos) + Vec3::new(0.55, 0.55, 1.0),
|
||||
0.0,
|
||||
color,
|
||||
);
|
||||
if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right)
|
||||
{
|
||||
terrain.set_texel(&pos, Texel2D { id, ..default() }, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Visualize dirty rects
|
||||
*/
|
||||
fn dirty_rect_visualizer(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
|
||||
for (chunk_index, chunk) in terrain.chunk_iter() {
|
||||
let rect = if let Some(rect) = chunk.dirty_rect {
|
||||
rect
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let offset = Vec3::from(chunk_index_to_global(chunk_index));
|
||||
let min = offset + Vec3::from(rect.min);
|
||||
let max = offset + Vec3::from(rect.max + Vector2I::ONE);
|
||||
draw_box(&mut debug_draw, min, max, Color::RED, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn chunk_debugger(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
|
||||
for (chunk_index, chunk) in terrain.chunk_iter() {
|
||||
println!("chunk contents: {chunk_index:?}");
|
||||
let offset = Vec3::from(chunk_index_to_global(chunk_index));
|
||||
let min = offset + Vec3::ZERO;
|
||||
let max = offset + Vec3::from(Chunk2D::SIZE);
|
||||
draw_box(
|
||||
&mut debug_draw,
|
||||
min,
|
||||
max,
|
||||
Color::rgba(0.5, 0.0, 0.5, 0.5),
|
||||
0.0,
|
||||
);
|
||||
|
||||
let mut tile_counter: HashMap<TexelID, (u32, u32)> = HashMap::new();
|
||||
for y in 0..Chunk2D::SIZE_Y as i32 {
|
||||
for x in 0..Chunk2D::SIZE_X as i32 {
|
||||
let local = Vector2I::new(x, y);
|
||||
let global = local_to_global(&local, chunk_index);
|
||||
if let (Some(texel), _) = terrain.get_texel_behaviour(&global) {
|
||||
if !tile_counter.contains_key(&texel.id) {
|
||||
tile_counter.insert(texel.id, (0, 0));
|
||||
}
|
||||
let (old_count, old_density) = tile_counter[&texel.id].clone();
|
||||
tile_counter.insert(
|
||||
texel.id,
|
||||
(old_count + 1, old_density + texel.density as u32),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut counts: Vec<(u8, String, u32, u32)> = vec![];
|
||||
|
||||
for (id, (count, total_density)) in tile_counter.iter() {
|
||||
let name =
|
||||
TexelBehaviour2D::from_id(id).map_or("unknown".to_string(), |b| b.name.to_string());
|
||||
counts.push((*id, name, *count, *total_density));
|
||||
}
|
||||
counts.sort_unstable_by_key(|c| c.0);
|
||||
for (id, name, count, total_density) in counts {
|
||||
println!(
|
||||
"\tmaterial: {name:<24}id: {id:<8}count: {count:<8}total_density: {total_density:<8}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_box(debug_draw: &mut DebugLines, min: Vec3, max: Vec3, color: Color, duration: f32) {
|
||||
let points = vec![
|
||||
Vec3::new(min.x, min.y, min.z),
|
||||
Vec3::new(max.x, min.y, min.z),
|
||||
Vec3::new(max.x, max.y, min.z),
|
||||
Vec3::new(min.x, max.y, min.z),
|
||||
];
|
||||
for i in 0..points.len() {
|
||||
debug_draw.line_colored(points[i], points[(i + 1) % points.len()], duration, color);
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ impl Default for KinematicBundle {
|
|||
#[derive(Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
pub struct KinematicState {
|
||||
// TODO: fork rapier2d to make it reflect?
|
||||
#[reflect(ignore)]
|
||||
pub last_move: Option<MoveShapeOutput>,
|
||||
pub did_jump: bool,
|
||||
|
|
|
|||
|
|
@ -63,10 +63,6 @@ pub fn player_spawn(mut commands: Commands) {
|
|||
transform: TransformBundle::from_transform(Transform::from_translation(Vec3::new(
|
||||
256.0, 128.0, 0.0,
|
||||
))),
|
||||
properties: KinematicProperties {
|
||||
gravity: None,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
};
|
||||
|
||||
|
|
@ -81,16 +77,27 @@ pub fn player_spawn(mut commands: Commands) {
|
|||
},
|
||||
..default()
|
||||
})
|
||||
.insert(TransformBundle::from_transform(Transform::from_xyz(
|
||||
256.0, 128.0, 0.0,
|
||||
)))
|
||||
.insert(Collider::cuboid(3.0, 6.0))
|
||||
.insert(PlayerBundle {
|
||||
kinematic,
|
||||
..default()
|
||||
})
|
||||
// .insert(RigidBody::KinematicPositionBased)
|
||||
// .insert(Velocity::default())
|
||||
// .insert(GravityScale(1.0))
|
||||
// .insert(KinematicCharacterController {
|
||||
// offset: CharacterLength::Absolute(0.01),
|
||||
// up: Vec2::Y,
|
||||
// ..default()
|
||||
// })
|
||||
.insert(KinematicInput::default())
|
||||
.insert(Ccd::enabled())
|
||||
.insert(Sleeping::disabled())
|
||||
.insert(CameraFollow {
|
||||
priority: 1,
|
||||
movement: FollowMovement::Instant,
|
||||
movement: FollowMovement::Smooth(18.0),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
641
src/terrain2d.rs
641
src/terrain2d.rs
|
|
@ -3,349 +3,195 @@ use std::collections::{
|
|||
HashMap,
|
||||
};
|
||||
|
||||
use bevy::ecs::prelude::SystemStage;
|
||||
use bevy::prelude::*;
|
||||
use bevy_rapier2d::prelude::*;
|
||||
use bevy::{input::mouse::MouseWheel, prelude::*, render::camera::RenderTarget};
|
||||
use bevy_prototype_debug_lines::DebugLines;
|
||||
|
||||
mod chunk2d;
|
||||
mod terrain_gen2d;
|
||||
mod texel2d;
|
||||
mod texel_behaviour2d;
|
||||
|
||||
pub use chunk2d::*;
|
||||
pub use terrain_gen2d::*;
|
||||
pub use texel2d::*;
|
||||
pub use texel_behaviour2d::*;
|
||||
|
||||
use crate::util::{frame_counter::FrameCounter, math::*, Vector2I};
|
||||
use crate::{
|
||||
game::camera::GameCamera,
|
||||
util::{math::*, Vector2I},
|
||||
};
|
||||
|
||||
pub struct Terrain2DPlugin;
|
||||
|
||||
impl Plugin for Terrain2DPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// Add terrain stages
|
||||
app.add_stage_before(
|
||||
CoreStage::Update,
|
||||
TerrainStages::Simulation,
|
||||
SystemStage::parallel(),
|
||||
);
|
||||
// After update, but before rapier
|
||||
app.add_stage_before(
|
||||
PhysicsStages::SyncBackend,
|
||||
TerrainStages::EventHandler,
|
||||
SystemStage::parallel(),
|
||||
)
|
||||
.add_stage_after(
|
||||
TerrainStages::EventHandler,
|
||||
TerrainStages::ChunkSync,
|
||||
SystemStage::parallel(),
|
||||
);
|
||||
|
||||
app.register_type::<TerrainChunk2D>()
|
||||
.insert_resource(Terrain2D::new(
|
||||
Some(Terrain2D::WORLD_HEIGHT),
|
||||
Some(0),
|
||||
Some(0),
|
||||
Some(Terrain2D::WORLD_WIDTH),
|
||||
))
|
||||
.insert_resource(Terrain2D::new())
|
||||
.insert_resource(TerrainBrush2D::default())
|
||||
.add_event::<TerrainEvent2D>()
|
||||
.add_system_to_stage(TerrainStages::Simulation, terrain_simulation)
|
||||
.add_system_to_stage(TerrainStages::EventHandler, emit_terrain_events)
|
||||
.add_system(debug_painter)
|
||||
.add_system_to_stage(
|
||||
TerrainStages::EventHandler,
|
||||
// TODO: Figure out why .after() creates a lagspike for the first frame
|
||||
CoreStage::PostUpdate,
|
||||
dirty_rect_visualizer.before(emit_terrain_events),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
chunk_spawner.before(emit_terrain_events),
|
||||
)
|
||||
.add_system_to_stage(TerrainStages::ChunkSync, chunk_sprite_sync)
|
||||
.add_system_to_stage(CoreStage::PostUpdate, chunk_collision_sync);
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
chunk_sprite_sync.after(chunk_spawner),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
chunk_collision_sync.after(chunk_spawner),
|
||||
)
|
||||
.add_system_to_stage(CoreStage::PostUpdate, emit_terrain_events);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(StageLabel)]
|
||||
pub enum TerrainStages {
|
||||
/// Terrain simulation stage. Should run before update.
|
||||
Simulation,
|
||||
/// The stage that Handles collected events and creates new chunk entities as needed. Should run after update.
|
||||
EventHandler,
|
||||
/// Chunk sync systems (e.g. collsion and sprite) run in this stage.
|
||||
ChunkSync,
|
||||
#[derive(Resource)]
|
||||
struct TerrainBrush2D {
|
||||
pub radius: i32,
|
||||
pub tile: TexelID,
|
||||
}
|
||||
|
||||
fn terrain_simulation(
|
||||
impl Default for TerrainBrush2D {
|
||||
fn default() -> Self {
|
||||
TerrainBrush2D { radius: 7, tile: 3 }
|
||||
}
|
||||
}
|
||||
|
||||
// REM: Dirty and hopefully temporary
|
||||
fn debug_painter(
|
||||
mut terrain: ResMut<Terrain2D>,
|
||||
frame_counter: Res<FrameCounter>,
|
||||
mut debug_draw: ResMut<bevy_prototype_debug_lines::DebugLines>,
|
||||
mut debug_draw: ResMut<DebugLines>,
|
||||
mut brush: ResMut<TerrainBrush2D>,
|
||||
windows: Res<Windows>,
|
||||
mouse_input: Res<Input<MouseButton>>,
|
||||
key_input: Res<Input<KeyCode>>,
|
||||
mut mouse_wheel: EventReader<MouseWheel>,
|
||||
camera_query: Query<(&Camera, &GlobalTransform), With<GameCamera>>,
|
||||
) {
|
||||
let simulation_frame = (frame_counter.frame % u8::MAX as u64) as u8 + 1;
|
||||
let allow_painting = key_input.pressed(KeyCode::LControl);
|
||||
|
||||
let indices = terrain
|
||||
.chunk_iter()
|
||||
.map(|(chunk_index, _)| *chunk_index)
|
||||
.collect::<Vec<Chunk2DIndex>>()
|
||||
.clone();
|
||||
|
||||
for chunk_index in indices.iter() {
|
||||
// Mark few chunks dirty in interval. Should help activate stale chunks
|
||||
if let Some(chunk) = terrain.index_to_chunk_mut(&chunk_index) {
|
||||
let interval = 1;
|
||||
if frame_counter.frame % interval == 0 {
|
||||
let i = ((frame_counter.frame / interval) % 100) as i32;
|
||||
if (chunk_index.y % 10) * 10 + (chunk_index.x % 10) == i {
|
||||
chunk.mark_all_dirty();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(rect) = &terrain
|
||||
.index_to_chunk(&chunk_index)
|
||||
.map_or(None, |chunk| chunk.dirty_rect.clone())
|
||||
{
|
||||
if let Some(chunk) = terrain.index_to_chunk_mut(&chunk_index) {
|
||||
chunk.mark_clean();
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Texel simulation
|
||||
let mut y_range: Vec<_> = (rect.min.y..rect.max.y + 1).collect();
|
||||
let mut x_range: Vec<_> = (rect.min.x..rect.max.x + 1).collect();
|
||||
if frame_counter.frame % 2 == 0 {
|
||||
y_range.reverse();
|
||||
}
|
||||
if frame_counter.frame / 2 % 2 == 0 {
|
||||
x_range.reverse();
|
||||
}
|
||||
|
||||
for y in y_range.iter() {
|
||||
for x in x_range.iter() {
|
||||
let local = Vector2I::new(*x, *y);
|
||||
let global = local_to_global(&local, &chunk_index);
|
||||
|
||||
if terrain
|
||||
.get_latest_simulation(&global)
|
||||
.map_or(true, |frame| frame == simulation_frame)
|
||||
{
|
||||
continue;
|
||||
};
|
||||
|
||||
simulate_texel(global, &mut terrain, &frame_counter);
|
||||
}
|
||||
}
|
||||
|
||||
// Gas dispersion
|
||||
let alternate_dispersion = frame_counter.frame % 2 == 0;
|
||||
let alternate = if alternate_dispersion { 1 } else { 0 };
|
||||
let y_range =
|
||||
((rect.min.y - alternate)..rect.max.y + 1 + alternate).collect::<Vec<_>>();
|
||||
let x_range =
|
||||
((rect.min.x - alternate)..rect.max.x + 1 + alternate).collect::<Vec<_>>();
|
||||
const DISPERSION_WIDTH: usize = 2;
|
||||
const DISPERSION_HEIGHT: usize = 2;
|
||||
for y_arr in y_range.chunks(DISPERSION_HEIGHT) {
|
||||
for x_arr in x_range.chunks(DISPERSION_WIDTH) {
|
||||
let mut global_positions = vec![];
|
||||
for y in y_arr.iter() {
|
||||
for x in x_arr.iter() {
|
||||
let local = Vector2I::new(*x, *y);
|
||||
let global = local_to_global(&local, &chunk_index);
|
||||
global_positions.push(global);
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute gas
|
||||
disperse_gas(
|
||||
global_positions,
|
||||
&mut terrain,
|
||||
&frame_counter,
|
||||
&mut debug_draw,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Don't update if the result of dispersion is similar to before
|
||||
fn disperse_gas(
|
||||
global_positions: Vec<Vector2I>,
|
||||
terrain: &mut Terrain2D,
|
||||
frame_counter: &FrameCounter,
|
||||
debug_draw: &mut bevy_prototype_debug_lines::DebugLines,
|
||||
) {
|
||||
use u32 as Capacity;
|
||||
use u8 as Min;
|
||||
use u8 as Max;
|
||||
let mut total_densities: HashMap<TexelID, (Capacity, Min, Max)> = HashMap::new();
|
||||
let mut valid_globals = vec![];
|
||||
for global in global_positions.iter() {
|
||||
let (texel, behaviour) = terrain.get_texel_behaviour(global);
|
||||
if behaviour.clone().map_or(true, |b| b.form == TexelForm::Gas) {
|
||||
valid_globals.push(*global);
|
||||
}
|
||||
match (texel, behaviour) {
|
||||
(Some(texel), Some(behaviour)) => {
|
||||
if behaviour.form == TexelForm::Gas {
|
||||
if let Some((old_density, old_min, old_max)) = total_densities.get(&texel.id) {
|
||||
total_densities.insert(
|
||||
texel.id,
|
||||
(
|
||||
texel.density as u32 + *old_density,
|
||||
texel.density.min(*old_min),
|
||||
texel.density.max(*old_max),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
total_densities.insert(
|
||||
texel.id,
|
||||
(texel.density as u32, texel.density, texel.density),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
(_, _) => (),
|
||||
// Change brush
|
||||
for event in mouse_wheel.iter() {
|
||||
if allow_painting {
|
||||
brush.radius = (brush.radius + event.y.round() as i32).clamp(1, 128);
|
||||
}
|
||||
}
|
||||
|
||||
let mut total_densities: Vec<(TexelID, Capacity, Min, Max)> = total_densities
|
||||
.iter()
|
||||
.map(|(t, (d, min, max))| (*t, *d, *min, *max))
|
||||
.collect();
|
||||
|
||||
if total_densities.len() == 0 {
|
||||
if !allow_painting {
|
||||
return;
|
||||
}
|
||||
|
||||
total_densities.sort_unstable_by_key(|(_, density, _, _)| *density);
|
||||
total_densities.reverse();
|
||||
// https://bevy-cheatbook.github.io/cookbook/cursor2world.html#2d-games
|
||||
// get the camera info and transform
|
||||
// assuming there is exactly one main camera entity, so query::single() is OK
|
||||
let (camera, camera_transform) = camera_query.single();
|
||||
|
||||
const TILE_CAPACITY: u32 = u8::MAX as u32;
|
||||
let free_slots = valid_globals.len() as u32
|
||||
- total_densities
|
||||
.iter()
|
||||
.map(|(_, v, _, _)| (*v / (TILE_CAPACITY + 1)) + 1)
|
||||
.sum::<u32>();
|
||||
|
||||
// Stop if the gas is already close to a stable state
|
||||
const STABLE_TRESHOLD: u8 = 3;
|
||||
if total_densities.iter().all(|(_, _, min, max)| {
|
||||
if u8::abs_diff(*min, *max) > STABLE_TRESHOLD {
|
||||
return false;
|
||||
}
|
||||
free_slots > 0 && *max <= STABLE_TRESHOLD
|
||||
}) {
|
||||
// // DEBUG: draw box for stabilized area
|
||||
// let mut min = valid_globals.first().unwrap().clone();
|
||||
// let mut max = valid_globals.first().unwrap().clone();
|
||||
// for global in valid_globals.iter() {
|
||||
// min = Vector2I::min(&min, global);
|
||||
// max = Vector2I::max(&max, global);
|
||||
// }
|
||||
// max = max + Vector2I::ONE;
|
||||
// crate::game::debug::terrain::draw_box(
|
||||
// debug_draw,
|
||||
// Vec3::from(min),
|
||||
// Vec3::from(max),
|
||||
// Color::CYAN,
|
||||
// 0.0,
|
||||
// );
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate slots
|
||||
let mut slots: Vec<(TexelID, u32)> = vec![];
|
||||
for (id, density, _, _) in total_densities.iter() {
|
||||
let min_slots = (density / (TILE_CAPACITY + 1)) + 1;
|
||||
slots.push((*id, min_slots));
|
||||
}
|
||||
for i in 0..free_slots as usize {
|
||||
let len = slots.len();
|
||||
slots[i % len].1 += 1;
|
||||
}
|
||||
|
||||
// Disperse into given slots
|
||||
let mut texels: Vec<Texel2D> = vec![];
|
||||
for (id, total_density, _, _) in total_densities.iter() {
|
||||
let slots = slots.iter().find(|s| s.0 == *id).unwrap().1;
|
||||
let mut density_left = *total_density;
|
||||
for i in 0..slots {
|
||||
let density = if i < (slots - 1) {
|
||||
(total_density / slots).min(density_left)
|
||||
} else {
|
||||
density_left
|
||||
}
|
||||
.min(u8::MAX as u32);
|
||||
if density > 0 {
|
||||
texels.push(Texel2D {
|
||||
id: *id,
|
||||
density: density as u8,
|
||||
});
|
||||
density_left -= density;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply changes
|
||||
if texels.len() > valid_globals.len() {
|
||||
panic!("disperse_gas() - valid_globals is shorter than texels");
|
||||
}
|
||||
|
||||
fastrand::shuffle(&mut valid_globals);
|
||||
for i in 0..valid_globals.len() {
|
||||
let global = valid_globals[i];
|
||||
if i < texels.len() {
|
||||
let texel = texels[i];
|
||||
terrain.set_texel(&global, texel, None);
|
||||
} else {
|
||||
terrain.set_texel(&global, Texel2D::default(), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn simulate_texel(global: Vector2I, terrain: &mut Terrain2D, frame_counter: &FrameCounter) {
|
||||
let (_, behaviour) = match terrain.get_texel_behaviour(&global) {
|
||||
(Some(texel), Some(behaviour)) => (texel, behaviour),
|
||||
(_, _) => return,
|
||||
// get the window that the camera is displaying to (or the primary window)
|
||||
let window = if let RenderTarget::Window(id) = camera.target {
|
||||
windows.get(id).unwrap()
|
||||
} else {
|
||||
windows.get_primary().unwrap()
|
||||
};
|
||||
|
||||
let simulation_frame = (frame_counter.frame % u8::MAX as u64) as u8 + 1;
|
||||
// check if the cursor is inside the window and get its position
|
||||
let world_pos = if let Some(screen_pos) = window.cursor_position() {
|
||||
// get the size of the window
|
||||
let window_size = Vec2::new(window.width() as f32, window.height() as f32);
|
||||
|
||||
// Gravity
|
||||
if let Some(gravity) = behaviour.gravity {
|
||||
let grav_offset = Vector2I::from(gravity);
|
||||
let grav_pos = global + grav_offset;
|
||||
// convert screen position [0..resolution] to ndc [-1..1] (gpu coordinates)
|
||||
let ndc = (screen_pos / window_size) * 2.0 - Vec2::ONE;
|
||||
|
||||
if behaviour.form != TexelForm::Gas || gravity.abs() > fastrand::u8(0..u8::MAX) {
|
||||
// Try falling
|
||||
{
|
||||
let (_, other_behaviour) = terrain.get_texel_behaviour(&grav_pos);
|
||||
if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
|
||||
terrain.swap_texels(&global, &grav_pos, Some(simulation_frame));
|
||||
return;
|
||||
}
|
||||
if terrain.can_transfer_density(&global, &grav_pos) {
|
||||
terrain.transfer_density(&global, &grav_pos, gravity, Some(simulation_frame))
|
||||
// matrix for undoing the projection and camera transform
|
||||
let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix().inverse();
|
||||
|
||||
// use it to convert ndc to world-space coordinates
|
||||
let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0));
|
||||
|
||||
// reduce it to a 2D value
|
||||
world_pos.truncate()
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if key_input.just_pressed(KeyCode::Key1) {
|
||||
brush.tile = 1;
|
||||
}
|
||||
if key_input.just_pressed(KeyCode::Key2) {
|
||||
brush.tile = 2;
|
||||
}
|
||||
if key_input.just_pressed(KeyCode::Key3) {
|
||||
brush.tile = 3;
|
||||
}
|
||||
|
||||
let colors = vec![
|
||||
Color::rgba(1.0, 0.25, 0.25, 1.0),
|
||||
Color::rgba(0.25, 1.0, 0.25, 1.0),
|
||||
Color::rgba(0.25, 0.25, 1.0, 1.0),
|
||||
];
|
||||
|
||||
let origin = Vector2I::from(world_pos);
|
||||
let radius = brush.radius;
|
||||
let color = colors[brush.tile as usize % colors.len()];
|
||||
let id = match (
|
||||
mouse_input.pressed(MouseButton::Left),
|
||||
mouse_input.pressed(MouseButton::Right),
|
||||
) {
|
||||
(true, false) => brush.tile,
|
||||
(_, _) => 0,
|
||||
};
|
||||
|
||||
for y in origin.y - (radius - 1)..origin.y + radius {
|
||||
for x in origin.x - (radius - 1)..origin.x + radius {
|
||||
let dx = (x - origin.x).abs();
|
||||
let dy = (y - origin.y).abs();
|
||||
|
||||
if dx * dx + dy * dy <= (radius - 1) * (radius - 1) {
|
||||
let pos: Vector2I = Vector2I { x, y };
|
||||
debug_draw.line_colored(
|
||||
Vec3::from(pos) + Vec3::new(0.45, 0.45, 0.0),
|
||||
Vec3::from(pos) + Vec3::new(0.55, 0.55, 0.0),
|
||||
0.0,
|
||||
color,
|
||||
);
|
||||
if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right)
|
||||
{
|
||||
terrain.set_texel(&pos, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try "sliding"
|
||||
let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT];
|
||||
if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 {
|
||||
dirs.reverse();
|
||||
}
|
||||
for dir in dirs.iter() {
|
||||
let slide_pos = match behaviour.form {
|
||||
TexelForm::Solid => grav_pos + *dir,
|
||||
TexelForm::Liquid | TexelForm::Gas => global + *dir,
|
||||
};
|
||||
let (_, other_behaviour) = terrain.get_texel_behaviour(&slide_pos);
|
||||
if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
|
||||
terrain.swap_texels(&global, &slide_pos, Some(simulation_frame));
|
||||
return;
|
||||
}
|
||||
if terrain.can_transfer_density(&global, &grav_pos) {
|
||||
terrain.transfer_density(&global, &grav_pos, gravity, Some(simulation_frame))
|
||||
}
|
||||
}
|
||||
/**
|
||||
Visualize dirty rects
|
||||
*/
|
||||
fn dirty_rect_visualizer(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
|
||||
for (chunk_index, chunk) in terrain.chunk_iter() {
|
||||
let rect = if let Some(rect) = chunk.dirty_rect {
|
||||
rect
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let color = Color::RED;
|
||||
|
||||
let points = vec![
|
||||
Vec3::new(rect.min.x as f32, rect.min.y as f32, 0.0),
|
||||
Vec3::new((rect.max.x + 1) as f32, rect.min.y as f32, 0.0),
|
||||
Vec3::new((rect.max.x + 1) as f32, (rect.max.y + 1) as f32, 0.0),
|
||||
Vec3::new(rect.min.x as f32, (rect.max.y + 1) as f32, 0.0),
|
||||
];
|
||||
for i in 0..points.len() {
|
||||
let offset = Vec3::from(chunk_index_to_global(chunk_index));
|
||||
debug_draw.line_colored(
|
||||
offset + points[i],
|
||||
offset + points[(i + 1) % points.len()],
|
||||
0.0,
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -360,6 +206,7 @@ fn emit_terrain_events(
|
|||
for (chunk_index, chunk) in terrain.chunk_iter_mut() {
|
||||
if let Some(rect) = &chunk.dirty_rect {
|
||||
terrain_events.send(TerrainEvent2D::TexelsUpdated(*chunk_index, *rect));
|
||||
chunk.mark_clean();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -374,29 +221,13 @@ pub enum TerrainEvent2D {
|
|||
pub struct Terrain2D {
|
||||
chunk_map: HashMap<Chunk2DIndex, Chunk2D>,
|
||||
events: Vec<TerrainEvent2D>,
|
||||
pub top_boundary: Option<i32>,
|
||||
pub bottom_boundary: Option<i32>,
|
||||
pub left_boundary: Option<i32>,
|
||||
pub right_boundary: Option<i32>,
|
||||
}
|
||||
|
||||
impl Terrain2D {
|
||||
pub const WORLD_WIDTH: i32 = 512;
|
||||
pub const WORLD_HEIGHT: i32 = Self::WORLD_WIDTH * 2;
|
||||
|
||||
pub fn new(
|
||||
top_boundary: Option<i32>,
|
||||
bottom_boundary: Option<i32>,
|
||||
left_boundary: Option<i32>,
|
||||
right_boundary: Option<i32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
pub fn new() -> Terrain2D {
|
||||
Terrain2D {
|
||||
chunk_map: HashMap::new(),
|
||||
events: Vec::new(),
|
||||
top_boundary,
|
||||
bottom_boundary,
|
||||
left_boundary,
|
||||
right_boundary,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -448,162 +279,17 @@ impl Terrain2D {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn mark_dirty(&mut self, global: &Vector2I) {
|
||||
pub fn set_texel(&mut self, global: &Vector2I, id: TexelID) {
|
||||
let index = global_to_chunk_index(global);
|
||||
if let Some(chunk) = self.index_to_chunk_mut(&index) {
|
||||
chunk.mark_dirty(&global_to_local(global));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_within_boundaries(&self, global: &Vector2I) -> bool {
|
||||
if let Some(top) = self.top_boundary {
|
||||
if global.y >= top {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(bottom) = self.bottom_boundary {
|
||||
if global.y < bottom {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(left) = self.left_boundary {
|
||||
if global.x < left {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(right) = self.right_boundary {
|
||||
if global.x >= right {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn get_texel(&self, global: &Vector2I) -> Option<Texel2D> {
|
||||
self.global_to_chunk(global)
|
||||
.map_or(None, |chunk| chunk.get_texel(&global_to_local(global)))
|
||||
}
|
||||
|
||||
pub fn get_latest_simulation(&self, global: &Vector2I) -> Option<u8> {
|
||||
self.global_to_chunk(global).map_or(None, |chunk| {
|
||||
chunk.get_latest_simulation(&global_to_local(global))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_texel_behaviour(
|
||||
&self,
|
||||
global: &Vector2I,
|
||||
) -> (Option<Texel2D>, Option<TexelBehaviour2D>) {
|
||||
let texel = self.get_texel(global);
|
||||
(
|
||||
texel,
|
||||
if self.is_within_boundaries(global) {
|
||||
texel.map_or(None, |t| TexelBehaviour2D::from_id(&t.id))
|
||||
} else {
|
||||
Some(TexelBehaviour2D::OUT_OF_BOUNDS)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_texel(
|
||||
&mut self,
|
||||
global: &Vector2I,
|
||||
new_texel: Texel2D,
|
||||
simulation_frame: Option<u8>,
|
||||
) {
|
||||
if !self.is_within_boundaries(global) {
|
||||
return;
|
||||
}
|
||||
let index = global_to_chunk_index(global);
|
||||
let changed = match self.index_to_chunk_mut(&index) {
|
||||
Some(chunk) => chunk.set_texel(&global_to_local(global), new_texel, simulation_frame),
|
||||
match self.index_to_chunk_mut(&index) {
|
||||
Some(chunk) => chunk.set_texel(&global_to_local(global), id),
|
||||
None => {
|
||||
let mut chunk = Chunk2D::new();
|
||||
let changed =
|
||||
chunk.set_texel(&global_to_local(global), new_texel, simulation_frame);
|
||||
chunk.set_texel(&global_to_local(global), id);
|
||||
self.add_chunk(index, chunk);
|
||||
changed
|
||||
}
|
||||
};
|
||||
if changed {
|
||||
self.mark_dirty(&(*global + Vector2I::UP));
|
||||
self.mark_dirty(&(*global + Vector2I::RIGHT));
|
||||
self.mark_dirty(&(*global + Vector2I::DOWN));
|
||||
self.mark_dirty(&(*global + Vector2I::LEFT));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_texels(
|
||||
&mut self,
|
||||
from_global: &Vector2I,
|
||||
to_global: &Vector2I,
|
||||
simulation_frame: Option<u8>,
|
||||
) {
|
||||
let from = self.get_texel(from_global).unwrap_or_default();
|
||||
let to = self.get_texel(to_global).unwrap_or_default();
|
||||
self.set_texel(to_global, from, simulation_frame);
|
||||
// REM: The displaced texel is also marked as simulated
|
||||
self.set_texel(from_global, to, simulation_frame);
|
||||
}
|
||||
|
||||
fn can_transfer_density(&self, from_global: &Vector2I, to_global: &Vector2I) -> bool {
|
||||
let from = self.get_texel(from_global).unwrap_or_default();
|
||||
let to = self.get_texel(to_global).unwrap_or_default();
|
||||
if from.id != to.id {
|
||||
return false;
|
||||
}
|
||||
|
||||
let behaviour = if let Some(behaviour) = from.behaviour() {
|
||||
behaviour
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
behaviour.form == TexelForm::Gas
|
||||
}
|
||||
|
||||
fn transfer_density(
|
||||
&mut self,
|
||||
from_global: &Vector2I,
|
||||
to_global: &Vector2I,
|
||||
gravity: TexelGravity,
|
||||
simulation_frame: Option<u8>,
|
||||
) {
|
||||
let from = self.get_texel(from_global).unwrap_or_default();
|
||||
let to = self.get_texel(to_global).unwrap_or_default();
|
||||
let max_transfer = gravity.abs();
|
||||
|
||||
// DEBUG: Test this out, another property?
|
||||
const MAX_TARGET_DENSITY: u8 = 25;
|
||||
let transfer = (u8::MAX - to.density)
|
||||
.min(max_transfer)
|
||||
.min(from.density)
|
||||
.min(MAX_TARGET_DENSITY.max(to.density) - to.density);
|
||||
if transfer == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if from.density - transfer == 0 {
|
||||
self.set_texel(&from_global, Texel2D::default(), simulation_frame);
|
||||
} else {
|
||||
self.set_texel(
|
||||
&from_global,
|
||||
Texel2D {
|
||||
density: from.density - transfer,
|
||||
..from
|
||||
},
|
||||
simulation_frame,
|
||||
);
|
||||
}
|
||||
self.set_texel(
|
||||
&to_global,
|
||||
Texel2D {
|
||||
density: to.density + transfer,
|
||||
..to
|
||||
},
|
||||
simulation_frame,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_to_texel_index(position: &Vector2I) -> Option<usize> {
|
||||
|
|
@ -624,21 +310,10 @@ pub fn texel_index_to_local(i: usize) -> Vector2I {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn texel_index_to_global(i: usize, chunk_index: &Chunk2DIndex) -> Vector2I {
|
||||
pub fn global_to_local(position: &Vector2I) -> Vector2I {
|
||||
Vector2I {
|
||||
x: i as i32 % Chunk2D::SIZE.x,
|
||||
y: i as i32 / Chunk2D::SIZE.y,
|
||||
} + chunk_index_to_global(chunk_index)
|
||||
}
|
||||
|
||||
pub fn local_to_global(local: &Vector2I, chunk_index: &Chunk2DIndex) -> Vector2I {
|
||||
chunk_index_to_global(chunk_index) + *local
|
||||
}
|
||||
|
||||
pub fn global_to_local(global: &Vector2I) -> Vector2I {
|
||||
Vector2I {
|
||||
x: wrapping_remainder(global.x, Chunk2D::SIZE.x),
|
||||
y: wrapping_remainder(global.y, Chunk2D::SIZE.y),
|
||||
x: wrapping_remainder(position.x, Chunk2D::SIZE.x),
|
||||
y: wrapping_remainder(position.y, Chunk2D::SIZE.y),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,31 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use super::*;
|
||||
use super::{
|
||||
local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent2D, Texel2D, TexelID,
|
||||
NEIGHBOUR_INDEX_MAP,
|
||||
};
|
||||
use crate::util::{CollisionLayers, Segment2I, Vector2I};
|
||||
use bevy::render::{render_resource::Extent3d, texture::ImageSampler};
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
render::{render_resource::Extent3d, texture::ImageSampler},
|
||||
};
|
||||
use bevy_rapier2d::prelude::*;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
type Island = VecDeque<Segment2I>;
|
||||
pub type Chunk2DIndex = Vector2I;
|
||||
pub type NeighbourMask = u8;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref COLOR_MAP: HashMap<TexelID, [u8; 4]> = {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(0, [0x00, 0x00, 0x00, 0x00]);
|
||||
// map.insert(0, [0x03, 0x03, 0x03, 0xff]);
|
||||
// map.insert(1, [0x47, 0x8e, 0x48, 0xff]);
|
||||
map.insert(1, [0x9e, 0x7f, 0x63, 0xff]);
|
||||
map.insert(2, [0x38, 0x32, 0x2d, 0xff]);
|
||||
map.insert(3, [0x1e, 0x1e, 0x1e, 0xff]);
|
||||
map
|
||||
};
|
||||
|
||||
/// Marching Square case dictionary.
|
||||
///
|
||||
/// Key is a bitmask of neighbouring tiles (up, right, down, left - least significant bit first).
|
||||
|
|
@ -44,14 +60,6 @@ lazy_static! {
|
|||
/* down */ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO },
|
||||
/* left */ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP },
|
||||
];
|
||||
|
||||
static ref NEIGHBOUR_INDEX_MAP: HashMap<Vector2I, u8> = {
|
||||
let mut map = HashMap::new();
|
||||
for i in 0..Chunk2D::NEIGHBOUR_OFFSET_VECTORS.len() {
|
||||
map.insert(Chunk2D::NEIGHBOUR_OFFSET_VECTORS[i], i as u8);
|
||||
}
|
||||
map
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Reflect, Component, Default)]
|
||||
|
|
@ -82,28 +90,17 @@ pub struct ChunkColliderBundle {
|
|||
pub transform: TransformBundle,
|
||||
}
|
||||
|
||||
pub type Chunk2DIndex = Vector2I;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ChunkRect {
|
||||
pub min: Vector2I,
|
||||
pub max: Vector2I,
|
||||
}
|
||||
|
||||
impl ChunkRect {
|
||||
pub fn include_point(&self, point: Vector2I) -> Self {
|
||||
ChunkRect {
|
||||
min: Vector2I::min(&self.min, &point),
|
||||
max: Vector2I::max(&self.max, &point),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Chunk2D {
|
||||
pub texels: [Texel2D; Self::SIZE_X * Self::SIZE_Y],
|
||||
/// bitmask of empty/non-empty neighbours, see NEIGHBOUR_OFFSET_VECTORS for the order
|
||||
pub neighbour_mask: [NeighbourMask; Self::SIZE_X * Self::SIZE_Y],
|
||||
/// Used in simulation step so that texels won't be updated twice. Value of 0 is always updated.
|
||||
pub simulation_frames: [u8; Self::SIZE_X * Self::SIZE_Y],
|
||||
// TODO: handle multiple dirty rects?
|
||||
pub texels: [Texel2D; (Self::SIZE_X * Self::SIZE_Y) as usize],
|
||||
// TODO: handle multiple dirty rects
|
||||
pub dirty_rect: Option<ChunkRect>,
|
||||
}
|
||||
|
||||
|
|
@ -114,22 +111,65 @@ impl Chunk2D {
|
|||
x: Self::SIZE_X as i32,
|
||||
y: Self::SIZE_Y as i32,
|
||||
};
|
||||
pub const NEIGHBOUR_OFFSET_VECTORS: [Vector2I; 4] = [
|
||||
Vector2I { x: 0, y: 1 },
|
||||
Vector2I { x: 1, y: 0 },
|
||||
Vector2I { x: 0, y: -1 },
|
||||
Vector2I { x: -1, y: 0 },
|
||||
];
|
||||
|
||||
pub fn new() -> Chunk2D {
|
||||
Chunk2D {
|
||||
texels: [Texel2D::default(); Self::SIZE_X * Self::SIZE_Y],
|
||||
neighbour_mask: [0; Self::SIZE_X * Self::SIZE_Y],
|
||||
simulation_frames: [0; Self::SIZE_X * Self::SIZE_Y],
|
||||
texels: Self::new_texel_array(),
|
||||
dirty_rect: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_full() -> Chunk2D {
|
||||
let mut chunk = Chunk2D {
|
||||
texels: Self::new_texel_array(),
|
||||
dirty_rect: None,
|
||||
};
|
||||
for y in 0..Self::SIZE_Y {
|
||||
for x in 0..Self::SIZE_X {
|
||||
chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1);
|
||||
}
|
||||
}
|
||||
chunk
|
||||
}
|
||||
|
||||
pub fn new_half() -> Chunk2D {
|
||||
let mut chunk = Chunk2D {
|
||||
texels: Self::new_texel_array(),
|
||||
dirty_rect: None,
|
||||
};
|
||||
for y in 0..Self::SIZE_Y {
|
||||
for x in 0..Self::SIZE_X {
|
||||
if x <= Self::SIZE_Y - y {
|
||||
chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
chunk
|
||||
}
|
||||
|
||||
pub fn new_circle() -> Chunk2D {
|
||||
let mut chunk = Chunk2D {
|
||||
texels: Self::new_texel_array(),
|
||||
dirty_rect: None,
|
||||
};
|
||||
let origin = Self::SIZE / 2;
|
||||
let radius = Self::SIZE_X as i32 / 2;
|
||||
for y in 0..Self::SIZE_Y {
|
||||
for x in 0..Self::SIZE_X {
|
||||
let dx = (x as i32 - origin.x).abs();
|
||||
let dy = (y as i32 - origin.y).abs();
|
||||
if dx * dx + dy * dy <= (radius - 1) * (radius - 1) {
|
||||
chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
chunk
|
||||
}
|
||||
|
||||
pub fn new_texel_array() -> [Texel2D; Self::SIZE_X * Self::SIZE_Y] {
|
||||
[Texel2D::default(); Self::SIZE_X * Self::SIZE_Y]
|
||||
}
|
||||
|
||||
pub fn xy_vec() -> Vec<Vector2I> {
|
||||
let mut result = Vec::with_capacity(Self::SIZE_X * Self::SIZE_Y);
|
||||
for y in 0..Self::SIZE_Y {
|
||||
|
|
@ -146,13 +186,18 @@ impl Chunk2D {
|
|||
pub fn mark_all_dirty(&mut self) {
|
||||
self.dirty_rect = Some(ChunkRect {
|
||||
min: Vector2I::ZERO,
|
||||
max: Self::SIZE - Vector2I::ONE,
|
||||
max: Self::SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn mark_dirty(&mut self, position: &Vector2I) {
|
||||
match &self.dirty_rect {
|
||||
Some(rect) => self.dirty_rect = Some(rect.include_point(*position)),
|
||||
Some(rect) => {
|
||||
self.dirty_rect = Some(ChunkRect {
|
||||
min: Vector2I::min(&rect.min, position),
|
||||
max: Vector2I::max(&rect.max, position),
|
||||
})
|
||||
}
|
||||
None => {
|
||||
self.dirty_rect = Some(ChunkRect {
|
||||
min: *position,
|
||||
|
|
@ -170,69 +215,58 @@ impl Chunk2D {
|
|||
local_to_texel_index(position).map(|i| self.texels[i])
|
||||
}
|
||||
|
||||
pub fn get_latest_simulation(&self, position: &Vector2I) -> Option<u8> {
|
||||
local_to_texel_index(position).map(|i| self.simulation_frames[i])
|
||||
}
|
||||
|
||||
pub fn get_texel_mut(&mut self, position: &Vector2I) -> Option<&mut Texel2D> {
|
||||
local_to_texel_index(position).map(|i| &mut self.texels[i])
|
||||
}
|
||||
|
||||
pub fn set_texel(
|
||||
&mut self,
|
||||
position: &Vector2I,
|
||||
new_texel: Texel2D,
|
||||
simulation_frame: Option<u8>,
|
||||
) -> bool {
|
||||
pub fn set_texel(&mut self, position: &Vector2I, id: TexelID) {
|
||||
let i = local_to_texel_index(position).expect("Texel index out of range");
|
||||
if self.texels[i] == new_texel {
|
||||
return false;
|
||||
}
|
||||
self.mark_dirty(position);
|
||||
let update_neighbours = self.texels[i].has_collision() != new_texel.has_collision();
|
||||
self.texels[i] = new_texel;
|
||||
// Update simulation frame
|
||||
if let Some(simulation_frame) = simulation_frame {
|
||||
self.simulation_frames[i] = simulation_frame;
|
||||
if self.texels[i].id != id {
|
||||
self.mark_dirty(position);
|
||||
}
|
||||
let update_neighbours = self.texels[i].is_empty()
|
||||
!= (Texel2D {
|
||||
id,
|
||||
..self.texels[i]
|
||||
})
|
||||
.is_empty();
|
||||
self.texels[i].id = id;
|
||||
// Update neighbour mask
|
||||
if update_neighbours {
|
||||
for offset in Self::NEIGHBOUR_OFFSET_VECTORS {
|
||||
for offset in Texel2D::NEIGHBOUR_OFFSET_VECTORS {
|
||||
// Flip neighbour's bit
|
||||
match local_to_texel_index(&(*position + offset)) {
|
||||
Some(index) => {
|
||||
self.neighbour_mask[index] ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset];
|
||||
match self.get_texel_mut(&(*position + offset)) {
|
||||
Some(mut neighbour) => {
|
||||
neighbour.neighbour_mask ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset];
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn create_texture_data(&self) -> Vec<u8> {
|
||||
let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4);
|
||||
let fallback: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
|
||||
for y in (0..Chunk2D::SIZE_Y).rev() {
|
||||
for x in 0..Chunk2D::SIZE_X {
|
||||
let texel = &self.get_texel(&Vector2I::new(x as i32, y as i32)).unwrap();
|
||||
let behaviour = texel.behaviour();
|
||||
let mut color =
|
||||
behaviour.map_or(Color::rgba_u8(0, 0, 0, 0), |behaviour| behaviour.color);
|
||||
color.set_a(color.a() * ((texel.density as f32) / 256.0));
|
||||
let color_data = color.as_rgba_u32();
|
||||
let mut color_data: Vec<u8> = vec![
|
||||
((color_data >> 0) & 0xff) as u8,
|
||||
((color_data >> 8) & 0xff) as u8,
|
||||
((color_data >> 16) & 0xff) as u8,
|
||||
((color_data >> 24) & 0xff) as u8,
|
||||
];
|
||||
image_data.append(&mut color_data);
|
||||
image_data.append(
|
||||
&mut COLOR_MAP
|
||||
.get(
|
||||
&self
|
||||
.get_texel(&Vector2I::new(x as i32, y as i32))
|
||||
.unwrap()
|
||||
.id,
|
||||
)
|
||||
.unwrap_or(&fallback)
|
||||
.to_vec()
|
||||
.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
image_data
|
||||
}
|
||||
|
||||
// TODO: Don't create collision for falling texels, it's pretty annoying that a stream of small grains blocks movement
|
||||
pub fn create_collision_data(&self) -> Vec<Vec<Vec2>> {
|
||||
let mut islands: Vec<Island> = Vec::new();
|
||||
for i in 0..self.texels.len() {
|
||||
|
|
@ -250,9 +284,8 @@ impl Chunk2D {
|
|||
| if local.x == 0 { 1 << 3 } else { 0 };
|
||||
|
||||
let mut sides: Vec<Segment2I>;
|
||||
let has_collision = TexelBehaviour2D::has_collision(&self.texels[i].id);
|
||||
if !has_collision {
|
||||
sides = MST_CASE_MAP[self.neighbour_mask[i] as usize]
|
||||
if self.texels[i].is_empty() {
|
||||
sides = MST_CASE_MAP[self.texels[i].neighbour_mask as usize]
|
||||
.iter()
|
||||
.clone()
|
||||
.map(|side| Segment2I {
|
||||
|
|
@ -260,7 +293,7 @@ impl Chunk2D {
|
|||
to: side.to + local,
|
||||
})
|
||||
.collect();
|
||||
} else if has_collision && edge_mask != 0 {
|
||||
} else if !self.texels[i].is_empty() && edge_mask != 0 {
|
||||
sides = Vec::with_capacity(Chunk2D::SIZE_X * 2 + Chunk2D::SIZE_Y * 2);
|
||||
for i in 0..MST_EDGE_CASE_MAP.len() {
|
||||
if edge_mask & (1 << i) != 0 {
|
||||
|
|
@ -401,7 +434,7 @@ pub fn chunk_spawner(
|
|||
..default()
|
||||
},
|
||||
texture,
|
||||
transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 1.0)),
|
||||
transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 0.0)),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
|
|
@ -438,7 +471,9 @@ pub fn chunk_spawner(
|
|||
}
|
||||
}
|
||||
|
||||
/// Update the chunk sprite as needed
|
||||
/**
|
||||
Update the chunk sprite as needed
|
||||
*/
|
||||
pub fn chunk_sprite_sync(
|
||||
mut terrain_events: EventReader<TerrainEvent2D>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
|
|
@ -495,7 +530,9 @@ pub fn chunk_sprite_sync(
|
|||
}
|
||||
}
|
||||
|
||||
/// Create and update colliders for chunk as needed
|
||||
/**
|
||||
Create and update colliders for chunk as needed
|
||||
*/
|
||||
pub fn chunk_collision_sync(
|
||||
mut terrain_events: EventReader<TerrainEvent2D>,
|
||||
mut commands: Commands,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use noise::{NoiseFn, PerlinSurflet};
|
||||
|
||||
use super::*;
|
||||
use crate::util::{inverse_lerp, lerp};
|
||||
use super::{chunk_index_to_global, Chunk2D, Chunk2DIndex};
|
||||
|
||||
pub struct TerrainGen2D {
|
||||
pub seed: u32,
|
||||
|
|
@ -31,16 +30,16 @@ impl TerrainGen2D {
|
|||
|
||||
let mut id = 0;
|
||||
if value > 0.35 {
|
||||
id = 11;
|
||||
id = 1;
|
||||
}
|
||||
if value > 0.42 {
|
||||
id = 12;
|
||||
id = 2;
|
||||
}
|
||||
if value > 0.9 {
|
||||
id = 13;
|
||||
id = 3;
|
||||
}
|
||||
|
||||
chunk.set_texel(&local, Texel2D { id, ..default() }, None);
|
||||
chunk.set_texel(&local, id);
|
||||
}
|
||||
chunk
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,38 @@
|
|||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use u8 as TexelID;
|
||||
pub use u8 as NeighbourMask;
|
||||
|
||||
use super::TexelBehaviour2D;
|
||||
use crate::util::Vector2I;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Texel2D {
|
||||
/// Identifier for a set of properties
|
||||
pub id: TexelID,
|
||||
/// Used by gas materials
|
||||
pub density: u8,
|
||||
/// bitmask of empty/non-empty neighbours, see NEIGHBOUR_OFFSET_VECTORS for the order
|
||||
pub neighbour_mask: NeighbourMask,
|
||||
}
|
||||
|
||||
impl Default for Texel2D {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: TexelID::default(),
|
||||
density: u8::MAX,
|
||||
lazy_static! {
|
||||
pub static ref NEIGHBOUR_INDEX_MAP: HashMap<Vector2I, u8> = {
|
||||
let mut map = HashMap::new();
|
||||
for i in 0..Texel2D::NEIGHBOUR_OFFSET_VECTORS.len() {
|
||||
map.insert(Texel2D::NEIGHBOUR_OFFSET_VECTORS[i], i as u8);
|
||||
}
|
||||
}
|
||||
map
|
||||
};
|
||||
}
|
||||
|
||||
impl Texel2D {
|
||||
pub const EMPTY: TexelID = 0;
|
||||
pub const NEIGHBOUR_OFFSET_VECTORS: [Vector2I; 4] = [
|
||||
Vector2I { x: 0, y: 1 },
|
||||
Vector2I { x: 1, y: 0 },
|
||||
Vector2I { x: 0, y: -1 },
|
||||
Vector2I { x: -1, y: 0 },
|
||||
];
|
||||
|
||||
pub fn has_collision(&self) -> bool {
|
||||
TexelBehaviour2D::has_collision(&self.id)
|
||||
}
|
||||
|
||||
pub fn behaviour(&self) -> Option<TexelBehaviour2D> {
|
||||
TexelBehaviour2D::from_id(&self.id)
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.id == 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,255 +0,0 @@
|
|||
use crate::util::Vector2I;
|
||||
|
||||
use super::TexelID;
|
||||
use bevy::prelude::*;
|
||||
use lazy_static::lazy_static;
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
lazy_static! {
|
||||
static ref ID_MAP: HashMap<TexelID, TexelBehaviour2D> = {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
result.insert(
|
||||
1,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("loose sand"),
|
||||
color: Color::rgb(0.61, 0.49, 0.38),
|
||||
gravity: Some(TexelGravity::Down(200)),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
2,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("loose stone"),
|
||||
color: Color::rgb(0.21, 0.19, 0.17),
|
||||
gravity: Some(TexelGravity::Down(200)),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
3,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("loose sturdy stone"),
|
||||
color: Color::rgb(0.11, 0.11, 0.11),
|
||||
gravity: Some(TexelGravity::Down(200)),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
4,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("water"),
|
||||
color: Color::rgba(0.0, 0.0, 1.0, 0.5),
|
||||
form: TexelForm::Liquid,
|
||||
gravity: Some(TexelGravity::Down(50)),
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
5,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("oil"),
|
||||
color: Color::rgba(0.5, 0.5, 0.25, 0.5),
|
||||
form: TexelForm::Liquid,
|
||||
gravity: Some(TexelGravity::Down(20)),
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
6,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("light gas"),
|
||||
color: Color::rgba(0.0, 1.0, 0.0, 0.5),
|
||||
form: TexelForm::Gas,
|
||||
gravity: Some(TexelGravity::Up(160)),
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
7,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("heavy gas"),
|
||||
color: Color::rgba(1.0, 0.5, 0.5, 0.5),
|
||||
form: TexelForm::Gas,
|
||||
gravity: Some(TexelGravity::Down(60)),
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
8,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("oxygen"),
|
||||
color: Color::rgba(0.5, 0.5, 0.5, 0.5),
|
||||
form: TexelForm::Gas,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
11,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("sand"),
|
||||
color: Color::rgb(0.61, 0.49, 0.38),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
12,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("stone"),
|
||||
color: Color::rgb(0.21, 0.19, 0.17),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
13,
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("sturdy stone"),
|
||||
color: Color::rgb(0.11, 0.11, 0.11),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result
|
||||
};
|
||||
static ref FORM_DISPLACEMENT_PRIORITY: HashMap<TexelForm, u8> = {
|
||||
let mut result = HashMap::new();
|
||||
result.insert(TexelForm::Gas, 0);
|
||||
result.insert(TexelForm::Liquid, 1);
|
||||
result.insert(TexelForm::Solid, 2);
|
||||
result
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub enum TexelForm {
|
||||
#[default]
|
||||
// Solid materials, when affected by gravity, create pyramid-like piles
|
||||
Solid,
|
||||
// Liquid materials, when affected by gravity, act like solids but also try to stabilise the surface level by traveling flat surfaces
|
||||
Liquid,
|
||||
// Gas materials act like liquids, but also have density/pressure that causes them to disperse
|
||||
Gas,
|
||||
}
|
||||
|
||||
impl TexelForm {
|
||||
fn priority(&self) -> u8 {
|
||||
FORM_DISPLACEMENT_PRIORITY
|
||||
.get(self)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum TexelGravity {
|
||||
Down(u8),
|
||||
Up(u8),
|
||||
}
|
||||
|
||||
impl From<TexelGravity> for Vector2I {
|
||||
fn from(gravity: TexelGravity) -> Self {
|
||||
match gravity {
|
||||
TexelGravity::Down(_) => Vector2I::DOWN,
|
||||
TexelGravity::Up(_) => Vector2I::UP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TexelGravity {
|
||||
pub fn abs(&self) -> u8 {
|
||||
match self {
|
||||
TexelGravity::Down(grav) => *grav,
|
||||
TexelGravity::Up(grav) => *grav,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TexelBehaviour2D {
|
||||
pub name: Cow<'static, str>,
|
||||
pub color: Color,
|
||||
pub form: TexelForm,
|
||||
pub has_collision: bool,
|
||||
pub gravity: Option<TexelGravity>,
|
||||
pub toughness: Option<f32>,
|
||||
}
|
||||
|
||||
impl Default for TexelBehaviour2D {
|
||||
fn default() -> Self {
|
||||
TexelBehaviour2D {
|
||||
name: Cow::Borrowed("Unnamed material"),
|
||||
color: Color::PINK,
|
||||
form: TexelForm::Solid,
|
||||
has_collision: false,
|
||||
gravity: None,
|
||||
toughness: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TexelBehaviour2D {
|
||||
pub const OUT_OF_BOUNDS: Self = TexelBehaviour2D {
|
||||
name: Cow::Borrowed(":)"),
|
||||
color: Color::BLACK,
|
||||
has_collision: true,
|
||||
form: TexelForm::Solid,
|
||||
gravity: None,
|
||||
toughness: None,
|
||||
};
|
||||
|
||||
pub fn from_id(id: &TexelID) -> Option<Self> {
|
||||
ID_MAP.get(id).cloned()
|
||||
}
|
||||
|
||||
pub fn is_empty(id: &TexelID) -> bool {
|
||||
ID_MAP.get(id).is_none()
|
||||
}
|
||||
|
||||
pub fn has_collision(id: &TexelID) -> bool {
|
||||
ID_MAP.get(id).map_or(false, |b| b.has_collision)
|
||||
}
|
||||
|
||||
/// Can this type of material displace another?
|
||||
pub fn can_displace(from: &TexelBehaviour2D, to: &Option<TexelBehaviour2D>) -> bool {
|
||||
let to = if let Some(to) = to { to } else { return true };
|
||||
|
||||
match (from.form, to.form) {
|
||||
(from_form, to_form) => {
|
||||
if from_form.priority() != to_form.priority() {
|
||||
return from_form.priority() > to_form.priority();
|
||||
}
|
||||
if let (Some(from_grav), Some(to_grav)) = (from.gravity, to.gravity) {
|
||||
match (from_grav, to_grav) {
|
||||
(TexelGravity::Down(from_grav), TexelGravity::Down(to_grav)) => {
|
||||
from_grav > to_grav
|
||||
}
|
||||
(TexelGravity::Up(from_grav), TexelGravity::Up(to_grav)) => {
|
||||
from_grav > to_grav
|
||||
}
|
||||
(_, _) => true,
|
||||
}
|
||||
} else {
|
||||
// Solids can also be displaced, but only if the other material has gravity
|
||||
to_form != TexelForm::Solid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
mod collision_layers;
|
||||
pub mod frame_counter;
|
||||
pub mod math;
|
||||
mod segment2_i32;
|
||||
mod vector2;
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
pub struct FrameCounterPlugin;
|
||||
|
||||
impl Plugin for FrameCounterPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(FrameCounter { frame: 0 })
|
||||
.add_system_to_stage(CoreStage::First, frame_increment);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct FrameCounter {
|
||||
pub frame: u64,
|
||||
}
|
||||
|
||||
fn frame_increment(mut frame_counter: ResMut<FrameCounter>) {
|
||||
frame_counter.frame += 1;
|
||||
}
|
||||
Loading…
Reference in New Issue