Compare commits
No commits in common. "master" and "0.2.0" have entirely different histories.
4669
Cargo.lock
generated
73
Cargo.toml
@ -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
|
||||
|
@ -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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -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;
|
||||
}
|
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.7 KiB |
@ -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);
|
||||
}
|
||||
}
|
102
src/camera.rs
@ -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));},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
38
src/lib.rs
@ -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();
|
||||
}
|
370
src/map.rs
@ -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()
|
||||
|
@ -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,
|
||||
}
|
136
src/map/cells.rs
@ -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
|
||||
}
|
@ -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.;
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
@ -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
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
34
src/time.rs
@ -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
@ -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);
|
||||
}
|
||||
}
|
||||
}
|