A21/A23: физган — взять проп под прицелом и таскать по взгляду
Some checks are pending
CI / checks (push) Waiting to run

Первый срез взаимодействия с пропами (то, что в GMod делает weapon_physgun):
ЛКМ берёт ближайшее тело под прицелом — луч из глаз, расстояние 8..260 юнитов,
допуск 40 в стороны, побеждает ближайшее; пока кнопка зажата, тело каждый кадр
телепортируется на луч взгляда на взятом расстоянии (углы сохраняются);
отпустил — тело свободно и падает само.

Добавлен переключатель SOURCE_RS_PHYSGUN=1: держит ближайшее тело без мыши —
нужен для проверки скриптом, а не руками (SDL-событий мыши в скрипте нет).

Проверка: код собирается, clippy чистый, все 23 сьюта зелёные. Интерактивно
проверить не смог — в скриптовых кадрах камера упирается в стены (наводить
нечем). Проверять у кучи пропов в районе -2930 -1370 -64: посмотреть на проп,
зажать ЛКМ. Записано в docs/DIARY.md.
This commit is contained in:
Niko Marmeladkov 2026-10-16 23:53:51 +03:00
parent 35e04409c4
commit 03edb27a33
2 changed files with 99 additions and 2 deletions

View file

@ -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<PathBuf>, 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<PathBuf>, 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<PathBuf>, 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 смотрит влево, поэтому курс

View file

@ -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`