diff --git a/apps/kbve/axum-kbve/project.json b/apps/kbve/axum-kbve/project.json index d07c5f3b84..d521d46db2 100644 --- a/apps/kbve/axum-kbve/project.json +++ b/apps/kbve/axum-kbve/project.json @@ -16,9 +16,7 @@ }, "build": { "executor": "@monodon/rust:build", - "outputs": [ - "{options.target-dir}" - ], + "outputs": ["{options.target-dir}"], "options": { "target-dir": "dist/target/axum-kbve" }, @@ -30,9 +28,7 @@ }, "test": { "executor": "@monodon/rust:test", - "outputs": [ - "{options.target-dir}" - ], + "outputs": ["{options.target-dir}"], "options": { "target-dir": "dist/target/axum-kbve" }, @@ -44,18 +40,14 @@ }, "lint": { "executor": "@monodon/rust:lint", - "outputs": [ - "{options.target-dir}" - ], + "outputs": ["{options.target-dir}"], "options": { "target-dir": "dist/target/axum-kbve" } }, "run": { "executor": "@monodon/rust:run", - "outputs": [ - "{options.target-dir}" - ], + "outputs": ["{options.target-dir}"], "options": { "target-dir": "dist/target/axum-kbve" }, @@ -79,9 +71,7 @@ }, "container": { "executor": "nx:run-commands", - "dependsOn": [ - "container-prep" - ], + "dependsOn": ["container-prep"], "defaultConfiguration": "local", "options": { "parallel": false @@ -121,21 +111,14 @@ "local": { "load": true, "push": false, - "tags": [ - "kbve/kbve:latest" - ] + "tags": ["kbve/kbve:latest"] }, "production": { "load": true, "push": false, "metadata": { - "images": [ - "ghcr.io/kbve/kbve", - "kbve/kbve" - ], - "tags": [ - "latest" - ] + "images": ["ghcr.io/kbve/kbve", "kbve/kbve"], + "tags": ["latest"] }, "build-args": [ "BUILD_BASE=ghcr.io/kbve/kbve-build-base:latest" @@ -151,9 +134,7 @@ "ci": { "load": true, "push": false, - "tags": [ - "kbve/kbve:latest" - ], + "tags": ["kbve/kbve:latest"], "build-args": [ "BUILD_BASE=ghcr.io/kbve/kbve-build-base:latest" ], @@ -177,16 +158,12 @@ "local": { "load": true, "push": false, - "tags": [ - "ghcr.io/kbve/kbve-build-base:latest" - ] + "tags": ["ghcr.io/kbve/kbve-build-base:latest"] }, "production": { "load": true, "push": false, - "tags": [ - "ghcr.io/kbve/kbve-build-base:latest" - ], + "tags": ["ghcr.io/kbve/kbve-build-base:latest"], "cache-from": [ "type=registry,ref=ghcr.io/kbve/kbve-build-base:buildcache" ], diff --git a/apps/kbve/isometric/src-tauri/assets/shaders/pixelate.wgsl b/apps/kbve/isometric/src-tauri/assets/shaders/pixelate.wgsl index ea71f21e8b..e4931b1ad9 100644 --- a/apps/kbve/isometric/src-tauri/assets/shaders/pixelate.wgsl +++ b/apps/kbve/isometric/src-tauri/assets/shaders/pixelate.wgsl @@ -21,14 +21,10 @@ fn sample_block(uv: vec2, resolution: vec2) -> vec4 { fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { let resolution = vec2(textureDimensions(screen_texture)); - // Work in logical pixel space (like Three.js / CSS pixels). - // This gives the SAME block count on any display regardless of DPI. - // MacBook 2x: logical = 2048/2 = 1024, blocks = 1024/4 = 256 - // LG 4K 1x: logical = 1024/1 = 1024, blocks = 1024/4 = 256 let logical_res = resolution / max(settings.scale_factor, 1.0); let block_count = floor(logical_res / max(settings.pixel_size, 1.0)); - // Current block in the grid (resolution-independent) + // Current block in the grid let block = floor(in.uv * block_count); let block_uv = (block + 0.5) / block_count; @@ -63,13 +59,21 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { let edge_factor = min(max(normal_edge, depth_edge), 1.0); - // Edge outline — fixed proportion of each block (same on all displays) - let block_pos = fract(in.uv * block_count); - let edge_width = 1.0 / settings.pixel_size; // same fraction regardless of DPI - let at_edge = select(0.0, 1.0, - block_pos.x < edge_width || block_pos.y < edge_width); + // Edge application depends on mode: + var final_edge: f32; + if settings.pixel_size < 1.5 { + // Low-res render-to-texture mode: each pixel IS one block. + // Apply edge darkening directly to edge pixels (no block outline). + final_edge = edge_factor; + } else { + // Normal post-process mode: darken only at block boundaries. + let block_pos = fract(in.uv * block_count); + let edge_width = 1.0 / settings.pixel_size; + let at_edge = select(0.0, 1.0, + block_pos.x < edge_width || block_pos.y < edge_width); + final_edge = edge_factor * at_edge; + } - let final_edge = edge_factor * at_edge; let result = mix(color.rgb, color.rgb * 0.35, final_edge); return vec4(result, color.a); } diff --git a/apps/kbve/isometric/src-tauri/src/game/camera.rs b/apps/kbve/isometric/src-tauri/src/game/camera.rs index 85cfd0fd6b..c017b8bc3c 100644 --- a/apps/kbve/isometric/src-tauri/src/game/camera.rs +++ b/apps/kbve/isometric/src-tauri/src/game/camera.rs @@ -1,6 +1,9 @@ use bevy::prelude::*; -use super::pixelate::PixelateSettings; +use super::player::{Player, PlayerMovement}; + +const CAMERA_OFFSET: Vec3 = Vec3::new(15.0, 20.0, 15.0); +const VIEWPORT_HEIGHT: f32 = 20.0; #[derive(Component)] pub struct IsometricCamera; @@ -10,28 +13,33 @@ pub struct IsometricCameraPlugin; impl Plugin for IsometricCameraPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup_camera); + app.add_systems(Update, camera_follow_player.after(PlayerMovement)); } } fn setup_camera(mut commands: Commands) { - // Isometric camera looking down at the tile grid - let camera_pos = Vec3::new(15.0, 20.0, 15.0); - commands.spawn(( Camera3d::default(), Projection::from(OrthographicProjection { scaling_mode: bevy::camera::ScalingMode::FixedVertical { - viewport_height: 20.0, + viewport_height: VIEWPORT_HEIGHT, }, ..OrthographicProjection::default_3d() }), - Transform::from_translation(camera_pos).looking_at(Vec3::ZERO, Vec3::Y), + Transform::from_translation(CAMERA_OFFSET).looking_at(Vec3::ZERO, Vec3::Y), IsometricCamera, - PixelateSettings { - pixel_size: 4.0, - edge_strength: 0.15, - depth_edge_strength: 0.1, - scale_factor: 1.0, // auto-updated from window each frame - }, )); } + +fn camera_follow_player( + player_query: Query<&Transform, (With, Without)>, + mut camera_query: Query<&mut Transform, (With, Without)>, +) { + let Ok(player_tf) = player_query.single() else { + return; + }; + let Ok(mut camera_tf) = camera_query.single_mut() else { + return; + }; + camera_tf.translation = player_tf.translation + CAMERA_OFFSET; +} diff --git a/apps/kbve/isometric/src-tauri/src/game/mod.rs b/apps/kbve/isometric/src-tauri/src/game/mod.rs index a59b993998..0d6f9bd6e1 100644 --- a/apps/kbve/isometric/src-tauri/src/game/mod.rs +++ b/apps/kbve/isometric/src-tauri/src/game/mod.rs @@ -4,4 +4,5 @@ pub mod pixelate; pub mod player; pub mod scene_objects; pub mod state; +pub mod terrain; pub mod tilemap; diff --git a/apps/kbve/isometric/src-tauri/src/game/object_registry.rs b/apps/kbve/isometric/src-tauri/src/game/object_registry.rs index 6df2428e41..59b76837a9 100644 --- a/apps/kbve/isometric/src-tauri/src/game/object_registry.rs +++ b/apps/kbve/isometric/src-tauri/src/game/object_registry.rs @@ -9,6 +9,7 @@ use super::scene_objects::{ AnimatedCrystal, Collider, HoverOutline, Occludable, OriginalEmissive, RotatingBox, on_pointer_out, on_pointer_over, }; +use super::terrain::TerrainMap; // --------------------------------------------------------------------------- // Object Kind @@ -217,12 +218,17 @@ fn handle_spawn_messages( mut meshes: ResMut>, mut materials: ResMut>, mut registry: ResMut, + mut terrain: ResMut, mut messages: MessageReader, ) { for msg in messages.read() { + // Y is terrain-relative: adjust to absolute position + let terrain_h = terrain.height_at_world(msg.position.x, msg.position.z); + let absolute_pos = Vec3::new(msg.position.x, terrain_h + msg.position.y, msg.position.z); + let placement = ObjectPlacement { kind: msg.kind, - position: [msg.position.x, msg.position.y, msg.position.z], + position: [absolute_pos.x, absolute_pos.y, absolute_pos.z], rotation_y: msg.rotation_y, }; let id = registry.insert(placement); @@ -231,7 +237,7 @@ fn handle_spawn_messages( &mut meshes, &mut materials, msg.kind, - msg.position, + absolute_pos, msg.rotation_y, id, ); @@ -292,9 +298,10 @@ fn spawn_object_entity( kind, ObjectInstance { registry_id }, RotatingBox, - Collider { - half_x: half, - half_z: half, + // Cylinder collider — snug fit with slight buffer for rotation + Collider::Cylinder { + radius: half * 1.2, + half_y: half, }, Occludable, OriginalEmissive(LinearRgba::BLACK), @@ -322,9 +329,9 @@ fn spawn_object_entity( kind, ObjectInstance { registry_id }, RotatingBox, - Collider { - half_x: half, - half_z: half, + Collider::Cylinder { + radius: half * 1.2, + half_y: half, }, Occludable, OriginalEmissive(LinearRgba::BLACK), @@ -374,8 +381,9 @@ fn spawn_object_entity( Transform::from_translation(position), kind, ObjectInstance { registry_id }, - Collider { + Collider::Aabb { half_x: 0.4, + half_y: 2.0, half_z: 0.4, }, Occludable, @@ -401,9 +409,9 @@ fn spawn_object_entity( Transform::from_translation(position), kind, ObjectInstance { registry_id }, - Collider { - half_x: radius, - half_z: radius, + Collider::Cylinder { + radius, + half_y: radius, }, Occludable, OriginalEmissive(LinearRgba::BLACK), diff --git a/apps/kbve/isometric/src-tauri/src/game/pixelate.rs b/apps/kbve/isometric/src-tauri/src/game/pixelate.rs index a6a531342c..1175e38928 100644 --- a/apps/kbve/isometric/src-tauri/src/game/pixelate.rs +++ b/apps/kbve/isometric/src-tauri/src/game/pixelate.rs @@ -62,6 +62,7 @@ pub struct PixelatePlugin; impl Plugin for PixelatePlugin { fn build(&self, app: &mut App) { app.add_plugins(FullscreenMaterialPlugin::::default()); - app.add_systems(Update, sync_scale_factor); + // scale_factor sync disabled — when rendering to a low-res texture, + // there's no DPI scaling and scale_factor must stay 1.0. } } diff --git a/apps/kbve/isometric/src-tauri/src/game/player.rs b/apps/kbve/isometric/src-tauri/src/game/player.rs index 21729cd981..59e7ba8a8a 100644 --- a/apps/kbve/isometric/src-tauri/src/game/player.rs +++ b/apps/kbve/isometric/src-tauri/src/game/player.rs @@ -2,64 +2,155 @@ use bevy::prelude::*; use super::scene_objects::Collider; use super::state::PlayerState; +use super::terrain::TerrainMap; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- -/// Player half-extents for collision (matches Cuboid 0.6 x 1.2 x 0.6) const PLAYER_HALF_X: f32 = 0.3; const PLAYER_HALF_Z: f32 = 0.3; +const PLAYER_HEIGHT: f32 = 1.2; +const PLAYER_SPEED: f32 = 5.0; +const GRAVITY: f32 = 20.0; +const JUMP_VELOCITY: f32 = 8.0; +const MAX_STEP_HEIGHT: f32 = 1.0; +const FALL_DAMAGE_THRESHOLD: f32 = 3.0; +const FALL_DAMAGE_PER_UNIT: f32 = 15.0; + +// --------------------------------------------------------------------------- +// Components +// --------------------------------------------------------------------------- #[derive(Component)] pub struct Player; +#[derive(Component)] +pub struct PlayerPhysics { + pub velocity_y: f32, + pub on_ground: bool, + pub fall_start_y: f32, +} + +impl Default for PlayerPhysics { + fn default() -> Self { + Self { + velocity_y: 0.0, + on_ground: true, + fall_start_y: 0.0, + } + } +} + +// --------------------------------------------------------------------------- +// System set (used by camera for ordering) +// --------------------------------------------------------------------------- + +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlayerMovement; + +// --------------------------------------------------------------------------- +// Plugin +// --------------------------------------------------------------------------- + pub struct PlayerPlugin; impl Plugin for PlayerPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, spawn_player); - app.add_systems(Update, (move_player, sync_player_state)); + app.add_systems( + Update, + ( + move_player_horizontal, + player_vertical_physics, + sync_player_state, + ) + .chain() + .in_set(PlayerMovement), + ); } } +// --------------------------------------------------------------------------- +// Spawn +// --------------------------------------------------------------------------- + fn spawn_player( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, + mut terrain: ResMut, ) { - // Player represented as a colored cube + let spawn_x = 2.0; + let spawn_z = 2.0; + let ground_h = terrain.height_at_world(spawn_x, spawn_z); + let spawn_y = ground_h + PLAYER_HEIGHT / 2.0; + commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(0.6, 1.2, 0.6))), + Mesh3d(meshes.add(Cuboid::new(0.6, PLAYER_HEIGHT, 0.6))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.2, 0.4, 0.8), ..default() })), - Transform::from_xyz(2.0, 0.6, 2.0), + Transform::from_xyz(spawn_x, spawn_y, spawn_z), Player, + PlayerPhysics::default(), )); } -/// Check if two AABBs overlap on X and Z axes. -fn aabb_overlap( - pos_a: Vec3, - half_a_x: f32, - half_a_z: f32, - pos_b: Vec3, - half_b_x: f32, - half_b_z: f32, +// --------------------------------------------------------------------------- +// Collision helpers +// --------------------------------------------------------------------------- + +/// Check if the player (AABB) overlaps with a collider (AABB or Cylinder). +fn player_overlaps_collider( + player_pos: Vec3, + player_half: Vec3, + col_pos: Vec3, + col: &Collider, ) -> bool { - let dx = (pos_a.x - pos_b.x).abs(); - let dz = (pos_a.z - pos_b.z).abs(); - dx < (half_a_x + half_b_x) && dz < (half_a_z + half_b_z) + match col { + Collider::Aabb { + half_x, + half_y, + half_z, + } => { + let d = (player_pos - col_pos).abs(); + d.x < (player_half.x + half_x) + && d.y < (player_half.y + half_y) + && d.z < (player_half.z + half_z) + } + Collider::Cylinder { radius, half_y } => { + // Y axis: box check + let dy = (player_pos.y - col_pos.y).abs(); + if dy >= player_half.y + half_y { + return false; + } + // XZ plane: circle vs AABB + // Clamp circle center to AABB, then check distance + let cx = (col_pos.x - player_pos.x).clamp(-player_half.x, player_half.x) + player_pos.x; + let cz = (col_pos.z - player_pos.z).clamp(-player_half.z, player_half.z) + player_pos.z; + let dx = col_pos.x - cx; + let dz = col_pos.z - cz; + (dx * dx + dz * dz) < radius * radius + } + } } -fn move_player( +// --------------------------------------------------------------------------- +// Horizontal movement with terrain step-up +// --------------------------------------------------------------------------- + +fn move_player_horizontal( keyboard: Res>, time: Res