Compare commits

..

No commits in common. "master" and "0.2.0" have entirely different histories.

26 changed files with 1825 additions and 4756 deletions

4669
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,23 @@
[package] [package]
name = "forestiles" name = "forestiles"
version = "0.6.0" version = "0.2.0"
edition = "2021" edition = "2021"
[lib] [lib]
name = "forestiles" name = "forestiles"
path = "src/lib.rs" path = "src/lib.rs"
crate-type = ["cdylib"] crate-type=[
"staticlib",
"cdylib",
"rlib"
]
[[bin]] [[bin]]
path = "src/lib.rs" path = "src/lib.rs"
name = "forestiles" name = "forestiles"
[features]
debug = ["dep:bevy_editor_pls"]
[dependencies] [dependencies]
# git = "https://git.arkitu.fr/forestia/bevy.git" bevy = { version = "0.15", default-features = false, features = [
bevy = { git = "https://git.arkitu.fr/forestia/bevy.git", default-features = false, features = [
"android-native-activity", "android-native-activity",
"android_shared_stdcxx", "android_shared_stdcxx",
"bevy_color", "bevy_color",
@ -27,7 +27,6 @@ bevy = { git = "https://git.arkitu.fr/forestia/bevy.git", default-features = fal
"bevy_text", "bevy_text",
"bevy_ui", "bevy_ui",
"bevy_ui_picking_backend", "bevy_ui_picking_backend",
"bevy_mesh_picking_backend",
"bevy_window", "bevy_window",
"bevy_winit", "bevy_winit",
"default_font", "default_font",
@ -35,40 +34,29 @@ bevy = { git = "https://git.arkitu.fr/forestia/bevy.git", default-features = fal
"sysinfo_plugin", "sysinfo_plugin",
"webgl2", "webgl2",
"wayland", "wayland",
"png", ]}
"tonemapping_luts", bevy-inspector-egui = { version = "0.28", default-features = false, features = [
"bevy_pbr", "bevy_image",
"bevy_gltf", "bevy_render",
"animation", "egui_open_url"
"async_executor", ]}
"bevy_animation",
"bevy_asset",
"bevy_scene",
] }
#bevy-inspector-egui = { version = "0.28", default-features = false, features = [
# "bevy_image",
# "bevy_render",
# "egui_open_url",
#] }
mevy = { version = "0.2", features = ["0.16"] }
log = "0.4" log = "0.4"
rand = { version = "0.9", features = ["small_rng"] } rand = { version = "0.8", features = ["small_rng"] }
voronoice = "0.2" voronoice = "0.2"
noise = "0.9" noise = "0.9"
bevy_editor_pls = { version = "0.11", git = "https://github.com/jakobhellermann/bevy_editor_pls.git", optional = true }
async-channel = "*"
bitflags = "*"
bytemuck = { version = "1", features = ["derive", "must_cast"] }
include_dir = "0.7"
# [target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
# console_error_panic_hook = "0.1.6" console_error_panic_hook = "0.1.6"
# console_log = "1.0" console_log = "1.0"
# wgpu = { version = "22", features = ["webgl"] } wgpu = { version = "22", features = ["webgl"]}
# wasm-bindgen = "0.2" wasm-bindgen = "0.2"
# wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
# web-sys = { version = "0.3", features = ["Document", "Window", "Element"] } web-sys = { version = "0.3", features = [
# getrandom = { version = "*", features = ["js"] } "Document",
"Window",
"Element",
]}
getrandom ={ version = "*", features = ["js"]}
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.14" android_logger = "0.14"
@ -77,12 +65,15 @@ android_logger = "0.14"
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
[profile.release]
debug = true
# Enable a large amount of optimization in the dev profile for dependencies. # Enable a large amount of optimization in the dev profile for dependencies.
# [profile.dev.package."*"] [profile.dev.package."*"]
# opt-level = 1 opt-level = 3
[package.metadata.android] [package.metadata.android]
package = "org.forestiles.game" package = "org.forestiles.example"
apk_name = "forestiles" apk_name = "forestiles"
strip = "strip" strip = "strip"
# see https://github.com/rust-mobile/cargo-apk # see https://github.com/rust-mobile/cargo-apk

Binary file not shown.

Binary file not shown.

View File

@ -1,374 +0,0 @@
{
"accessors": [
{
"bufferView": 1,
"componentType": 5126,
"count": 3797,
"max": [
13.791570663452148,
3.689924955368042,
6.587912082672119
],
"min": [
8.412104606628418,
-3.378239154815674,
3.2653744220733643
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 45564,
"componentType": 5126,
"count": 3797,
"max": [
0.999547004699707,
0.9979217648506165,
0.9994550347328186
],
"min": [
-0.9993845820426941,
-0.99895840883255,
-0.9959900379180908
],
"type": "VEC3"
},
{
"bufferView": 0,
"componentType": 5125,
"count": 3804,
"type": "SCALAR"
},
{
"bufferView": 1,
"byteOffset": 91128,
"componentType": 5126,
"count": 858,
"max": [
10.12944221496582,
1.2994542121887207,
7.381155014038086
],
"min": [
7.413111686706543,
-1.4659144878387451,
5.502954006195068
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 101424,
"componentType": 5126,
"count": 858,
"max": [
0.9817375540733337,
0.9940162301063538,
0.9970744848251343
],
"min": [
-0.9994650483131409,
-0.9916966557502747,
-0.9975656270980835
],
"type": "VEC3"
},
{
"bufferView": 0,
"byteOffset": 15216,
"componentType": 5125,
"count": 858,
"type": "SCALAR"
},
{
"bufferView": 1,
"byteOffset": 111720,
"componentType": 5126,
"count": 3223,
"max": [
13.084306716918945,
2.6238322257995605,
7.360774993896484
],
"min": [
6.982709884643555,
-2.6466314792633057,
4.409972190856934
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 150396,
"componentType": 5126,
"count": 3223,
"max": [
0.9894658327102661,
0.9988145232200623,
0.9966117143630981
],
"min": [
-0.9995312094688416,
-0.9980282187461853,
-0.9977282285690308
],
"type": "VEC3"
},
{
"bufferView": 0,
"byteOffset": 18648,
"componentType": 5125,
"count": 3354,
"type": "SCALAR"
},
{
"bufferView": 1,
"byteOffset": 189072,
"componentType": 5126,
"count": 7794,
"max": [
13.333925247192383,
2.5410332679748535,
6.903109073638916
],
"min": [
7.7406463623046875,
-2.3769795894622803,
-0.008923768997192383
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 282600,
"componentType": 5126,
"count": 7794,
"max": [
0.9995447397232056,
0.998537003993988,
0.9921087622642517
],
"min": [
-0.9995536804199219,
-0.9998522996902466,
-0.9576923847198486
],
"type": "VEC3"
},
{
"bufferView": 0,
"byteOffset": 32064,
"componentType": 5125,
"count": 15528,
"type": "SCALAR"
}
],
"asset": {
"extras": {
"author": "UtsavSharma (https://sketchfab.com/UtsavSharma)",
"license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)",
"source": "https://sketchfab.com/3d-models/low-poly-tree-7f080d12b5074ffc9d194ef8c2a0bfb9",
"title": "Low Poly Tree"
},
"generator": "Sketchfab-12.68.0",
"version": "2.0"
},
"bufferViews": [
{
"buffer": 0,
"byteLength": 94176,
"name": "floatBufferViews",
"target": 34963
},
{
"buffer": 0,
"byteLength": 376128,
"byteOffset": 94176,
"byteStride": 12,
"name": "floatBufferViews",
"target": 34962
}
],
"buffers": [
{
"byteLength": 470304,
"uri": "scene.bin"
}
],
"materials": [
{
"doubleSided": true,
"name": "GreenDark",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.12549,
0.505882,
0.133333,
1.0
],
"metallicFactor": 0.0,
"roughnessFactor": 0.6
}
},
{
"doubleSided": true,
"name": "Green",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.239216,
1.0,
0.254902,
1.0
],
"metallicFactor": 0.0,
"roughnessFactor": 0.6
}
},
{
"doubleSided": true,
"name": "GreenDark1",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.196078,
0.513726,
0.0901961,
1.0
],
"metallicFactor": 0.0,
"roughnessFactor": 0.6
}
},
{
"doubleSided": true,
"name": "Material.002",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.301961,
0.145098,
0.0352941,
1.0
],
"metallicFactor": 0.0,
"roughnessFactor": 0.6
}
}
],
"meshes": [
{
"name": "Object_0",
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 0
},
"indices": 2,
"material": 0,
"mode": 4
}
]
},
{
"name": "Object_1",
"primitives": [
{
"attributes": {
"NORMAL": 4,
"POSITION": 3
},
"indices": 5,
"material": 1,
"mode": 4
}
]
},
{
"name": "Object_2",
"primitives": [
{
"attributes": {
"NORMAL": 7,
"POSITION": 6
},
"indices": 8,
"material": 2,
"mode": 4
}
]
},
{
"name": "Object_3",
"primitives": [
{
"attributes": {
"NORMAL": 10,
"POSITION": 9
},
"indices": 11,
"material": 3,
"mode": 4
}
]
}
],
"nodes": [
{
"children": [
1
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
2.220446049250313e-16,
-1.0,
0.0,
0.0,
1.0,
2.220446049250313e-16,
0.0,
0.0,
0.0,
0.0,
1.0
],
"name": "Sketchfab_model"
},
{
"children": [
2,
3,
4,
5
],
"name": "a303c315ac2a43a583a938036f0b9bed.3ds"
},
{
"mesh": 0,
"name": "Object_2"
},
{
"mesh": 1,
"name": "Object_3"
},
{
"mesh": 2,
"name": "Object_4"
},
{
"mesh": 3,
"name": "Object_5"
}
],
"scene": 0,
"scenes": [
{
"name": "Sketchfab_Scene",
"nodes": [
0
]
}
]
}

View File

@ -1,72 +0,0 @@
#import bevy_pbr::{
mesh_bindings::mesh,
mesh_functions,
skinning,
morph::morph,
forward_io::{Vertex, VertexOutput},
view_transformations::position_world_to_clip,
}
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
let mesh_world_from_local = mesh_functions::get_world_from_local(vertex.instance_index);
#ifdef SKINNED
var world_from_local = skinning::skin_model(
vertex.joint_indices,
vertex.joint_weights,
vertex.instance_index
);
#else
var world_from_local = mesh_world_from_local;
#endif
#ifdef VERTEX_NORMALS
#ifdef SKINNED
out.world_normal = skinning::skin_normals(world_from_local, vertex.normal);
#else
out.world_normal = mesh_functions::mesh_normal_local_to_world(
vertex.normal,
vertex.instance_index
);
#endif
#endif
#ifdef VERTEX_POSITIONS
out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4<f32>(vertex.position, 1.0));
out.position = position_world_to_clip(out.world_position.xyz);
#endif
#ifdef VERTEX_UVS_A
out.uv = vertex.uv;
#endif
#ifdef VERTEX_UVS_B
out.uv_b = vertex.uv_b;
#endif
#ifdef VERTEX_TANGENTS
out.world_tangent = mesh_functions::mesh_tangent_local_to_world(
world_from_local,
vertex.tangent,
vertex.instance_index
);
#endif
#ifdef VERTEX_COLORS
out.color = vertex.color;
#endif
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
out.instance_index = vertex.instance_index;
#endif
#ifdef VISIBILITY_RANGE_DITHER
out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level(
vertex.instance_index, mesh_world_from_local[3]
);
#endif
return out;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,21 +0,0 @@
use bevy::{asset::io::embedded::EmbeddedAssetRegistry, prelude::*};
use include_dir::{include_dir, Dir, DirEntry, File};
const ASSETS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/assets");
fn add_assets(assets: &mut EmbeddedAssetRegistry, dir: Dir<'static>) {
for f in dir.files() {
assets.insert_asset(f.path().to_path_buf(), f.path(), f.contents());
}
for d in dir.dirs() {
add_assets(assets, d.to_owned());
}
}
pub struct Plugin;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {
let mut embedded = app.world_mut().resource_mut::<EmbeddedAssetRegistry>();
add_assets(&mut embedded, ASSETS_DIR);
}
}

View File

@ -1,32 +1,92 @@
use bevy::prelude::*; use std::time::Duration;
use bevy::{input::mouse::MouseWheel, math::NormedVectorSpace, picking::{focus::HoverMap, pointer::PointerId}, prelude::*, utils::HashMap, window::PrimaryWindow};
use crate::ui;
pub struct Plugin; pub struct Plugin;
impl bevy::prelude::Plugin for Plugin { impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(Startup, setup); app.add_systems(Startup, setup);
// .add_systems(Update, move_cam)
// .init_resource::<Pointers>();
} }
} }
#[derive(Component)] fn setup(mut cmds: Commands, window: Query<&Window>) {
pub struct CameraMarker; let zoom = 2./window.single().width().min(window.single().height());
fn setup(mut cmds: Commands) {
cmds.spawn(( cmds.spawn((
CameraMarker, Camera2d,
Camera3d { Transform::from_scale(Vec3::new(zoom, zoom, zoom))
..Default::default()
},
Projection::default(),
Transform::from_translation(Vec3::new(0., 1., 1.))
.looking_to(Vec3::new(0., -1., -1.), Vec3::Y),
));
cmds.spawn((
DirectionalLight {
// color: Color::WHITE,
// illuminance: 17000.,
// shadows_enabled: true
..Default::default()
},
Transform::default().looking_to(Vec3::new(1., -1., -1.), Vec3::ZERO),
)); ));
} }
#[derive(Resource, Default)]
struct Pointers(HashMap<PointerId, (Vec2, Option<Duration>)>);
fn move_cam(
mut cam: Query<&mut Transform, With<Camera2d>>,
map_ui_entity: Query<Entity, With<ui::MapUIComponent>>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mut ev_scroll: EventReader<MouseWheel>,
touches: Res<Touches>,
window: Query<&Window, With<PrimaryWindow>>,
mut pointers: ResMut<Pointers>,
hover_map: Res<HoverMap>,
time: Res<Time>
) {
let window = window.single();
let mut cam = cam.single_mut();
let map_ui_entity = map_ui_entity.single();
let ps = hover_map.iter().filter_map(|(id, hit_map)| match id {
PointerId::Mouse => window.cursor_position().map(|p|
match pointers.0.get(id) {
Some(p_cache) => (p_cache.1.filter(|_| mouse_buttons.pressed(MouseButton::Left)), p, p_cache.0),
None => (None, p, p)
}
),
PointerId::Touch(i) => touches.get_pressed(*i).map(|t| (pointers.0.get(id).map(|(pos, start)| (*start).unwrap_or(time.elapsed())), t.position(), t.previous_position())),
_ => None
}.map(|(pressed_start,new_pos, old_pos)| (pressed_start,new_pos,old_pos,id,hit_map))
).collect::<Vec<_>>();
let pressed_on_map = ps.iter().filter(|p| p.0.is_some() && p.4.contains_key(&map_ui_entity)).collect::<Vec<_>>();
let old_midpoint = pressed_on_map.iter().fold(Vec2::ZERO, |acc, (_, _, old_pos, _, _)| {
acc + (old_pos/pressed_on_map.len() as f32)
});
let new_midpoint = pressed_on_map.iter().fold(Vec2::ZERO, |acc, (_, new_pos, _, _, _)| {
acc + (new_pos/pressed_on_map.len() as f32)
});
// move camera
cam.translation.x -= (new_midpoint.x - old_midpoint.x)*cam.scale.x;
cam.translation.y += (new_midpoint.y - old_midpoint.y)*cam.scale.y;
// multiple fingers zoom
if pressed_on_map.len() > 1 {
let old_d_to_midpoint = pressed_on_map.iter().fold(0., |acc, (_, _, old_pos, _, _)| {
acc + (old_midpoint-old_pos).norm()
});
let new_d_to_midpoint = pressed_on_map.iter().fold(0., |acc, (_, new_pos, _, _, _)| {
acc + (new_midpoint-new_pos).norm()
});
let zoom = new_d_to_midpoint/old_d_to_midpoint;
cam.scale /= zoom;
}
// mouse scroll zoom
for ev in ev_scroll.read() {
let scale = (cam.scale.x-(ev.y*0.1/window.width().min(window.height()))).clamp(0.0001, 2./window.width().min(window.height()));
cam.scale = Vec3::new(scale, scale, scale);
}
// update cached pointer positions
pointers.0.clear();
for (pressed_start, new_pos, _, id, _) in ps {
match id {
PointerId::Mouse => {pointers.0.insert(*id, (new_pos, pressed_start));},
PointerId::Touch(_) => {pointers.0.insert(*id, (new_pos, pressed_start));},
_ => {}
}
}
}

View File

@ -1,48 +1,24 @@
#![feature(iter_array_chunks)] use bevy::{prelude::*, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}};
use bevy::{ use bevy_inspector_egui::quick::WorldInspectorPlugin;
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
platform::collections::HashSet,
prelude::*,
};
pub mod assets;
pub mod camera;
pub mod map; pub mod map;
pub mod time; pub mod camera;
pub mod ui; pub mod ui;
#[bevy_main] #[bevy_main]
pub fn main() { pub fn main() {
App::new() App::new()
.add_plugins(( .add_plugins((
DefaultPlugins, // .set(bevy::render::RenderPlugin { DefaultPlugins,
// render_creation: bevy::render::settings::RenderCreation::Automatic( // WorldInspectorPlugin::default(),
// bevy::render::settings::WgpuSettings {
// backends: Some(bevy::render::settings::Backends::GL),
// ..default()
// },
// ),
// ..default()
// })
// bevy_inspector_egui::DefaultInspectorConfigPlugin,
// #[cfg(feature = "debug")]
// bevy_editor_pls::EditorPlugin::default(), // for debug
camera::Plugin, camera::Plugin,
map::Plugin, map::Plugin,
ui::Plugin, ui::Plugin,
time::Plugin, FrameTimeDiagnosticsPlugin,
assets::Plugin,
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin { LogDiagnosticsPlugin {
filter: Some(vec![FrameTimeDiagnosticsPlugin::FPS]), filter: Some(vec![FrameTimeDiagnosticsPlugin::FPS]),
// filter: Some({
// let mut set = HashSet::new();
// set.insert(FrameTimeDiagnosticsPlugin::FPS);
// set
// }),
..Default::default() ..Default::default()
}, }
)) ))
.run(); .run();
} }

View File

@ -1,305 +1,209 @@
use bevy::{ use std::time::Duration;
asset::{embedded_asset, RenderAssetUsages},
picking::PickSet, use bevy::{asset::RenderAssetUsages, picking::PickSet, prelude::*, render::mesh::{Indices, PrimitiveTopology}, utils::HashMap};
prelude::*,
render::mesh::{Indices, PrimitiveTopology},
};
use noise::{Fbm, MultiFractal, NoiseFn, Perlin}; use noise::{Fbm, MultiFractal, NoiseFn, Perlin};
use rand::{thread_rng, Rng, SeedableRng}; use rand::{thread_rng, Rng, SeedableRng};
use voronoice::{BoundingBox, Point, VoronoiBuilder}; use voronoice::{BoundingBox, Point, VoronoiBuilder};
mod animals;
mod cells; mod cells;
pub use animals::AnimalKind; mod picking;
pub use cells::CellKind; use picking::*;
use cells::*; use cells::*;
use crate::{
map::cells::{
grow::GrowThread,
material::{cell_material, CellMaterial},
},
time::GameTime,
};
pub struct Plugin; pub struct Plugin;
impl bevy::prelude::Plugin for Plugin { impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugins((cells::Plugin, MeshPickingPlugin)) app.add_systems(Startup, setup)
.add_systems(Startup, setup) .insert_resource(Time::<Fixed>::from_seconds(0.25)) // Time for a day
.add_systems(Update, update_chunk_mesh) .add_systems(FixedUpdate, update_cells)
.add_systems(PreUpdate, picking_backend.in_set(PickSet::Backend))
.add_systems(Update, update_map_mesh)
.insert_resource(ClearColor(Color::srgb(0., 0., 1.))) .insert_resource(ClearColor(Color::srgb(0., 0., 1.)))
.insert_resource(Seed(thread_rng().gen())); .insert_resource(Seed(thread_rng().gen()));
} }
} }
/// Determined empirically pub const HEIGHT: f32 = 2.;
pub const AVERAGE_NEIGHBORS_NUMBER: f64 = 6.; pub const WIDTH: f32 = 2.;
pub const REAL_HEIGHT: f32 = 500.;
pub const HEIGHT: f64 = 4.; pub const REAL_WIDTH: f32 = 500.;
pub const WIDTH: f64 = 4.; pub const CELL_AREA: f32 = REAL_HEIGHT * REAL_WIDTH / SIZE as f32;
pub const REAL_HEIGHT: f32 = 8000.; pub const SIZE: usize = 10000;
pub const REAL_WIDTH: f32 = 8000.;
pub const CELL_AREA: f32 = REAL_HEIGHT * REAL_WIDTH / CELLS as f32;
pub const CELLS_TARGET_NUMBER: usize = 100000;
pub const CHUNKS_RESOLUTION: usize = 8;
pub const CHUNKS: usize = CHUNKS_RESOLUTION.pow(2);
pub const CELLS_PER_CHUNK: usize = CELLS_TARGET_NUMBER / CHUNKS;
pub const CELLS: usize = CELLS_PER_CHUNK * CHUNKS;
#[derive(Resource)] #[derive(Resource)]
struct Seed(u32); struct Seed(u32);
#[derive(Resource)] #[derive(Component)]
pub struct Voronoi(voronoice::Voronoi); pub struct Voronoi (voronoice::Voronoi);
#[derive(Component)] #[derive(Component)]
pub struct MeshMarker; pub struct MapMarker;
#[derive(Component)] #[derive(Component)]
struct MeshColors(Vec<[f32; 4]>); struct MapColors (Vec<[f32; 4]>);
#[derive(Resource)]
pub struct CellsEntities(Vec<Entity>);
#[derive(Component)] #[derive(Component)]
pub struct MeshNeedsUpdate(f32); pub struct CellsEntities (Vec<Entity>);
#[derive(Component)] #[derive(Component)]
pub struct Chunk(usize); pub struct MeshNeedsUpdate(bool);
fn setup( fn setup(
mut cmds: Commands, mut cmds: Commands,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
// mut materials: ResMut<Assets<StandardMaterial>>, mut materials: ResMut<Assets<ColorMaterial>>,
mut materials: ResMut<Assets<CellMaterial>>, seed: Res<Seed>
asset_server: Res<AssetServer>,
seed: Res<Seed>,
gt: Res<GameTime>,
) { ) {
// cmds.spawn(SceneRoot(asset_server.load(
// GltfAssetLabel::Scene(0).from_asset("embedded://models/tree.glb"),
// )));
// return;
let mut rng = rand::rngs::SmallRng::seed_from_u64(seed.0 as u64); let mut rng = rand::rngs::SmallRng::seed_from_u64(seed.0 as u64);
let mut sites = Vec::with_capacity(CELLS); let mut sites = Vec::with_capacity(SIZE);
for chunk_x in 0..CHUNKS_RESOLUTION { for _ in 0..SIZE {
let min_x = WIDTH / 2. * ((2. * (chunk_x as f64) / (CHUNKS_RESOLUTION as f64)) - 1.); sites.push(Point { x:rng.gen_range(-WIDTH/2.0..WIDTH/2.0) as f64, y:rng.gen_range(-HEIGHT/2.0..HEIGHT/2.0) as f64 })
let max_x = min_x + (WIDTH as f64 / CHUNKS_RESOLUTION as f64);
for chunk_y in 0..CHUNKS_RESOLUTION {
let min_y = HEIGHT / 2. * ((2. * (chunk_y as f64) / (CHUNKS_RESOLUTION as f64)) - 1.);
let max_y = min_y + (HEIGHT as f64 / CHUNKS_RESOLUTION as f64);
for _ in 0..CELLS_PER_CHUNK {
sites.push(Point {
x: rng.gen_range(min_x..max_x),
y: rng.gen_range(min_y..max_y),
});
}
}
} }
// dbg!(sites.len());
let voronoi = VoronoiBuilder::default() let voronoi = VoronoiBuilder::default()
.set_sites(sites) .set_sites(sites)
.set_bounding_box(BoundingBox::new_centered(WIDTH, HEIGHT)) .set_bounding_box(BoundingBox::new_centered(WIDTH as f64, HEIGHT as f64))
.set_lloyd_relaxation_iterations(3) .set_lloyd_relaxation_iterations(3)
.build() .build()
.unwrap(); .unwrap();
let mut cells_data = Vec::with_capacity(SIZE);
let mut cells = Vec::with_capacity(CELLS); let z_noise = Fbm::<Perlin>::new(seed.0);
let z_noise = Fbm::<Perlin>::new(seed.0).set_octaves(4).set_lacunarity(3.); let moisture_noise = Fbm::<Perlin>::new(seed.0+1)
let moisture_noise = Fbm::<Perlin>::new(seed.0 + 1).set_frequency(2.); .set_frequency(2.);
for i in 0..CELLS { for i in 0..SIZE {
let c = voronoi.cell(i); let c = voronoi.cell(i);
let site = c.site_position(); let site = c.site_position();
let z = get_altitude(&z_noise, &[site.x as f32, site.y as f32]); let z = (
let _m = ( 0.3 // Arbitrary value
(moisture_noise.get([site.x, site.y]) + 1.) / 2. + ((z_noise.get([site.x, site.y])+1.)/2.) // Noise + [0; 1]
// Noise + [0; 1] - ((site.x.powi(2)+site.y.powi(2)).sqrt()*0.5) // Distance - [0; sqrt(2)] * 0.5
) ).clamp(0., 1.);
.clamp(0., 1.) as f32; let m = (
let k = if z <= 0. { (moisture_noise.get([site.x, site.y])+1.)/2. // Noise + [0; 1]
).clamp(0., 1.) as f32;
let k = if z <= 0.5 {
CellKind::Sea CellKind::Sea
} else if z <= 0.04 { } else if z <= 0.52 {
CellKind::Beach CellKind::Beach
} else if z < 0.6 { } else if z < 0.8 {
CellKind::Dirt CellKind::Dirt
} else { } else {
CellKind::Stone CellKind::Stone
}; };
cells.push(Cell { cells_data.push(CellData::new(k, i, (z*255.) as u8, (m*255.) as u8, 0, vec![]));
kind: k,
voronoi_id: i,
});
} }
// let mut poss = [Vec::new(); CHUNKS];
let mut indices: [Vec<u32>; CHUNKS] = std::array::from_fn(|_| Vec::new());
// let mut normals = Vec::new();
let mut poss = voronoi.sites().chunks_exact(CELLS_PER_CHUNK).map(|ch| { let mut poss = Vec::new();
ch.iter() let mut colors = Vec::new();
.map(|pos| { let mut indices = Vec::new();
let z = get_altitude(&z_noise, &[pos.x as f32, pos.y as f32]);
Vec3::new(pos.x as f32, z, -pos.y as f32) // adapt to bevy's coordinate system
})
.collect::<Vec<_>>()
});
let mut poss: [Vec<Vec3>; CHUNKS] = std::array::from_fn(|_| poss.next().unwrap());
// for (i, pos) in voronoi.sites().iter().enumerate() { for (c, cd) in voronoi.iter_cells().zip(cells_data.iter_mut()).filter(|(_,cd)| cd.kind != CellKind::Forest) {
// let z = get_altitude(&z_noise, &[pos.x as f32, pos.y as f32]); let color = cd.color();
// poss.push(Vec3::new(pos.x as f32, pos.y as f32, z)); // if c.site() == selected_tile {
// cells[i].vertices.push(i); // color[0] = (color[0]+0.4).clamp(0., 1.);
// } // color[1] = (color[1]+0.4).clamp(0., 1.);
// let mut hybrid_cells: HashMap<usize, usize> = HashMap::new(); // color[2] = (color[2]+0.4).clamp(0., 1.);
for t in voronoi.triangulation().triangles.chunks_exact(3) { // }
let on_hull = t let vs = c.iter_vertices().collect::<Vec<_>>();
.iter() let i = poss.len();
.filter(|v| voronoi.triangulation().hull.contains(v)) for v in vs.iter() {
.count(); poss.push(Vec3::new(v.x as f32, v.y as f32, 0.));// [v.x as f32, v.y as f32, 0.]);
// Don't draw triangles on the hull that are often long and look glitchy // poss.push(Vertex::new_col([v.x as f32, v.y as f32], color, 1));
if on_hull > 0 { colors.push(color);
continue;
} }
let mut chs = t.iter().map(|c| Cell::chunk_from_id(*c)); for v in 1..(vs.len()-1) {
let chs: [usize; 3] = std::array::from_fn(|_| chs.next().unwrap()); indices.extend_from_slice(&[i as u32, (i+v) as u32, (i+v+1) as u32]);
for ch in 0..CHUNKS { cd.vertices.extend_from_slice(&[i, i+v, i+v+1]);
if !chs.contains(&ch) {
continue;
}
// let ch = if chs[1] == chs[2] { chs[1] } else { chs[0] };
// Add vertex to chunk if it is from an external chunk but we need it for a triangle. Else, just make the triangle
if chs[2] != ch {
poss[ch].push(poss[chs[2]][t[2] % CELLS_PER_CHUNK]);
// hybrid_cells.insert(chs[2], chs[0]);
indices[ch].push((poss[ch].len() - 1) as u32);
} else {
indices[ch].push((t[2] % CELLS_PER_CHUNK) as u32);
}
if chs[1] != ch {
poss[ch].push(poss[chs[1]][t[1] % CELLS_PER_CHUNK]);
// hybrid_cells.insert(chs[1], chs[0]);
indices[ch].push((poss[ch].len() - 1) as u32);
} else {
indices[ch].push((t[1] % CELLS_PER_CHUNK) as u32);
}
if chs[0] != ch {
poss[ch].push(poss[chs[0]][t[0] % CELLS_PER_CHUNK]);
// hybrid_cells.insert(chs[0], chs[1]);
indices[ch].push((poss[ch].len() - 1) as u32);
} else {
indices[ch].push((t[0] % CELLS_PER_CHUNK) as u32);
}
} }
} }
let mut cells_entities = Vec::with_capacity(cells.len()); let mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default())
for (ch_id, ((poss, indices), ch_cells)) in poss // Add 4 vertices, each with its own position attribute (coordinate in
.into_iter() // 3D space), for each of the corners of the parallelogram.
.zip(indices.into_iter()) .with_inserted_attribute(
.zip(cells.chunks_exact(CELLS_PER_CHUNK)) Mesh::ATTRIBUTE_POSITION,
.enumerate() poss
{ )
let mut cmd_mesh = cmds.spawn_empty(); .with_inserted_attribute(
cmd_mesh.with_children(|parent| { Mesh::ATTRIBUTE_COLOR,
for cell in ch_cells { colors.clone()
let mut cmd = parent.spawn((
cell.clone(),
Transform::from_translation(poss[cell.voronoi_id % CELLS_PER_CHUNK]),
));
cells_entities.push(cmd.id());
}
});
let colors = vec![[0.; 4]; poss.len()];
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
) )
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, poss)
.with_inserted_attribute(Mesh::ATTRIBUTE_COLOR, colors.clone())
// .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
.with_inserted_indices(Indices::U32(indices)); .with_inserted_indices(Indices::U32(indices));
mesh.compute_smooth_normals(); let mut cells_entities = Vec::with_capacity(cells_data.len());
cmds.spawn((
Mesh2d(meshes.add(mesh)),
MeshMaterial2d(materials.add(ColorMaterial::default())),
Transform::default(),
Voronoi(voronoi),
MapColors(colors),
MeshNeedsUpdate(true),
MapMarker
)).with_children(|parent| {
for cd in cells_data {
let mut cmd = parent.spawn((cd, LastUpdate(0)));
cmd.observe(|trigger: Trigger<Pointer<Click>>, mut cells: Query<&mut CellData>, mut map_needs_update: Query<&mut MeshNeedsUpdate, With<MapMarker>>| {
if trigger.duration > Duration::from_millis(100) {
return
}
let mut cd = cells.get_mut(trigger.target).unwrap();
match cd.kind {
CellKind::Dirt | CellKind::Grass => {
cd.kind = CellKind::Forest;
map_needs_update.single_mut().0 = true;
},
_ => {}
}
dbg!(trigger.duration);
});
cells_entities.push(cmd.id());
}
}).insert(CellsEntities(cells_entities));
cmd_mesh
.insert((
Mesh3d(meshes.add(mesh)),
// StandardMaterial
// MeshMaterial3d(materials.add(cells::pbr_material::StandardMaterial::default())),
MeshMaterial3d(materials.add(cell_material())),
Transform::default(),
MeshColors(colors),
MeshNeedsUpdate(f32::MAX),
MeshMarker,
Chunk(ch_id),
))
.observe(self::cells::input::on_click);
}
cmds.insert_resource(GrowThread::new(
voronoi.clone(),
gt.secs.clone(),
cells,
rng,
));
cmds.insert_resource(Voronoi(voronoi));
cmds.insert_resource(CellsEntities(cells_entities));
} }
// TODO: update this to take chunks into account #[derive(Component)]
fn update_chunk_mesh( pub struct LastUpdate(usize);
cells: Query<&Cell>,
mut chunks: Query< fn update_cells(
(&Mesh3d, &mut MeshColors, &mut MeshNeedsUpdate, &Children), mut cells: Query<(&mut CellData, &mut LastUpdate)>,
With<MeshMarker>, mut map_needs_update: Query<&mut MeshNeedsUpdate, With<MapMarker>>
>,
mut meshes: ResMut<Assets<Mesh>>,
) { ) {
for (mesh, mut cols, mut needs_update, children) in chunks let mut map_needs_update = map_needs_update.single_mut();
.iter_mut() for (mut cd, mut lu) in cells.iter_mut() {
.sort_unstable_by::<&MeshNeedsUpdate>(|nu1, nu2| nu2.0.total_cmp(&nu1.0)) lu.0 += 1;
{ if lu.0 > match cd.kind {
if needs_update.0 > 0. { CellKind::Void | CellKind::Sea | CellKind::Beach | CellKind::Dirt | CellKind::Stone => usize::MAX,
if let Some(mesh) = meshes.get_mut(mesh) { CellKind::Forest => 100*365/4, // Let's say that a forest takes 100 years to mature
// let mut modified = false; CellKind::Grass => 7*7/4 // Let's say that grass takes 7 weaks to reach its max
// let mut rng = thread_rng(); } {
for child in children.iter() { lu.0 = 0;
let cell = cells.get(child).unwrap(); cd.resource = (cd.resource + 1).clamp(0, 4);
// let col: [f32; 4] = [rng.gen(), rng.gen(), rng.gen(), 1.]; map_needs_update.0 = true;
let col = cell.color();
// modified = modified || cols.0[cell.voronoi_id % CELLS_PER_CHUNK] != col;
cols.0[cell.voronoi_id % CELLS_PER_CHUNK] = col;
// for id in cell.vertices.iter() {
// modified = modified || cols.0[*id] != col;
// cols.0[*id] = col.clone();
// }
}
// if modified {
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, cols.0.clone());
// }
}
needs_update.0 = 0.;
// Update at most one chunk per frame to avoid freezes
return;
} }
} }
} }
/// Between 0 and 1 fn update_map_mesh(
fn get_altitude(z_noise: &Fbm<Perlin>, pos: &[f32; 2]) -> f32 { cells: Query<&CellData>,
let z_noise = ((z_noise.get([pos[0] as f64, pos[1] as f64]) as f32) + 1.) / 2.; // Noise [0; 1] mut map: Query<(&Mesh2d, &mut MapColors, &mut MeshNeedsUpdate), With<MapMarker>>,
mut meshes: ResMut<Assets<Mesh>>
-0.2// Arbitrary value ) {
+ (z_noise.exp() / 1f32.exp()) let (mesh, mut cols, mut needs_update) = map.single_mut();
// Noise + [0; 1] if needs_update.0 {
// - ((pos[0].powi(2)+pos[1].powi(2)).sqrt()*0.3) if let Some(mesh) = meshes.get_mut(mesh) {
// Distance - [0; sqrt(2)] * 0.5 let mut modified = false;
for cd in cells.iter() {
// .clamp(-1., 1.) let col = cd.color();
for id in cd.vertices.iter() {
modified = modified || cols.0[*id] != col;
cols.0[*id] = col.clone();
}
}
if modified {
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, cols.0.clone());
}
}
needs_update.0 = false;
}
} }
// fn map_input()

View File

@ -1,17 +0,0 @@
use bevy::prelude::*;
pub struct Plugin;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {}
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum AnimalKind {
Goat,
}
#[derive(Component)]
#[require(Transform)]
pub struct Animal {
pub kind: AnimalKind,
}

View File

@ -1,129 +1,45 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::{time::GameTime, ui::CurrentAction};
use super::{AnimalKind, CELLS_PER_CHUNK, CELL_AREA};
pub mod grow;
pub mod input;
pub mod material;
use material::CellMaterial;
pub struct Plugin;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {
app.add_plugins(MaterialPlugin::<CellMaterial> {
prepass_enabled: false,
shadows_enabled: false,
..Default::default()
})
.add_plugins(grow::Plugin);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellKind { pub enum CellKind {
Void,
Sea, Sea,
Beach, Beach,
Forest { wealth: WealthType }, Forest,
Dirt, Dirt,
Stone, Stone,
Grass { wealth: WealthType }, Grass
}
impl CellKind {
pub const FOREST: CellKind = CellKind::Forest { wealth: 0 };
pub const GRASS: CellKind = CellKind::Grass { wealth: 0 };
pub const fn growth_duration(&self) -> u64 {
match self {
CellKind::Forest { .. } => 100 * 365 * 24 * 60 * 60, // a forest takes 100 years to mature
CellKind::Grass { .. } => 7 * 7 * 24 * 60 * 60, // grass takes 7 weeks to reach its max
_ => unreachable!(),
}
}
/// Time before trying to extend to one neighbor
pub const fn extend_duration(&self) -> usize {
match self {
CellKind::Grass { wealth } => {
// "Grass can expand into adjacent dirt at a rate of 2.5 to 7.5 cm (1 to 3 inches) per month.", DeepSeek
59_388_377
* (CELL_AREA as usize).isqrt()
* WealthType::MAX.saturating_div(*wealth) as usize
}
CellKind::Forest { wealth } => {
// Arbitrary value : 1m per 10 years
(10 * 365 * 24 * 3600)
* (CELL_AREA as usize).isqrt()
* WealthType::MAX.saturating_div(*wealth) as usize
}
_ => unreachable!(),
}
}
pub const fn can_place_animal(&self, kind: AnimalKind) -> bool {
match self {
CellKind::Sea => false,
_ => true,
}
}
pub const fn grass_wealth(&self) -> Option<WealthType> {
match self {
CellKind::Grass { wealth } => Some(*wealth),
CellKind::Forest { wealth } => Some(wealth.saturating_mul(
(CellKind::FOREST.growth_duration() / CellKind::GRASS.growth_duration())
as WealthType,
)),
_ => None,
}
}
pub fn diff(&self, other: &CellKind) -> f32 {
match (self, other) {
(CellKind::Grass { wealth: w1 }, CellKind::Grass { wealth: w2 }) => {
wealth_to_unit(w2.abs_diff(*w1))
}
(CellKind::Forest { wealth: w1 }, CellKind::Forest { wealth: w2 }) => {
wealth_to_unit(w2.abs_diff(*w1))
}
(k1, k2) => {
if k1 == k2 {
0.
} else {
1.
}
}
}
}
} }
#[derive(Debug, Component, Clone)] #[derive(Debug, Component)]
pub struct Cell { pub struct CellData {
pub kind: CellKind, pub kind: CellKind,
pub voronoi_id: usize, pub cid: usize,
z: u8,
pub moisture: u8,
pub resource: u8, // How much resource there is (between 0 and 4)
pub vertices: Vec<usize>
} }
impl Cell { impl CellData {
pub const fn color(&self) -> [f32; 4] { pub fn new(kind: CellKind, cell: usize, z: u8, moisture: u8, resource: u8, vertices: Vec<usize>) -> Self {
Self {
kind,
cid: cell,
z,
moisture,
resource,
vertices
}
}
pub fn color(&self) -> [f32; 4] {
match self.kind { match self.kind {
CellKind::Void => [0.; 4],
CellKind::Sea => [0., 0., 1., 1.], CellKind::Sea => [0., 0., 1., 1.],
CellKind::Beach => [0.82, 0.84, 0.51, 1.], CellKind::Beach => [0.82, 0.84, 0.51, 1.],
CellKind::Forest { wealth } => [0., 0.5 - (wealth_to_unit(wealth) * 0.4), 0., 1.], CellKind::Forest => [0., 0.5 - (self.resource as f32/4.*0.4), 0., 1.],
CellKind::Dirt => [0.53, 0.38, 0.29, 1.], CellKind::Dirt => [0.53 - (self.resource as f32/4.*0.4), 0.38-(self.resource as f32/4.*0.4), 0.29-(self.resource as f32/4.*0.4), 1.],
CellKind::Stone => [0.5, 0.5, 0.5, 1.], CellKind::Stone => [0.5, 0.5, 0.5, 1.],
CellKind::Grass { wealth } => [ CellKind::Grass => [(136./255.) - (self.resource as f32/4.*0.4), (204./255.) - (self.resource as f32/4.*0.4), (59./255.) - (self.resource as f32/4.*0.4), 1.]
(136. / 255.) - (wealth_to_unit(wealth) * 0.15),
(154. / 255.) + (wealth_to_unit(wealth) * 0.1),
(59. / 255.) - (wealth_to_unit(wealth) * 0.15),
1.,
],
} }
} }
pub const fn chunk_from_id(id: usize) -> usize {
id / CELLS_PER_CHUNK
}
pub const fn chunk(&self) -> usize {
Self::chunk_from_id(self.voronoi_id)
}
}
pub type WealthType = u16;
const fn wealth_to_unit(wealth: WealthType) -> f32 {
wealth as f32 / WealthType::MAX as f32
} }

View File

@ -1,195 +0,0 @@
use std::net::UdpSocket;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use bevy::asset::{embedded_asset, embedded_path};
use bevy::prelude::*;
use bevy::tasks::{AsyncComputeTaskPool, Task};
use log::info;
use rand::rngs::SmallRng;
use rand::seq::IteratorRandom;
use rand::{rng, Rng};
use voronoice::Voronoi;
use crate::map::cells::{wealth_to_unit, WealthType};
use crate::map::{CellKind, CellsEntities, MeshNeedsUpdate};
use super::Cell;
pub struct Plugin;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, rx_chunks_updates);
// embedded_asset!(app, "../../../", "assets/models/tree.glb");
}
}
// Background task that manages terrain
#[derive(Resource)]
pub struct GrowThread(
pub Task<()>,
pub async_channel::Sender<Cell>,
pub async_channel::Receiver<Cell>,
);
impl GrowThread {
pub fn new(voronoi: Voronoi, gt: Arc<AtomicU64>, cells: Vec<Cell>, rng: SmallRng) -> Self {
let task_pool = AsyncComputeTaskPool::get();
let (tx_control, rx_control) = async_channel::unbounded();
let (tx_update, rx_update) = async_channel::unbounded();
Self(
task_pool.spawn(grow_thread(voronoi, gt, cells, rng, rx_control, tx_update)),
tx_control,
rx_update,
)
}
}
// #[derive(Component)]
// struct GetChunkTask(Task<Vec<Cell>>);
// const MAX_UPDATE_PER_FRAME: usize = 10000;
fn rx_chunks_updates(
mut chunks: Query<&mut MeshNeedsUpdate>,
mut cells: Query<(&mut Cell, &ChildOf, Entity)>,
cells_entities: Res<CellsEntities>,
grow_thread: Res<GrowThread>,
mut cmds: Commands,
asset_server: Res<AssetServer>,
) {
while let Ok(c) = grow_thread.2.try_recv() {
let id = c.voronoi_id;
let (mut cell, parent, id) = cells.get_mut(cells_entities.0[id]).unwrap();
chunks.get_mut(parent.parent()).unwrap().0 += cell.kind.diff(&c.kind);
if matches!(c.kind, CellKind::Forest { .. })
&& !matches!(cell.kind, CellKind::Forest { .. })
{
// Spawn tree
cmds.spawn((
SceneRoot(
asset_server
.load(GltfAssetLabel::Scene(0).from_asset("embedded://models/tree.glb")),
),
ChildOf(id),
Transform::from_scale(Vec3::new(0.1, 0.1, 0.1)).with_rotation(
Quat::from_axis_angle(
Vec3::Y,
rng().random_range(-std::f32::consts::PI..std::f32::consts::PI),
),
),
));
}
*cell = c;
}
}
const GRASS_EXTEND_STEP: u64 = CellKind::GRASS.growth_duration();
const FOREST_EXTEND_STEP: u64 = CellKind::FOREST.growth_duration();
pub async fn grow_thread(
voronoi: Voronoi,
gt: Arc<AtomicU64>,
cells: Vec<Cell>,
mut rng: SmallRng,
rx_control: async_channel::Receiver<Cell>,
tx_update: async_channel::Sender<Cell>,
) {
// Add "modified" flag
let mut cells: Vec<(Cell, f32)> = cells.into_iter().map(|c| (c, 0.)).collect();
let mut last_grass_grow = 0;
let mut last_forest_grow = 0;
let mut last_grass_extend = 0;
let mut last_forest_extend = 0;
loop {
// Receive control from the game
while let Ok(c) = rx_control.try_recv() {
let id = c.voronoi_id;
cells[id] = (c, 1.);
}
let gt = gt.load(Ordering::Acquire);
// Update step
let grass_grow = gt.saturating_sub(last_grass_grow) * WealthType::MAX as u64
/ CellKind::GRASS.growth_duration();
let forest_grow = gt.saturating_sub(last_forest_grow) * WealthType::MAX as u64
/ CellKind::FOREST.growth_duration();
if grass_grow > 0 || forest_grow > 0 {
for (c, modified) in cells.iter_mut() {
if grass_grow > 0 {
if let CellKind::Grass { ref mut wealth } = c.kind {
*wealth = wealth.saturating_add(grass_grow as WealthType);
*modified += grass_grow as f32 / WealthType::MAX as f32;
}
}
if forest_grow > 0 {
if let CellKind::Forest { ref mut wealth } = c.kind {
*wealth = wealth.saturating_add(forest_grow as WealthType);
*modified += grass_grow as f32 / WealthType::MAX as f32;
}
}
}
// Don't make multiple updates at once to avoid problems and lags
} else if gt - last_grass_extend > GRASS_EXTEND_STEP {
for i in 0..cells.len() {
if cells[i].1 < 1. {
if let Some(wealth) = cells[i].0.kind.grass_wealth() {
if rng.gen_bool(wealth_to_unit(wealth) as f64) {
let target = &mut cells
[voronoi.cell(i).iter_neighbors().choose(&mut rng).unwrap()];
if matches!(target.0.kind, CellKind::Dirt) {
target.0.kind = CellKind::Grass { wealth: 0 };
target.1 = 1.;
}
}
}
}
}
last_grass_extend += GRASS_EXTEND_STEP;
} else if gt - last_forest_extend > FOREST_EXTEND_STEP {
for i in 0..cells.len() {
if cells[i].1 < 1. {
if let CellKind::Forest { wealth } = cells[i].0.kind {
if rng.gen_bool(wealth_to_unit(wealth) as f64) {
let target = &mut cells
[voronoi.cell(i).iter_neighbors().choose(&mut rng).unwrap()];
match target.0.kind {
CellKind::Dirt => {
target.0.kind = CellKind::Forest { wealth: 0 };
target.1 = 1.;
}
CellKind::Grass { wealth: w } => {
target.0.kind = CellKind::Forest {
wealth: (w as f32
* CellKind::GRASS.growth_duration() as f32
/ CellKind::FOREST.growth_duration() as f32)
as WealthType,
};
target.1 = 1.;
}
_ => {}
}
}
}
}
}
last_forest_extend += FOREST_EXTEND_STEP;
}
let grass_count = cells
.iter()
.filter(|(c, _)| matches!(c.kind, CellKind::Grass { .. }))
.count();
last_grass_grow += grass_grow * CellKind::GRASS.growth_duration() / WealthType::MAX as u64;
last_forest_grow += forest_grow * CellKind::FOREST.growth_duration();
// Send modifications
let modification_treshold = 1. - (1. / (tx_update.len() as f32));
for (c, m) in cells.iter_mut().filter(|(_, m)| *m > modification_treshold) {
tx_update.send(c.clone()).await.unwrap();
*m = 0.;
}
}
}

View File

@ -1,82 +0,0 @@
use std::time::Duration;
use bevy::prelude::*;
use voronoice::Point;
use crate::{
map::{
animals::Animal, cells::grow::GrowThread, CellsEntities, MeshMarker, MeshNeedsUpdate,
Voronoi,
},
ui::CurrentAction,
};
use super::{Cell, CellKind, WealthType};
pub fn on_click(
trigger: Trigger<Pointer<Click>>,
mut cells: Query<(&mut Cell, &ChildOf)>,
voronoi: Res<Voronoi>,
mut chunks: Query<&mut MeshNeedsUpdate, With<MeshMarker>>,
mut cmds: Commands,
ca: Res<CurrentAction>,
grow_task: Res<GrowThread>,
cells_entities: Res<CellsEntities>,
) {
if trigger.duration > Duration::from_millis(200) || trigger.hit.position.is_none() {
return;
}
let pos = trigger.hit.position.unwrap();
let (mut cell, parent) = cells
.get_mut(
cells_entities.0[voronoi
.0
.cell(0)
.iter_path(Point {
x: pos.x as f64,
y: -pos.z as f64,
})
.last()
.unwrap()],
)
.unwrap();
// let (mut cell, parent) = cells.get_mut(trigger.target).unwrap();
// dbg!(&cell);
match *ca {
CurrentAction::ChangeCell(ck) => match ck {
CellKind::Forest { .. } => match cell.kind {
CellKind::Dirt | CellKind::Grass { .. } => {
cell.kind = CellKind::Forest {
wealth: WealthType::MAX / 2,
};
chunks.get_mut(parent.parent()).unwrap().0 += f32::MAX;
grow_task.1.send_blocking(cell.clone()).unwrap();
}
_ => {}
},
CellKind::Grass { .. } => match cell.kind {
CellKind::Dirt => {
cell.kind = CellKind::Grass {
wealth: WealthType::MAX / 2,
};
chunks.get_mut(parent.parent()).unwrap().0 += f32::MAX;
dbg!(grow_task.1.send_blocking(cell.clone()));
}
_ => {}
},
_ => {}
},
CurrentAction::AddAnimal(ak) => {
if cell.kind.can_place_animal(ak) {
let v_cell = voronoi.0.cell(cell.voronoi_id);
let cell_pos = v_cell.site_position();
cmds.spawn((
Animal { kind: ak },
Transform::from_xyz(cell_pos.x as f32, 0., -cell_pos.y as f32),
));
}
}
_ => {}
}
}

View File

@ -1,92 +0,0 @@
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
use bevy::prelude::*;
use bevy::render::render_resource::*;
use noise::utils::{NoiseMapBuilder, PlaneMapBuilder};
use noise::{Fbm, Perlin};
const SHADER_PATH: &str = "shaders/cell.wgsl";
#[derive(Asset, AsBindGroup, Clone, Debug, Reflect)]
pub struct CellMaterialExtension {}
pub type CellMaterial = ExtendedMaterial<StandardMaterial, CellMaterialExtension>;
impl MaterialExtension for CellMaterialExtension {
// fn vertex_shader() -> ShaderRef {
// SHADER_PATH.into()
// }
// fn fragment_shader() -> ShaderRef {
// SHADER_PATH.into()
// }
// fn deferred_vertex_shader() -> ShaderRef {
// info!("d_vertex_shader");
// SHADER_PATH.into()
// }
// fn deferred_fragment_shader() -> ShaderRef {
// info!("d_fragment_shader");
// SHADER_PATH.into()
// }
// fn prepass_vertex_shader() -> ShaderRef {
// info!("p_vertex_shader");
// SHADER_PATH.into()
// }
// fn prepass_fragment_shader() -> ShaderRef {
// info!("p_fragment_shader");
// SHADER_PATH.into()
// }
}
pub fn cell_material() -> CellMaterial {
CellMaterial {
base: StandardMaterial {
alpha_mode: AlphaMode::Mask(0.5),
interpolation_method: bevy::pbr::InterpolationMethod::Flat,
..Default::default()
},
extension: CellMaterialExtension {},
}
}
// #[derive(Clone, AsBindGroup, Asset, TypePath)]
// pub struct CellMaterial {}
// impl Material for CellMaterial {
// fn vertex_shader() -> ShaderRef {
// info!("vertex_shader");
// SHADER_PATH.into()
// }
// fn fragment_shader() -> ShaderRef {
// info!("fragment_shader");
// SHADER_PATH.into()
// }
// fn alpha_mode(&self) -> AlphaMode {
// AlphaMode::Mask(0.5)
// }
// fn opaque_render_method(&self) -> bevy::pbr::OpaqueRendererMethod {
// bevy::pbr::OpaqueRendererMethod::Forward
// }
// fn specialize(
// pipeline: &bevy::pbr::MaterialPipeline<Self>,
// descriptor: &mut RenderPipelineDescriptor,
// layout: &bevy::render::mesh::MeshVertexBufferLayoutRef,
// key: bevy::pbr::MaterialPipelineKey<Self>,
// ) -> Result<(), SpecializedMeshPipelineError> {
// let vertex_layout = layout.0.get_layout(&[
// Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
// Mesh::ATTRIBUTE_NORMAL.at_shader_location(1),
// Mesh::ATTRIBUTE_COLOR.at_shader_location(2),
// ])?;
// descriptor.vertex.buffers = vec![vertex_layout];
// // let fragment_layout = layout.0.get_layout(&[
// // Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
// // Mesh::ATTRIBUTE_NORMAL.at_shader_location(1),
// // Mesh::ATTRIBUTE_COLOR.at_shader_location(2),
// // ])?;
// Ok(())
// }
// }
// pub fn cell_material() -> CellMaterial {
// CellMaterial {}
// }

39
src/map/picking.rs Normal file
View File

@ -0,0 +1,39 @@
use bevy::{picking::{backend::{HitData, PointerHits}, pointer::{PointerId, PointerLocation}}, prelude::*, window::PrimaryWindow};
use voronoice::Point;
use super::{CellsEntities, MapMarker, Voronoi};
pub fn picking_backend(
cam: Query<(&Transform, Entity), With<Camera2d>>,
window: Query<&Window, With<PrimaryWindow>>,
pointers: Query<(&PointerId, &PointerLocation)>,
map: Query<(&Voronoi, &CellsEntities, &Transform), With<MapMarker>>,
mut output: EventWriter<PointerHits>
) {
let (cam, cam_id) = cam.single();
let window = window.single();
let (voronoi, cells_entities, map_pos) = map.single();
let mut last_cell = 0;
for (id, l) in pointers.iter() {
if let Some(mut pos) = l.location().map(|l| l.position) {
pos -= window.size()/2.;
pos *= cam.scale.xy();
pos.x += cam.translation.x;
pos.y -= cam.translation.y;
if let Some(c) = voronoi.0.cell(last_cell).iter_path(Point { x: pos.x as f64, y: -pos.y as f64 }).last() {
last_cell = c;
output.send(PointerHits {
pointer: *id,
picks: vec![(cells_entities.0[c], HitData {
camera: cam_id,
depth: map_pos.translation.z,
position: Some(Vec3 { x: pos.x, y: pos.y, z: map_pos.translation.z }),
normal: None
})],
order: map_pos.translation.z
});
}
}
}
}

View File

@ -1,34 +0,0 @@
use std::{
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Duration,
};
use bevy::prelude::*;
pub struct Plugin;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {
app.insert_resource(GameTime {
current: Duration::ZERO,
secs: Arc::new(AtomicU64::new(0)),
speed: 3600. * 24. * 365.,
})
.add_systems(PreUpdate, update_time);
}
}
#[derive(Resource)]
pub struct GameTime {
pub current: Duration,
pub secs: Arc<AtomicU64>,
pub speed: f32, // = game time / real time
}
fn update_time(mut gt: ResMut<GameTime>, time: Res<Time>) {
let speed = gt.speed;
gt.current += Duration::from_secs_f32(time.delta_secs() * speed);
gt.secs.store(gt.current.as_secs(), Ordering::Release);
}

243
src/ui.rs
View File

@ -1,211 +1,96 @@
use std::collections::HashMap; use std::collections::BTreeMap;
use bevy::{ use bevy::{input::mouse::MouseWheel, math::{NormedVectorSpace, VectorSpace}, picking::{focus::HoverMap, pointer::PointerId}, prelude::*, utils::HashMap, window::PrimaryWindow};
asset::embedded_asset,
input::mouse::MouseWheel,
math::NormedVectorSpace,
picking::{hover::HoverMap, pointer::PointerId},
prelude::*,
};
use mevy::*;
use crate::camera::CameraMarker; use crate::map::{self, MapMarker};
use crate::map::{AnimalKind, CellKind};
// #77767b
const TABBAR_COLOR: Color = Color::srgb(119. / 255., 118. / 255., 123. / 255.);
// #E8E8E8
const ENABLED_BUTTON_COLOR: Color = Color::srgb(232. / 255., 232. / 255., 232. / 255.);
pub struct Plugin; pub struct Plugin;
impl bevy::prelude::Plugin for Plugin { impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_resource::<CurrentAction>() app.add_systems(Startup, setup)
.add_systems(Startup, setup)
.add_systems(Update, zoom_with_scroll); .add_systems(Update, zoom_with_scroll);
embedded_asset!(app, "../assets/ui/enabled_tree.png");
embedded_asset!(app, "../assets/ui/disabled_tree.png");
embedded_asset!(app, "../assets/ui/enabled_grass.png");
embedded_asset!(app, "../assets/ui/disabled_grass.png");
embedded_asset!(app, "../assets/ui/enabled_cross.png");
embedded_asset!(app, "../assets/ui/disabled_cross.png");
embedded_asset!(app, "../assets/ui/enabled_goat.png");
embedded_asset!(app, "../assets/ui/disabled_goat.png");
} }
} }
#[derive(Resource, Default, PartialEq, Eq)] #[derive(Component)]
pub enum CurrentAction {
#[default]
None,
ChangeCell(CellKind),
AddAnimal(AnimalKind),
}
#[derive(Component, Debug)]
struct PointersDragging(HashMap<PointerId, Vec2>); struct PointersDragging(HashMap<PointerId, Vec2>);
#[derive(Component)] #[derive(Component)]
pub struct MapUIComponent; pub struct MapUIComponent;
fn setup(mut world: Commands, asset_server: Res<AssetServer>) { fn setup(
// Spawn all ui elements as children of this one mut cmds: Commands
entity! { ) {
<world> cmds.spawn((
Node {width: 100%, height: 100%, display: Display::Flex, flex_direction: FlexDirection::Column, !}; Node {
Pickable { width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
PickingBehavior {
should_block_lower: false, should_block_lower: false,
is_hoverable: true is_hoverable: true
}; },
.observe(|trigger: Trigger<Pointer<DragEnd>>, mut ptrs: Query<&mut PointersDragging>| { MapUIComponent,
if trigger.button == PointerButton::Primary { PointersDragging(HashMap::new())
ptrs.single_mut().unwrap().0.remove(&trigger.pointer_id); )).observe(|trigger: Trigger<Pointer<DragStart>>, mut ptrs: Query<&mut PointersDragging>| {
} let event = trigger.event();
}); // dbg!(event);
.observe(| if event.button == PointerButton::Primary {
trigger: Trigger<Pointer<Drag>>, ptrs.get_mut(event.target).unwrap().0.insert(event.pointer_id, event.pointer_location.position);
mut ptrs: Query<&mut PointersDragging>, }
mut cam: Query<&mut Transform, With<CameraMarker>>, }).observe(|trigger: Trigger<Pointer<DragEnd>>, mut ptrs: Query<&mut PointersDragging>| {
| { let event = trigger.event();
if trigger.button == PointerButton::Primary { // dbg!(event);
let mut ptrs = ptrs.single_mut().unwrap(); if event.button == PointerButton::Primary {
if !ptrs.0.contains_key(&trigger.pointer_id) { ptrs.get_mut(event.target).unwrap().0.remove(&event.pointer_id);
return }
} }).observe(|
let mut cam = cam.single_mut().unwrap(); trigger: Trigger<Pointer<Drag>>,
mut ptrs: Query<&mut PointersDragging>,
mut cam: Query<&mut Transform, With<Camera2d>>,
| {
let event = trigger.event();
// dbg!(event);
if event.button == PointerButton::Primary {
let mut cam = cam.single_mut();
let mut ptrs = ptrs.get_mut(event.target).unwrap();
let old_midpoint = ptrs.0.values().fold(Vec2::ZERO, |acc, pos| acc + (pos/ptrs.0.len() as f32)); let old_midpoint = ptrs.0.values().fold(Vec2::ZERO, |acc, pos| acc + (pos/ptrs.0.len() as f32));
let old_d_to_midpoint = ptrs.0.values().fold(0., |acc, pos| acc + (old_midpoint-pos).norm()); let old_d_to_midpoint = ptrs.0.values().fold(0., |acc, pos| acc + (old_midpoint-pos).norm());
ptrs.0.insert(trigger.pointer_id, trigger.pointer_location.position); ptrs.0.insert(event.pointer_id, event.pointer_location.position);
let new_midpoint = ptrs.0.values().fold(Vec2::ZERO, |acc, pos| acc + (pos/ptrs.0.len() as f32)); let new_midpoint = ptrs.0.values().fold(Vec2::ZERO, |acc, pos| acc + (pos/ptrs.0.len() as f32));
let new_d_to_midpoint = ptrs.0.values().fold(0., |acc, pos| acc + (new_midpoint-pos).norm()); let new_d_to_midpoint = ptrs.0.values().fold(0., |acc, pos| acc + (new_midpoint-pos).norm());
// move camera // move camera
cam.translation.x -= (new_midpoint.x - old_midpoint.x)*cam.translation.z*0.001; cam.translation.x -= (new_midpoint.x - old_midpoint.x)*cam.scale.x;
cam.translation.z -= (new_midpoint.y - old_midpoint.y)*cam.translation.z*0.001; cam.translation.y += (new_midpoint.y - old_midpoint.y)*cam.scale.y;
if ptrs.0.len() > 1 { if ptrs.0.len() > 1 {
let forward = cam.forward(); cam.scale *= old_d_to_midpoint/new_d_to_midpoint;
let z = cam.translation.z;
cam.translation += forward * (z * (1. - (new_d_to_midpoint/old_d_to_midpoint)) / forward.z);
// cam.scale *= old_d_to_midpoint/new_d_to_midpoint;
}
} }
} }
); }
[map][ );
Node{ // Spawn all ui elements as children of this one
flex_grow: 1.,
!};
PointersDragging(HashMap::new());
MapUIComponent;
Pickable {
should_block_lower: false,
is_hoverable: true
};
.observe(|trigger: Trigger<Pointer<DragStart>>, mut ptrs: Query<&mut PointersDragging>| {
if trigger.button == PointerButton::Primary {
if let Ok(mut ptrs) = ptrs.get_mut(trigger.target()) {
ptrs.0.insert(trigger.pointer_id, trigger.pointer_location.position);
}
}
});
]
[
Node{
width: 100%,
height: 10vh,
align_self: AlignSelf::FlexEnd,
!};
Button;
BackgroundColor(TABBAR_COLOR);
[forest][
ImageNode::new(asset_server.load("embedded://forestiles/../assets/ui/enabled_tree.png"));
Node {
// height: 80%,
// margin: [>1vh],
!};
BackgroundColor(TABBAR_COLOR);
.observe(move |trigger: Trigger<Pointer<Click>>, mut ca: ResMut<CurrentAction>, mut bg: Query<&mut BackgroundColor>| {
if trigger.button == PointerButton::Primary {
if matches!(*ca, CurrentAction::ChangeCell(CellKind::Forest {..})) {
*ca = CurrentAction::None;
bg.get_mut(forest).unwrap().0 = TABBAR_COLOR;
} else {
*ca = CurrentAction::ChangeCell(CellKind::Forest {wealth: 0});
bg.get_mut(forest).unwrap().0 = ENABLED_BUTTON_COLOR;
}
bg.get_mut(grass).unwrap().0 = TABBAR_COLOR;
bg.get_mut(goat).unwrap().0 = TABBAR_COLOR;
}
});
]
[grass][
ImageNode::new(asset_server.load("embedded://forestiles/../assets/ui/enabled_grass.png"));
Node {
// height: 80%,
// margin: [>1vh],
!};
BackgroundColor(TABBAR_COLOR);
.observe(move |trigger: Trigger<Pointer<Click>>, mut ca: ResMut<CurrentAction>, mut bg: Query<&mut BackgroundColor>| {
if trigger.button == PointerButton::Primary {
if matches!(*ca, CurrentAction::ChangeCell(CellKind::Grass {..})) {
*ca = CurrentAction::None;
bg.get_mut(grass).unwrap().0 = TABBAR_COLOR;
} else {
*ca = CurrentAction::ChangeCell(CellKind::Grass {wealth: 0});
bg.get_mut(grass).unwrap().0 = ENABLED_BUTTON_COLOR;
}
bg.get_mut(forest).unwrap().0 = TABBAR_COLOR;
bg.get_mut(goat).unwrap().0 = TABBAR_COLOR;
}
});
]
[goat][
ImageNode::new(asset_server.load("embedded://forestiles/../assets/ui/enabled_goat.png"));
Node {
// height: 80%,
// margin: [>1vh],
!};
BackgroundColor(TABBAR_COLOR);
.observe(move |trigger: Trigger<Pointer<Click>>, mut ca: ResMut<CurrentAction>, mut bg: Query<&mut BackgroundColor>| {
if trigger.button == PointerButton::Primary {
if *ca == CurrentAction::AddAnimal(AnimalKind::Goat) {
*ca = CurrentAction::None;
bg.get_mut(goat).unwrap().0 = TABBAR_COLOR;
} else {
*ca = CurrentAction::AddAnimal(AnimalKind::Goat);
bg.get_mut(goat).unwrap().0 = ENABLED_BUTTON_COLOR;
}
bg.get_mut(forest).unwrap().0 = TABBAR_COLOR;
bg.get_mut(grass).unwrap().0 = TABBAR_COLOR;
}
});
]
]
}
} }
fn zoom_with_scroll( fn zoom_with_scroll(
mut cam: Query<&mut Transform, With<CameraMarker>>, mut cam: Query<&mut Transform, With<Camera2d>>,
mut ev_scroll: EventReader<MouseWheel>, mut ev_scroll: EventReader<MouseWheel>,
hover_map: Res<HoverMap>, hover_map: Res<HoverMap>,
map_ui_id: Query<Entity, With<MapUIComponent>>, window: Query<&Window, With<PrimaryWindow>>,
map_ui_id: Query<Entity, With<MapUIComponent>>
) { ) {
let map_ui_id = map_ui_id.single().unwrap(); let map_ui_id = map_ui_id.single();
if hover_map if hover_map.get(&PointerId::Mouse).and_then(|hovered_ids| hovered_ids.get(&map_ui_id)).is_some() {
.get(&PointerId::Mouse) let window = window.single();
.and_then(|hovered_ids| hovered_ids.get(&map_ui_id)) let mut cam = cam.single_mut();
.is_some()
{
let mut cam = cam.single_mut().unwrap();
for ev in ev_scroll.read() { for ev in ev_scroll.read() {
let forward = cam.forward(); let scale = (cam.scale.x-(ev.y*0.1/window.width().min(window.height()))).clamp(0.0001, 2./window.width().min(window.height()));
cam.translation += forward * (ev.y * 0.1); cam.scale = Vec3::new(scale, scale, scale);
// cam.fov = cam.fov + (ev.y * 0.1);
// let scale = (cam.scale.x - (ev.y * 0.1 / window.width().min(window.height())))
// .clamp(0.0001, 2. / window.width().min(window.height()));
} }
} }
} }