Phase 4: split state.rs → bridges.rs + app.rs

state.rs is gone. Its content split by responsibility:

  src/bridges.rs (~290 lines)
    The only place we own shared mutable state. Holds the four
    thread_locals (touch input, game status, network, settings) and
    the wasm-bindgen JS interface that mutates them. Exposes a typed
    accessor API — current_settings(), snapshot_touch_bridge(),
    with_touch_bridge(f), is_touch_mode(), clear_touch_inputs(),
    take_respawn_request(), set_status(hp, alive), take_inbox(),
    push_outbox(msg), with_net_bridge(f), net_connection_snapshot(),
    is_connected(), set_my_id(id), snapshot_remote_players(). Callers
    never touch the RefCells; if storage ever moves to a Mutex or
    OnceCell, only this file changes.

  src/app.rs (~510 lines)
    The App + winit ApplicationHandler + tick + drain_net_inbox +
    do_respawn + render_frame + FrameClock. Uses the bridges
    accessors instead of poking thread_locals directly, so the call
    sites read as a pipeline rather than a chain of `.with(|x|
    x.borrow_mut())`.

  src/lib.rs
    Modules now declared in alphabetical order:
      app, bridges, camera, mesh, net, proto, render,
      shader_source, sim, world.
    `run()` constructs `app::App::default()` instead of the old
    `state::App`.

  src/render/mod.rs
    `use crate::bridges::RemotePlayer` (was `crate::state::`).

Net effect of the four phases combined: state.rs at the start of
alpha-0.0.2 was 2500 lines doing everything; today the same logic
spans nine focused modules totalling ~3000 lines (with full doc
comments). 53 tests pass, native + wasm release build green, server
build green.
This commit is contained in:
Maximus Gorog 2026-05-23 23:27:03 -06:00
parent 549662ddc8
commit 55276b7ce0
4 changed files with 362 additions and 296 deletions

View file

@ -1,13 +1,29 @@
//! Imperative shell: owns the App + JS bridges + winit handler.
//! The Renderer lives in `crate::render`; pure logic in `crate::sim`
//! and `crate::net`. This file's job is to compose them per tick.
//! The App — winit `ApplicationHandler` plus the tick loop. This is
//! the orchestrator that threads pure simulation (`sim`), pure net
//! parsing (`net`), and the GPU shell (`render`) together every frame.
//!
//! The tick is a pipeline:
//!
//! 1. Compute dt + scaled shader time
//! 2. (paused?) clear inputs + render last frame + return
//! 3. Drain touch bridge → per-tick input
//! 4. Honor respawn-request one-shot
//! 5. Drain net inbox → parsed events → fold into world / remote map
//! 6. Send Hello on (re)connection
//! 7. Apply mouse look to camera
//! 8. step_movement(world, body, MoveInput) → (body', events)
//! 9. Fold sim events into damage
//! 10. Sync camera position with body
//! 11. Block interaction (raycast → break/place) → maybe broadcast edit
//! 12. Apply damage; periodic state broadcast
//! 13. Render frame
use crate::bridges::{self, RemotePlayer, Settings};
use crate::camera::{Camera, InputState, KbHeld};
use crate::net::{parse_inbox, NetEvent};
use crate::proto::{ClientMsg, EditRec};
use crate::render::Renderer;
use crate::sim::collision::{aabb_overlap_player, AabbI, EYE_HEIGHT};
use crate::sim::edit::{apply_edit, block_from_u8, chunks_for_edit};
use crate::sim::input::TouchBridge;
use crate::sim::physics::MoveInput;
use crate::sim::spawn::{fall_damage, find_safe_spawn};
use crate::sim::visibility::compute_visible_chunks;
@ -15,7 +31,6 @@ use crate::sim::{merge_held, step_movement, PlayerBody, SimEvent};
use crate::world::{Block, World, WORLD_RADIUS};
use glam::{IVec3, Vec3};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
@ -33,197 +48,6 @@ use wasm_bindgen::JsCast;
const REACH: f32 = 6.0;
thread_local! {
/// Storage for the touch/controller bridge. The struct itself lives
/// in `crate::sim::input`; this RefCell is the only place it's
/// mutated, by the wasm bindings below.
static TOUCH_BRIDGE: RefCell<TouchBridge> = RefCell::new(TouchBridge::default());
static GAME_STATUS: RefCell<GameStatus> = RefCell::new(GameStatus {
hp: 20,
alive: true,
respawn_requested: false,
});
static NET_BRIDGE: RefCell<NetBridge> = RefCell::new(NetBridge::default());
static SETTINGS: RefCell<Settings> = RefCell::new(Settings::default());
}
#[derive(Clone, Copy)]
struct Settings {
mouse_sens: f32,
fov_deg: f32,
render_dist: f32,
paused: bool,
/// Multiplier on real time used to drive the day/night cycle.
/// 0 = frozen, 1 = realtime (one in-game day every 5 minutes per
/// shader constant), up to 8x for fast-forward playtesting.
time_scale: f32,
}
impl Default for Settings {
fn default() -> Self {
Self {
mouse_sens: 0.005,
fov_deg: 70.0,
render_dist: 240.0,
paused: false,
time_scale: 1.0,
}
}
}
struct GameStatus {
hp: u8,
alive: bool,
respawn_requested: bool,
}
#[derive(Default)]
pub struct NetBridge {
pub connected: bool,
pub inbox: Vec<String>,
pub outbox: Vec<String>,
pub pending_name: Option<String>,
pub my_id: Option<u32>,
pub remote_players: HashMap<u32, RemotePlayer>,
}
#[derive(Clone, Debug)]
pub struct RemotePlayer {
pub name: String,
pub pos: Vec3,
pub yaw: f32,
pub pitch: f32,
}
fn is_touch_mode() -> bool {
TOUCH_BRIDGE.with(|b| b.borrow().touch_mode)
}
#[cfg(target_arch = "wasm32")]
mod wasm_api {
use super::TOUCH_BRIDGE;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn set_touch_mode(on: bool) {
TOUCH_BRIDGE.with(|b| b.borrow_mut().touch_mode = on);
}
#[wasm_bindgen]
pub fn touch_move(forward: bool, back: bool, left: bool, right: bool) {
TOUCH_BRIDGE.with(|b| {
let mut br = b.borrow_mut();
br.forward = forward;
br.back = back;
br.left = left;
br.right = right;
});
}
#[wasm_bindgen]
pub fn touch_jump(on: bool) {
TOUCH_BRIDGE.with(|b| b.borrow_mut().jump = on);
}
#[wasm_bindgen]
pub fn touch_sprint(on: bool) {
TOUCH_BRIDGE.with(|b| b.borrow_mut().sprint = on);
}
#[wasm_bindgen]
pub fn touch_look(dx: f32, dy: f32) {
TOUCH_BRIDGE.with(|b| {
let mut br = b.borrow_mut();
br.look_dx += dx;
br.look_dy += dy;
});
}
#[wasm_bindgen]
pub fn touch_break() {
TOUCH_BRIDGE.with(|b| b.borrow_mut().break_pressed = true);
}
#[wasm_bindgen]
pub fn touch_place() {
TOUCH_BRIDGE.with(|b| b.borrow_mut().place_pressed = true);
}
#[wasm_bindgen]
pub fn select_block(b: u8) {
TOUCH_BRIDGE.with(|x| x.borrow_mut().selected = Some(b));
}
#[wasm_bindgen]
pub fn get_hp() -> u8 {
super::GAME_STATUS.with(|s| s.borrow().hp)
}
#[wasm_bindgen]
pub fn is_alive() -> bool {
super::GAME_STATUS.with(|s| s.borrow().alive)
}
#[wasm_bindgen]
pub fn respawn() {
super::GAME_STATUS.with(|s| s.borrow_mut().respawn_requested = true);
}
#[wasm_bindgen]
pub fn set_player_name(name: String) {
super::NET_BRIDGE.with(|n| n.borrow_mut().pending_name = Some(name));
}
#[wasm_bindgen]
pub fn on_ws_message(text: String) {
super::NET_BRIDGE.with(|n| n.borrow_mut().inbox.push(text));
}
#[wasm_bindgen]
pub fn on_ws_open() {
super::NET_BRIDGE.with(|n| n.borrow_mut().connected = true);
}
#[wasm_bindgen]
pub fn on_ws_close() {
super::NET_BRIDGE.with(|n| {
let mut n = n.borrow_mut();
n.connected = false;
n.my_id = None;
n.remote_players.clear();
});
}
#[wasm_bindgen]
pub fn drain_outbox() -> Vec<String> {
super::NET_BRIDGE.with(|n| std::mem::take(&mut n.borrow_mut().outbox))
}
#[wasm_bindgen]
pub fn set_paused(on: bool) {
super::SETTINGS.with(|s| s.borrow_mut().paused = on);
}
#[wasm_bindgen]
pub fn set_mouse_sens(s: f32) {
super::SETTINGS.with(|x| x.borrow_mut().mouse_sens = s.clamp(0.0005, 0.05));
}
#[wasm_bindgen]
pub fn set_fov(deg: f32) {
super::SETTINGS.with(|x| x.borrow_mut().fov_deg = deg.clamp(40.0, 110.0));
}
#[wasm_bindgen]
pub fn set_render_distance(blocks: f32) {
super::SETTINGS.with(|x| x.borrow_mut().render_dist = blocks.clamp(32.0, 1200.0));
}
#[wasm_bindgen]
pub fn set_time_scale(s: f32) {
super::SETTINGS.with(|x| x.borrow_mut().time_scale = s.clamp(0.0, 8.0));
}
/// Clears all bridge input (move/look/buttons) — called on init,
/// pause, and visibility-change so we never resume with stale state.
#[wasm_bindgen]
pub fn reset_input() {
super::TOUCH_BRIDGE.with(|b| {
let mut br = b.borrow_mut();
br.forward = false;
br.back = false;
br.left = false;
br.right = false;
br.jump = false;
br.sprint = false;
br.look_dx = 0.0;
br.look_dy = 0.0;
br.break_pressed = false;
br.place_pressed = false;
});
}
}
#[derive(Default)]
pub struct App {
window: Option<Arc<Window>>,
@ -238,9 +62,9 @@ pub struct App {
pointer_locked: bool,
last_net_send: f32,
was_connected: bool,
/// Accumulated *scaled* time. Real dt × `settings.time_scale` per
/// tick — the shader's day/night cycle slows / freezes / fast-
/// forwards according to the player's setting.
/// Accumulated *scaled* time. Real dt × settings.time_scale per
/// tick — feeds `camera.frame.x` so the shader's day/night cycle
/// slows / freezes / fast-forwards by the player's setting.
shader_time: f32,
}
@ -389,16 +213,11 @@ impl ApplicationHandler for App {
self.last_frame = Some(FrameClock::now());
self.start_clock = Some(FrameClock::now());
self.input.selected_block = Block::Stone as u8;
self.push_status();
bridges::set_status(self.body.hp, self.body.alive);
window.request_redraw();
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_id: WindowId,
event: WindowEvent,
) {
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => {
@ -418,9 +237,7 @@ impl ApplicationHandler for App {
KeyCode::KeyA => self.keyboard.left = pressed,
KeyCode::KeyD => self.keyboard.right = pressed,
KeyCode::Space => self.keyboard.up = pressed,
KeyCode::ShiftLeft | KeyCode::ShiftRight => {
self.keyboard.down = pressed
}
KeyCode::ShiftLeft | KeyCode::ShiftRight => self.keyboard.down = pressed,
KeyCode::ControlLeft | KeyCode::ControlRight => {
self.keyboard.sprint = pressed;
}
@ -429,21 +246,11 @@ impl ApplicationHandler for App {
KeyCode::Digit3 if pressed => self.input.selected_block = Block::Stone as u8,
KeyCode::Digit4 if pressed => self.input.selected_block = Block::Sand as u8,
KeyCode::Digit5 if pressed => self.input.selected_block = Block::Wood as u8,
KeyCode::Digit6 if pressed => {
self.input.selected_block = Block::Leaves as u8
}
KeyCode::Digit7 if pressed => {
self.input.selected_block = Block::Cobble as u8
}
KeyCode::Digit8 if pressed => {
self.input.selected_block = Block::Brick as u8
}
KeyCode::Digit9 if pressed => {
self.input.selected_block = Block::Snow as u8
}
KeyCode::Digit0 if pressed => {
self.input.selected_block = Block::Ice as u8
}
KeyCode::Digit6 if pressed => self.input.selected_block = Block::Leaves as u8,
KeyCode::Digit7 if pressed => self.input.selected_block = Block::Cobble as u8,
KeyCode::Digit8 if pressed => self.input.selected_block = Block::Brick as u8,
KeyCode::Digit9 if pressed => self.input.selected_block = Block::Snow as u8,
KeyCode::Digit0 if pressed => self.input.selected_block = Block::Ice as u8,
_ => {}
}
}
@ -455,7 +262,7 @@ impl ApplicationHandler for App {
MouseButton::Right => self.input.secondary_clicked = true,
_ => {}
}
if !is_touch_mode() {
if !bridges::is_touch_mode() {
if let Some(w) = &self.window {
let _ = w.set_cursor_grab(winit::window::CursorGrabMode::Locked);
w.set_cursor_visible(false);
@ -505,14 +312,6 @@ impl ApplicationHandler for App {
}
impl App {
fn push_status(&self) {
GAME_STATUS.with(|s| {
let mut s = s.borrow_mut();
s.hp = self.body.hp;
s.alive = self.body.alive;
});
}
fn do_respawn(&mut self) {
let feet = self
.world
@ -524,14 +323,13 @@ impl App {
if let Some(cam) = self.camera.borrow_mut().as_mut() {
cam.position = Vec3::new(feet.x, feet.y + EYE_HEIGHT, feet.z);
}
self.push_status();
bridges::set_status(self.body.hp, self.body.alive);
}
/// Drain the JS-fed inbox, parse via `net::parse_inbox` into typed
/// events, then fold each one into the world / remote-player map.
fn drain_net_inbox(&mut self) {
let inbox: Vec<String> =
NET_BRIDGE.with(|n| std::mem::take(&mut n.borrow_mut().inbox));
let inbox = bridges::take_inbox();
if inbox.is_empty() {
return;
}
@ -545,7 +343,7 @@ impl App {
for ev in events {
match ev {
NetEvent::Welcome { id, edits } => {
NET_BRIDGE.with(|n| n.borrow_mut().my_id = Some(id));
bridges::set_my_id(id);
for e in edits {
if apply_edit(world, &e) {
for c in chunks_for_edit(IVec3::new(e.x, e.y, e.z)) {
@ -555,8 +353,7 @@ impl App {
}
}
NetEvent::PlayerList(list) => {
NET_BRIDGE.with(|n| {
let mut n = n.borrow_mut();
bridges::with_net_bridge(|n| {
let my = n.my_id;
n.remote_players.clear();
for p in list {
@ -583,8 +380,8 @@ impl App {
}
}
NetEvent::Leave { id } => {
NET_BRIDGE.with(|n| {
n.borrow_mut().remote_players.remove(&id);
bridges::with_net_bridge(|n| {
n.remote_players.remove(&id);
});
}
}
@ -601,9 +398,7 @@ impl App {
}
}
/// One frame: collect inputs → step physics → fold sim events →
/// world edits → render. The pure pieces live in `crate::sim` and
/// `crate::net`; this function only composes them.
/// One frame. See module doc-comment for the pipeline shape.
fn tick(&mut self) {
let dt = match self.last_frame.as_ref() {
Some(c) => c.elapsed().as_secs_f32().min(0.1),
@ -615,38 +410,24 @@ impl App {
.as_ref()
.map(|c| c.elapsed().as_secs_f32())
.unwrap_or(0.0);
let settings = SETTINGS.with(|s| *s.borrow());
let settings = bridges::current_settings();
self.shader_time += dt * settings.time_scale;
// While paused, freeze inputs and skip physics — render the
// last frame so the menu draws over the world.
// Paused: clear inputs, render the last frame, return.
if settings.paused {
self.keyboard = KbHeld::default();
self.input.mouse_dx = 0.0;
self.input.mouse_dy = 0.0;
self.input.primary_clicked = false;
self.input.secondary_clicked = false;
TOUCH_BRIDGE.with(|b| {
let mut br = b.borrow_mut();
br.forward = false;
br.back = false;
br.left = false;
br.right = false;
br.jump = false;
br.sprint = false;
br.look_dx = 0.0;
br.look_dy = 0.0;
br.break_pressed = false;
br.place_pressed = false;
});
bridges::clear_touch_inputs();
self.drain_net_inbox();
self.render_frame(settings, None);
return;
}
// Snapshot + drain the touch bridge into the per-tick one-shots.
let bridge = TOUCH_BRIDGE.with(|b| {
let mut br = b.borrow_mut();
// Drain the touch bridge into the per-tick one-shots.
let bridge = bridges::with_touch_bridge(|br| {
self.input.mouse_dx += br.look_dx;
self.input.mouse_dy += br.look_dy;
br.look_dx = 0.0;
@ -665,30 +446,21 @@ impl App {
br.clone()
});
// Respawn from JS?
let respawn_now = GAME_STATUS.with(|s| {
let mut s = s.borrow_mut();
let r = s.respawn_requested;
s.respawn_requested = false;
r
});
if respawn_now {
if bridges::take_respawn_request() {
self.do_respawn();
}
self.drain_net_inbox();
// Hello on (re)connection.
let connected = NET_BRIDGE.with(|n| n.borrow().connected);
let connected = bridges::is_connected();
if connected && !self.was_connected {
let name = NET_BRIDGE
.with(|n| n.borrow_mut().pending_name.take())
.unwrap_or_else(|| {
format!("guest-{}", (real_time * 1000.0) as u32 % 10000)
});
let (_, pending_name) = bridges::net_connection_snapshot();
let name = pending_name
.unwrap_or_else(|| format!("guest-{}", (real_time * 1000.0) as u32 % 10000));
let msg = ClientMsg::Hello { name };
if let Ok(s) = serde_json::to_string(&msg) {
NET_BRIDGE.with(|n| n.borrow_mut().outbox.push(s));
bridges::push_outbox(s);
}
}
self.was_connected = connected;
@ -710,7 +482,7 @@ impl App {
return;
};
// ---- Physics: pure step ----
// Pure physics step.
let held = merge_held(&self.keyboard, &bridge);
let outcome = step_movement(
world,
@ -724,8 +496,7 @@ impl App {
);
self.body = outcome.body;
// Fold sim events into damage. (Edits come from block
// interaction below, not from movement.)
// Fold sim events into damage (edits come from block interaction below).
let mut total_damage: u8 = 0;
for e in outcome.events {
match e {
@ -746,7 +517,7 @@ impl App {
self.body.feet.z,
);
// ---- Block interaction (pick → break/place) ----
// Block interaction (raycast → break / place).
let hit = world.raycast(camera.position, camera.forward(), REACH);
let primary = std::mem::replace(&mut self.input.primary_clicked, false);
let secondary = std::mem::replace(&mut self.input.secondary_clicked, false);
@ -758,8 +529,7 @@ impl App {
let (target, set_to) = if primary {
(hit_pos, Block::Air)
} else {
let block = block_from_u8(self.input.selected_block);
(prev_pos, block)
(prev_pos, block_from_u8(self.input.selected_block))
};
let blocks_player = set_to.solid()
&& aabb_overlap_player(AabbI::block(target), self.body.feet);
@ -797,13 +567,13 @@ impl App {
block: e.block,
};
if let Ok(s) = serde_json::to_string(&msg) {
NET_BRIDGE.with(|n| n.borrow_mut().outbox.push(s));
bridges::push_outbox(s);
}
}
if total_damage > 0 {
self.body = self.body.take_damage(total_damage);
self.push_status();
bridges::set_status(self.body.hp, self.body.alive);
}
// Periodic state broadcast.
@ -817,7 +587,7 @@ impl App {
pitch: camera.pitch,
};
if let Ok(s) = serde_json::to_string(&msg) {
NET_BRIDGE.with(|n| n.borrow_mut().outbox.push(s));
bridges::push_outbox(s);
}
}
@ -834,8 +604,8 @@ impl App {
}
/// Render-only path used by both the active tick and the paused
/// branch. Keeps the camera-upload + visibility cull + remote-
/// player upload in one place.
/// branch. One place to assemble camera upload + visibility cull +
/// remote-player buffer + draw.
fn render_frame(&self, settings: Settings, outline: Option<IVec3>) {
let camera_borrow = self.camera.borrow();
let Some(camera) = camera_borrow.as_ref() else {
@ -848,9 +618,7 @@ impl App {
} else {
Vec::new()
};
let remotes: Vec<RemotePlayer> = NET_BRIDGE.with(|n| {
n.borrow().remote_players.values().cloned().collect()
});
let remotes = bridges::snapshot_remote_players();
if let Some(r) = self.renderer.borrow_mut().as_mut() {
r.set_outline(outline);
r.set_visible(visible);
@ -867,4 +635,3 @@ impl App {
}
}
}

298
src/bridges.rs Normal file
View file

@ -0,0 +1,298 @@
//! JS ⇄ Rust bridges and per-process mutable state. This module owns
//! every `thread_local!` cell in the project; everything else accesses
//! them through the typed accessor functions below.
//!
//! Why this split: it's the only place we *have* to do shared mutable
//! state, because wasm-bindgen callbacks from JS need a destination for
//! events (touch input, network messages, UI state changes). The rest
//! of the codebase stays pure by going through accessors that return
//! values or take closures.
use crate::sim::input::TouchBridge;
use glam::Vec3;
use std::cell::RefCell;
use std::collections::HashMap;
// ---------------- Data types stored in the bridges ----------------
/// User-tunable settings, mirrored from the JS settings menu.
#[derive(Clone, Copy)]
pub struct Settings {
pub mouse_sens: f32,
pub fov_deg: f32,
pub render_dist: f32,
pub paused: bool,
/// Multiplier on real time used to drive the day/night cycle.
/// 0 = frozen, 1 = realtime, up to 8x for fast-forward playtesting.
pub time_scale: f32,
}
impl Default for Settings {
fn default() -> Self {
Self {
mouse_sens: 0.005,
fov_deg: 70.0,
render_dist: 240.0,
paused: false,
time_scale: 1.0,
}
}
}
/// HP + alive flag exposed to JS (HUD, death screen) plus a "JS asked
/// for respawn" one-shot.
pub struct GameStatus {
pub hp: u8,
pub alive: bool,
pub respawn_requested: bool,
}
/// WebSocket + remote-player state.
#[derive(Default)]
pub struct NetBridge {
pub connected: bool,
pub inbox: Vec<String>,
pub outbox: Vec<String>,
pub pending_name: Option<String>,
pub my_id: Option<u32>,
pub remote_players: HashMap<u32, RemotePlayer>,
}
/// Best-known state of another connected player. Built from
/// `proto::PlayerInfo` after `net::parse_inbox` returns it as a
/// `PlayerList` event.
#[derive(Clone, Debug)]
pub struct RemotePlayer {
pub name: String,
pub pos: Vec3,
pub yaw: f32,
pub pitch: f32,
}
// ---------------- Thread-local storage ----------------
thread_local! {
static TOUCH_BRIDGE: RefCell<TouchBridge> = RefCell::new(TouchBridge::default());
static GAME_STATUS: RefCell<GameStatus> = RefCell::new(GameStatus {
hp: 20,
alive: true,
respawn_requested: false,
});
static NET_BRIDGE: RefCell<NetBridge> = RefCell::new(NetBridge::default());
static SETTINGS: RefCell<Settings> = RefCell::new(Settings::default());
}
// ---------------- Public typed accessors ----------------
//
// Callers never touch the RefCells directly — they go through these.
// That keeps the call sites readable and means the storage strategy can
// change (e.g. to a single `Box<Mutex<…>>`) without touching the App.
/// Read the current settings as a value snapshot.
pub fn current_settings() -> Settings {
SETTINGS.with(|s| *s.borrow())
}
/// Read a snapshot of the touch bridge (clone, no live borrow held).
pub fn snapshot_touch_bridge() -> TouchBridge {
TOUCH_BRIDGE.with(|b| b.borrow().clone())
}
/// Mutate the touch bridge in-place under a closure.
pub fn with_touch_bridge<R>(f: impl FnOnce(&mut TouchBridge) -> R) -> R {
TOUCH_BRIDGE.with(|b| f(&mut b.borrow_mut()))
}
/// Whether the player is in touch-input mode.
pub fn is_touch_mode() -> bool {
TOUCH_BRIDGE.with(|b| b.borrow().touch_mode)
}
/// Clear movement holds, jump, sprint, look deltas, and the
/// break/place one-shots. Used by the paused branch of the tick.
pub fn clear_touch_inputs() {
with_touch_bridge(|br| {
br.forward = false;
br.back = false;
br.left = false;
br.right = false;
br.jump = false;
br.sprint = false;
br.look_dx = 0.0;
br.look_dy = 0.0;
br.break_pressed = false;
br.place_pressed = false;
});
}
/// Read & clear the "JS requested a respawn" one-shot.
pub fn take_respawn_request() -> bool {
GAME_STATUS.with(|s| {
let mut s = s.borrow_mut();
let r = s.respawn_requested;
s.respawn_requested = false;
r
})
}
/// Publish HP + alive to JS (HUD, death screen).
pub fn set_status(hp: u8, alive: bool) {
GAME_STATUS.with(|s| {
let mut s = s.borrow_mut();
s.hp = hp;
s.alive = alive;
});
}
/// Drain the incoming WebSocket inbox to be parsed by `net::parse_inbox`.
pub fn take_inbox() -> Vec<String> {
NET_BRIDGE.with(|n| std::mem::take(&mut n.borrow_mut().inbox))
}
/// Queue an outbound message for JS to send over the WebSocket.
pub fn push_outbox(msg: String) {
NET_BRIDGE.with(|n| n.borrow_mut().outbox.push(msg));
}
/// Mutate the net bridge in-place under a closure.
pub fn with_net_bridge<R>(f: impl FnOnce(&mut NetBridge) -> R) -> R {
NET_BRIDGE.with(|n| f(&mut n.borrow_mut()))
}
/// `(connected, pending_name)` — used by App::tick when sending the
/// initial Hello on a new connection.
pub fn net_connection_snapshot() -> (bool, Option<String>) {
NET_BRIDGE.with(|n| {
let mut n = n.borrow_mut();
(n.connected, n.pending_name.take())
})
}
/// Just the `connected` flag, no clearing.
pub fn is_connected() -> bool {
NET_BRIDGE.with(|n| n.borrow().connected)
}
/// Set the local player id assigned by the server.
pub fn set_my_id(id: u32) {
NET_BRIDGE.with(|n| n.borrow_mut().my_id = Some(id));
}
/// Snapshot the current set of remote players for rendering. Returns a
/// fresh `Vec` so the bridge borrow doesn't span the render call.
pub fn snapshot_remote_players() -> Vec<RemotePlayer> {
NET_BRIDGE.with(|n| n.borrow().remote_players.values().cloned().collect())
}
// ---------------- wasm-bindgen JS interface ----------------
#[cfg(target_arch = "wasm32")]
mod wasm_api {
use super::{GAME_STATUS, NET_BRIDGE, SETTINGS, TOUCH_BRIDGE};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn set_touch_mode(on: bool) {
TOUCH_BRIDGE.with(|b| b.borrow_mut().touch_mode = on);
}
#[wasm_bindgen]
pub fn touch_move(forward: bool, back: bool, left: bool, right: bool) {
TOUCH_BRIDGE.with(|b| {
let mut br = b.borrow_mut();
br.forward = forward;
br.back = back;
br.left = left;
br.right = right;
});
}
#[wasm_bindgen]
pub fn touch_jump(on: bool) {
TOUCH_BRIDGE.with(|b| b.borrow_mut().jump = on);
}
#[wasm_bindgen]
pub fn touch_sprint(on: bool) {
TOUCH_BRIDGE.with(|b| b.borrow_mut().sprint = on);
}
#[wasm_bindgen]
pub fn touch_look(dx: f32, dy: f32) {
TOUCH_BRIDGE.with(|b| {
let mut br = b.borrow_mut();
br.look_dx += dx;
br.look_dy += dy;
});
}
#[wasm_bindgen]
pub fn touch_break() {
TOUCH_BRIDGE.with(|b| b.borrow_mut().break_pressed = true);
}
#[wasm_bindgen]
pub fn touch_place() {
TOUCH_BRIDGE.with(|b| b.borrow_mut().place_pressed = true);
}
#[wasm_bindgen]
pub fn select_block(b: u8) {
TOUCH_BRIDGE.with(|x| x.borrow_mut().selected = Some(b));
}
#[wasm_bindgen]
pub fn get_hp() -> u8 {
GAME_STATUS.with(|s| s.borrow().hp)
}
#[wasm_bindgen]
pub fn is_alive() -> bool {
GAME_STATUS.with(|s| s.borrow().alive)
}
#[wasm_bindgen]
pub fn respawn() {
GAME_STATUS.with(|s| s.borrow_mut().respawn_requested = true);
}
#[wasm_bindgen]
pub fn set_player_name(name: String) {
NET_BRIDGE.with(|n| n.borrow_mut().pending_name = Some(name));
}
#[wasm_bindgen]
pub fn on_ws_message(text: String) {
NET_BRIDGE.with(|n| n.borrow_mut().inbox.push(text));
}
#[wasm_bindgen]
pub fn on_ws_open() {
NET_BRIDGE.with(|n| n.borrow_mut().connected = true);
}
#[wasm_bindgen]
pub fn on_ws_close() {
NET_BRIDGE.with(|n| {
let mut n = n.borrow_mut();
n.connected = false;
n.my_id = None;
n.remote_players.clear();
});
}
#[wasm_bindgen]
pub fn drain_outbox() -> Vec<String> {
NET_BRIDGE.with(|n| std::mem::take(&mut n.borrow_mut().outbox))
}
#[wasm_bindgen]
pub fn set_paused(on: bool) {
SETTINGS.with(|s| s.borrow_mut().paused = on);
}
#[wasm_bindgen]
pub fn set_mouse_sens(s: f32) {
SETTINGS.with(|x| x.borrow_mut().mouse_sens = s.clamp(0.0005, 0.05));
}
#[wasm_bindgen]
pub fn set_fov(deg: f32) {
SETTINGS.with(|x| x.borrow_mut().fov_deg = deg.clamp(40.0, 110.0));
}
#[wasm_bindgen]
pub fn set_render_distance(blocks: f32) {
SETTINGS.with(|x| x.borrow_mut().render_dist = blocks.clamp(32.0, 1200.0));
}
#[wasm_bindgen]
pub fn set_time_scale(s: f32) {
SETTINGS.with(|x| x.borrow_mut().time_scale = s.clamp(0.0, 8.0));
}
/// Clears all bridge input (move/look/buttons) — called on init,
/// pause, and visibility-change so we never resume with stale state.
#[wasm_bindgen]
pub fn reset_input() {
super::clear_touch_inputs();
}
}

View file

@ -1,3 +1,5 @@
pub mod app;
pub mod bridges;
pub mod camera;
pub mod mesh;
pub mod net;
@ -5,7 +7,6 @@ pub mod proto;
pub mod render;
pub mod shader_source;
pub mod sim;
pub mod state;
pub mod world;
use winit::event_loop::EventLoop;
@ -28,7 +29,7 @@ pub fn run() {
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
#[allow(unused_mut)]
let mut app = state::App::default();
let mut app = app::App::default();
#[cfg(not(target_arch = "wasm32"))]
{

View file

@ -11,9 +11,9 @@ pub mod pipelines;
pub mod scene_target;
pub mod uniform;
use crate::bridges::RemotePlayer;
use crate::camera::Camera;
use crate::mesh::{build_chunk_mesh, emit_oriented_box, name_hash, Vertex};
use crate::state::RemotePlayer;
use crate::world::World;
use glam::{IVec3, Vec3};
use std::collections::HashMap;