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:
parent
549662ddc8
commit
55276b7ce0
4 changed files with 362 additions and 296 deletions
|
|
@ -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
298
src/bridges.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue