From 03edb27a33e58f44bcceb9ff84c8258ab0f5ad30 Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Fri, 16 Oct 2026 23:53:51 +0300 Subject: [PATCH] =?UTF-8?q?A21/A23:=20=D1=84=D0=B8=D0=B7=D0=B3=D0=B0=D0=BD?= =?UTF-8?q?=20=E2=80=94=20=D0=B2=D0=B7=D1=8F=D1=82=D1=8C=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=BF=20=D0=BF=D0=BE=D0=B4=20=D0=BF=D1=80=D0=B8=D1=86?= =?UTF-8?q?=D0=B5=D0=BB=D0=BE=D0=BC=20=D0=B8=20=D1=82=D0=B0=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BF=D0=BE=20=D0=B2=D0=B7=D0=B3=D0=BB?= =?UTF-8?q?=D1=8F=D0=B4=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Первый срез взаимодействия с пропами (то, что в GMod делает weapon_physgun): ЛКМ берёт ближайшее тело под прицелом — луч из глаз, расстояние 8..260 юнитов, допуск 40 в стороны, побеждает ближайшее; пока кнопка зажата, тело каждый кадр телепортируется на луч взгляда на взятом расстоянии (углы сохраняются); отпустил — тело свободно и падает само. Добавлен переключатель SOURCE_RS_PHYSGUN=1: держит ближайшее тело без мыши — нужен для проверки скриптом, а не руками (SDL-событий мыши в скрипте нет). Проверка: код собирается, clippy чистый, все 23 сьюта зелёные. Интерактивно проверить не смог — в скриптовых кадрах камера упирается в стены (наводить нечем). Проверять у кучи пропов в районе -2930 -1370 -64: посмотреть на проп, зажать ЛКМ. Записано в docs/DIARY.md. --- crates/client/src/map.rs | 87 ++++++++++++++++++++++++++++++++++++++++ docs/DIARY.md | 14 ++++++- 2 files changed, 99 insertions(+), 2 deletions(-) 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`