diff --git a/src/state.rs b/src/app.rs similarity index 67% rename from src/state.rs rename to src/app.rs index 9ea70d4..1a8ebc3 100644 --- a/src/state.rs +++ b/src/app.rs @@ -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 = RefCell::new(TouchBridge::default()); - static GAME_STATUS: RefCell = RefCell::new(GameStatus { - hp: 20, - alive: true, - respawn_requested: false, - }); - static NET_BRIDGE: RefCell = RefCell::new(NetBridge::default()); - static SETTINGS: RefCell = 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, - pub outbox: Vec, - pub pending_name: Option, - pub my_id: Option, - pub remote_players: HashMap, -} - -#[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 { - 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>, @@ -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 = - 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) { 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 = 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 { } } } - diff --git a/src/bridges.rs b/src/bridges.rs new file mode 100644 index 0000000..b0d95ef --- /dev/null +++ b/src/bridges.rs @@ -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, + pub outbox: Vec, + pub pending_name: Option, + pub my_id: Option, + pub remote_players: HashMap, +} + +/// 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 = RefCell::new(TouchBridge::default()); + static GAME_STATUS: RefCell = RefCell::new(GameStatus { + hp: 20, + alive: true, + respawn_requested: false, + }); + static NET_BRIDGE: RefCell = RefCell::new(NetBridge::default()); + static SETTINGS: RefCell = 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>`) 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(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 { + 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(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) { + 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 { + 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 { + 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(); + } +} diff --git a/src/lib.rs b/src/lib.rs index afb2c15..f0fdf18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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"))] { diff --git a/src/render/mod.rs b/src/render/mod.rs index d421346..7a555ad 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -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;