Compare commits

...

34 Commits

Author SHA1 Message Date
Arkitu
a8a4fe5e95 change version number 2025-07-23 18:40:39 +02:00
Arkitu
de0e80c2dc remove dbg 2025-07-23 18:39:31 +02:00
Arkitu
5f427cb446 real trees 2025-07-23 18:34:34 +02:00
Arkitu
58ca713003 change version 2025-07-20 12:20:25 +02:00
Arkitu
e80e585cf5 remove some debug logs (PS: the graphical bug in the precedent commit is probably my computer's fault, not the game's) 2025-07-20 12:08:36 +02:00
Arkitu
c5973b2204 works (kinda, some graphical bugs) 2025-07-20 12:00:43 +02:00
Arkitu
b4ca8bb032 cleaning 2025-06-27 16:14:55 +02:00
Arkitu
6c74ed9272 grow thread + communications (not tested) 2025-06-25 19:32:12 +02:00
Arkitu
1a68ab5af1 save 2025-06-22 21:40:19 +02:00
Arkitu
0699c5da2e working chunks 2025-06-14 23:05:38 +02:00
Arkitu
ab7756c3bc chunks working (=not crashing) but with triangulation problems 2025-06-13 11:26:57 +02:00
Arkitu
eb3742e21d chunks generation (not tested) 2025-06-12 21:52:30 +02:00
Arkitu
00f75f2de6 fixed update for regen and expand 2025-06-12 15:07:16 +02:00
Arkitu
22f59b059e better expansion (optimization) 2025-06-11 21:16:18 +02:00
Arkitu
a181cbf7ad clean map 2025-06-10 22:08:52 +02:00
Arkitu
9b4d9d74fc working voronoi representation using custom flat interpolation in
material shader
2025-06-10 19:16:42 +02:00
Arkitu
f46536ecc8 working zoom on mobile 2025-05-25 09:42:51 +02:00
Arkitu
2552d0dcdd save 2025-05-25 08:41:05 +02:00
Arkitu
4a401182fb camera movement with drag + scroll zoom 2025-05-23 22:18:11 +02:00
Arkitu
c6e8a3e232 3d map + light 2025-05-14 21:44:15 +02:00
Arkitu
52dde29d13 3d view 2025-05-13 22:02:40 +02:00
Arkitu
d210af93f8 fmt + move cell::on_click into its own function 2025-05-12 15:15:54 +02:00
Arkitu
dbb17f0c48 animal 2025-03-16 21:12:15 +01:00
Arkitu
74708f8b7b add goat button 2025-03-01 16:15:51 +01:00
Arkitu
24436a0bf4 update version number 2025-03-01 15:18:44 +01:00
Arkitu
3f5d4cc9aa select buttons 2025-02-28 22:56:30 +01:00
Arkitu
ec12ed122b add new buttons (grass + cross) 2025-02-20 19:30:25 +01:00
Arkitu
a9e711f3b9 forest button (functional) 2025-01-23 21:41:32 +01:00
Arkitu
7bd31de293 tree button (not yet clickable) 2025-01-23 19:42:39 +01:00
Arkitu
b448c40e54 start building ui + optimize build time for debug 2025-01-22 21:17:14 +01:00
Arkitu
de45368463 change version number 2025-01-20 19:54:48 +01:00
Arkitu
6d7567d0ae expansion + better grass colors 2025-01-20 19:53:48 +01:00
Arkitu
533cb2fae4 reorganize cells, add custom game time, switch click detection to 200ms 2025-01-19 22:40:19 +01:00
Arkitu
97eb82e009 lower size by desabling debug infos 2025-01-17 23:23:25 +01:00
26 changed files with 4810 additions and 1879 deletions

4781
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

BIN
assets/models/scene.bin Normal file

Binary file not shown.

BIN
assets/models/tree.glb Normal file

Binary file not shown.

374
assets/models/tree.gltf Normal file
View File

@ -0,0 +1,374 @@
{
"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
]
}
]
}

72
assets/shaders/cell.wgsl Normal file
View File

@ -0,0 +1,72 @@
#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.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/ui/disabled_goat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/ui/disabled_tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/ui/enabled_cross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/ui/enabled_goat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/ui/enabled_grass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
assets/ui/enabled_tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

21
src/assets.rs Normal file
View File

@ -0,0 +1,21 @@
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,92 +1,32 @@
use std::time::Duration; use bevy::prelude::*;
use bevy::{input::mouse::MouseWheel, math::NormedVectorSpace, picking::{focus::HoverMap, pointer::PointerId}, prelude::*, utils::HashMap, window::PrimaryWindow};
use crate::ui;
pub struct Plugin; pub struct Plugin;
impl bevy::prelude::Plugin for Plugin { impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(Startup, setup); app.add_systems(Startup, setup);
// .add_systems(Update, move_cam)
// .init_resource::<Pointers>();
} }
} }
fn setup(mut cmds: Commands, window: Query<&Window>) { #[derive(Component)]
let zoom = 2./window.single().width().min(window.single().height()); pub struct CameraMarker;
fn setup(mut cmds: Commands) {
cmds.spawn(( cmds.spawn((
Camera2d, CameraMarker,
Transform::from_scale(Vec3::new(zoom, zoom, zoom)) 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),
)); ));
} }
#[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,24 +1,48 @@
use bevy::{prelude::*, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}}; #![feature(iter_array_chunks)]
use bevy_inspector_egui::quick::WorldInspectorPlugin; use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
platform::collections::HashSet,
prelude::*,
};
pub mod map; pub mod assets;
pub mod camera; pub mod camera;
pub mod map;
pub mod time;
pub mod ui; pub mod ui;
#[bevy_main] #[bevy_main]
pub fn main() { pub fn main() {
App::new() App::new()
.add_plugins(( .add_plugins((
DefaultPlugins, DefaultPlugins, // .set(bevy::render::RenderPlugin {
// WorldInspectorPlugin::default(), // 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
camera::Plugin, camera::Plugin,
map::Plugin, map::Plugin,
ui::Plugin, ui::Plugin,
FrameTimeDiagnosticsPlugin, time::Plugin,
assets::Plugin,
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin { LogDiagnosticsPlugin {
filter: Some(vec![FrameTimeDiagnosticsPlugin::FPS]), filter: Some(vec![FrameTimeDiagnosticsPlugin::FPS]),
// filter: Some({
// let mut set = HashSet::new();
// set.insert(FrameTimeDiagnosticsPlugin::FPS);
// set
// }),
..Default::default() ..Default::default()
} },
)) ))
.run(); .run();
} }

View File

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

17
src/map/animals.rs Normal file
View File

@ -0,0 +1,17 @@
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,45 +1,129 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::{time::GameTime, ui::CurrentAction};
use super::{AnimalKind, CELLS_PER_CHUNK, CELL_AREA};
pub mod grow;
pub mod input;
pub mod material;
use material::CellMaterial;
pub struct Plugin;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {
app.add_plugins(MaterialPlugin::<CellMaterial> {
prepass_enabled: false,
shadows_enabled: false,
..Default::default()
})
.add_plugins(grow::Plugin);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellKind { pub enum CellKind {
Void,
Sea, Sea,
Beach, Beach,
Forest, Forest { wealth: WealthType },
Dirt, Dirt,
Stone, Stone,
Grass 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.
}
}
}
}
} }
#[derive(Debug, Component)] #[derive(Debug, Component, Clone)]
pub struct CellData { pub struct Cell {
pub kind: CellKind, pub kind: CellKind,
pub cid: usize, pub voronoi_id: usize,
z: u8,
pub moisture: u8,
pub resource: u8, // How much resource there is (between 0 and 4)
pub vertices: Vec<usize>
} }
impl CellData { impl Cell {
pub fn new(kind: CellKind, cell: usize, z: u8, moisture: u8, resource: u8, vertices: Vec<usize>) -> Self { pub const fn color(&self) -> [f32; 4] {
Self {
kind,
cid: cell,
z,
moisture,
resource,
vertices
}
}
pub fn color(&self) -> [f32; 4] {
match self.kind { match self.kind {
CellKind::Void => [0.; 4],
CellKind::Sea => [0., 0., 1., 1.], CellKind::Sea => [0., 0., 1., 1.],
CellKind::Beach => [0.82, 0.84, 0.51, 1.], CellKind::Beach => [0.82, 0.84, 0.51, 1.],
CellKind::Forest => [0., 0.5 - (self.resource as f32/4.*0.4), 0., 1.], CellKind::Forest { wealth } => [0., 0.5 - (wealth_to_unit(wealth) * 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::Dirt => [0.53, 0.38, 0.29, 1.],
CellKind::Stone => [0.5, 0.5, 0.5, 1.], CellKind::Stone => [0.5, 0.5, 0.5, 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.] 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.,
],
} }
} }
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
} }

195
src/map/cells/grow.rs Normal file
View File

@ -0,0 +1,195 @@
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.;
}
}
}

82
src/map/cells/input.rs Normal file
View File

@ -0,0 +1,82 @@
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),
));
}
}
_ => {}
}
}

92
src/map/cells/material.rs Normal file
View File

@ -0,0 +1,92 @@
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 {}
// }

View File

@ -1,39 +0,0 @@
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 Normal file
View File

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

243
src/ui.rs
View File

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