terainia/src/camera.rs
Maximus Gorog 3a4ae970b2 pre-alpha 0.0.1 — initial multiplayer voxel sandbox
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>
2026-05-22 23:33:47 -06:00

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
}
}