844fa6d767
Порт seg003:0798..08A9 + seg004:0239 + seg002:081D/1131 + seg006:1945. - is_obstacle (pop_map.c): ветка зеркала — Кид, кадры бегового прыжка 39..43, направление ВЛЕВО -> modif = 0x56, pop_jumped_mirror = -1, препятствия нет (пролетает насквозь). - mirror_image / jump_through_mirror / pop_check_mirror (pop_map.c): отражённый Char уходит в слот Guard как CHARID_1_SHADOW, guardhp = hitp_max, у Кида hitp_curr = 1. savekid НЕ делается — как в оригинале, отражается только копия. Полосы HP перерисует pop_hp_draw сам. - pop_check_mirror() зовётся из главного цикла ПЕРЕД отрисовкой персонажей (в оригинале — первая строка draw_people, seg008:228A). - autocontrol_shadow + autocontrol_shadow_level4 + clear_char (guards.c): тень идёт СВОЕЙ веткой целиком, к стражьему ИИ не сводится — на уровне 4 она не дерётся, а бежит влево и при x < 80 исчезает. АТЛАС ТЕНИ — вскрылось при чтении seg006:0532. Тень вне боевых кадров 150..189 ходит по таблице КИДА, и image оттуда индексирует спрайты Кида, а не стража. Выбор атласа в pop_cdraw шёл по СЛОТУ, то есть тень рисовалась бы спрайтами стража. Условие вынесено в pop_frame_tbl_is_guard() (pop_kid.c) — его теперь читают и load_frame, и отрисовка, разъехаться не могут. Заодно из pop_load_fram_det_col выделен pop_load_frame() без determine_col: jump_through_mirror берёт ось отражения из curr_col, и пересчёт колонки по x там был бы вреден. Константы MIRROR_* и DIR_56_NONE переехали в pop_guard.h — нужны и постановке тайла (банк 6), и ИИ тени (банк 1). Звука sound_45_jump_through_mirror в порте нет, пропущен. Шаг 5 (клип тени слева от зеркала) НЕ сделан и оказался не однострочником: в pop_cdraw есть клип сверху/снизу/справа, левого нет вовсе — нужен новый примитив либо срез исходных колонок. Расписано в TASKS_OPEN. Цена: _CODE +18 Б, банк 1 2311 -> 2367, банк 3 10328 -> 10551, банк 4 8243 -> 8267. tests-host: все 5 наборов прошли. НЕ ПРОВЕРЕНО В MAME — сценарий проверки записан в TASKS_OPEN. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
882 lines
71 KiB
Markdown
882 lines
71 KiB
Markdown
# roomtest — доска ОТКРЫТЫХ задач (обновлено 2026-08-08)
|
||
|
||
Что берём в работу сейчас и в каком порядке. Каждая запись: что сделать,
|
||
почему именно сейчас, чем подтверждать результат.
|
||
|
||
- закрытые задачи с протоколами и замерами — [`TASKS_CLOSED.md`](TASKS_CLOSED.md);
|
||
- открытые баги — [`bug_list.md`](bug_list.md), закрытые с разбором корней —
|
||
[`bug_closed.md`](bug_closed.md);
|
||
- планы фаз — `../docs/PORT_PLAN.md`, `../docs/layout_plan_v2.md`,
|
||
`../docs/levels_plan.md`.
|
||
|
||
**Состояние на 2026-08-07:** пользователь прогнал ВСЕ комнаты уровней 1 и 2 —
|
||
крупных багов нет. Приёмки [L1-PASS](TASKS_CLOSED.md#l1-pass) и
|
||
[L2-PASS](TASKS_CLOSED.md#l2-pass) закрыты; с прогона открыты три записи по
|
||
уровню 2, из них цвет стража и брызги уже закрыты (см.
|
||
[`bug_closed.md`](bug_closed.md)); открытым остался чит `+`/`−` в бою.
|
||
Уровень 3: **скелет сделан и проверен в MAME 2026-08-07** (бой, падение в
|
||
пропасть, окклюзия), чомперов ещё нет.
|
||
|
||
**Разгрузка банка 2 сделана 2026-08-08** ([MEM-BANK2](TASKS_CLOSED.md#mem-bank2)): 90.4 % →
|
||
**35.8 %, свободно 10 512 Б** — холодная половина уехала в банк 7
|
||
(`pop_room.c`), чомперам места с запасом.
|
||
|
||
**DRAW-CHAR сделана и ПРОВЕРЕНА 2026-08-08** (протокол и замеры —
|
||
[`TASKS_CLOSED.md`](TASKS_CLOSED.md#draw-char)): отрисовка теперь одна на
|
||
всех `Char`. Банку 2 она дала всего +265 Б — место под чомперов дала уже
|
||
[MEM-BANK2](TASKS_CLOSED.md#mem-bank2).
|
||
|
||
Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга;
|
||
диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не
|
||
гипотезой (memory `defer_unexplained_quirks`).
|
||
|
||
---
|
||
|
||
## ТЕКУЩАЯ ЦЕЛЬ: уровни 1–3 (подземелье) отлажены целиком
|
||
|
||
Решение 2026-08-04: **palace (уровни 4+) откладываем**, доводим до
|
||
играбельности три dungeon-уровня. Основание — они не требуют ни одного
|
||
нового ассета фона: инвентарь тайлов, снятый с `res200N.bin`, показывает,
|
||
что новое появляется только так —
|
||
|
||
```
|
||
ур. 1 empty, floor, spike, pillar, gate, closer, potion, loose, debris,
|
||
opener, level_door L/R, torch, wall, skeleton, sword ← всё есть
|
||
ур. 2 bigpillar_bottom(8), bigpillar_top(9), doortop(12) ← есть (2026-08-04)
|
||
ур. 3 chomper(18) ← НЕТ механики
|
||
ур. 4 lattice_pillar(25)…lattice_right(29) + тайлсет palace ← отложено
|
||
```
|
||
|
||
| # | Задача | Что | Блокирует |
|
||
|---|--------|-----|-----------|
|
||
| 1 | [GUARD-PHYS](#guard-phys) | физика стража = физика Кида (одна над `Char`) | **ядро сделано 2026-08-07**; открыт живой сценарий в MAME |
|
||
| — | [DRAW-CHAR](TASKS_CLOSED.md#draw-char) | отрисовка одна на всех `Char` — **закрыта и проверена 2026-08-08** | — |
|
||
| — | [MEM-BANK2](TASKS_CLOSED.md#mem-bank2) | разгрузка банка 2 — **закрыта 2026-08-08**: 90.4 % → 35.8 %, свободно 10 512 Б | — |
|
||
| 2 | [L3-CHOMP](#l3-chomp) | **СЛЕДУЮЩАЯ**: чомперы (5 шт) | прохождение ур. 3 |
|
||
| — | [L3-SKEL](#l3-skel) | скелет ур. 3 — **сделан 2026-08-07**, ждёт финальной приёмки | — |
|
||
| 3 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели |
|
||
| 4 | [L4-MIRROR](#l4-mirror) | **АКТИВНА с 2026-08-10 по решению пользователя**: зеркало уровня 4 + тень | прохождение ур. 4 |
|
||
| — | [DRAW-COST](#draw-cost) | **кадр уложился в бюджет 2026-08-09**: 470 964 -> 425 600 тактов, период цикла 4 растровых кадра -> **3** (игра быстрее на треть). Дальнейшее — запас, не срочность | плавность на ВСЕХ уровнях |
|
||
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
|
||
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
|
||
|
||
Сделанное по этой цели — в [`TASKS_CLOSED.md`](TASKS_CLOSED.md): L2 (машинерия
|
||
уровней), L2-PASS, L1-PASS, [L3-CHKP](TASKS_CLOSED.md#l3-chkp) (чекпойнт
|
||
уровня 3), MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
|
||
|
||
---
|
||
|
||
## Ждёт ФИНАЛЬНОЙ приёмки уровней 1–3
|
||
|
||
Сюда попадает то, что уже работает в проверочном прогоне, но должно быть
|
||
подтверждено на сквозных прогонах уровней — потому что задевает механику
|
||
шире, чем собственный сценарий.
|
||
|
||
- **Зацеп ПРЯМО В ПРЫЖКЕ** (`POP_ENABLE_JUMP_GRAB`, `pop_tune.h`, сделан
|
||
2026-08-06, предварительно проверен пользователем). Почему нужен именно
|
||
финальный прогон: точки вызова стоят не только в `check_action`, но и в
|
||
ОБЕИХ ветках `check_bumped` — то есть код вклинивается перед обычным
|
||
ударом о стену. Регрессия проявится не в самом зацепе, а рядом: удар о
|
||
стену с зажатым Shift, осторожный шаг у стены, отскок в прыжке. На
|
||
уровнях 1–3 это надо специально потрогать в паре мест каждого уровня.
|
||
Напоминание: в ВАНИЛИ этого зацепа нет (у SDLPoP — `enable_jump_grab`),
|
||
так что сверять его с оригиналом «как есть» нельзя — только с SDLPoP при
|
||
включённых enhancements.
|
||
Прогон 2026-08-07 (уровни 1 и 2) регрессий рядом не показал, но специально
|
||
на удар о стену с Shift не проверялся.
|
||
|
||
## P0 — делаем сейчас
|
||
|
||
### <a id="guard-phys"></a>GUARD-PHYS. Страж живёт по тем же правилам, что Кид — **ЯДРО СДЕЛАНО 2026-08-07**
|
||
|
||
> **Что уже работает** (решение пользователя: переносим физику на `Char`,
|
||
> без предварительных замеров — иначе третий-четвёртый экземпляр той же
|
||
> логики неизбежен).
|
||
>
|
||
> - **физика переведена на `Char`**: `pop_map.c` целиком работает с активным
|
||
> персонажем, а кто в `Char` — решают окна `loadkid`/`savekid` и
|
||
> `loadshad`/`saveshad`, как в оригинале (seg006:809). Два входа:
|
||
> `pop_phys_tick` (порт хвоста play_kid_frame) и `pop_guard_phys_tick`
|
||
> (порт play_guard_frame) — списки вызовов отличаются ровно тем, чем в
|
||
> оригинале;
|
||
> - **`take_hp` стал общим** (`pop_take_hp` в резиденте `pop_guard.c`): урон
|
||
> идёт тому, кто в `Char`, по `charid`. Раньше у боёвки и у физики были
|
||
> свои копии, причём у физики неверная — правила `hitp_curr` мимо дельты и
|
||
> про не-Кидов не знала;
|
||
> - **порт веток по charid в `land()`** (seg005:173): страж гибнет с двух
|
||
> рядов, тень падает как с одного, у не-Кида приземление даёт боевую
|
||
> стойку; `check_guard_bumped` (seg004:0522), `droppedout` +
|
||
> `guard_follows_kid_down` (seg002:09F8), `check_guard_fallout`
|
||
> (seg002:0241);
|
||
> - **`pop_savekid_state` снова полное `Kid = Char`**, а
|
||
> `pop_load_fram_det_col` пересчитывает колонку ЛЮБОМУ персонажу — обе
|
||
> заплатки существовали только потому, что физика знала один `Kid`.
|
||
>
|
||
> **Проверено:** `tests-host` — трассы Кида не изменились (`[phys] ok: 1723`,
|
||
> тот же эталон), новый набор `t_char` (32 проверки) покрывает ветки по
|
||
> персонажам; в MAME проверено, что игра жива (респавн, бег, падение,
|
||
> приземление). Цена: `_CODE` +80 Б, банк 3 (`pop_map`) 7973 → 8485
|
||
> (51.8 %), банк 1 (`guards`) 2042 → 2156.
|
||
>
|
||
> **Проверено пользователем 2026-08-07:** страж СПРЫГИВАЕТ ЗА КИДОМ на ряд
|
||
> ниже — связка «ИИ + физика» работает вживую, не только в тестах.
|
||
>
|
||
> **`follow_guard` портирован и проверен в MAME (2026-08-07):** уровень 1,
|
||
> бой в комнате 3, Кид отступает влево — страж приходит следом (`Guard.room`
|
||
> 3 → 2, X перенесён через шов), ровно как в SDLPoP. Условия отбора
|
||
> покрыты тестами `t_char` (7 сценариев: пороги 91/165, «не бой», мёртвый,
|
||
> вверх/вниз, занятая соседняя комната). **Сцена вскрыла отдельный баг —
|
||
> [BUG-SWORD-GHOST-1](bug_list.md#bug-sword-ghost-1): при переходе в бою Кид
|
||
> прячет меч и дальше дерётся пустой рукой.**
|
||
>
|
||
> **Осталось (потому и запись открыта):**
|
||
> 1. `check_chomped_guard` — вместе с [L3-CHOMP](#l3-chomp);
|
||
> 2. ветки `check_guard_fallout` для тени и скелета (скелет возрождается в
|
||
> комнате 3) — вместе с [L3-SKEL](#l3-skel);
|
||
> 3. страж, нажимающий напольную кнопку, вживую не проверялся (код —
|
||
> общий `check_press`).
|
||
|
||
**Почему это была ОДНА физика, а не «сделаем стражу свою».** В оригинале
|
||
слот `Guard` — не «стражи», а все НЕ-Киды: тем же `play_guard_frame` ходят
|
||
**страж** (charid 2), **скелет** (4, ур. 3), **тень** (1, ур. 4/5/6/12),
|
||
**визирь-Джаффар** (ур. 13), **толстяк** (FAT, ур. 12) и **мышь** (0x18,
|
||
ур. 8) — `tbl_guard_type` = {0,0,0,2,0,0,1,0,0,0,0,0,4,3,−1,−1}, а
|
||
`autocontrol_opponent` (seg002:628) разводит их ТОЛЬКО по ИИ. То есть
|
||
второй экземпляр логики пришлось бы делать не один раз, а пять.
|
||
|
||
**Гард по X: страж не уходит САМ — но его МОГУТ ПЕРЕНЕСТИ.** Поправка к
|
||
формулировке, которая была здесь раньше («страж не покидает комнату ни в
|
||
каком виде») — она неверна, контрпример дал пользователь: в SDLPoP страж из
|
||
комнаты 3 оказывается в комнате 2 вслед за отступающим Кидом.
|
||
|
||
Разделять надо два разных механизма:
|
||
|
||
- **своим ходом — не может.** Физика персонажа слота Guard обёрнута
|
||
`Char.room == drawn_room` и `Char.x >= 44 && Char.x < 211` (seg000:1252),
|
||
и никакого `check_leave` в его списке вызовов нет. Провалившегося ниже
|
||
комнаты убирает `check_guard_fallout` (seg002:0241) — вниз он не уходит.
|
||
- **следом за Кидом — переносит движок.** `exit_room` (seg002:03C7)
|
||
вызывается ПОСЛЕ того, как комнату сменил Кид, и решает судьбу стража:
|
||
|
||
```c
|
||
if (Guard.alive < 0 && Guard.sword == sword_2_drawn) { // жив и В БОЮ
|
||
if (guards_tile[kid_room−1] >= 30 || // в новой комнате
|
||
guards_seq_hi[kid_room−1] != 0) { // своего живого нет
|
||
if (ушёл ВЛЕВО) { if (Guard.x >= 91) leave = 1; } // страж далеко — остаётся
|
||
else if (ВПРАВО) { if (Guard.x < 165) leave = 1; }
|
||
else if (ВВЕРХ) { if (Guard.curr_row >= 0) leave = 1; } // всегда → не идёт
|
||
else { if (Guard.curr_row < 3) leave = 1; } // вниз → не идёт
|
||
} else leave = 1;
|
||
} else leave = 1;
|
||
leave ? leave_guard() : follow_guard();
|
||
```
|
||
|
||
`follow_guard` (seg002:039E) стирает `guards_tile` в ОБЕИХ комнатах
|
||
(0xFF — «стража здесь нет», чтобы он не раздвоился) и гонит стража через
|
||
`goto_other_room` в окне `loadshad`/`saveshad`.
|
||
|
||
**Что это значит для нас.** У нас в `enter_room` (`roomtest.c:265`) стоит
|
||
безусловный `pop_guard_leave()` — то есть всегда ветка `leave_guard`, и
|
||
страж ВСЕГДА остаётся. Портировать надо сам `exit_room`-выбор: условия
|
||
«жив + меч вынут + в целевой комнате нет своего стража + он у нужного
|
||
края» и `follow_guard`. Пороги 91/165 — это «страж у того края, в который
|
||
ушёл Кид»; вверх и вниз оригинал не пускает никогда.
|
||
|
||
**Чем подтверждать:** уровень 1, комната 3 — начать бой, отступить влево в
|
||
комнату 2: страж обязан прийти следом и продолжить бой (как в SDLPoP на
|
||
скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ
|
||
живой страж — переход не происходит.
|
||
|
||
### <a id="l3-chomp"></a>L3-CHOMP. Чомперы — механика уровня 3
|
||
|
||
**Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти
|
||
на каждом уровне, так что это вложение не только в ур. 3.
|
||
|
||
**Что портировать:** `animate_chomper` (seg007) — состояние в `room_modif`,
|
||
как у пик/ворот, значит шаблон уже отработан (`gates_spikes_plan.md`);
|
||
коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка
|
||
кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`).
|
||
|
||
**Место в банке 2 расчищено** ([MEM-BANK2](TASKS_CLOSED.md#mem-bank2), 2026-08-08):
|
||
35.8 %, **свободно 10 512 Б** (было 1074 Б) — холодная половина слоя фона
|
||
уехала в банк 7 (`pop_room.c`). Считать место ДО кодинга всё равно
|
||
обязательно (`levels_plan.md` §5.1); чомпер — это анимация тайла (холодная
|
||
перерисовка), поэтому его отрисовка ложится в `pop_room.c`, а состояние —
|
||
в `room_modif`, как у пик и ворот.
|
||
|
||
**Ассеты УЖЕ упакованы (2026-08-07):** `pop_pack_bg.py` кладёт в атлас весь
|
||
набор кадров чомпера явно (`CHOMPER_BOT_IDS` 101-105, `CHOMPER_TOP_IDS`
|
||
111-113, `CHOMPER_FORE_IDS` 106-110 + кровь 114-123 mono-силуэтом цветом 12).
|
||
Раньше в атласе не было НИ ОДНОГО его кадра: `render_room.py` пропускает
|
||
анимированные тайлы, а `tile_table[0x12].base_id = 0` — отсюда и «чомпера не
|
||
видно вовсе». Рост: `pop_env3.atl` 5200 -> 12937 Б, `pop_fore.atl` 7763 ->
|
||
12359 Б, число EMM-страниц НЕ изменилось (7).
|
||
|
||
**Чем подтверждать:** комната 22 уровня 3 — чомпер (2,6) анимируется и
|
||
рисуется (сейчас его не видно вовсе, см. раздел «Уровень 3» в
|
||
[`bug_list.md`](bug_list.md)); смерть Кида в сомкнутых челюстях.
|
||
|
||
### <a id="l3-skel"></a>L3-SKEL. Скелет — единственный противник уровня 3
|
||
|
||
**Важно:** в данных уровня 3 **стражей нет вообще** (`guards_tile` пуст во
|
||
всех 24 комнатах). Единственный враг — скелет, и он не «страж из данных»,
|
||
а **спецсобытие** `check_skel` (seg002:1044):
|
||
|
||
> в комнате 1, когда `Kid.curr_col` == 2 или 3 и дверь уровня открыта,
|
||
> тайл `tiles_21_skeleton` (комната 1, тайлпос 15) стирается в пол, а на
|
||
> его месте поднимается персонаж: `charid_4_skeleton`, меч сразу вынут,
|
||
> `seq_88_skel_wake_up`, skill 2, HP 3.
|
||
|
||
Ещё два тайла скелета (комнаты 17 и 19) — декорация, они не оживают.
|
||
|
||
> **Сделано 2026-08-07 (ждёт живой проверки на уровне 3):**
|
||
> - **атлас скелета** — `pop_pack_guard.py SKEL` собирает `poc/res/skel/g0..g3.atl`
|
||
> (28 кадров, 30 КБ, 4 EMM-страницы); упаковщик получил параметр набора
|
||
> (`GUARD`/`SKEL`). Палитра у скелета СВОЯ (`SKEL/res750.pal`), а не из
|
||
> `res10.bin`: на уровне 3 `tbl_guard_type != 0`, значит
|
||
> `curr_guard_color = 0` и оригинал палитру не подменяет вовсе;
|
||
> - `pop_guard_load` выбирает набор по типу уровня и заливает палитру
|
||
> скелета; Makefile кладёт атласы в `SKEL\` на диск;
|
||
> - **`check_skel`** (seg002:1042) — порт в `guards.c`: уровень 3, комната 1,
|
||
> дверь уровня открыта, `Kid.curr_col` 2 или 3, тайл 21 на (5,1) → тайл
|
||
> стирается в пол (обе страницы), персонаж встаёт с `seq_88_skel_wake_up`,
|
||
> мечом наголо, skill 2, HP 3. Зовётся из главного цикла ДО логики
|
||
> персонажей, как в `play_frame`;
|
||
> - **`leveldoor_open`** — флаг появился (`pop_state.c`), взводит анимация
|
||
> двери при `modif >= 43` (seg007:456);
|
||
> - **`enter_guard`** — ветка `charid_4_skeleton`: встаёт сразу активным
|
||
> (меч вынут), а не в стойке покоя;
|
||
> - **возрождение** — `check_guard_fallout`: упавший скелет, под комнатой
|
||
> которого лежит комната 3, появляется там снова (x 133, ряд 1);
|
||
> - **ИИ** — `autocontrol_skeleton` (seg002:685): меч у скелета вынут всегда.
|
||
>
|
||
> Регресс: `tests-host` зелёные, `t_char` вырос до 65 проверок (добавлены два
|
||
> сценария возрождения). Цена: `_CODE` +80 Б, банк 1 (`guards`) 2342 → 2519,
|
||
> банк 4 (`gdraw`) 3875 → 4269.
|
||
>
|
||
> **Не проверено вживую:** сцена требует пройти уровень 3 до кнопки, которая
|
||
> открывает выход — без открытой двери скелет по условию не встаёт.
|
||
|
||
**Что нужно:**
|
||
- `charid_4_skeleton` в `enter_guard`/`pop_guard_enter` (seg002:196): при
|
||
`tbl_guard_type[level] == 2` персонаж поднимается **с вынутым мечом** и
|
||
последовательностью `seq_63_guard_active_after_fall`, а не
|
||
`seq_77_guard_stand_inactive`;
|
||
- скелет **бессмертен** — чит `K` его уже не берёт (`pop_guard_kill`
|
||
проверяет `CHARID_4_SKELETON`), но и боёвка должна возвращать его к
|
||
жизни (seg002:252);
|
||
- **новый атлас**: `../SDLPoP/data/SKEL/` (29 файлов) — `pop_pack_guard.py`
|
||
сейчас прибит к `GUARD/`, нужен параметр набора (`tbl_guard_dat` =
|
||
GUARD/FAT/SKEL/VIZIER/SHADOW). Здесь же удобно закрыть
|
||
[BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1): палитра стража
|
||
подменяется по `guards_color`, и оба изменения живут в одном упаковщике.
|
||
|
||
### <a id="l3-color"></a>L3-COLOR. Палитра КЛАДКИ уровня 3 (в оригинале он зелёный)
|
||
|
||
**Наблюдение (пользователь, 2026-08-07, со сравнением карт VGA).** В
|
||
оригинальной VGA-версии кладка уровня 3 ЗЕЛЁНАЯ, а уровней 1-2 — серо-синяя.
|
||
В SDLPoP все подземелья одинаковые, поэтому по нему разницу не увидеть.
|
||
|
||
**Почему в SDLPoP её нет — проверено, не гипотеза.** Механизм там ЕСТЬ
|
||
(seg000:1140, «Level colors (1.3)»):
|
||
|
||
```c
|
||
int level_color = custom->tbl_level_color[current_level];
|
||
if (level_color != 0) {
|
||
byte* env_pal = level_var_palettes + 0x30*(level_color-1);
|
||
byte* wall_pal = env_pal + 0x30 * custom->tbl_level_type[current_level];
|
||
set_pal_arr(0x50, 0x10, (rgb_type*)env_pal); /* chtab_6 environment */
|
||
set_pal_arr(0x60, 0x10, (rgb_type*)wall_pal); /* chtab_7 wall */
|
||
}
|
||
```
|
||
|
||
`tbl_level_color` (data.h:842) = `{0,0,0,1,0,0,0,1,2,2,0,0,3,3,4,0}` — у
|
||
уровня 3 цвет **1**, у 7 тоже 1, у 8/9 — 2, у 12/13 — 3, у 14 — 4. Но
|
||
`level_var_palettes` — это ресурс **20** из `PRINCE.DAT` (только версии
|
||
1.3/1.4), а в `SDLPoP/data/PRINCE/` его НЕТ: там лежит лишь `res10.bin`
|
||
(палитры стражей). Значит `level_var_palettes == NULL` и вся ветка молча
|
||
пропускается — отсюда одинаковые подземелья.
|
||
|
||
**Данные у нас есть.** В `../MSDOS/PRINCE.DAT` ресурс 20 присутствует:
|
||
offset 22790, **240 байт** = 5 палитр × 16 цветов × 3 байта (6-битные
|
||
каналы, как res10).
|
||
|
||
**Что делать (когда дойдём до вида уровня 3).**
|
||
1. Достать ресурс 20 из `MSDOS/PRINCE.DAT` (упаковщику придётся читать сам
|
||
`.DAT` — сейчас все скрипты берут распакованные PNG из SDLPoP);
|
||
2. сгенерировать таблицу палитр рядом с `pop_guard_pal.h`;
|
||
3. при загрузке уровня заливать слоты **0x50..0x5F** (env) и **0x60..0x6F**
|
||
(wall) — у нас ровно эти базы (`pop_pack_bg.load_indexed`: `pal_base =
|
||
0x60` для WALL, `0x50` для env), то есть совпадение со `set_pal_arr`
|
||
один в один;
|
||
4. `wall_pal = env_pal + 0x30 * tbl_level_type[level]` — для подземелья
|
||
(`level_type == 0`) обе палитры одинаковые.
|
||
|
||
**Грабли, уже пойманные на цвете стражей:** `gfx_pal_load` отдаёт указатель
|
||
в BIOS (`$A4` через `rst #0x08`), а BIOS читает только `#4000-#BFFF` —
|
||
таблицу нельзя передавать прямо из банка (0xC000+), надо копировать в
|
||
W1/W2 (см. [BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1)).
|
||
|
||
### <a id="draw-cost"></a>DRAW-COST. Кадр НЕ УКЛАДЫВАЕТСЯ в бюджет — нужна оптимизация
|
||
|
||
> **ШАГ 1 СДЕЛАН 2026-08-08: пропуск неизменившегося персонажа.** Комната
|
||
> 1.3, труп стража, Кид стоит: было **210 %** кадрового периода, стало
|
||
> **116 %** (500 772 такта при бюджете 430 000). Отрисовка перестала быть
|
||
> узким местом: персонажи в покое не рисуются ВООБЩЕ (ноль вызовов
|
||
> `pop_heal_fast` за кадр), весь фон — ДВА блита факелов (44 136 тактов).
|
||
> Механизм и почему метка позиционная — в шапке `pop_cdraw.h`.
|
||
|
||
**Исходные замеры пользователя (полосы бордюра, до шага 1).** Уровень 1,
|
||
Кид СТОИТ — то есть НЕ худший случай, ни боя, ни движения:
|
||
|
||
| комната | синяя (ввод+heal+логика) | зелёная (фон) | циан (спрайты) | итого |
|
||
|---|---|---|---|---|
|
||
| 3, страж УБИТ | ~80 % | ~20 % | ~110 % | **~210 %** |
|
||
| 2, стража НЕТ | ~60 % | ~20 % | ~60 % | ~140 % |
|
||
|
||
Разница ровно в теле убитого стража: +20 % синей и +50 % циана. Труп
|
||
сохраняет `charid != 0`, поэтому каждый кадр честно проходил весь путь
|
||
живого персонажа (heal → спрайт+clip_char+брызги+клинок → fore-проход), хотя
|
||
его кадр постоянен до выхода из комнаты.
|
||
|
||
**Что сделано (шаг 1).** Не спецкейс «мёртвый», а общее правило: у каждой
|
||
страницы дабл-буфера свой снимок ВХОДОВ отрисовки слота; совпал снимок,
|
||
спрайт этой страницы цел и фон в его прямоугольнике не трогали — heal, блит
|
||
и fore-проход пропускаются целиком. Покрывает и труп, и стоящего Кида, и
|
||
ждущего стража. Детали контракта — `pop_cdraw.h`, реализация —
|
||
`pop_char_skip_mask` / `cd_quiet` в `pop_cdraw.c`, метка фона —
|
||
`pop_cd_touch` в резидентном `pop_tile.c`.
|
||
|
||
Грабли, на которые наступили по дороге: сначала метка была ФЛАГОМ «фон
|
||
трогали хоть где-то» — и выигрыш оказался ровно нулевым, потому что факелы
|
||
анимируются каждый кадр и гасили пропуск для всех персонажей сразу (замер:
|
||
597 684 такта, как без оптимизации). Метка обязана быть ПОЗИЦИОННОЙ.
|
||
|
||
**Остаточный эффект от объединения прямоугольников.** Метка одна на
|
||
страницу — объединение всех правок фона. В комнате 1 два факела дают
|
||
прямоугольник x 40…88, а стоящий там Кид занимает x 32…44: пересечение 4 px,
|
||
и он перерисовывается каждый кадр (140 % против 94 %, если отойти). Если
|
||
понадобится — держать не один прямоугольник, а 3-4 отдельных и мержить
|
||
только при переполнении.
|
||
|
||
### Шаг 2 сделан частично: ЛОГИКА (синяя полоса) 2026-08-09
|
||
|
||
Пользователь: «на стоящем Киде с двумя факелами на логику уходит 60 %
|
||
кадрового периода — недопустимо». Разобрано брейкпоинтами в MAME
|
||
(`z80_profiling_method`), сцена: уровень 1 комната 1, Кид СТОИТ вплотную к
|
||
левому факелу (то есть пропуск персонажа НЕ срабатывает — худший случай).
|
||
|
||
**Калибровка, которую надо знать заранее.** Один такт `totalcycles` в MAME
|
||
— НЕ один номинальный T-такт Z80: у Sprinter на обращениях к ОЗУ есть
|
||
wait-state'ы, и замеренная стоимость выходит **≈ 2,4× номинала**
|
||
(`get_tile`: 574 номинальных против 1 422 замеренных). Считать бюджет по
|
||
таблице T-тактов из справочника нельзя — только мерить. Кадр растра =
|
||
430 000; главный цикл спейсится тремя `gfx_wait_vsync`, поэтому работа
|
||
СВЫШЕ 430 000 стоит сразу целый лишний кадр.
|
||
|
||
**Что нашли и починили:**
|
||
|
||
| правка | что было | стало |
|
||
|---|---|---|
|
||
| окно перебора коллизии как в оригинале (было: все 14 колонок каждый кадр) | `check_collisions` 60 888 | 50 940 |
|
||
| `get_tile_div_mod` — таблицей (`tile_div_tbl`/`tile_mod_tbl`), было `/14` и `%14` | 5 400 тактов на вызов, 13 вызовов за кадр ≈ 70 000 = **16 % кадра** | ~250 на вызов |
|
||
| разрешение ряда вынесено из цикла колонок + грань шагом 14 + `wall_type` таблицей | `check_collisions` 58 026 | 42 750 |
|
||
| `move_coll_to_prev` — `memcpy` (LDIR) вместо цикла на C | 14 байт за 5 514 тактов (390 на байт!) | ~1 200 |
|
||
| окно режется на непрерывные пробеги (левый сосед / своя / правый), пролог ряда вынесен на кадр | `check_collisions` 44 022 | **38 334** |
|
||
|
||
Замер одной итерации перебора: **пустая колонка 750 тактов, колонка-стена
|
||
~1 700** (две 16-битные знаковые сверки граней — SDCC пишет их через
|
||
`jp PO / xor 0x80 / jp P`). Ловушка, на которую наступили: «быстрый путь для
|
||
окна внутри комнаты» не срабатывал ПОЧТИ НИКОГДА — Кид, стоящий в колонке 0,
|
||
даёт окно с −1, и шёл медленный сбор во временный буфер с тернарником на
|
||
колонку (1 340 тактов на колонку). Отсюда разбиение на пробеги: вопрос «чья
|
||
это колонка» решается раз на пробег, а `coll_scan` сам двигает `scan_left`.
|
||
|
||
Самое дорогое было НЕ там, где ожидалось: `/14` и `%14` SDCC разворачивает
|
||
в `__divsint` + `__modsint`, а `__modsint` внутри зовёт `__divsint` ещё раз —
|
||
два полноценных 16-битных деления на каждый вопрос «в какой колонке точка».
|
||
Оригинал делит таблицей (seg006:702) — мы просто не портировали это место.
|
||
|
||
**ГЛАВНОЕ: логический кадр уложился в бюджет.** Главный цикл спейсится
|
||
тремя `gfx_wait_vsync`, поэтому работа сверх 430 000 тактов стоит СРАЗУ
|
||
целый лишний растровый кадр. Было 470 964 (период цикла 4 кадра), стало
|
||
**409 956 + ~15 600 на ввод = 425 600** — период цикла **3 растровых кадра**.
|
||
Игра стала быстрее на треть (16,7 логических кадров/с против 12,5).
|
||
|
||
Что дало последние тысячи (по убыванию):
|
||
|
||
| правка | экономия |
|
||
|---|---|
|
||
| `pop_y_to_row` — цепочка сравнений вместо `/63 % 4` | ~12 000 |
|
||
| расширение окна fore-прохода арифметикой вместо перебора 10 колонок и 3 рядов | ~8 500 |
|
||
| `col_from_x` — таблицей (те же `POP_TILE_DIV`, вынесены в резидент) | ~11 000 |
|
||
| `pop_cd_touch` — развёрнутый цикл по страницам, `x+w`/`y+h` один раз | ~8 600 (зовётся с каждого блита фона) |
|
||
| `tp / 10`, `tp % 10` у факелов — таблицей | ~4 000 |
|
||
| пустой слот соперника считается «тихим» | ~8 200 |
|
||
|
||
**Ловушка SDCC, на которой я потерял один прогон:** в `pop_y_to_row` одно и
|
||
то же выражение `t / 63 % 4 - 1` стояло в двух ветках, и компилятор поднял
|
||
деление В ВЕРШИНУ функции — быстрые возвраты не спасали, `__divsint` звался
|
||
всё равно. Лечится выносом медленного хвоста в ОТДЕЛЬНУЮ функцию. Тот же
|
||
эффект уже был описан в `pop_loose_tick`; теперь ясно, что это правило, а не
|
||
частный случай: **любое деление, встречающееся дважды, SDCC поднимает выше
|
||
всех проверок.**
|
||
|
||
Приём, которым это ловится: брейкпоинт на `__divsint`/`__divuint`/
|
||
`__divuchar` с печатью адреса возврата (`printf "%04X", w@(sp)`) — сразу
|
||
видно, кто и сколько раз делит за кадр.
|
||
|
||
**Хвост подобран 2026-08-10.** После переписи `pop_y_to_row` в pop_room.c
|
||
осталось ТРИ места, считавших `(y+60)/63 % 4 - 1` вручную (927 в
|
||
`mob_tick_one`, 976/977 в `mob_render`) — сгенерированный asm подтвердил
|
||
пару `__divsint`+`__modsint` в каждом. Это ~16 200 тактов (3,8 % кадра), но
|
||
только пока кусок плиты в полёте — то есть ровно в самом тяжёлом кадре.
|
||
Заменены вызовом `pop_y_to_row`; в банке 7 теперь НОЛЬ `__divsint`.
|
||
Эквивалентность закреплена тестом `geom_y_to_row_matches_formula`
|
||
(перебор −400..400 против исходной формулы) — вызовы разбросаны по трём
|
||
банкам, и соблазн написать деление «по месту» возвращается.
|
||
|
||
### Полная инвентаризация делений 2026-08-10
|
||
|
||
Способ (повторяемый одной командой из `.sprinter-cc-roomtest/`):
|
||
|
||
```
|
||
awk '/^;[a-z_0-9]+\.[ch]:[0-9]+:/{s=$0} /^\tcall\t__(div|mod|mul)/{printf "%-22s %-12s %s\n",FILENAME,$2,s}' *.asm
|
||
```
|
||
|
||
Найден **31 вызов в 9 модулях**. Прибрано три места, остальное разобрано и
|
||
осознанно оставлено.
|
||
|
||
Убрано:
|
||
|
||
| место | что было | почему стоило |
|
||
|---|---|---|
|
||
| `pop_cdraw.c` `calc_screen_x_coord` (2 вызова) | `x * 8 / 7` = `__divsint`, **2 400 тактов НА ПЕРСОНАЖА КАЖДЫЙ КАДР** | последнее деление в горячем пути; ~4 800/кадр при живом сопернике |
|
||
| `pop_guard.c` `guard_col_from_x` | `/14` + `%14` **безусловно**, мимо `POP_TILE_DIV` | единственное 16-битное деление, у которого таблица вообще не была подключена |
|
||
| `pop_trob.c` `animate_chomper` | `tp / 10` = `__divuchar` на чомпера каждый кадр | таблицы `TP_ROW`/`TP_COL` уже лежали в ЭТОМ ЖЕ файле, но ниже по тексту — чомперы их не видели |
|
||
|
||
Приём для `×8/7`: **таблица** `SCRX7[1152]` (int8_t, хранит `x/7`) в банке 4,
|
||
индекс `x + 448`, результат `x + SCRX7[i]`. 1 152 байта, резидент не тронут.
|
||
|
||
Два решения по дороге, оба проверены, а не угаданы:
|
||
|
||
1. **Диапазон — весь, включая отрицательные.** Первая версия крыла 0..255 по
|
||
тождеству `8x/7 == x + x/7` с байтовой таблицей `x/7`. Ошибка: `obj_x =
|
||
2*fwd − 116` уходит в минус, как только `fwd < 58` (левее `x_bump[5]`) —
|
||
то есть у ЛЕВОЙ КРОМКИ комнаты, а это не экзотика, и там мы продолжали
|
||
делить. Границы взяты из фактических данных: `kid_data.bin` даёт `dx`
|
||
кадров Кида −5..+10, стража −2..+10; при `Char.x` типа uint8_t и
|
||
`render_dx ∈ {−140, 0, +140}` полный диапазон `obj_x` = **−416..695**.
|
||
Таблица кроет −448..703, деление стало недостижимым (оставлено
|
||
страховкой на третьего персонажа / другой `render_dx`).
|
||
2. **Хранить `x/7` байтом, а не готовое `x*8/7` словом** — по тождеству
|
||
`8x/7 == x + x/7`. Первая версия хранила готовое, «раз всё равно
|
||
индексация двухбайтная». Собраны ОБА варианта, такты посчитаны по
|
||
сгенерированному asm:
|
||
|
||
| вариант | хвост после проверки границ | тактов | байт |
|
||
|---|---|---|---|
|
||
| `int16_t` готовое | `add hl,hl / add hl,de / ld e,(hl) / inc hl / ld d,(hl)` | **132** | 2 304 |
|
||
| `int8_t` `x/7` | `add hl,bc / ld a,(hl) / ld l,a / rlca / sbc a,a / ld h,a / add hl,de` | **131** | 1 152 |
|
||
|
||
Расширение знака и 16-битное сложение стоят ровно столько же, сколько
|
||
лишний `add hl,hl` при двухбайтном индексе, а обращений к памяти на одно
|
||
меньше — под wait-state'ами Sprinter (такт ≈ 2,4× номинала именно на
|
||
обращениях к ОЗУ) байтовый вариант ещё чуть выгоднее номинала. Итог:
|
||
байтовая таблица не хуже по скорости и на килобайт меньше. **Урок:
|
||
«двухбайтный индекс съест выигрыш» — гипотеза; она не подтвердилась.**
|
||
|
||
Кодоген проверен глазами (`bank4_pop_cdraw.asm`): `ld hl,#0x01C0; add hl,de`,
|
||
16-битное беззнаковое сравнение — индекс полный, старший байт не теряется
|
||
(грабли `sdcc_z80_const_ptr_index_bug` обойдены отдельной
|
||
`uint16_t`-переменной). 131 такт номинала против ~1 000 у `__divsint`.
|
||
|
||
Проверка таблицы: все 1 152 записи сверены питоном обратно из `.c`, а ПРАВИЛА
|
||
генерации (тождество + усечение к нулю) — тестом `geom_mul8div7_table_rules`
|
||
на целевом компиляторе: округляй SDCC к минус бесконечности, вся
|
||
отрицательная половина уехала бы на пиксель, и поймалось бы это только
|
||
глазами на левой кромке.
|
||
|
||
Оставлено сознательно (НЕ трогать, это не забытые места):
|
||
|
||
- **Хвосты за таблицей** — `guards.c:84`, `pop_bg.c:497`, `roomtest.c:163`,
|
||
`pop_map.c:412`: срабатывают только при x вне 0..255, то есть когда
|
||
персонаж в соседней комнате. Убирать их — это расширять `POP_TILE_DIV` до
|
||
−140..395 (+280 Б резидента) ради редкого пути.
|
||
- **`pop_geom.c:21`** — намеренный медленный хвост `pop_y_to_row` (см. выше
|
||
про подъём деления SDCC).
|
||
- **`pop_geom.c:52`** — `v % n` в `pop_rnd_fit` для не-степени двойки:
|
||
оригинал зовёт `prandom(1)/(255)/(0xFF)`, все три идут веткой с маской,
|
||
сюда управление не приходит вовсе.
|
||
- **Холодные, раз на комнату/уровень/событие**: `pop_guard.c:266/268/269`
|
||
(вход стража), `pop_map.c:310` (пробуждение скелета), `pop_trob.c` дверь
|
||
уровня, `roomtest.c:473/663/887` (читы и старт), `pop_level.c:131/132`
|
||
(имя файла уровня), `roomtest.c:976/977` (отладочный HUD номера комнаты).
|
||
Каждое — единицы вызовов за секунды игры; таблицы под них только раздули
|
||
бы код.
|
||
|
||
Итог: **в горячем пути делений не осталось**. В банках 4, 6, 7 — ноль
|
||
`__div*`; всё, что видно в списке выше, либо за быстрым путём, либо холодное.
|
||
|
||
### Замер в MAME: A/B со сборкой `ec3cca5` (до правок)
|
||
|
||
Обе сборки прогнаны через полный цикл (`make hdd` → рестарт MAME → загрузка
|
||
уровня 1), сцена — **комната 1, Кид стоит, соперника нет**; скриншоты обеих
|
||
сборок идентичны. Фазы сняты брейкпоинтами на инструкциях `out
|
||
(_io_border), a` профилировочного бордюра, медиана по 60 логическим кадрам:
|
||
|
||
| фаза | до | после | Δ |
|
||
|---|---|---|---|
|
||
| ввод + heal | 62 361 | 62 364 | +3 |
|
||
| логика | 79 878 | 79 878 | 0 |
|
||
| фон | 119 502 | 119 502 | 0 |
|
||
| **спрайты** | **129 568** | **127 510** | **−2 058** |
|
||
| РАБОТА за кадр | 391 258 | 389 221 | −2 037 |
|
||
|
||
Счётчик делений (брейкпоинты на `__divsint`/`__modsint`/`__divuchar`/
|
||
`__moduchar` с печатью адреса возврата):
|
||
|
||
| сцена | до | после |
|
||
|---|---|---|
|
||
| комната 1, только Кид | **1,00 `__divsint`/кадр** (возврат 0xD3AA = `pop_cdraw`) | **0** |
|
||
| комната 3, бой со стражем | **2,01 `__divsint`/кадр** | **0** (82 кадра боя) |
|
||
|
||
Всё сходится в одну картину: единственное деление горячего пути — `×8/7` на
|
||
персонажа, по одному вызову на каждого Char. Экономия ровно в фазе спрайтов
|
||
и ровно на стоимость одного вызова: **2 058 тактов** (документированная
|
||
оценка была ~2 400). С соперником на сцене — вдвое.
|
||
|
||
**Чего этот замер НЕ показывает, и это важно:**
|
||
|
||
- Период цикла как был 3 растровых кадра, так и остался — 2 000 тактов его не
|
||
двигают. Выигрыш ушёл в запас: до нижней границы 430 000 стало 40 800
|
||
тактов вместо 38 700.
|
||
- Остальные правки (три `y_to_row` в `pop_room`, `guard_col_from_x`,
|
||
`tp/10` у чомпера) в этой сцене **не срабатывают вовсе** — им нужны
|
||
падающая плита, переход стража между комнатами и уровень с чомперами.
|
||
Их стоимость известна поштучно, но в бою я их не ловил.
|
||
- Работа в комнате 3 между прогонами **несравнима** (391 204 против 318 145):
|
||
страж живой, фаза боя и срабатывание `pop_char_skip_mask` от прогона к
|
||
прогону разные. Оттуда взят только СЧЁТЧИК делений — он от таймингов не
|
||
зависит.
|
||
|
||
**Профиль работы за логический кадр СЕЙЧАС (409 956 тактов + ~15 600 ввод):**
|
||
|
||
| блок | тактов | % растрового кадра |
|
||
|---|---|---|
|
||
| see_kid + ctrl_tick + skip + heal + kid_tick | 52 536 | 12 |
|
||
| физика Кида + страж + боёвка | 73 452 | 17 |
|
||
| `pop_loose_tick` | 27 438 | 6,4 |
|
||
| **`pop_process_trobs`** (два факела) | **75 720** | **17,6** |
|
||
| `pop_redraw_needed` + шов + skip_mask | 10 932 | 2,5 |
|
||
| **`pop_char_draw` Кида** | **56 250** | **13** |
|
||
| **`pop_char_fore` Кида + борта** | **113 628** | **26** |
|
||
|
||
Синяя полоса (ввод + логика) была ~60 % → стала ~29 %.
|
||
|
||
**Оптимизация закрыта по решению пользователя 2026-08-10.** Всё, что
|
||
осталось неcделанным, вынесено с замерами в
|
||
[`../docs/perf_backlog.md`](../docs/perf_backlog.md) — там же протокол «как
|
||
мерить», чтобы не наступать заново на wait-state'ы и на устаревшие адреса
|
||
символов. Что доделано после таблицы выше: `pop_cd_touch` развёрнут,
|
||
tp/10 у факелов таблицей, пустой слот соперника считается тихим, ранний
|
||
выход в fore-проходе (нулевой эффект, оставлен как порт), быстрый путь без
|
||
клипа в `pop_blit_b` (факел 41 778 -> 31 218 тактов).
|
||
|
||
**Что осталось (запас на будущее, срочности больше нет):**
|
||
|
||
1. **`pop_char_fore` 113 628.** Внутри: `char_footprint` + расширение окна
|
||
~21 000, дальше шесть `fore_tile`, из которых два реально рисуют (по
|
||
~32 000). Дальше резать — кэш «в этом тайле переднего слоя нет вовсе».
|
||
2. **`pop_process_trobs` 75 720 на два факела** (~31 000 на факел). Внутри
|
||
одного факела: `gfx_blit_noclip` 8 700, чтение w/h и клип 6 400,
|
||
`pop_cd_touch` (теперь дешевле), маппинг окна 0 и возвраты. Пламя
|
||
перерисовывается каждый кадр обязательно (`TORCH_ANIM_DIV = 1`, кадр
|
||
меняется), так что пропуск тут не поможет — только удешевление блита.
|
||
3. `pop_loose_tick` 27 438 при полном отсутствии падающих плит.
|
||
4. Одно `__divsint` осталось в `pop_char_draw` (`obj_x * 8 / 7`) — ~2 400.
|
||
|
||
### Шаг 2 (СЛЕДУЮЩИЙ, назначен пользователем): ДВИЖУЩИЙСЯ Кид
|
||
|
||
Пропуск закрывает только покой. Наблюдение пользователя 2026-08-08: как
|
||
только Кид побежал, циан-полоса (спрайт + fore-проход) вырастает почти до
|
||
**100 % кадрового периода** — и это на ОДНОГО персонажа. То есть цена
|
||
одной перерисовки персонажа сама по себе непозволительно велика, и её надо
|
||
резать по существу, а не пропусками.
|
||
|
||
Куда смотреть (мерить каждое, метод — `z80_profiling_method`):
|
||
1. разделить замером спрайт-блит и fore-проход: у fore уже была история
|
||
78 % кадра до кэша кладки (`pop_fore_layer_cost`), он и сейчас главный
|
||
подозреваемый;
|
||
2. fore-проход перебирает 2×2…3 тайла и в каждом разбирает кладку заново —
|
||
кэш «в этом тайле переднего слоя нет вовсе» снял бы половину;
|
||
3. расширять перебор ТОЛЬКО по накладным спрайтам (клинок/брызги), а не по
|
||
всему окну клипа: габарит кадра уже покрыт футпринтом + правилом меча;
|
||
4. heal + блит спрайта: сейчас это два прохода по одной площади; посмотреть,
|
||
нельзя ли стирать только РАЗНОСТЬ прямоугольников при мелком сдвиге.
|
||
|
||
**Куда идти дальше в ПОКОЕ — там это ЛОГИКА, а не отрисовка.** Разбивка
|
||
работы кадра брейкпоинтами (комната 3, труп, Кид стоит; всего 500 772
|
||
такта):
|
||
|
||
| участок | тактов | % кадра |
|
||
|---|---:|---:|
|
||
| ввод + `check_skel` + луч видимости + `ctrl_tick` + heal | 65 862 | 15 % |
|
||
| `kid_tick` + физика + страж + боёвка | **260 022** | **60 %** |
|
||
| `loose_tick` + `process_trobs` | 119 562 | 28 % |
|
||
| `redraw_needed` + шов + спрайты + метка | 55 152 | 13 % |
|
||
| — из них два блита факелов | 44 136 | 10 % |
|
||
|
||
1. **60 % на тик персонажей** при том, что оба СТОЯТ — первый кандидат.
|
||
Смотреть `pop_phys_tick`/`pop_guard_phys_tick` (банк 3) и `pop_guard_tick`
|
||
(банк 1): сколько там работы, которую неподвижный персонаж делать не
|
||
обязан, и сколько стоит трамплин на каждом шаге.
|
||
2. **28 % на `loose_tick` + `process_trobs`** в комнате БЕЗ единой ловушки —
|
||
явно перебор; разобрать, что там сканируется каждый кадр (список trob,
|
||
`pop_trob_modif` соседа для шва, `pop_room_link`).
|
||
3. Запечь труп в фон (как щебень: тело в фоне + fore-часть поверх Кида) —
|
||
тогда бесплатным станет и проход Кида ПО телу, который сейчас снимает
|
||
пропуск по правилу «слоты рядом». Предложено пользователем 2026-08-08.
|
||
|
||
Мерить в ЭТОЙ точке (комната 3, страж убит, Кид стоит) — она воспроизводима
|
||
и даёт нижнюю границу; худший случай (бой + бег + падающая плита) считать
|
||
отдельно. Метод — memory `z80_profiling_method` (брейкпоинты с
|
||
`totalcycles`); полосы бордюра показывают только одну картинку из периода и
|
||
годятся лишь для раскладки по фазам.
|
||
|
||
**Наблюдение пользователя на приёмке DRAW-CHAR 2026-08-08:** циан-полоса
|
||
профиля (спрайты + fore) выросла — тогда «в пределах».
|
||
|
||
**Отчего именно.** Раньше перебор тайлов у Кида шёл строго по футпринту
|
||
кадра (`char_x_left/right`, seg006:1021), а он УЖЕ спрайта; расширение
|
||
перебора окном спрайта (клинок и брызги уходят за габарит кадра) было
|
||
только у соперника. DRAW-CHAR сделала его общим — то есть у Кида теперь на
|
||
колонку-другую больше `fore_tile` за кадр. Это не регрессия «лишней
|
||
работы», а недостающая ранее корректность: тайл, который спрайт задевает,
|
||
обязан вернуть свой передний слой поверх него.
|
||
|
||
**Если fore-проход снова станет узким местом** (сейчас он в покое не
|
||
выполняется вовсе): расширять перебор ТОЛЬКО по накладным спрайтам
|
||
(клинок/брызги), а не по всему окну; кэш «в этом тайле fore-слоя нет
|
||
вовсе»; считать окно клипа в тайловых координатах один раз.
|
||
|
||
### <a id="draw-char"></a>DRAW-CHAR — **ЗАКРЫТА И ПРОВЕРЕНА 2026-08-08**
|
||
|
||
Отрисовка сведена к одному набору функций над `Char` (`pop_cdraw.c`), проход
|
||
окклюзии — один на всех (`pop_fore_over_char`). Разбор, таблица «было →
|
||
стало», пять починенных расхождений и замеры — в
|
||
[`TASKS_CLOSED.md`](TASKS_CLOSED.md#draw-char). Оттуда же список того, что
|
||
надо потрогать вживую при приёмке уровней.
|
||
|
||
### <a id="l3-pass"></a>L3-PASS. Приёмка уровня 3 — обход всех комнат
|
||
|
||
Как L1-PASS/L2-PASS: сквозной проход руками плюс обход комнат читом ROOMNAV.
|
||
Осмысленна ТОЛЬКО после L3-CHOMP и L3-SKEL — без них уровень заведомо
|
||
неполон, и половина наблюдений будет «механики нет».
|
||
|
||
Что уже снято и пригодится (справка ниже): комнаты **23 и 24 недостижимы** и
|
||
полностью пусты — баги в них не в приоритете; **чекпойнт** уровня 3 сделан
|
||
([L3-CHKP](TASKS_CLOSED.md#l3-chkp)), его тоже надо потрогать вживую: уйти
|
||
влево из комнаты 7, умереть, проверить респавн в комнате 2 и снятую
|
||
loose-плиту (7, кол 4, ряд 0).
|
||
|
||
### <a id="rooms-graph"></a>Справка: связность комнат уровней 1–3 (снято 2026-08-05)
|
||
|
||
Обход графа `roomlinks` (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет
|
||
соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись
|
||
13/18/24 (см. «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)). Скрипт разовый,
|
||
в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии.
|
||
|
||
| уровень | старт | недостижимы | признак |
|
||
|---------|-------|-------------|---------|
|
||
| 1 | к.1 (0,0) | **13, 18, 24** | ссылки наружу есть, обратных нет |
|
||
| 2 | к.5 (1,3) | **нет** | граф полностью симметричен, все 24 достижимы |
|
||
| 3 | к.9 (2,4) | **23, 24** | то же, что на 1: односторонние ссылки, обе комнаты **полностью пустые** |
|
||
|
||
```
|
||
ур.3: 23 L→4, у 4 R=22 | 24 L→2, у 2 R=7
|
||
23 R→22, у 22 L=4 | 24 R→7, у 7 L=2
|
||
| 24 U→16, у 16 D=0
|
||
на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty
|
||
```
|
||
|
||
То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в
|
||
брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот
|
||
же: **в 23/24 возможен «мусор в шве»** (наш рендер кромки читает крайнюю
|
||
колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов
|
||
там низкий, в игре их не видно.
|
||
|
||
**Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода** (единственная
|
||
связь — вверх, откуда Кид падает):
|
||
|
||
```
|
||
к.10 U→4 пики(2,2) + пол — падение из комнаты 4
|
||
к.14 U→21 шахта 2 тайла шириной, дно = обломки
|
||
к.17 U→15 то же
|
||
```
|
||
|
||
Это не баги данных: в 14/17 попадают только падением насмерть, а из 10
|
||
(если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать
|
||
«застрял» багом.
|
||
|
||
---
|
||
|
||
### <a id="l4-mirror"></a>L4-MIRROR. Зеркало уровня 4 и тень — **АКТИВНА с 2026-08-10**
|
||
|
||
Приоритет переставлен пользователем: palace-уровни были отложены решением
|
||
2026-08-04, но уровень 4 уже гоняется в MAME, и зеркало — единственное, что
|
||
мешает пройти его сюжетно.
|
||
|
||
**Как это работает в оригинале** (SDLPoP, всё сверено по коду):
|
||
|
||
| что | где | суть |
|
||
|---|---|---|
|
||
| постановка тайла | `animate_leveldoor`, seg007:0457 | дверь выхода ДОРИСОВАЛА открытие (`modif >= 43`, `leveldoor_open` 0/2 -> 1) -> в комнату 4, колонку 4, ряд 0 пишется `tiles_13_mirror` |
|
||
| отражение | `check_mirror`, seg003:0798 | пока Кид на тайле зеркала — каждый кадр рисуется зеркальная копия: `Char.x = (xpos<<1) - Char.x`, `direction` инвертируется, копия идёт в objtable типом 4 с клипом `left = (curr_col<<5)+9`, `top = y_clip[row+1]` |
|
||
| прыжок сквозь | seg004:0240 | засчитывается ТОЛЬКО `run-jump` СПРАВА НАЛЕВО (`frame 39..43`, `direction < 0`) -> `modif = 0x56` (разбитое), `jumped_through_mirror = -1` |
|
||
| рождение тени | `jump_through_mirror`, seg003:080A | Кид копируется в слот Guard как `charid_1_shadow`; `guardhp = hitp_max`, у Кида `hitp_curr = 1` |
|
||
| поведение тени | `autocontrol_shadow_level4`, seg002:1131 | в зеркальной комнате при `x < 80` исчезает (`clear_char`), иначе бежит вперёд. Боя нет |
|
||
| клип тени | seg008:1699 | `obj_clip_left = 137 + (mirror_column-4)*32` — тень видна только СПРАВА от зеркала |
|
||
| коллизия | `wall_type`, seg006:1632 | зеркало = 2 «стена слева» — **уже портировано** (`pop_map.c`), как и спецкейс `can_climb_up` |
|
||
|
||
**Шаги.**
|
||
|
||
1. **АТЛАС — СДЕЛАНО 2026-08-10.** `tile_table[0x0D]` = база 75, фронт 77
|
||
(наша таблица совпадает с SDLPoP такт в такт). Тайла 13 **нет ни в одном
|
||
уровне статически** — проверено перебором всех 15 `res200N.bin`, ноль
|
||
попаданий; санити-проверка разбора: ур.1 без чомпера, ур.3 с 18, ур.4 с
|
||
паласными 25..29. Значит `render_room` эти id не увидит и на месте
|
||
зеркала был бы чёрный провал (memory `pop_atlas_dynamic_ids`). Добавлены
|
||
`MIRROR_ENV_IDS = {75, 77}` в `pop_pack_bg.py`, 77 — ещё и в
|
||
`FORE_ENV_IDS`. Оба набора переупакованы: fore 17 -> 18 спрайтов, все
|
||
страницы EMM в пределах 16 КБ.
|
||
2. **ПОСТАНОВКА ТАЙЛА — СДЕЛАНО И ПРОВЕРЕНО ПОЛЬЗОВАТЕЛЕМ 2026-08-10.**
|
||
`place_mirror()` в `pop_trob.c`, вызывается из `animate_leveldoor` по
|
||
переходу `pop_leveldoor_open` 0/2 -> 1 (условие оригинала — иначе тайл
|
||
ставился бы заново каждый кадр открытой двери). Если комната зеркала уже
|
||
на экране, ставится `POP_RD_FLOOR` на обе страницы.
|
||
Зеркало появляется, спрайты из атласа читаются. Остался незакрытый
|
||
краевой вопрос: не устареет ли `g_fg` коллизии, если игрок окажется в
|
||
комнате 4 ровно в момент постановки (в обычном прохождении дверь в другой
|
||
комнате).
|
||
3. **ОТРАЖЕНИЕ.** Самый дорогой шаг: это ЧЕТВЁРТЫЙ рисуемый Char помимо
|
||
Кида и соперника, со своим клипом. Отрисовка у нас уже общая
|
||
(`pop_cdraw.c`, слоты — memory `pop_char_draw_unified`), вопрос в числе
|
||
слотов и в том, что отражение меняется вместе с Кидом каждый кадр, то
|
||
есть `pop_char_skip_mask` на нём не сработает. ЧИСТО КОСМЕТИКА —
|
||
делается последним.
|
||
4. **ПРЫЖОК СКВОЗЬ + РОЖДЕНИЕ ТЕНИ — СДЕЛАНО 2026-08-10, НЕ ПРОВЕРЕНО В MAME.**
|
||
- `is_obstacle` (`pop_map.c`): ветка зеркала (seg004:0239) — Кид, кадры
|
||
бегового прыжка 39..43, направление влево -> `modif = 0x56`,
|
||
`pop_jumped_mirror = -1`, препятствия нет.
|
||
- `mirror_image` / `jump_through_mirror` / `pop_check_mirror`
|
||
(`pop_map.c`, порт seg003:0798..08A9). Отражённый Char уходит в слот
|
||
Guard как `CHARID_1_SHADOW`; `guardhp = hitp_max`, у Кида `hitp_curr = 1`.
|
||
`savekid` НЕ делается — как в оригинале.
|
||
- `pop_check_mirror()` зовётся из главного цикла ПЕРЕД отрисовкой
|
||
персонажей (в оригинале — первая строка `draw_people`).
|
||
- `autocontrol_shadow` + `autocontrol_shadow_level4` + `clear_char`
|
||
(`guards.c`, seg002:081D/1131/seg006:1945). Тень идёт СВОЕЙ веткой
|
||
целиком: к стражьему ИИ она не сводится, на ур. 4 не дерётся вовсе.
|
||
- **АТЛАС ТЕНИ.** Вскрылось при чтении seg006:0532: тень вне боевых
|
||
кадров 150..189 ходит по таблице КИДА, и `image` оттуда индексирует
|
||
спрайты Кида, а не стража. Выбор атласа в `pop_cdraw` шёл по СЛОТУ —
|
||
тень рисовалась бы спрайтами стража. Условие вынесено в
|
||
`pop_frame_tbl_is_guard()` (`pop_kid.c`), его теперь читают и
|
||
`load_frame`, и отрисовка — разъехаться не могут.
|
||
- Звука (`sound_45_jump_through_mirror`) в порте нет, пропущен.
|
||
Цена: `_CODE` +18 Б, банк 1 2311 -> 2367, банк 3 10328 -> 10551,
|
||
банк 4 8243 -> 8267.
|
||
**Что проверить в MAME:** на ур. 4 открыть дверь выхода, дойти до
|
||
комнаты 4, разбежаться СПРАВА НАЛЕВО и прыгнуть в зеркало — должна
|
||
появиться тень (спрайтами Кида, зеркально) и убежать влево, растворившись
|
||
при `x < 80`. У Кида после этого 1 HP, у тени полная полоса.
|
||
5. **КЛИП ТЕНИ** (seg008:1699): `obj_clip_left = 137 + (mirror_column-4)*32`
|
||
— тень видна только СПРАВА от зеркала. **Не сделано и не однострочник:**
|
||
в `pop_cdraw` есть клип сверху, снизу и СПРАВА (`vis_w`), а левого нет
|
||
вовсе — `gfx_blit_cols_part_w` умеет ограничить ширину, но не пропустить
|
||
колонки слева. Нужен либо новый примитив, либо сдвиг `bx` со срезом
|
||
исходных колонок. Пока без него тень будет вылезать левее зеркала.
|
||
|
||
Порядок: 1 -> 2 -> 4 -> 5 -> 3. После 4 и 5 уровень уже проходится, потому
|
||
что сюжетно достаточно прыгнуть сквозь зеркало.
|
||
|
||
---
|
||
|
||
## P1 — берётся в любой момент
|
||
|
||
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)
|
||
|
||
Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на
|
||
логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою
|
||
`fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()`
|
||
= 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости
|
||
эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою.
|
||
Условие «делать ПОСЛЕ CLIP-1» снято — CLIP-1 закрыт. Проверка —
|
||
секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз».
|
||
|
||
### <a id="tune-1"></a>TUNE-1. Параметры движка — в конфиг, а не в код
|
||
|
||
**Что уже есть.** `pop_tune.h` — все настраиваемые числа собраны в одном
|
||
заголовке: чекпойнт уровня 3 (`POP_CHKP_*`), отладочное окно решётки
|
||
(`POP_DBG_GATE_HOLD`), включатель зацепа в прыжке (`POP_ENABLE_JUMP_GRAB`).
|
||
Правило уже действует: новое «особое событие» или тайминг заводится ТАМ, а
|
||
не константой по месту.
|
||
|
||
**Что нужно сделать.** Читать их из ФАЙЛА рядом с exe, чтобы менять без
|
||
пересборки — под отладку («растянуть решётку, чтобы успеть пройти») и под
|
||
моды. Формат: простой ini/`ключ=значение`, парсер на ~50 строк (числа,
|
||
комментарии `;`, неизвестные ключи игнорировать), файл необязателен —
|
||
нет файла, значит зашитые дефолты. Секции по смыслу: `[level]`,
|
||
`[debug]`, `[enhancements]`.
|
||
|
||
**Ориентир — SDLPoP.** У него это `custom_options_type` (types.h) +
|
||
`SDLPoP.ini` + меню Settings/Mods; наши имена намеренно совпадают с его
|
||
(`custom->имя`), чтобы сверка оставалась механической. Осмотр его меню и
|
||
опций — часть задачи: у него уже разложены по группам стартовые
|
||
HP/минуты, номера «особых» комнат и уровней (шадоу, скелет, зеркало,
|
||
чекпойнт), тайминги ворот и пик, скорости, а отдельной группой —
|
||
`fixes`/`enhancements` (включая `enable_jump_grab`, который мы уже
|
||
портировали). Брать всё подряд не надо: переносим по мере того, как
|
||
константа реально понадобилась в игре.
|
||
|
||
**Оговорка по памяти.** Парсер и таблица параметров — холодный код,
|
||
исполняется один раз при старте: кандидат в банк, а не в резидент W1.
|
||
|
||
### <a id="mem-next"></a>MEM. Следующий шаг разгрузки W1/W2
|
||
|
||
`pop_ctrl.c` уехал в банк 5 ([MEM-BANK5](TASKS_CLOSED.md#mem-bank5), куча
|
||
180 Б → 2298 Б; после снижения `--max-allocs` — 2751 Б). Следующий кандидат
|
||
по тому же критерию (**не размер, а частота вызова и отсутствие горячих
|
||
банк→банк переходов**) — **расщепление `pop_kid.c`**: холодная половина
|
||
(загрузка страниц спрайтов, `pop_kid_load`) в банк, движок кадров
|
||
(`load_frame`/`play_seq`, 2×/кадр) оставить в резиденте.
|
||
|
||
Брать по факту нехватки места, не заранее. Таблица резидентного кода по
|
||
модулям и разбор, почему `pop_level.c` в банк НЕЛЬЗЯ, — в
|
||
[`TASKS_CLOSED.md`](TASKS_CLOSED.md#mem-bank5).
|
||
|
||
---
|
||
|
||
## Отложено осознанно (не брать, пока не появится причина)
|
||
|
||
- **KBD-1, остаток** — «иногда при зажатом Shift стрелка всё-таки
|
||
пропускается» (ощущение пользователя, не замер: счётчики на 35 нажатиях
|
||
подряд потерь не показывают). Решение пользователя 2026-08-01: отложено до
|
||
финальной полировки. Где именно осталась дыра и что делать, если вернёмся,
|
||
— в [`TASKS_CLOSED.md`](TASKS_CLOSED.md#kbd-1) (там же весь протокол
|
||
замеров и список того, что делать НЕЛЬЗЯ).
|
||
- **Quickload (Shift+F9) и остальные читы SDLPoP** — оценка сделана
|
||
([DBG-CHEATS](TASKS_CLOSED.md#dbg-cheats)), код не написан. Самое ценное и
|
||
самое дорогое: сериализация `Char` + `room_modif` всех комнат + trob'ов +
|
||
стражей (`levels_plan.md` §4), зато даёт воспроизводимый регресс «вот кадр,
|
||
где баг» вместо ручной подгонки позы.
|
||
- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует.
|
||
- **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6.
|
||
- **[T-1](bug_list.md#t-1)** (пики: перерисовка по причине) — отдаётся почти
|
||
бесплатно после [T-2](bug_list.md#t-2), отдельно не окупается.
|
||
- **Отключение мыши на время игры** и **замена PRNG** —
|
||
`../docs/ideas_backlog.md` (оба дают доли процента кадра).
|
||
- **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость
|
||
транзиентная; разбор в [`bug_closed.md`](bug_closed.md).
|