Web/wasm Rust voxel game with: - wgpu 23 client (WebGPU when available, WebGL2 fallback) - Chunked terrain (17x17 chunks, deterministic value-noise generator) - Greedy meshing with frustum + distance culling - Sky shader, leaf wind shader, distance fog - Player physics: substepped AABB collision, gravity, fall damage, natural-surface respawn - Touch UI (MCPE-style joystick + jump/break/place/sprint), gamepad polling with axis calibration, mouse+keyboard - HP / death-screen / respawn flow - 10-slot hotbar with mouse-wheel + hotkey + tap cycling - Settings menu (mouse sens, FOV, render distance, input mode toggle) - Axum multiplayer server: WebSocket protocol, edit log, 10Hz player broadcasts - 31 unit tests covering spawn invariants, collision sweeps, raycast hit/miss, greedy mesh winding, fall damage, oriented box rotation, hotbar block roundtrip, and the input-merge regression that latched movement after touch release Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
2.2 KiB
Rust
81 lines
2.2 KiB
Rust
use glam::{Mat4, Vec3};
|
|
|
|
pub struct Camera {
|
|
pub position: Vec3,
|
|
pub yaw: f32,
|
|
pub pitch: f32,
|
|
pub aspect: f32,
|
|
pub fovy: f32,
|
|
pub near: f32,
|
|
pub far: f32,
|
|
}
|
|
|
|
impl Camera {
|
|
pub fn new(aspect: f32) -> Self {
|
|
Self {
|
|
position: Vec3::new(0.0, 32.0, 0.0),
|
|
yaw: -std::f32::consts::FRAC_PI_2,
|
|
pitch: -0.3,
|
|
aspect,
|
|
fovy: 70f32.to_radians(),
|
|
near: 0.05,
|
|
far: 800.0,
|
|
}
|
|
}
|
|
|
|
pub fn forward(&self) -> Vec3 {
|
|
let cp = self.pitch.cos();
|
|
Vec3::new(self.yaw.cos() * cp, self.pitch.sin(), self.yaw.sin() * cp).normalize()
|
|
}
|
|
|
|
pub fn forward_flat(&self) -> Vec3 {
|
|
Vec3::new(self.yaw.cos(), 0.0, self.yaw.sin()).normalize_or_zero()
|
|
}
|
|
|
|
pub fn right_flat(&self) -> Vec3 {
|
|
let f = self.forward_flat();
|
|
Vec3::new(-f.z, 0.0, f.x)
|
|
}
|
|
|
|
pub fn view_proj(&self) -> Mat4 {
|
|
let view = Mat4::look_to_rh(self.position, self.forward(), Vec3::Y);
|
|
let proj = Mat4::perspective_rh(self.fovy, self.aspect, self.near, self.far);
|
|
proj * view
|
|
}
|
|
}
|
|
|
|
/// Sticky keyboard hold state. The KbInput handler writes `true` on KeyDown
|
|
/// and `false` on KeyUp, so the field always reflects whether the key is
|
|
/// *currently* held. Merged with `TouchBridge` each tick into a local — never
|
|
/// folded back into a persistent field, which was the source of the
|
|
/// "joystick release leaves the player walking" bug.
|
|
#[derive(Default, Clone, Debug, PartialEq, Eq)]
|
|
pub struct KbHeld {
|
|
pub forward: bool,
|
|
pub back: bool,
|
|
pub left: bool,
|
|
pub right: bool,
|
|
pub up: bool,
|
|
pub down: bool,
|
|
pub sprint: bool,
|
|
}
|
|
|
|
/// Per-tick input that *isn't* sticky directional state: pending mouse
|
|
/// motion to consume, click one-shots, and the currently-selected block.
|
|
#[derive(Default)]
|
|
pub struct InputState {
|
|
pub mouse_dx: f32,
|
|
pub mouse_dy: f32,
|
|
pub primary_clicked: bool,
|
|
pub secondary_clicked: bool,
|
|
pub selected_block: u8,
|
|
}
|
|
|
|
impl InputState {
|
|
pub fn consume_mouse(&mut self) -> (f32, f32) {
|
|
let r = (self.mouse_dx, self.mouse_dy);
|
|
self.mouse_dx = 0.0;
|
|
self.mouse_dy = 0.0;
|
|
r
|
|
}
|
|
}
|