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]
name = "forestiles"
version = "0.6.0"
version = "0.2.0"
edition = "2021"
[lib]
name = "forestiles"
path = "src/lib.rs"
crate-type = ["cdylib"]
crate-type=[
"staticlib",
"cdylib",
"rlib"
]
[[bin]]
path = "src/lib.rs"
name = "forestiles"
[features]
debug = ["dep:bevy_editor_pls"]
[dependencies]
# git = "https://git.arkitu.fr/forestia/bevy.git"
bevy = { git = "https://git.arkitu.fr/forestia/bevy.git", default-features = false, features = [
bevy = { version = "0.15", default-features = false, features = [
"android-native-activity",
"android_shared_stdcxx",
"bevy_color",
@ -27,7 +27,6 @@ bevy = { git = "https://git.arkitu.fr/forestia/bevy.git", default-features = fal
"bevy_text",
"bevy_ui",
"bevy_ui_picking_backend",
"bevy_mesh_picking_backend",
"bevy_window",
"bevy_winit",
"default_font",
@ -35,40 +34,29 @@ bevy = { git = "https://git.arkitu.fr/forestia/bevy.git", default-features = fal
"sysinfo_plugin",
"webgl2",
"wayland",
"png",
"tonemapping_luts",
"bevy_pbr",
"bevy_gltf",
"animation",
"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"] }
]}
bevy-inspector-egui = { version = "0.28", default-features = false, features = [
"bevy_image",
"bevy_render",
"egui_open_url"
]}
log = "0.4"
rand = { version = "0.9", features = ["small_rng"] }
rand = { version = "0.8", features = ["small_rng"] }
voronoice = "0.2"
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]
# console_error_panic_hook = "0.1.6"
# console_log = "1.0"
# wgpu = { version = "22", features = ["webgl"] }
# wasm-bindgen = "0.2"
# wasm-bindgen-futures = "0.4"
# web-sys = { version = "0.3", features = ["Document", "Window", "Element"] }
# getrandom = { version = "*", features = ["js"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
console_log = "1.0"
wgpu = { version = "22", features = ["webgl"]}
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = [
"Document",
"Window",
"Element",
]}
getrandom ={ version = "*", features = ["js"]}
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.14"
@ -77,12 +65,15 @@ android_logger = "0.14"
[profile.dev]
opt-level = 1
[profile.release]
debug = true
# Enable a large amount of optimization in the dev profile for dependencies.
# [profile.dev.package."*"]
# opt-level = 1
[profile.dev.package."*"]
opt-level = 3
[package.metadata.android]
package = "org.forestiles.game"
package = "org.forestiles.example"
apk_name = "forestiles"
strip = "strip"
# 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;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, setup);
// .add_systems(Update, move_cam)
// .init_resource::<Pointers>();
}
}
#[derive(Component)]
pub struct CameraMarker;
fn setup(mut cmds: Commands) {
fn setup(mut cmds: Commands, window: Query<&Window>) {
let zoom = 2./window.single().width().min(window.single().height());
cmds.spawn((
CameraMarker,
Camera3d {
..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),
Camera2d,
Transform::from_scale(Vec3::new(zoom, zoom, zoom))
));
}
#[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::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
platform::collections::HashSet,
prelude::*,
};
use bevy::{prelude::*, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}};
use bevy_inspector_egui::quick::WorldInspectorPlugin;
pub mod assets;
pub mod camera;
pub mod map;
pub mod time;
pub mod camera;
pub mod ui;
#[bevy_main]
pub fn main() {
App::new()
.add_plugins((
DefaultPlugins, // .set(bevy::render::RenderPlugin {
// render_creation: bevy::render::settings::RenderCreation::Automatic(
// 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
DefaultPlugins,
// WorldInspectorPlugin::default(),
camera::Plugin,
map::Plugin,
ui::Plugin,
time::Plugin,
assets::Plugin,
FrameTimeDiagnosticsPlugin::default(),
FrameTimeDiagnosticsPlugin,
LogDiagnosticsPlugin {
filter: Some(vec![FrameTimeDiagnosticsPlugin::FPS]),
// filter: Some({
// let mut set = HashSet::new();
// set.insert(FrameTimeDiagnosticsPlugin::FPS);
// set
// }),
..Default::default()
},
}
))
.run();
}

View File

@ -1,305 +1,209 @@
use bevy::{
asset::{embedded_asset, RenderAssetUsages},
picking::PickSet,
prelude::*,
render::mesh::{Indices, PrimitiveTopology},
};
use std::time::Duration;
use bevy::{asset::RenderAssetUsages, picking::PickSet, prelude::*, render::mesh::{Indices, PrimitiveTopology}, utils::HashMap};
use noise::{Fbm, MultiFractal, NoiseFn, Perlin};
use rand::{thread_rng, Rng, SeedableRng};
use voronoice::{BoundingBox, Point, VoronoiBuilder};
mod animals;
mod cells;
pub use animals::AnimalKind;
pub use cells::CellKind;
mod picking;
use picking::*;
use cells::*;
use crate::{
map::cells::{
grow::GrowThread,
material::{cell_material, CellMaterial},
},
time::GameTime,
};
pub struct Plugin;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {
app.add_plugins((cells::Plugin, MeshPickingPlugin))
.add_systems(Startup, setup)
.add_systems(Update, update_chunk_mesh)
app.add_systems(Startup, setup)
.insert_resource(Time::<Fixed>::from_seconds(0.25)) // Time for a day
.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(Seed(thread_rng().gen()));
}
}
/// Determined empirically
pub const AVERAGE_NEIGHBORS_NUMBER: f64 = 6.;
pub const HEIGHT: f64 = 4.;
pub const WIDTH: f64 = 4.;
pub const REAL_HEIGHT: f32 = 8000.;
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;
pub const HEIGHT: f32 = 2.;
pub const WIDTH: f32 = 2.;
pub const REAL_HEIGHT: f32 = 500.;
pub const REAL_WIDTH: f32 = 500.;
pub const CELL_AREA: f32 = REAL_HEIGHT * REAL_WIDTH / SIZE as f32;
pub const SIZE: usize = 10000;
#[derive(Resource)]
struct Seed(u32);
#[derive(Resource)]
pub struct Voronoi(voronoice::Voronoi);
#[derive(Component)]
pub struct Voronoi (voronoice::Voronoi);
#[derive(Component)]
pub struct MeshMarker;
pub struct MapMarker;
#[derive(Component)]
struct MeshColors(Vec<[f32; 4]>);
#[derive(Resource)]
pub struct CellsEntities(Vec<Entity>);
struct MapColors (Vec<[f32; 4]>);
#[derive(Component)]
pub struct MeshNeedsUpdate(f32);
pub struct CellsEntities (Vec<Entity>);
#[derive(Component)]
pub struct Chunk(usize);
pub struct MeshNeedsUpdate(bool);
fn setup(
mut cmds: Commands,
mut meshes: ResMut<Assets<Mesh>>,
// mut materials: ResMut<Assets<StandardMaterial>>,
mut materials: ResMut<Assets<CellMaterial>>,
asset_server: Res<AssetServer>,
seed: Res<Seed>,
gt: Res<GameTime>,
mut materials: ResMut<Assets<ColorMaterial>>,
seed: Res<Seed>
) {
// 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 sites = Vec::with_capacity(CELLS);
for chunk_x in 0..CHUNKS_RESOLUTION {
let min_x = WIDTH / 2. * ((2. * (chunk_x as f64) / (CHUNKS_RESOLUTION as f64)) - 1.);
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),
});
let mut sites = Vec::with_capacity(SIZE);
for _ in 0..SIZE {
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 })
}
}
}
// dbg!(sites.len());
let voronoi = VoronoiBuilder::default()
.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)
.build()
.unwrap();
let mut cells = Vec::with_capacity(CELLS);
let z_noise = Fbm::<Perlin>::new(seed.0).set_octaves(4).set_lacunarity(3.);
let moisture_noise = Fbm::<Perlin>::new(seed.0 + 1).set_frequency(2.);
for i in 0..CELLS {
let mut cells_data = Vec::with_capacity(SIZE);
let z_noise = Fbm::<Perlin>::new(seed.0);
let moisture_noise = Fbm::<Perlin>::new(seed.0+1)
.set_frequency(2.);
for i in 0..SIZE {
let c = voronoi.cell(i);
let site = c.site_position();
let z = get_altitude(&z_noise, &[site.x as f32, site.y as f32]);
let _m = (
(moisture_noise.get([site.x, site.y]) + 1.) / 2.
// Noise + [0; 1]
)
.clamp(0., 1.) as f32;
let k = if z <= 0. {
let z = (
0.3 // Arbitrary value
+ ((z_noise.get([site.x, site.y])+1.)/2.) // Noise + [0; 1]
- ((site.x.powi(2)+site.y.powi(2)).sqrt()*0.5) // Distance - [0; sqrt(2)] * 0.5
).clamp(0., 1.);
let m = (
(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
} else if z <= 0.04 {
} else if z <= 0.52 {
CellKind::Beach
} else if z < 0.6 {
} else if z < 0.8 {
CellKind::Dirt
} else {
CellKind::Stone
};
cells.push(Cell {
kind: k,
voronoi_id: i,
});
cells_data.push(CellData::new(k, i, (z*255.) as u8, (m*255.) as u8, 0, vec![]));
}
// 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| {
ch.iter()
.map(|pos| {
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());
let mut poss = Vec::new();
let mut colors = Vec::new();
let mut indices = Vec::new();
// for (i, pos) in voronoi.sites().iter().enumerate() {
// let z = get_altitude(&z_noise, &[pos.x as f32, pos.y as f32]);
// poss.push(Vec3::new(pos.x as f32, pos.y as f32, z));
// cells[i].vertices.push(i);
for (c, cd) in voronoi.iter_cells().zip(cells_data.iter_mut()).filter(|(_,cd)| cd.kind != CellKind::Forest) {
let color = cd.color();
// if c.site() == selected_tile {
// color[0] = (color[0]+0.4).clamp(0., 1.);
// color[1] = (color[1]+0.4).clamp(0., 1.);
// color[2] = (color[2]+0.4).clamp(0., 1.);
// }
// let mut hybrid_cells: HashMap<usize, usize> = HashMap::new();
for t in voronoi.triangulation().triangles.chunks_exact(3) {
let on_hull = t
.iter()
.filter(|v| voronoi.triangulation().hull.contains(v))
.count();
// Don't draw triangles on the hull that are often long and look glitchy
if on_hull > 0 {
continue;
}
let mut chs = t.iter().map(|c| Cell::chunk_from_id(*c));
let chs: [usize; 3] = std::array::from_fn(|_| chs.next().unwrap());
for ch in 0..CHUNKS {
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 vs = c.iter_vertices().collect::<Vec<_>>();
let i = poss.len();
for v in vs.iter() {
poss.push(Vec3::new(v.x as f32, v.y as f32, 0.));// [v.x as f32, v.y as f32, 0.]);
// poss.push(Vertex::new_col([v.x as f32, v.y as f32], color, 1));
colors.push(color);
}
for v in 1..(vs.len()-1) {
indices.extend_from_slice(&[i as u32, (i+v) as u32, (i+v+1) as u32]);
cd.vertices.extend_from_slice(&[i, i+v, i+v+1]);
}
}
let mut cells_entities = Vec::with_capacity(cells.len());
for (ch_id, ((poss, indices), ch_cells)) in poss
.into_iter()
.zip(indices.into_iter())
.zip(cells.chunks_exact(CELLS_PER_CHUNK))
.enumerate()
{
let mut cmd_mesh = cmds.spawn_empty();
cmd_mesh.with_children(|parent| {
for cell in ch_cells {
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(),
let mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default())
// Add 4 vertices, each with its own position attribute (coordinate in
// 3D space), for each of the corners of the parallelogram.
.with_inserted_attribute(
Mesh::ATTRIBUTE_POSITION,
poss
)
.with_inserted_attribute(
Mesh::ATTRIBUTE_COLOR,
colors.clone()
)
.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));
mesh.compute_smooth_normals();
cmd_mesh
.insert((
Mesh3d(meshes.add(mesh)),
// StandardMaterial
// MeshMaterial3d(materials.add(cells::pbr_material::StandardMaterial::default())),
MeshMaterial3d(materials.add(cell_material())),
let mut cells_entities = Vec::with_capacity(cells_data.len());
cmds.spawn((
Mesh2d(meshes.add(mesh)),
MeshMaterial2d(materials.add(ColorMaterial::default())),
Transform::default(),
MeshColors(colors),
MeshNeedsUpdate(f32::MAX),
MeshMarker,
Chunk(ch_id),
))
.observe(self::cells::input::on_click);
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));
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
fn update_chunk_mesh(
cells: Query<&Cell>,
mut chunks: Query<
(&Mesh3d, &mut MeshColors, &mut MeshNeedsUpdate, &Children),
With<MeshMarker>,
>,
mut meshes: ResMut<Assets<Mesh>>,
#[derive(Component)]
pub struct LastUpdate(usize);
fn update_cells(
mut cells: Query<(&mut CellData, &mut LastUpdate)>,
mut map_needs_update: Query<&mut MeshNeedsUpdate, With<MapMarker>>
) {
for (mesh, mut cols, mut needs_update, children) in chunks
.iter_mut()
.sort_unstable_by::<&MeshNeedsUpdate>(|nu1, nu2| nu2.0.total_cmp(&nu1.0))
{
if needs_update.0 > 0. {
let mut map_needs_update = map_needs_update.single_mut();
for (mut cd, mut lu) in cells.iter_mut() {
lu.0 += 1;
if lu.0 > match cd.kind {
CellKind::Void | CellKind::Sea | CellKind::Beach | CellKind::Dirt | CellKind::Stone => usize::MAX,
CellKind::Forest => 100*365/4, // Let's say that a forest takes 100 years to mature
CellKind::Grass => 7*7/4 // Let's say that grass takes 7 weaks to reach its max
} {
lu.0 = 0;
cd.resource = (cd.resource + 1).clamp(0, 4);
map_needs_update.0 = true;
}
}
}
fn update_map_mesh(
cells: Query<&CellData>,
mut map: Query<(&Mesh2d, &mut MapColors, &mut MeshNeedsUpdate), With<MapMarker>>,
mut meshes: ResMut<Assets<Mesh>>
) {
let (mesh, mut cols, mut needs_update) = map.single_mut();
if needs_update.0 {
if let Some(mesh) = meshes.get_mut(mesh) {
// let mut modified = false;
// let mut rng = thread_rng();
for child in children.iter() {
let cell = cells.get(child).unwrap();
// let col: [f32; 4] = [rng.gen(), rng.gen(), rng.gen(), 1.];
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();
// }
let mut modified = false;
for cd in cells.iter() {
let col = cd.color();
for id in cd.vertices.iter() {
modified = modified || cols.0[*id] != col;
cols.0[*id] = col.clone();
}
// if modified {
}
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;
}
needs_update.0 = false;
}
}
/// Between 0 and 1
fn get_altitude(z_noise: &Fbm<Perlin>, pos: &[f32; 2]) -> f32 {
let z_noise = ((z_noise.get([pos[0] as f64, pos[1] as f64]) as f32) + 1.) / 2.; // Noise [0; 1]
-0.2// Arbitrary value
+ (z_noise.exp() / 1f32.exp())
// Noise + [0; 1]
// - ((pos[0].powi(2)+pos[1].powi(2)).sqrt()*0.3)
// Distance - [0; sqrt(2)] * 0.5
// .clamp(-1., 1.)
}
// 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 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)]
pub enum CellKind {
Void,
Sea,
Beach,
Forest { wealth: WealthType },
Forest,
Dirt,
Stone,
Grass { wealth: WealthType },
}
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.
}
}
}
}
Grass
}
#[derive(Debug, Component, Clone)]
pub struct Cell {
#[derive(Debug, Component)]
pub struct CellData {
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 {
pub const fn color(&self) -> [f32; 4] {
impl CellData {
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 {
CellKind::Void => [0.; 4],
CellKind::Sea => [0., 0., 1., 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::Dirt => [0.53, 0.38, 0.29, 1.],
CellKind::Forest => [0., 0.5 - (self.resource as f32/4.*0.4), 0., 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::Grass { wealth } => [
(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.,
],
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.]
}
}
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);
}

219
src/ui.rs
View File

@ -1,211 +1,96 @@
use std::collections::HashMap;
use std::collections::BTreeMap;
use bevy::{
asset::embedded_asset,
input::mouse::MouseWheel,
math::NormedVectorSpace,
picking::{hover::HoverMap, pointer::PointerId},
prelude::*,
};
use mevy::*;
use bevy::{input::mouse::MouseWheel, math::{NormedVectorSpace, VectorSpace}, picking::{focus::HoverMap, pointer::PointerId}, prelude::*, utils::HashMap, window::PrimaryWindow};
use crate::camera::CameraMarker;
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.);
use crate::map::{self, MapMarker};
pub struct Plugin;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {
app.init_resource::<CurrentAction>()
.add_systems(Startup, setup)
app.add_systems(Startup, setup)
.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)]
pub enum CurrentAction {
#[default]
None,
ChangeCell(CellKind),
AddAnimal(AnimalKind),
}
#[derive(Component, Debug)]
#[derive(Component)]
struct PointersDragging(HashMap<PointerId, Vec2>);
#[derive(Component)]
pub struct MapUIComponent;
fn setup(mut world: Commands, asset_server: Res<AssetServer>) {
// Spawn all ui elements as children of this one
entity! {
<world>
Node {width: 100%, height: 100%, display: Display::Flex, flex_direction: FlexDirection::Column, !};
Pickable {
fn setup(
mut cmds: Commands
) {
cmds.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
PickingBehavior {
should_block_lower: false,
is_hoverable: true
};
.observe(|trigger: Trigger<Pointer<DragEnd>>, mut ptrs: Query<&mut PointersDragging>| {
if trigger.button == PointerButton::Primary {
ptrs.single_mut().unwrap().0.remove(&trigger.pointer_id);
},
MapUIComponent,
PointersDragging(HashMap::new())
)).observe(|trigger: Trigger<Pointer<DragStart>>, mut ptrs: Query<&mut PointersDragging>| {
let event = trigger.event();
// dbg!(event);
if event.button == PointerButton::Primary {
ptrs.get_mut(event.target).unwrap().0.insert(event.pointer_id, event.pointer_location.position);
}
});
.observe(|
}).observe(|trigger: Trigger<Pointer<DragEnd>>, mut ptrs: Query<&mut PointersDragging>| {
let event = trigger.event();
// dbg!(event);
if event.button == PointerButton::Primary {
ptrs.get_mut(event.target).unwrap().0.remove(&event.pointer_id);
}
}).observe(|
trigger: Trigger<Pointer<Drag>>,
mut ptrs: Query<&mut PointersDragging>,
mut cam: Query<&mut Transform, With<CameraMarker>>,
mut cam: Query<&mut Transform, With<Camera2d>>,
| {
if trigger.button == PointerButton::Primary {
let mut ptrs = ptrs.single_mut().unwrap();
if !ptrs.0.contains_key(&trigger.pointer_id) {
return
}
let mut cam = cam.single_mut().unwrap();
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_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_d_to_midpoint = ptrs.0.values().fold(0., |acc, pos| acc + (new_midpoint-pos).norm());
// move camera
cam.translation.x -= (new_midpoint.x - old_midpoint.x)*cam.translation.z*0.001;
cam.translation.z -= (new_midpoint.y - old_midpoint.y)*cam.translation.z*0.001;
cam.translation.x -= (new_midpoint.x - old_midpoint.x)*cam.scale.x;
cam.translation.y += (new_midpoint.y - old_midpoint.y)*cam.scale.y;
if ptrs.0.len() > 1 {
let forward = cam.forward();
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;
cam.scale *= old_d_to_midpoint/new_d_to_midpoint;
}
}
}
);
[map][
Node{
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;
}
});
]
]
}
// Spawn all ui elements as children of this one
}
fn zoom_with_scroll(
mut cam: Query<&mut Transform, With<CameraMarker>>,
mut cam: Query<&mut Transform, With<Camera2d>>,
mut ev_scroll: EventReader<MouseWheel>,
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();
if hover_map
.get(&PointerId::Mouse)
.and_then(|hovered_ids| hovered_ids.get(&map_ui_id))
.is_some()
{
let mut cam = cam.single_mut().unwrap();
let map_ui_id = map_ui_id.single();
if hover_map.get(&PointerId::Mouse).and_then(|hovered_ids| hovered_ids.get(&map_ui_id)).is_some() {
let window = window.single();
let mut cam = cam.single_mut();
for ev in ev_scroll.read() {
let forward = cam.forward();
cam.translation += forward * (ev.y * 0.1);
// 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()));
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);
}
}
}