diff --git a/crates/client/src/map.rs b/crates/client/src/map.rs index b6f7e4e..2407717 100644 --- a/crates/client/src/map.rs +++ b/crates/client/src/map.rs @@ -7,6 +7,7 @@ use fs::VirtualFs; use render::{Gfx, WorldRenderer}; use sdl3::event::{Event, WindowEvent}; use sdl3::keyboard::Keycode; +use sdl3::mouse::MouseButton; use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -388,6 +389,12 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { } } + // Физган: тело, которое держим, и расстояние удержания (A21/A23). + let mut grabbed: Option<(engine::physics::BodyId, f32)> = None; + // `SOURCE_RS_PHYSGUN=1` — держать ближайшее тело без мыши: нужно, чтобы + // проверять физган кадром и скриптом, а не руками. + let auto_grab = std::env::var_os("SOURCE_RS_PHYSGUN").is_some(); + 'running: loop { for event in events.poll_iter() { match event { @@ -396,6 +403,52 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { keycode: Some(Keycode::Escape), .. } => break 'running, + Event::MouseButtonDown { + mouse_btn: MouseButton::Left, + .. + } => { + // Физган: берём ближайшее тело под прицелом — по + // расстоянию вдоль луча, с допуском на толщину пропа. + let origin = player.eye_position(); + let dir = camera.forward(); + let w = world.borrow(); + let mut best: Option<(f32, engine::physics::BodyId)> = None; + for id in w.physics.body_list() { + let Some((position, _)) = w.physics.transform(id) else { + continue; + }; + let to_body = position - origin; + let along = to_body.dot(dir); + if !(8.0..=260.0).contains(&along) { + continue; + } + let perpendicular = (to_body - dir * along).length(); + if perpendicular > 40.0 { + continue; + } + if best.is_none_or(|(best_along, _)| along < best_along) { + best = Some((along, id)); + } + } + if let Some((along, id)) = best { + grabbed = Some((id, along)); + log.push_back(format!("физган: взял ({} юнитов)", along as i32)); + while log.len() > 8 { + log.pop_front(); + } + } + } + Event::MouseButtonUp { + mouse_btn: MouseButton::Left, + .. + } => { + if grabbed.take().is_some() { + log.push_back("физган: отпустил".to_string()); + while log.len() > 8 { + log.pop_front(); + } + } + } Event::KeyDown { keycode: Some(Keycode::G), repeat: false, @@ -498,6 +551,40 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { let mut w = world.borrow_mut(); w.step_physics(dt); w.recover_fallen_props(-8192.0); + // Физган: держим тело на луче взгляда, на взятом расстоянии. + if auto_grab && grabbed.is_none() { + let origin = player.eye_position(); + let dir = camera.forward(); + let mut best: Option<(f32, engine::physics::BodyId)> = None; + for id in w.physics.body_list() { + let Some((position, _)) = w.physics.transform(id) else { + continue; + }; + let to_body = position - origin; + let along = to_body.dot(dir); + if !(8.0..=320.0).contains(&along) { + continue; + } + if (to_body - dir * along).length() > 64.0 { + continue; + } + if best.is_none_or(|(best_along, _)| along < best_along) { + best = Some((along, id)); + } + } + if let Some((along, id)) = best { + grabbed = Some((id, along)); + } + } + if let Some((id, along)) = grabbed { + let target = player.eye_position() + camera.forward() * along; + let angles = w + .physics + .transform(id) + .map(|(_, angles)| angles) + .unwrap_or(math::Vec3::ZERO); + w.physics.teleport(id, target, angles); + } // Модель игрока едет за игроком: позиция — ступни, поворот — курс. if let Some(index) = player_model_instance { // У моделей Source ось Y смотрит влево, поэтому курс diff --git a/docs/DIARY.md b/docs/DIARY.md index c030c5c..c5e4f93 100644 --- a/docs/DIARY.md +++ b/docs/DIARY.md @@ -57,9 +57,19 @@ SOURCE_RS_PLAYER_MODEL= DISPLAY=:99 SDL_VIDEODRIVER=x11 cargo run -q -p client - пересобирается целиком (медленно, но работает); позже нужна до-аллокация вершин под спавн. +**Физган, первый срез (A21/A23).** ЛКМ — взять ближайшее тело под прицелом +(луч из глаз, расстояние 8..260 юнитов, допуск 40 в стороны), пока держим — +тело каждый кадр телепортируется на луч взгляда на взятом расстоянии; отпустил — +свободно. `SOURCE_RS_PHYSGUN=1` держит ближайшее тело без мыши (для скриптов). + +**Нужна интерактивная проверка физгана:** мои скриптовые кадры упираются в +стены — камеру без глаз не навести. Проверять так: встать у кучи пропов +(она в районе `-2930 -1370 -64`), посмотреть на проп, зажать ЛКМ. + **Следующее:** (а) до-аллокация вершин под спавн вместо пересборки рендерера; -(б) реестр SWEP из Lua и вьюмодель; (в) физган (луч → проп → таскание); -(г) скрытие своей модели, когда камера внутри неё; (д) скелет+анимации. +(б) реестр SWEP из Lua и вьюмодель; (в) вращение пропа колесом и заморозка у +физгана; (г) скрытие своей модели, когда камера внутри неё; (д) скелет+анимации +моделей; (е) консольные команды спавна на данных из Lua (`gm_spawn`). ## 3. Что уже сделано и лежит в `main`