Commit graph

99 commits

Author SHA1 Message Date
8154f9d372 Физган: чистая пружина, угол от камеры; игрок толкает пропы; модель
Some checks are pending
CI / checks (push) Waiting to run
- Убран feed-forward скорости цели: он складывался с реакцией на ту же
  ошибку и на круговых движениях мышью давал перелёт. Осталась пружина
  v = ошибка / timeToArrive (отставание 60 юнитов при 1200 — как в GMod),
  тест circular_target_does_not_overshoot.
- Grab хранит ориентацию относительно камеры (кватернион): при повороте
  взгляда проп поворачивается вместе с ним, E вращает в осях камеры, а не
  мировые углы. Тест grab_keeps_angle_relative_to_camera.
- Вьюмодель физгана: телефото-посадка (distance 80, scale 2.12, right/up 59),
  перспектива FOV 100 больше не растягивает упоры и руки; idle-анимация.
- Физика игрока: кинематическая капсула высотой 72 (add_character /
  move_character), пропы не проходят сквозь игрока; луч физгана игнорирует
  Character-тела (иначе начинался внутри своей капсулы). push_props толкает
  пропы в сторону движения с sqrt(10/mass) — лёгкие отлетают, тяжёлые едут
  медленно; замороженные и удерживаемые не толкаются; noclip уводит капсулу
  под карту.
- Lua: DTextEntry IsMultiline/GetMultiline/Get- и SetEnterAllowed, заглушки
  panel_meta (OpenURL) — исправлены OnKeyCodeTyped и падения таймеров меню.

Проверки: cargo fmt, clippy -D warnings, cargo test --workspace — зелёные;
новые тесты капсулы, луча, угла и кругового движения.
2026-10-25 10:14:31 +03:00
35cfe38fdd Физган: колесо — дистанция, E — вращение, undo только спавн, модель и CCD
- Отставание пропа: в rapier 0.36 `normalized_max_linear_velocity` по
  умолчанию 400, всё выше обрезалось на шаге (1200 → 400). Поднято до 100k;
  тест-страж body_with_set_velocity_actually_moves (1200 за секунду).
- Управление как в GMod: колесо тянет/толкает вдоль луча (`physgun_wheelspeed`,
  шаг 16, минимум 24), вращение — зажатый E + мышь (камера стоит). Держится
  полная ориентация (pitch/yaw/roll), угловая скорость — по кватерниону
  кратчайшей дуги, а не только yaw.
- Демпфирование: `physgun_maxSpeedDamping`/`maxAngularDamping` = 10000 —
  предел изменения скорости (damp_towards), а не самой скорости.
  `physgun_DampingFactor` в вики не указан, у поверхностей dampening 0.
- Undo как в оригинале: отменяется только спавн; Move/Freeze из истории
  физгана убраны.
- Вьюмодель физгана: `weapon_physgun.txt` → `c_superphyscannon.mdl`, анимация
  `idle` обязательна (bind-поза разваливается), посадка right 25/up −25/
  dist 34/scale 0.9, ствол вдоль +X.
- CCD: пропы с `ccd_enabled(true)`, `max_ccd_substeps` 4,
  `num_solver_iterations` 8 — быстрые пропы не пролетают сквозь карту
  (тест на 5000 юнитов/с).
- HUD: wheel dist | E rotate.

Проверки: clippy -D warnings, cargo test --workspace зелёные; кадр
/tmp/opencode/physgun_final.png.
2026-10-24 23:23:31 +03:00
4a8e75635e Физика: поднят предел скорости Rapier — физган больше не отстаёт
Some checks are pending
CI / checks (push) Waiting to run
Корень отставания пропа найден замером и подтверждён на чистом Rapier:
в rapier 0.36 по умолчанию `normalized_max_linear_velocity = 400`, и любая
заданная скорость выше 400 обрезается на шаге. Отсюда 1200 -> 400 и «проп
плетётся за игроком»: никакая пружина не помогала.

- PhysicsWorld::new поднимает `normalized_max_linear_velocity` до 100k
  (в Source скорости выше: бег 650, физган до 5000, падения — тысячи);
  защита от нефинитных значений остаётся;
- тест-страж body_with_set_velocity_actually_moves: тело с 1200 обязано
  проехать >1000 за секунду (до фикса — 400, тест падал);
- временный телепорт-костыль был откачен ранее; трение на удержании
  выключается (combine rule Min), на отпускании возвращается 0.6.

Проверено: 29 сьютов зелёные (включая новый тест скорости).
2026-10-24 22:07:29 +03:00
07bc51e332 Тест физики: неиспользуемый счётчик (clippy)
Some checks are pending
CI / checks (push) Waiting to run
2026-10-24 21:58:57 +03:00
ae36d3aea6 Физган: трение отключается на удержании; найден баг шага физики (скорость /3)
Some checks are pending
CI / checks (push) Waiting to run
Откачен костыль с телепортом: это было лечение симптома.

Что найдено замером (тест body_with_set_velocity_actually_moves, ignored):
тело с заданной скоростью 1200 за шаг едет только 400 — и это НЕ контакт:
воспроизводится без пола, с friction 0 (combine rule Min), gravity_scale 0,
damping 0.000, dt=1/60 до и после шага. Скорость 400 читается уже после
world.step(), то есть шаг физики гасит её ровно втрое. Это и есть причина
отставания пропа при любой пружине.

Плюс сделано по делу: на время удержания у тела выключается трение
(set_body_friction + combine rule Min), на отпускании возвращается 0.6 —
это убирает потерю скорости о пол.

Дальше: разобраться, почему шаг Rapier делит скорость на 3 (подозрение —
настройки integration_parameters/units в нашей обёртке), затем включить
assert в тесте.
2026-10-24 21:58:30 +03:00
26a61b0e7e Слияние feat/viewmodel: посадка вьюмоделей и встроенный weapon_pistol
Some checks are pending
CI / checks (push) Waiting to run
Из ветки feat/viewmodel (32aad74):
- встроенный SWEP weapon_pistol (Slot 1, ClipSize 18, одиночный
  PrimaryAttack, HUD 17/18) — регистрируется только если в ассетах нет
  своего lua/weapons/weapon_pistol.lua (оригинал приоритетнее);
- посадка вьюмодели по модели: таблица right/up/distance/scale/angles
  (w_pistol, v_pistol, c_smg1); ключ — ось ствола: у w_pistol это +Y,
  у v_pistol/c_smg1 — +X; общий view_yaw=-90 ломал флечетган;
- движок: ModelInstance.scale, set_model_instance_scale,
  play_model_animation_oriented / move_model_instance_oriented;
  ViewModelFOV пересчитывается в масштаб, поля SWEP (ViewModelOffset/
  Angles/Scale) перекрывают таблицу;
- слот 1 возвращает пистолет после флечетгана/кулаков, 0 скрывает
  вьюмодель (c_arms без геометрии); тест slot_keys_match_gmod.

Проверено: 28 сьютов зелёные; кадры всех трёх моделей (ближе_near=0,
ствол вперёд), HUD пистолета, переключения.
2026-10-24 21:42:24 +03:00
32aad74eb5 Вьюмодель: посадка по модели и встроенный weapon_pistol
Посадка вьюмодели вынесена в таблицу по модели: у w_pistol ствол вдоль +Y, у v_pistol/c_smg1 — вдоль +X, поэтому общие view_yaw/дистанция не работали. В движке появился масштаб экземпляра и полный доворот модели; для w_pistol/v_pistol/c_smg1 подобраны дистанция, смещение, масштаб и yaw, near-плоскость не режет ни одну модель.

weapon_pistol в HL2 — движковое оружие, его Lua-скрипта в ассетах нет. Добавлен встроенный SWEP (Slot 1/SlotPos 1, 18 патронов, одиночный огонь) с регистрацией в weapons и list.Get("Weapon"); пользовательский lua/weapons/weapon_pistol.lua важнее встроенного. ViewModelFOV и необязательные ViewModelOffset/Angles/Scale из SWEP перекрывают таблицу.

Проверено кадрами на Xvfb: пистолет и флечетган в кадре, ближе_near=0, выстрел 18→17, слот 1 после флечетгана и кулаков возвращает пистолет, c_arms по-прежнему скрывается. Добавлен SOURCE_RS_TEST_SLOT для синтетической клавиши слота (xdotool не доставляет клавиши в SDL-окно на Xvfb).
2026-10-24 21:40:00 +03:00
ed63cb68b8 Физган: дефолты GMod и захват за точку меша
Some checks are pending
CI / checks (push) Waiting to run
- пружина GMod: v = clamp(error / physgun_timeToArrive, physgun_maxSpeed),
  линейная зависимость вместо прежнего разгона;
- дальность 4096 (physgun_maxrange), масса 500, maxAngular 5000;
- трассировка луча до коллайдера, local_offset и удержание за точку на меше;
- поворот колесом вокруг точки захвата с компенсацией omega × r;
- тесты закона скорости, луча и локальной точки; docs/DIARY.
2026-10-24 21:21:37 +03:00
21abe0d4b7 Слияние feat/sandbox: физган, freeze и undo
Some checks are pending
CI / checks (push) Waiting to run
Из ветки feat/sandbox (8c5b852):
- физган как отдельный инструмент (F): захват ЛКМ по лучу (250 юнитов,
  масса до 500, игрок/статика не берутся), удержание через скорость с
  экспоненциальным приближением (не телепорт), гравитация на время захвата
  выключена, колесо — доворот, ПКМ — freeze (fixed↔dynamic), луч из «дула»
  и крест в точке захвата, жёлтый маркер заморозки;
- undo по Z (Shift+Z — redo), стек 32: спавн (тело/сущность убираются, меш
  уводится под карту), перемещение/поворот, заморозка; новое действие
  сбрасывает redo; World::restore_prop возвращает проп;
- бинды/HUD: G спавн, Q меню, Z/Shift+Z undo/redo, F физган, V noclip,
  0-9 оружие; подсказка в две строки, строка режима с дистанцией и undo;
- конфликты с feat/models разрешены: сохранены и подъём пропов до опоры
  (lift_spawned_body), и учёт масс тел (masses) для физгана.

Проверено: 28 сьютов зелёные; кадры физгана (луч/HOLD/заморозка), undo/redo
спавна (alive 10→9→10), клавиши через xdotool.
2026-10-24 20:31:07 +03:00
8c5b852613 Sandbox: физган, freeze и undo
- физган отдельным режимом F: захват лучом, плавное удержание скоростью,
  поворот колесом, freeze по ПКМ, дальность 250 и масса до 500
- луч/крест в отладочном слое и жёлтый маркер замороженного пропа
- undo-стек на 32 действия (Z/Shift+Z): спавн, перемещение, freeze
- World::remove_prop/restore_prop: удаление тела и сущности, скрытие меша
- env-проверки SOURCE_RS_TEST_PHYSGUN/UNDO/REDO, подсказки HUD
- тесты физики/мира, обновлены docs/DIARY и docs/PARITY
2026-10-24 20:27:04 +03:00
47bc9a75ef Модели: локальные индексы вершин VTX и опора пропов
Some checks are pending
CI / checks (push) Waiting to run
- MDL: разбор mstudiomodel_t/mstudiomesh_t (vertex_offset, material,
  vertex_index); VTX origMeshVertID складывается со смещением модели и
  меша, а не читается как глобальный индекс VVD. Это чинит «взрывы»
  второго меша у v_pistol/c_smg1 и многоматериальных пропов.
- Материал меша берётся из MDL; пустой список замен VTX больше не
  выдаётся за индекс материала.
- Тест-страж real_model_meshes_are_coherent: вырожденные треугольники,
  максимальное ребро и диапазон вершин своего меша.
- Физика: динамический проп при спавне поднимается до статической опоры;
  real_map_props_rest_on_ground меряет проникновение лучом без своего тела.
2026-10-24 20:21:08 +03:00
210182a940 Слияние feat/swep: реестр оружия в клиенте, слоты, атака, HUD
Some checks are pending
CI / checks (push) Waiting to run
Из ветки feat/swep (a46e4d8, 1fe36b6, 5a8f971):
- выбор SWEP из list.Get('Weapon') (фильтр Spawnable + существующая
  ViewModel), слоты 1..9, SOURCE_RS_SWEP для старта, Holster/Deploy при смене,
  вьюмодель из SWEP.ViewModel (SOURCE_RS_VIEW_MODEL — override);
- ЛКМ → PrimaryAttack через call_weapon_method, автоматический/одиночный огонь,
  Think каждый кадр; без SWEP — прежние физган и fire-анимация;
- HUD: PrintName и патроны Clip1/ClipSize;
- заглушки GMod-API под реальные скрипты: CurTime, IsValid, NULL, player.GetAll,
  util.TraceLine/TraceHull/TraceEntity/TracePhysics, trace.*, sound.*/EmitSound,
  константы PLAYER_ATTACK*/ACT_VM_*/MASK_*, база SWEP (GetOwner/GetViewModel/
  NetworkVar/SetNextPrimaryFire/SetHoldType/Clip1/EmitSound/SendWeaponAnim/
  SequenceDuration/ShootEffects/Remove), game.SinglePlayer как функция,
  GetConVar для sv_defaultdeployspeed/phys_pushscale;
- тест real_gmod_weapon_methods_run_without_errors прогоняет настоящие
  weapon_fists/flechettegun/medkit (SetupDataTables..Holster) без ошибок Lua.

Конфликт в crates/lua/tests/scripts.rs разрешён: сохранены тесты include и
геймода из feat/lua-pre и добавлен тест SWEP из feat/swep.
Проверено: 27 сьютов, 46+4+4 теста glua зелёные.
2026-10-24 13:33:50 +03:00
a46e4d827d Lua: база SWEP и GMod-заглушки для оружия
- swep_base.lua: GetOwner/GetViewModel, NetworkVar, SetNextPrimaryFire,
  Clip1, SetHoldType, EmitSound и безопасные заглушки;
- глобальные CurTime, IsValid, player.GetAll (таблица player),
  util.TraceLine/TraceHull, trace.*, sound.*, константы CLIENT/SERVER;
- game.SinglePlayer стал функцией (как в GMod), GetConVar знает
  sv_defaultdeployspeed/phys_pushscale — ноль давал inf в Deploy;
- загрузчик SWEP даёт базовые методы через метатаблицу, инициализирует
  SetupDataTables/Initialize и стартовый магазин;
- модель без геометрии (c_arms) теперь логирует причину, а не молчит;
- тест прогоняет реальные Deploy/PrimaryAttack/Think/Holster трёх SWEP.
2026-10-24 13:29:35 +03:00
209412ea3d Нуклип: Ctrl замедляет (как в GMod), а не опускает
Some checks are pending
CI / checks (push) Waiting to run
Пользователь: в оригинале Ctrl в ноуклипе замедляет движение. Убрано
`move_up -= 1` по Ctrl; скорость: Shift ×3, Ctrl ×0.35, иначе обычная.
Плюс тесты-сканы карты: поверхности с «зеркальными» материалами (0 в зале) и
сущности рядом с залом — подтверждают, что func_reflective_glass один
(model *6, точка -2064 -2076 -286).
2026-10-24 13:23:42 +03:00
b347bc380f Декали infodecal: квады на ближайшей грани и альфа-проход
Some checks are pending
CI / checks (push) Waiting to run
- сборка мира: для каждого infodecal берётся ближайшая плоскость грани (замер:
  у всех 19 декалей расстояние 0), центр проецируется на неё, квадрат строится
  по осям текстуры грани (32×32 юнита), материал — `decals/<имя>`;
- рендер: материалы с префиксом `decals/` идут отдельным проходом с
  ALPHA_BLENDING, без записи глубины, после непрозрачного мира; новый шейдер
  `fs_decal` берёт цвет из обычного освещения, альфу — из текстуры;
- декаль освещается ambient'ом в своей точке (`model_light_at`).
Проверено кадром: граффити с прозрачностью лежит на кирпичной стене.

25 сьютов и 29 картовых тестов зелёные.
2026-10-24 11:50:32 +03:00
b564de5b25 Зеркало func_reflective_glass: отражённая камера и клип по плоскости
Some checks are pending
CI / checks (push) Waiting to run
- сборка мира: модель зеркала помечается материалом-меткой `<mirror>` до
  резолва материала (у reflectiveglass001 нет VTF, иначе грань выпадала),
  из её граней строится плоскость MirrorPlane (origin, normal);
- рендер: проход отражённой камерой в текстуру Rgba16Float, клип по плоскости
  зеркала (vec4 в uniform камеры, discard в шейдере), грань зеркала сэмплирует
  отражение по экранным координатам;
- отражение привязано на слот лайтмапы (группа 3) отдельным bind group:
  у устройства max_bind_groups = 4, пятая группа не создаётся;
- обычный проход пропускает зеркальные батчи (иначе чекер-заглушка
  перекрывала отражение), зеркало рисуется после непрозрачного мира;
- найден и исправлен пропуск слота `sun` в Camera у world.wgsl (из-за него
  fog_color читался со сдвигом).

Проверено: 25 сьютов и 29 картовых тестов зелёные; UV-градиент на грани
подтвердил плоскость и шейдер, отражение показывает стену и потолок.
2026-10-23 19:26:00 +03:00
fc2e981bfc Освещение моделей от запечённого света + стабильный каскад теней
Some checks are pending
CI / checks (push) Waiting to run
Пользователь: «диван в светлой комнате тёмный, чуть ближе к свету — светлый,
хотя пол одинаковый» и «свет обновляется резко».

Причины две:
1. Модели вообще не получали свет карты: у них `lightmap_uv` указывал на белый
   тексель атласа, то есть ambient + солнце без привязки к месту. Теперь при
   сборке мира собираются `light_samples` (центр грани + средний свет её блока
   лайтмапы), а модели берут ambient в своей точке с плавным ядром по
   расстоянию (радиус 256, ядро 1/(d²+256)), с нижним порогом 0,35 — одна
   ближайшая грань давала скачки «тёмный/светлый». `lightmap_uv.x < 0` —
   метка шейдеру брать свет из вершины; свет обновляется и при движении
   (rewrite_instance и play_model_animation).
2. Каскад теней был 2600 юнитов вокруг камеры: объект у стены выходил за его
   границу и «вспыхивал» при приближении камеры. Теперь 4200 юнитов и карта
   4096² — вся игровая зона gm_construct попадает в тень.

Все сьюты зелёные.
2026-10-23 18:42:33 +03:00
bb35f6f0c3 Цветокор карты из игры (LUT 32³) + замеры визуальных сущностей
Some checks are pending
CI / checks (push) Waiting to run
- PostProcess: 3D-текстура LUT 32³ Rgba8Unorm (по умолчанию тождественная),
  сэмплер, биндинги 4/5 в композите; set_color_lut принимает raw Source
  (32³ RGB, красный меняется быстрее всего) и включает флаг use_lut.
- post.wgsl: после ACES цвет переводится в sRGB, сэмплируется LUT и
  возвращается в линейный (поверхность sRGB-ная).
- Клиент: читает materials/colorcorrection/<карта>.raw из VFS; для
  gm_construct файл есть. Отключение — SOURCE_RS_NO_COLORCORRECTION=1.
- Замер: gm_construct.raw нейтральный (0/66/132/195/255 по диагонали), то есть
  грейда у карты нет by design — цветокор включён, но визуально не меняет.
- Тест real_map_visual_entities: на карте есть func_reflective_glass (зеркало),
  env_tonemap_controller, 19 infodecal, env_sky/tonemap — записано в дневник.

Все сьюты зелёные.
2026-10-23 18:14:17 +03:00
8c869e709e Физика: углы Source ↔ кватернион тела совпадают с рендером
Some checks are pending
CI / checks (push) Waiting to run
`quat_from_angles` собирал поворот как Rz(roll)·Ry(yaw)·Rx(pitch), а рендер
(`rotate_model_vector`) — как Rz(yaw)·Ry(pitch)·Rx(roll). Из-за этого меш
двигался по одной ориентации, а тело — по другой: арбуз катился, но выглядел
как юла (жалоба пользователя).

Теперь конверсия в обе стороны точная; добавлен тест
`physics_angles_match_render_rotation` — пять наборов углов, проверка
кватернион→вектор против рендера и обратно.

Плюс тест `real_map_props_stay_stable`: 10 секунд физики на gm_construct —
ни один проп не поднимается выше 20 юнитов и не разгоняется выше 500 ед/с
(«пропы летают» от выталкивания геометрией не подтвердилось — 0 из 4).
2026-10-23 18:06:45 +03:00
4f8a989398 Тест: sort_by_key вместо sort_by (clippy)
Some checks are pending
CI / checks (push) Waiting to run
2026-10-23 18:03:02 +03:00
256cbdc9a0 Вьюмодель: не играть @idle мировой модели (поза брошенного оружия)
Some checks are pending
CI / checks (push) Waiting to run
Пользователь видел пистолет «повёрнутым на 90° влево». Причина: клиент
выбирал для вьюмодели анимацию `idle`, а у `w_pistol` единственная `idle` —
это `@idle` мировой модели, где RAWROT2/RAWPOS задают позу **брошенного на
землю** пистолета (ствол вбок, меш на боку). В руках эта поза не годится.

Теперь для вьюмодели берутся только `idle01`/`idle01empty` (у моделей `v_*`);
если их нет — остаёмся в позе покоя, которую доворачивает `model_yaw` (-90°).

Замер в осях камеры (новый диагностический лог SOURCE_RS_VIEW_DEBUG):
до правки «вперёд 1,7 / вправо 10,6» (ствол вбок), после — «вперёд 10,6 /
вправо 1,7» (ствол вперёд). Кадр подтверждает: пистолет смотрит вперёд, виден
сзади, рукоять вниз.
2026-10-23 18:02:37 +03:00
d42b581180 fs pack: минимальный набор ассетов карты + встроенные ассеты в APK
Замер: для gm_construct реально читаются 335 файлов из VPK на 27,0 МБ
(277 материалов, 23 модели, 35 Lua-скриптов; крупнейшее — m_anm.ani 6,8 МБ)
плюс карта 35,0 МБ. Итого 62,1 МБ сырых, 31 МБ в tar.gz — против 6,7 ГБ
полной установки. Этого набора достаточно: клиент запускается с ним, карта
и пистолет полностью текстурированы (проверено кадром).

- VirtualFs: запись успешных чтений (start_recording/take_recorded);
- команда `gmod-rs fs pack <карта> --out <каталог>`: собирает мир, грузит
  модели игрока и оружия, автозагрузку Lua и копирует прочитанные файлы в
  структуру garrysmod/... плюс карту — получается самодостаточный корень
  ассетов для `--assets`;
- android/build.sh: если есть android/assets/gmod (или GMOD_RS_ANDROID_ASSETS),
  каталог кладётся в APK как assets/gmod;
- MainActivity: при первом запуске распаковывает assets/gmod в
  files/gmod_server/server, если карты там ещё нет — APK самодостаточен,
  adb push не нужен;
- .gitignore: android/assets/ (ассеты GMod не коммитим).

Все сьюты зелёные.
2026-10-23 17:03:50 +03:00
1f42bb9628 Слияние feat/hdr: попиксельная лайтмапа, HDR, мягкие тени
Some checks are pending
CI / checks (push) Waiting to run
Из ветки feat/hdr (dbecb3c, 1a34203, 74db5f5, dec8b67):
- свет по полигонам: LUMP_LIGHTING раскладывается в атлас Rgba16Float
  (полочная упаковка, рамка 1 тексель), у вершины появился lightmap_uv,
  шейдер мира читает атлас на каждый пиксель с билинейной фильтрацией.
  На gm_construct: 6891 грань, 751 718 люкселей, атлас 2048² (32 МиБ).
  Фолбэк на старый вершинный свет, если атлас не собрался;
- HDR: мир/небо/вода в Rgba16Float, композит экспозиция → ACES → bloom
  (порог в ½ разрешения, два прохода, аддитивно), UI отдельно в sRGB;
- тени: PCF 9 отсчётов (центр + два кольца) с поворотом по мировой позиции
  и нормальным смещением, без acne;
- переключатели SOURCE_RS_NO_HDR/EXPOSURE/BLOOM/BLOOM_THRESHOLD/
  DEBUG_SHADOW/NO_SHADOW/NO_VSYNC/LOG_FPS;
- замер A/B: до правок ~24,5 fps, с HDR ~23,7, без HDR ~24,4 — разница в
  пределах разброса; узкое место — ~2661 draw call на проход, не HDR;
- тесты lightmap_atlas_maps_face_block и half_conversion_known_values.

Конфликт был только в docs/DIARY.md (взята секция из ветки).
Проверено: 25 сьютов, 29 картовых тестов, 15 world_real — зелёные; кадр
туннеля показывает плавный градиент вместо ступеней по полигонам.
2026-10-23 14:30:13 +03:00
74db5f50f1 fix(engine): предохранители упаковки лайтмап
Повреждённый BSP с огромным lightmap_size больше не может переполнить
арифметику упаковки или зациклить сборку: блок ограничен 4096 текселями и
1 МиБ площади, иначе грань остаётся без атласа (вершинный/белый путь).
2026-10-23 14:17:48 +03:00
dbecb3c6c5 feat(render): попиксельная лайтмапа, HDR и мягкие тени
Свет больше не интерполируется по вершинам: LUMP_LIGHTING раскладывается в
атлас Rgba16Float (рамка 1 тексель на блок), в вершине появился lightmap_uv,
шейдер мира читает атлас на каждый пиксель с билинейной фильтрацией. Это
убирает перелом света по рёбрам брашей (A10). Для карт без атласа остаётся
вершинный путь fs_main_vertex.

Мир, небо и вода рисуются в Rgba16Float; затем экспозиция, ACES и bloom
(порог в ½ разрешения, два прохода размытия, аддитивно). UI и диагностика
идут отдельным проходом в sRGB-поверхность. Переключатели: SOURCE_RS_NO_HDR,
SOURCE_RS_EXPOSURE, SOURCE_RS_BLOOM, SOURCE_RS_BLOOM_THRESHOLD.

PCF теней: 9 отсчётов с поворотом по мировой позиции и нормальным смещением,
режим SOURCE_RS_DEBUG_SHADOW. Плюс SOURCE_RS_NO_VSYNC и SOURCE_RS_LOG_FPS для
замеров.

Тесты: lightmap_atlas_maps_face_block, half_conversion_known_values; все
прежние сьюты зелёные. Замер E11: ~22,1 fps до, ~21,0 с HDR (~3% на пост),
NO_HDR ~21,7.
2026-10-23 14:10:58 +03:00
3de8b8945f Слияние feat/anim: RAW-дорожки, анимации по состоянию, вьюмодель
Some checks are pending
CI / checks (push) Waiting to run
Из ветки feat/anim (d42d646):
- распаковка постоянных дорожек: Quaternion48 (RAWROT), Quaternion64
  (RAWROT2), Vector48 (RAWPOS) — раньше такие кости застревали в позе покоя
  (в @taunt_muscle_base 24 дорожки из 53);
- engine::animation — выбор секвенции по состоянию: стоя/шаг/бег/присед/
  воздух, 8 направлений относительно взгляда, сохранение фазы walk↔run;
- клиент: покачивание вьюмодели при ходьбе, инерция взгляда, одиночный
  выстрел fire по ЛКМ, компенсация RAWPOS-сдвига корня (у v_pistol 68 юнитов).

Разрешение конфликтов: вьюмодель осталась в камерном базисе (мои правки —
смещения через up камеры, доворот model_yaw), анимации и покачивание взяты
из ветки; play_model_animation с шестым аргументом model_yaw во всех вызовах.

Проверено: 23 сьюта, 29 картовых тестов, 15 тестов world_real — зелёные.
2026-10-23 14:08:03 +03:00
a491010aa6 Вьюмодель: доворот в координатах модели и камерный базис; коллизия из меша
Some checks are pending
CI / checks (push) Waiting to run
Вьюмодель (жалобы пользователя):
- доворот ствола (-90°) теперь применяется в координатах модели, до поворота
  камеры: прибавка к мировому yaw при взгляде вниз превращалась в крен;
- смещения (вправо/вниз) задаются в базисе камеры, а не мировым (0,0,-12) —
  иначе при наклоне камеры вьюмодель уезжала к центру экрана («ось ниже»);
- play_model_animation принимает model_yaw (доворот модели), rewrite_instance
  тоже — путь без скелета больше не теряет ориентацию.

Коллизия пропов из настоящего меша:
- Physics::add_prop_hull — выпуклая оболочка вершин модели для динамических
  пропов (как vcollide в Source), вместо коробки с выступами;
- Physics::add_static_mesh — тримеш из треугольников модели для prop_static:
  раньше у статических пропов коллизии не было вовсе, игрок проходил сквозь
  мебель;
- PropMesh хранит индексы модели; оба места создания тел используют оболочку
  (с откатом на коробку, если вершин < 4).

Все сьюты и 29 картовых тестов зелёные.
2026-10-23 14:03:45 +03:00
d42d6464f6 A26: постоянные дорожки RAW и выбор анимации по состоянию
- formats: Quaternion48/Quaternion64/Vector48 (RAWROT, RAWROT2, RAWPOS),
  приоритет постоянных значений в frame_pose;
- engine::animation: выбор idle/walk/run/crouch/jump по скорости, приседу,
  земле и восьми направлениям относительно взгляда, сохранение фазы walk↔run;
- клиент: модель игрока играет выбранное имя, вьюмодель качается на ходу,
  отстаёт при повороте и проигрывает fire по ЛКМ; центр анимированной позы
  компенсирует RAWPOS корня;
- тесты: RAW на v_pistol/m_anm, имена и проигрывание секвенций, чистый выбор
  и фаза; docs A26 обновлены.
2026-10-23 14:02:05 +03:00
b52da2ed2e Коллизия пропов: смещение коллайдера от центра модели
Some checks are pending
CI / checks (push) Waiting to run
Коробка-коллайдер ставилась от нуля модели, хотя геометрия часто смещена
(у куклы центр AABB на 13,5 юнита выше нуля, у стола — 9,5 вбок по X). Из-за
этого предметы висели на невидимых выступах: их держала коробка, сдвинутая
относительно меша (жалоба пользователя, кадр с парящими столом и куклой).

- Physics::add_prop_offset — смещение коллайдера в координатах модели,
  поворачивается вместе с телом (Rapier ColliderBuilder::translation);
- оба места создания тел (пропы карты и spawn_prop) передают bounds_center;
- World::prop_collision_boxes — коробки для коллизии игрока с учётом центра и
  поворота (AABB описанной коробки по модулю матрицы поворота); клиент больше
  не собирает их вручную;
- тест physics::prop_collider_honours_model_center_offset — без смещения проп
  висит на 30+ юнитов выше пола, со смещением низ ложится на пол;
- тест real_map_props_rest_on_ground — после 10 с физики низ коробки каждого
  пропа на опоре (кукла лежит краем на столе, центровой луч этого не видит).

Все сьюты зелёные.
2026-10-23 13:46:39 +03:00
7490ef3798 Тесты моделей оружия: структура, VVD, скиннинг вьюмодели
Some checks are pending
CI / checks (push) Waiting to run
Замеры на реальных ассетах, которые пригодятся для дальнейших правок:
- v_pistol: 1 бодипарт, 2 меша (1169/4356 и 988/2448), 2157 вершин, 2167 в
  VVD (10 не используются), кадры лежат внутри MDL, флаги дорожек
  2/3/8/12 (RAWROT/RAWPOS/ANIMROT/ANIMPOS);
- скиннинг вьюмодели не разваливает меш: между позой покоя и кадром idle01
  максимальный сдвиг 4 юнита, габарит 20×23×13;
- v_pistol рисуется «лепестком» даже в позе покоя (до скиннинга) — проблема
  загрузки геометрии именно этой модели, отложена; по умолчанию w_pistol.

Все сьюты зелёные.
2026-10-23 13:28:49 +03:00
c1b35b6263 Дневник: сессия по жалобам — вьюмодель, спавн, HDR/Android/анимации в работе 2026-10-23 13:23:59 +03:00
f58eafeb46 Клиент: спавн не падает, вьюмодель на глазах, HUD без отладки
Some checks are pending
CI / checks (push) Waiting to run
Падение wgpu на втором спавне («Copy at offset ... overrunning the bounds»):
буферы GPU создавались под размер мира при загрузке и не росли. Появился
WorldRenderer::ensure_capacity (рост вдвое + перезаливка) и refresh_materials
(bind group для материалов, появившихся после спавна, — иначе новые батчи
молча не рисуются).

Вьюмодель:
- обновляется после player.update и camera.pos, а не до них: раньше она
  отставала на кадр при ходьбе и «плыла» при повороте (жалоба пользователя);
- по умолчанию w_pistol.mdl вместо v_pistol.mdl: v_pistol у нас рисуется
  развёрнутым полотном (2157 вершин, 2 меша — разбираться отдельно), а w_pistol
  выглядит пистолетом;
- курс -90°: у модели ствол смотрит вдоль +Y (влево), а не +X — без доворота
  пистолет лежал поперёк кадра;
- дистанция 26, сдвиг вправо/вниз, анимация idle01 (у w_pistol свои кадры
  в MDL).

bone_world_transforms теперь обходит кости рекурсивно: порядок «родитель
раньше ребёнка» в файлах не гарантирован.

HUD: журнал и отладочные строки скрыты по умолчанию (клавиша `), строка
verts/tris убрана.

Плюс тесты на реальных ассетах: кости вьюмодели не разваливаются при
скиннинге (макс. сдвиг 4 юнита), структура моделей оружия, встроенные кадры.

Все сьюты зелёные, clippy чистый.
2026-10-23 13:20:03 +03:00
8a5f43dcd2 A26: кадры анимации играют — модель игрока и вьюмодель
Some checks failed
CI / checks (push) Has been cancelled
Спека декодера взята из настоящего bone_setup.cpp (Mapbase-зеркало SDK 2013).
Прошлые догадки были неверны, и это зафиксировано в docs/PARITY.md: каналы
дорожек — не компоненты кватерниона, а углы Эйлера (raw · rotscale + поза
покоя, затем AngleQuaternion); позиции — raw · posscale + bone.pos; значения
блоков не накапливаются дельтами (ExtractAnimValue идёт по блокам valid+1
записей, внутри блока value[k], дальше повтор последнего); флаг DELTA означает
относительные каналы без позы покоя.

Замеры на живых данных, три теста в tests/world_real.rs:
- @taunt_muscle_base (451 кадр, 53 дорожки): углы ≤ 4,75 рад, сдвиги позиций
  ≤ 28,5 юнитов, кватернионы единичные — при новой спеке нарушений нет;
- кости локальны родителю (цепочка 4,0 / 3,5 / 8,9), корень — таз на 38,6;
- police.mdl: 57 костей, своя анимация @ragdoll лежит в самом MDL
  (anim_block == 0); m_anm.mdl: 1563 анимации в models/m_anm.ani (7,1 МБ);
- модель игрока, idle_all_01: из 3147 вершин 1101 сдвинулась между кадрами 0 и
  30, высота тела 61 юнит и в покое, и в анимации — не схлопывается.

Движок: AnimData (буферы кадров под Arc), frame_pose, play_model_animation
(мировые позы через родителей, A·B⁻¹ по костям, скиннинг весами вершин);
кости меша ищутся в скелете анимации по именам — скелеты совпадают по именам,
но не по числу костей. Кадры читаются и из .ani, и из самого MDL.

Клиент: вьюмодель играет idle, модель игрока — idle_all_01; добавлен вид со
стороны --third-person (иначе свою модель не увидеть: в первом лице камера
внутри неё). Map(Box<MapArgs>) — против clippy::large_enum_variant.

Все 23 сьюта и 29 картовых тестов зелёные.
2026-10-17 12:37:45 +03:00
2c7ec501e2 A26: чтение анимационного файла .ani (кадры)
Some checks are pending
CI / checks (push) Waiting to run
formats: mdl.animations() — описания анимаций (mstudioanimdesc_t) с определением
размера записи по данным (как у секвенций); anim_block_names() — имена
анимационных файлов; anim_blocks() — границы блоков внутри .ani.

Замеры: у m_anm.mdl 1563 анимации (@reference 101 кадр, @idle_all_01 280,
@idle_all_02 151, все 30 fps), 247 блоков, и ВСЕ имеют anim_block > 0 — то есть
кадры не в MDL, а в отдельном файле. Файл models/m_anm.ani прочитан целиком:
7,1 МБ. В конце таблицы имён блоков есть пустая запись — чтение по ней падает и
пишет предупреждение, это безвредно.

Дальше: декодирование mstudioanim_t (6 каналов на кость, дельты с накоплением,
повороты кватернионом), позы костей с учётом родителей, интерполяция между
кадрами и выбор секвенции по состоянию.

Все 23 сьюта зелёные; docs/PARITY.md A26 обновлён.
2026-10-17 11:40:57 +03:00
3c0bef5169 A26: слияние включённых моделей — у игрока 465 секвенций
Some checks are pending
CI / checks (push) Waiting to run
police.mdl включает models/m_anm.mdl (библиотека анимаций GMod). Включённая
модель грузится лёгким путём: Mdl::parse + секвенции, без геометрии и текстур —
у анимационной библиотеки их нет. Замеры: у m_anm.mdl numlocalseq=464,
numlocalanim=1563, 56 костей; после слияния у модели игрока 465 секвенций
(была одна «ragdoll»).

Находка по пути: у «единичных» (unified) моделей GMod запись секвенции длиннее,
чем у моделей карты, и мой первый набор кандидатов (184..208) не подошёл —
разбор молча возвращал ноль. Перебор расширен до 176..320 с обязательной
проверкой имён ВСЕХ секвенций: иначе шаг не принимается.

Все 23 сьюта зелёные; docs/PARITY.md A26 обновлён.
2026-10-17 11:32:37 +03:00
50ab90debc A26 шаг в: CPU-скиннинг — модель двигается на экране
Some checks are pending
CI / checks (push) Waiting to run
PropMesh теперь несёт скелет и привязку вершин, а World::skin_model_instance
пересчитывает вершины по позе костей: угол кости складывается с родительскими,
вершина смешивается по трём весам, результат пишется в общий буфер и помечается
диапазоном для заливки на GPU — тем же механизмом moved_ranges, что возят
движущиеся пропы.

Проба подключена к вьюмодели: одна кость пистолета качается по синусу.
Проверено кадром — геометрия visibly деформируется, то есть цепочка «скелет →
веса → вершины → GPU» работает целиком. Это пока проба, а не анимация:
настоящие кадры из .ani следующим шагом.

Все 23 сьюта зелёные; docs/PARITY.md A26 обновлён.
2026-10-17 11:25:15 +03:00
f8db4cf73c A26 шаг б: кости и веса вершин в модели
Some checks are pending
CI / checks (push) Waiting to run
Model теперь несёт skins: Vec<VertexSkin> — привязку вершин к костям из VVD
(до трёх костей с весами и их число), по одной записи на вершину. Индексы
глобальные (после fixup VTX), считаться по ним будет матрица скиннинга вместе
со скелетом Model::bones.

Замер на живых моделях (лог «модель загружена»): companion_doll 1471 вершин /
1471 привязок, стол 136/136, диван 556/556 — привязка есть у всех вершин; у
пропов при этом один корневой узел (bones=1). Модель игрока police.mdl — 57
костей.

Все 23 сьюта зелёные; docs/PARITY.md A26 обновлён.
2026-10-17 11:19:21 +03:00
d7796974b1 A26 шаг а: разбор секвенций анимации (mstudioseqdesc_t)
Some checks are pending
CI / checks (push) Waiting to run
Размер записи секвенции версионно-зависим (184…208 байт), поэтому шаг берётся
не на глаз: перебираются кандидаты, и выбирается тот, у которого у ВСЕХ
секвенций имя читается непустой печатной строкой. Модель движка теперь несёт
bones и sequences.

Замеры на живых моделях: пропы карты — bones=1, sequences=1, «idle»; модель
игрока models/player/police.mdl — bones=57, sequences=1 («ragdoll»). Второе не
ошибка: у игроков активности лежат во включённых моделях (mdl.include_models()),
их надо сливать — записано следующим шагом.

Все 23 сьюта зелёные; обновлён docs/PARITY.md A26.
2026-10-17 11:05:09 +03:00
0a6504e04c Тест: движение пропа обязано отдавать диапазоны вершин на GPU
Some checks are pending
CI / checks (push) Waiting to run
Диагностика превращена в обычный картовый тест: собираем коробку-проп вручную
(VFS в тестах пуст), поднимаем телом физики на 200 юнитов, шагаем секунду и
проверяем обе половины — вершины переписались и мир перечислил их диапазон для
заливки. Это охраняет причину жалобы «физика видит, а картинка стоит»: раньше
`move_prop` писал вершины своей копией кода мимо общего помощника и диапазоны
не запоминались.

29 картовых тестов и весь workspace зелёные.
2026-10-17 10:31:48 +03:00
2043ebf39a Спавн и вьюмодель: рендерер перечитывает батчи, буферы принимают заливку
Some checks are pending
CI / checks (push) Waiting to run
Ещё две поломки на пути «новой геометрии»:

1. Буфер вершин мира создавался без COPY_DST, поэтому точечная заливка
диапазонов падала с ошибкой wgpu (у пользователя это уронило бы клиент —
поймано прогоном, а не тестом). Теперь VERTEX | COPY_DST, для индексов
INDEX | COPY_DST, добавлен upload_index_ranges.

2. Рендерер копировал список батчей при сборке, поэтому добавленная геометрия
(спавн) в кадр не попадала до полной пересборки. Добавлен
WorldRenderer::refresh_batches: когда появились новые индексы, список батчей и
признаки воды перечитываются, а полная пересборка нужна только при новой
текстуре.

Вьюмодель (A23, первый срез): создаётся тем же рендером моделей, следует за
взглядом, включается SOURCE_RS_VIEW_MODEL (по умолчанию физган, откат на
монтировку — v_physgun.mdl в ассетах не нашёлся). В кадре она пока не
появляется: создание работает, а звено «перенос позы каждый кадр + заливка»
нет — записано открытым вопросом в docs/DIARY.md.

Все 23 сьюта и 28 картовых тестов зелёные.
2026-10-17 07:53:56 +03:00
2737469f6a Найдена и закрыта причина «физика зависла на экране»: move_prop шёл мимо диапазонов
Some checks are pending
CI / checks (push) Waiting to run
Причина оказалась точной и локальной: когда перезапись геометрии выносили в
общий `rewrite_instance` (ради модели игрока и спавна), `move_prop` остался со
своей **собственной копией** перезаписи вершин. Диапазоны он не запоминал,
поэтому физика двигала пропы в памяти, а на GPU их вершины не попадали — и
картинка обновлялась только при спавне, который пересобирал рендерер целиком.

Замер (probe_moving_prop_reports_vertex_ranges, проп карты поднят телом на 200
юнитов и брошен): до правки — «сдвинулось 1, диапазонов 0» каждый шаг; после —
«диапазонов 1 [(39585, 8)]» и вершина уезжает вниз вместе с телом.

Дополнительно:
- `take_moved_ranges` дедуплицирует диапазоны: за кадр один проп может
  перезаписаться несколько раз (подшаги), лишние заливки на GPU ни к чему;
- `append_model_geometry` сразу помечает свежую геометрию как требующую
  заливки;
- клиент пересобирает рендерер при спавне только если появилась **новая
  текстура** (иначе батчу пропа не найти bind group материала): повторный спавн
  того же пропа теперь дешёвый.

Все 23 сьюта и 28 картовых тестов зелёные.
2026-10-17 07:42:31 +03:00
c58ad8817e Физика и картинка: вершины движущихся пропов уходят на GPU
Some checks are pending
CI / checks (push) Waiting to run
Жалоба пользователя: «физика вроде работает, но позиция рендера обновляется
только при спавне нового». Причина: мир двигал вершины в памяти, а буфер
рендера заливался один раз при сборке мира — потому пересборка рендерера при
спавне и «лечила» картинку.

Сделано: World::moved_ranges и take_moved_ranges (диапазоны заполняет
rewrite_instance, то есть любое движение пропа, модели игрока или физгана),
WorldRenderer::upload_vertex_ranges — точечная заливка queue.write_buffer по
этим диапазонам, и вызов в кадре клиента сразу после шага физики.

Тест probe_moving_prop_reports_vertex_ranges (диагностика, ignored) пока не
сходится и это записано честно: коробка в тесте задана в мировых координатах,
а меш обязан быть в локальных (видно по двойному смещению вершины), и список
диапазонов после step_physics пуст — проверить is_sleeping у тела после
телепорта. Проверка в интерактиве: заспавнить арбуз и посмотреть, падает ли он
на экране.

Все 23 сьюта и 28 картовых тестов зелёные; разбор в docs/DIARY.md.
2026-10-17 07:33:11 +03:00
35e04409c4 A24: спавн пропа из игры — ядро спавн-меню
Some checks are pending
CI / checks (push) Waiting to run
Движок: геометрия модели вынесена в append_model_geometry (меш, материалы,
вершины с трансформацией, батчи со смещением индексов на вершины экземпляра).
Поверх неё World::spawn_prop — то же, что делает Q-меню GMod: геометрия, сущность
класса prop_physics (её видят Lua и физика), тело в мире физики по габаритам
меша, экземпляр пропа для move_prop.

Клиент: клавиша G — спавн пропа по взгляду (модель из SOURCE_RS_SPAWN_MODEL, по
умолчанию models/props_junk/watermelon01.mdl), и аргумент --spawn <модель> для
отладки и скриптов. Проп сразу попадает в габариты коллизии игрока — их клиент
пересобирает каждый кадр.

Проверено кадром: арбуз заспавнен в 96 юнитах по взгляду и отрисован
(--player-pos="-2438 -966 48", камера у "-2500 -1050 60|5 0"). Грабли записаны
в дневник: буферы рендера собраны на сборке мира, поэтому после спавна рендерер
пересобирается целиком — позже заменим до-аллокацией вершин.
2026-10-16 23:49:43 +03:00
8a2eb4ef60 A20/A23 первый срез: модель игрока в мире
Some checks are pending
CI / checks (push) Waiting to run
Первый срез модели игрока, чтобы не блокировать Sandbox: рендер тот же, что у
пропов (статический меш PropMesh), скелет и анимации следующим срезом.

Движок: ModelInstance (экземпляр модели без сущности) плюс
World::spawn_model_instance (модель с диска, материалы с дедупликацией, вершины
с трансформацией, батчи) и World::move_model_instance; перезапись геометрии
вынесена в общий rewrite_instance, им пользуются и пропы.

Клиент: модель создаётся после разбора --player-pos и едет за игроком (позиция —
ступни, курс — взгляд, развёрнутый на 180°, потому что у моделей Source ось Y
смотрит влево). Включается переменной SOURCE_RS_PLAYER_MODEL: пустое значение —
models/player/police.mdl, иначе путь к модели.

Проверено кадром: модель стоит на площадке в -2438 -966 48, текстуры на месте,
вершин в мире стало 46359 против 43212 без модели. Грабли записаны в дневник:
индексы модели локальные, при вставке в общий буфер к ним прибавляется
first_vertex (без этого кадр превращался в мусор).

Заведён docs/DIARY.md — живой дневник состояния, замеров, инструментов и
грабель, чтобы уплотнение контекста ничего не теряло.
2026-10-16 23:44:39 +03:00
257ac0792a A13: туман карты подключён к рендеру
Some checks are pending
CI / checks (push) Waiting to run
Fog читается при сборке мира (приоритет env_fog_controller, иначе worldspawn),
доля тумана считается линейно от fogstart к fogend с потолком fogmaxdensity —
как в движке. В рендере два новых vec4 в uniform камеры (цвет и параметры),
буфер камеры 112 → 144 байта; world.wgsl смешивает цвет с туманом по
расстоянию до камеры.

Замер на gm_construct: туман есть — цвет (0,557 0,643 0,698), start 1024,
end 250000, max_density 1,0, farz −1. Это дальний туман: на 2000 юнитов доля
≈ 0,4%, поэтому кадр с камеры E11 не изменился (трава 73,2, бетон 116,0,
небо 207,6, тоннель 81,1 — как в E20). Механика подключена и проверена; заметен
туман будет у карт с близким fogend.

Осталось: применить туман к воде и к моделям. Все 23 сьюта и 28 картовых
тестов зелёные. Разбор — docs/PARITY.md E24.
2026-10-16 23:18:12 +03:00
d35efef711 A13 (первая половина): туман карты читается в мир
Some checks are pending
CI / checks (push) Waiting to run
Начал пункт «туман» с чтения данных: в Source параметры тумана лежат в
env_fog_controller (её можно менять в игре), а если её нет — в worldspawn.
Разобраны fogcolor (R G B, 0..255 либо 0..1), fogstart, fogend,
fogmaxdensity и farz; приоритет у контроллера.

Туман хранится в World как Option<Fog> и печатается в лог сборки мира; у Fog
есть fraction(distance) — линейная доля от start к end с потолком плотности,
как в движке. При end <= start тумана нет вовсе (None) — шейдер его тогда не
рисует.

Замер на gm_construct: туман есть — цвет (0,557 0,643 0,698), start 1024,
end 250000, max_density 1,0, farz -1. То есть дальний серо-синий оттенок у
карты задан, и после подключения шейдера он появится.

Тест fog_parses_controller_and_worldspawn: приоритет контроллера, разбор
worldspawn, поведение fraction, отсутствие тумана. Все 23 сьюта и 28 картовых
тестов зелёные. Следующий шаг — отдать туман в шейдер мира.
2026-10-16 23:08:28 +03:00
72bad60d8c A19: подшаг больше не гасит скорость о склон — 35° с 65 до 147
Some checks are pending
CI / checks (push) Waiting to run
Диагностика (покадровый замер в probe_slope_angles + трасса step_move) на 35°:
равновесие скорости было x = 66,8, z = +41,8 при разгоне 41,7 за кадр. Расчёт
«каждый кадр теряется нормальная компонента» (v·cos²) сходится с замером,
значит скорость гасилась о склон при посадке подшага.

В движке вертикальная часть шага — трассировка вниз с переносом позиции,
скорость после неё не меняется (StayOnGround; в StepMove «Move down a stair» —
трассировка, а не TryPlayerMove). У нас посадка шла через move_aabb, и её
контакты гасили скорость ещё и в update.

Правки в step_move: при победе подъёма скорость берётся без гашения о склон, и
контакты посадки наружу не отдаются.

Замер probe_slope_angles: 5° 239, 10° 233, 15° 226, 20° 214, 25° 195, 30° 175,
35° 147 (было 65), 40° 132 (было 49), 44° 107 (было 39). Цель — 250 до 44°;
остаток теряется на прямом (скользящем) пути, который иногда всё ещё выигрывает
выбор. Следующий шаг записан в docs/PARITY.md E23.8.

Все 28 картовых тестов и весь workspace зелёные.
2026-10-16 22:57:28 +03:00
e638607ee1 Замер: скорость подъёма по склону падает с углом — ищем в StepMove
Some checks are pending
CI / checks (push) Waiting to run
Жалоба: «на рампе в 35 градусов замедление есть; чекни, как вообще физика с
наклонными поверхностями работает».

Добавлен замер probe_slope_angles: синтетический клин с углом от 5° до 44°,
шаг вверх две секунды. Скорость: 5° — 239, 10° — 232, 15° — 196, 20° — 152,
25° — 116, 30° — 88, 35° — 65, 40° — 49, 44° — 39 юнитов/с.

По движку до sv_walkable_normal (0,7 = 45,6°) замедления быть не должно: на
земле вертикальная скорость обнуляется, движение по горизонтали, а склон
догоняется StepMove и StayOnGround. Наш замер говорит, что скорость держит
скольжение по грани (при отсечении по наклонной плоскости из горизонтальной
скорости уходит нормальная компонента, а вертикаль тут же обнуляется), то есть
step_move на склоне отдаёт победу прямому ходу. В CGameMovement::StepMove из
двух кандидатов выбирается тот, кто ушёл дальше ПО ГОРИЗОНТАЛИ — потому в
гору скорость и не падает.

Разбор и приёмка записаны в docs/PARITY.md E23.6: до 44° скорость не ниже 95%
от 250 на ровном полу.
2026-10-16 22:15:48 +03:00
3bfd078c14 Рельеф: нормаль развёрнута наружу — стены по рёбрам полигонов ушли
Some checks are pending
CI / checks (push) Waiting to run
После предыдущей правки пользователь написал, что стены по полигонам
дисплейсментов остались. Замер объяснил: у ВСЕХ 11776 треугольников рельефа
нормаль из намотки смотрит вниз — намотка у нас «гео внутрь» (так её
зафиксировал фикс E18 для брашей). Построенный по такой нормали выпуклый объём
лежал **над** поверхностью, и его боковые грани вставали стеной по каждому
ребру полигона.

Рельеф — heightfield, наружу он смотрит только вверх, поэтому clip_triangle
теперь разворачивает нормаль, если она ушла вниз.

Замер (real_map_hill_walk_keeps_speed, восемь склонов 13–18°): скорость в гору
250–287 юнитов/с при 250 на ровном полу, сползание равно тормозному пути
ровного пола (26,54 юнита). Худшее из четырёх направлений в четырёх точках —
170–222 (это уклон в гору), в четырёх — 0,0–72,7: там направление упирается в
крутой соседний склон, и это ещё предстоит проверить отдельно (стена может быть
настоящей — крутизна выше sv_walkable_normal — или мнимой).

28 картовых тестов и весь workspace зелёные; разбор в docs/PARITY.md E23.5.
2026-10-16 22:12:20 +03:00
d88988fc67 Рельеф: треугольники отсекают шаг, как кирпичи — стены и сползание ушли
Some checks are pending
CI / checks (push) Waiting to run
Жалобы: «невидимые стены уже на дисплейсментах, и коллизии их не видно;
в гору скорость замедляется; прыгать рядом нельзя, но проходятся, если
подойти и прыгнуть; на наклонных дисплейсментах скатывается» — и уточнение
пользователя: «стены ровно по полигонам дисплейсментов».

Причина одна: треугольники рельефа участвовали только в выталкивании.
Тело, въехав в склон, выталкивалось по нормали треугольника, а нормаль
наклонной поверхности смотрит вниз-вбок. Отсюда всё сразу: стена на ровном
склоне, потеря скорости в гору, сползание, странные прыжки и стены по рёбрам
полигонов (выталкивание срабатывает на границе треугольника).

Как в движке: CDispCollTree трассирует треугольник как выпуклый объём —
плоскость поверхности плюс три боковые грани через рёбра.

Правка: отсечение по набору полупространств вынесено в Collision::clip_planes;
Collision::clip_triangle строит четыре плоскости (нормаль из намотки, боковые
(to − from) × n, ориентированные внутрь) и зовёт его. Треугольники добавлены в
отсечение move_aabb и в ground_trace; выталкивание осталось страховкой.

Замер (новый тест real_map_hill_walk_keeps_speed: восемь точек на ходибельных
склонах 13–18°, четыре направления, эталон на ровном полу): скорость на склоне
252,2 против 250,0 на полу; «тормозной путь» после отпускания ввода 26,40
против 26,54 — то есть мнимое сползание оказалось тем же тормозным путём, что
и на полу, сползания нет.

28 картовых тестов и весь workspace зелёные. Разбор — docs/PARITY.md E23.
2026-10-16 21:54:05 +03:00