Files
Sprinter-SDCC/applications/PoP/docs/gates_spikes_plan.md
T
snark13 bf7ddfd4da roomtest: L3 переходы вбок (право+лево) + план объектов (кнопки/гейты/пики)
Переходы в соседние комнаты по горизонтали (порт SDLPoP leave_room/
goto_other_room/find_room_of_tile), проверено в MAME (room2↔room3↔room9,
room1↔room5 и т.д.):

- pop_level: pop_room_load теперь извлекает и rightcol (col0 правого соседа),
  как leftcol — для коллизии/рендера правого шва.
- pop_map: pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg) — связи + кромки швов.
  get_tile(col=-1)=lcol (col9 левого), get_tile(col=10)=rcol (col0 правого),
  с гейтом по связи (нет соседа → стена) — порт find_room_of_tile.  check_leave
  (порт leave_room): передний край char_x_left<=54 / char_x_right>=201 (и
  обратные <=57 / >=198) → x∓140, сигнал pop_leave_dir (1=left,2=right).
- roomtest: rcol-массивы; enter_room задаёт edges; обработчик pop_leave_dir
  переключает комнату по pop_room_link.
- Фикс двойного перехода: без коллизии ЛЕВОГО шва Kid, войдя справа в соседа
  (curr_col=-1), стоял «в стене» → in_wall выбрасывал его на второй переход.
  Левый шов (get_tile(-1)=lcol) это чинит.

docs/gates_spikes_plan.md — ПОДРОБНЫЙ самодостаточный план на след. сессию:
интерактивные объекты (RAISE/DROP-кнопки, гейты, пики) + HP/смерть.  Контекст
текущего roomtest, форматы уровня (LINKLOC/LINKMAP @1440/1696), данные room6,
декод связи кнопка(0,2)→гейт room8(0,9), триггер пик (check_spike_below),
точные ссылки SDLPoP, фазы P0(персистентное per-room состояние+trob)/
S(пики+HP/смерть)/B(кнопки+гейты).

NB: L3-вверх (climb-up в комнату сверху) ещё не сделан.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:14:03 +03:00

265 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
Статус: **план** (2026-07-20). Реализация — отдельной сессией. Документ
самодостаточный: рассчитан на старт «с чистого листа» (пустой контекст).
Всё сверено с `applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
перед кодингом читать соответствующий код seg*.c, не гадать.
---
## 0. КОНТЕКСТ: текущее состояние `applications/PoP/roomtest` (что уже готово)
roomtest — живой прототип порта PoP: комната 1 уровня 1 живой композицией
тайлов + Kid (анимация/управление/коллизия/падение/зацеп/переходы). Собрать:
`cd applications/PoP/roomtest && make`. Тест в MAME: см. memory
`mame_mcp_bridge`/`mame_hdd_test_disk` (канонический цикл: `make` → пересобрать
`mame/v306/IMG/test_hdd.chd` через `toolchain/make_hdd.sh` со всеми ассетами →
рестарт `run_bridge.sh``resume` → ~13с бут → `type_string("d:{ENTER}roomtest.exe{ENTER}")`).
Отладка: клавиши `1`=freeze / `2`=resume в roomtest; MCP-мост `mame-z80`
(read_logical_memory, set_breakpoint, disassemble); адреса символов —
`.sprinter-cc-roomtest/roomtest.map` (сдвигаются при пересборке!).
### Модули (все в `applications/PoP/roomtest/`)
- `roomtest.c` — главный цикл (дабл-буфер 2 стр.), `enter_room(room)`,
обработчики переходов. File-static рабочие массивы (W2):
`room_fg[30]`, `room_bg[30]`, `lcol_fg/lcol_bg[3]`, `rcol_fg/rcol_bg[3]`,
`below_fg[10]`, `cur_room`.
- `pop_level.c/.h`**уровень из файла** (Фаза L1):
- `pop_level_load("res2001.bin")` — читает сырой blueprnt в EMM-страницу
(данные с offset `0x100`, ISR-стаб как атлас).
- `pop_room_load(room, fg,bg, lcol_fg,lcol_bg, rcol_fg,rcol_bg, below_fg)`
извлекает комнату (fg маскирован `&0x1F`, bg raw) + срезы соседей:
leftcol=col9 левого соседа, rightcol=col0 правого, belowrow=row0 нижнего.
- `pop_room_link(room, side)` — связь (side 0=L,1=R,2=U,3=D; 0=нет).
- `pop_level_start_room/pos/dir()`.
- `pop_bg.c/.h` — отрисовка тайлов (порт seg008 draw_tile), fore-окклюзия над
Kid (`pop_fore_over_kid`, порт set_char_collision+redraw_at_char/char2),
**loose-полы** (shake/bake/mob). `draw_tile` — статическая, знает
`draw_gate_back` (грань гейта из левой комнаты).
- `pop_kid.c/.h` — анимация Kid (интерпретатор seqtbl `play_seq`, порт seg006),
`Kid` struct (frame,x,y,dir,curr_col,curr_row,action,fall_x,fall_y,repeat,
curr_seq); `knock`-флаг; `kid_cur_dx/flags`, `kid_fp_*` (футпринт).
- `pop_ctrl.c/.h` — ввод (порт seg005 control) через `<kbd_raw.h>`.
- `pop_map.c/.h` — коллизия/физика (порт seg005/006). Ключевое:
- `pop_map_set(fg)` — карта текущей комнаты.
- `pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg)` — связи + кромки швов для
коллизии (`get_tile(col=-1)`=lcol, `get_tile(col=10)`=rcol; порт
find_room_of_tile).
- `pop_phys_tick()` — кадр физики (fall/land/wall/knock/leave).
- Переходы: `pop_fell_out` (вниз, y>=211), `pop_leave_dir` (1=left,2=right,
x∓140).
- **loose-состояние**: `pop_loose_modif[30]` (публично, читает pop_bg),
`loose_bake[30]`, `loose_rest[30]` (static); `pop_loose_tick()`,
`pop_loose_reset()` (сброс при смене комнаты).
### Что сделано по фазам
- **L1** — данные уровня из файла (room1_data.h удалён). Коммит `21f978d`.
- **L2** — переход в комнату снизу (провал/спуск), фиксы окклюзии. Коммит `7f3e32d`.
- **L3** — переходы вбок (право+лево) через швы. **Не закоммичено** на момент
написания (вместе с этим планом). **L3-вверх (climb-up в комнату сверху) —
НЕ сделано.**
- **Loose-полы** (тряска knock / падение mob+окклюзия) — коммит `8b30dc2`.
### Известные ОГРАНИЧЕНИЯ (важно для этого плана)
1. **Нет персистентности тайлов**: `enter_room``pop_room_load` каждый раз
перезагружает ИСХОДНЫЕ тайлы из level-страницы. Изменения (упавший loose,
открытый гейт) при повторном входе ТЕРЯЮТСЯ. Для кнопок/гейтов это
блокер (см. P0).
2. **Нет HP/смерти** Кида (нужно для пик).
3. Loose-механика — частный случай trob (нужно обобщить).
---
## 1. ДАННЫЕ УРОВНЯ (формат, offsets, объекты)
Сырой `res2001.bin` (2305 Б) = blueprnt DAT 1.0 (Table 6 в
`POP-DAT-FormatSpecifications.txt`). Читается в EMM-страницу с offset `0x100`.
Тайл-код = байт `& 0x1F`; верхние биты (модификатор BLUETYPE) сейчас отброшены.
| Блок | Offset | Размер |
|------|--------|--------|
| foretable (fg) | 0 | 720 (24 комн × 30) |
| backtable (bg=modifier) | 720 | 720 |
| **LINKLOC** (doorlink1) | **1440** | 256 |
| **LINKMAP** (doorlink2) | **1696** | 256 |
| links (roomlinks) | 1952 | 96 (24×{L,R,U,D}) |
| start_position | 2112 | 3 (room,pos,dir) |
Тайл-коды: `0x00`empty `0x01`floor `0x02`**SPIKE** `0x03`pillar `0x04`**GATE**
`0x06`**DROP-кнопка(closer)** `0x0B`loose `0x0F`**RAISE-кнопка(opener)**
`0x10`lvldoor-L `0x11`lvldoor-R `0x13`torch `0x14`wall.
### Объекты уровня 1 (по комнатам)
```
room 5: DROP(0,2)m11 RAISE(0,4)m9 GATE(0,5)m2 RAISE(0,6)m8 GATE(0,9)m1
room 6: RAISE(0,2)m7 SPIKE(2,3) SPIKE(2,4) <-- тестовая
room 7: RAISE(0,2)m6 GATE(0,9)m2
room 8: RAISE(0,6)m5 GATE(0,9)m2 RAISE(1,7)m4
room 9: RAISE(0,0)m3 (+ lvldoor(1,3)/(1,4) — выход на level2, отложено)
room12: RAISE(0,3)m2 GATE(0,9)m2 SPIKE(2,4)
room10/13/14/16/19/24: только SPIKE
room20: DROP(1,4)m1 RAISE(1,7)m0
```
(m = modifier тайла = ИНДЕКС в LINKLOC/LINKMAP.)
### Room6 (тестовая) — раскладка
```
fg row0: 13 01 0F 00 03 01 01 13 01 03 (0,2)=RAISE-кнопка, (0,0)/(0,7)=torch
fg row1: 14 14 14 00 14 14 14 14 14 14 (1,3)=empty
fg row2: 14 14 14 02 02 14 14 14 14 14 (2,3)(2,4)=SPIKE
links: L=8 R=2 U=5 D=0
```
- **Шахта пик**: col3 (row0=empty, row1=empty, row2=spike) + col4 (spike).
- **Гейт, видимый у ЛЕВОЙ кромки room6, — это гейт room8 (0,9)**, отрисованный
в col0 room6 через левый шов (L=8, leftcol=col9 room8). В room6 гейта НЕТ.
### Связь кнопка→цель (декод LINKLOC/LINKMAP), проверено:
- `get_doorlink_tile(i) = d1[i] & 0x1F`
- `get_doorlink_next(i) = !(d1[i] & 0x80)` (0 = конец цепочки)
- `get_doorlink_room(i) = ((d1[i]&0x60)>>5) + ((d2[i]&0xE0)>>3)`
- `get_doorlink_timer(i) = d2[i] & 0x1F`
- где `d1`=LINKLOC@1440, `d2`=LINKMAP@1696. Цепочка: idx++ пока next.
**Кнопка room6 (0,2) mod=7 → цель: room8 tile(0,9) = ГЕЙТ.** Подтверждено
в SDLPoP-скринах: нажатие кнопки поднимает решётку у левой кромки room6.
Связь КРОСС-КОМНАТНАЯ (кнопка в room6, гейт в room8) и задаётся таблицей,
а НЕ позицией. Кнопка может открывать НЕСКОЛЬКО гейтов в разных комнатах.
---
## 2. МЕХАНИКА SDLPoP (точные ссылки)
### 2.1 Trob-система (анимируемые тайлы)
- `add_trob(room,tilepos,type)` seg007:0A5A — в список анимируемых.
- Каждый кадр `redraw_needed_tiles`/`process_trobs` продвигает; диспетч по
типу тайла → `animate_button/animate_door/animate_spike/animate_loose`
(seg007:0033+ таблица `animate_*`).
- Состояние тайла хранится в `curr_room_modif[tilepos]` (per-room modifier).
- У нас есть частный случай для loose (`pop_loose_tick`+`pop_loose_modif[30]`).
### 2.2 Пики (spike)
- **Триггер выдвижения** — `check_spike_below()` seg006:1199 (зовётся в
физике Кида каждый кадр): для каждой колонки футпринта Кида
(`get_tile_div_mod_m7(char_x_left)`..`char_x_right`) идёт ВНИЗ от
`Char.curr_row` через НЕ-floor тайлы; если встретил `tiles_2_spike`
`start_anim_spike(room,tilepos)`. → Кид у края (0,2) правым краём задевает
col3 → скан вниз col3 (empty/empty/spike) → пики вылезают.
- `start_anim_spike` seg007:596: если `modif<=0`: `modif==0` → add_trob(type1)
+ звук; `modif<0` (кроме 0xFF disabled) → `modif=0x8F`.
- `animate_spike` seg007:317: автомат по modif — выдвиг `++modif` (1..4; на 5 →
`0x8F`; на 9 → 0, trob кончился); убирание `& 0x80``--modif` (на 0 → `=6`).
`0xFF` = disabled (не двигать).
- `is_spike_harmful` seg007:1178: modif `0/-1`→0 (безопасно); `<0`→1;
`1..4`→2; `>=5`→0.
- **Смерть**: `check_spiked` seg006:0968 — если тайл под Кидом = spike И harmful
И кадр бега (7..14) / старта прыжка (34..39) с harmful>=2, ИЛИ кадр приземления
(43/26) с harmful!=0 → `spiked()`. Падение на пики — отдельный путь (см.
`is_dead` seg006:1907, frame_177_spiked). Осторожный ШАГ по невыдвинутым — ок.
### 2.3 Кнопки
- `trigger_button(playsound, button_type, modifier)` seg007:0C53: modifier =
индекс LINKLOC. `link_timer = get_doorlink_timer(mod)`; если `!=0x1F`
(не заклинено): `set_doorlink_timer(mod,5)`; если был `<2``add_trob`
(кнопка нажимается) + звук; затем `do_trigger_list(mod, button_type)`.
- `do_trigger_list` seg007:09E5: идёт по цепочке LINKLOC от idx, для каждой
цели `trigger_1(target_type,room,tilepos,button_type)` → если >=0
`add_trob(room,tilepos,result)`.
- `animate_button` seg007:0D3A: `timer=get_doorlink_timer(mod)-1`;
`set_doorlink_timer(mod,timer)`; `timer<2` → кнопка отжимается.
- Когда Кид ВСТАЁТ на кнопку: `check_press`-путь seg006 (opener → trigger_button,
closer → тоже; если Кид мёртв — `died_on_button`). RAISE=`tiles_15_opener`
(0x0F), DROP=`tiles_6_closer` (0x06).
### 2.4 Гейты
- `trigger_1` seg007:0999 → для `tiles_4_gate``trigger_gate`.
- `trigger_gate(room,tilepos,button_type)` seg007:092C: modif = высота открытия.
opener: `0xFF`→игнор; `>=188`(открыт)→держать `238`; иначе `modif=(modif+3)&0xFC`,
return 1 (открывать). closer/иначе: `modif!=0` → return 3 (закрыть быстро).
- `animate_door` seg007:0522: анимация открытия/закрытия; `door_delta[]={-1,4,4}`,
`gate_close_speeds[]={0,0,0,20,40,60,80,100,120}`. Гейт медленно закрывается
после истечения таймера кнопки.
- Отрисовка: кадры гейта; у нас `draw_gate_back` в pop_bg (грань из левой комн.).
- **Проходимость**: Кид блокируется недостаточно открытым гейтом (коллизия
как стена, порог по высоте открытия); проходит при `modif` открытом.
---
## 3. Поправки к описанию пользователя (что важно)
1. Гейт НЕ в room6 (0,0) — он в **room8 (0,9)**, виден через левый шов; связь
кросс-комнатная (по таблице LINKLOC, не по позиции).
2. Кнопка может открывать несколько гейтов в разных комнатах.
3. Пики выдвигаются по `check_spike_below` (Кид над колонкой с пиками),
имеют состояния (не всегда смертельны), убираются со временем.
4. **Кросс-комнатное состояние тайлов ДОЛЖНО ПЕРСИСТИТЬ** — блокер (см. P0).
5. HP/смерть — новая подсистема.
6. Кнопка сама анимируется (нажата/отжата).
---
## 4. ПЛАН РЕАЛИЗАЦИИ (фазы)
### P0 — Персистентное per-room modifier-состояние + trob-каркас (ПРЕРЕКВИЗИТ)
**Проблема:** нажатие кнопки в room6 меняет modif гейта room8 (не текущей
комнаты); при входе в room8 нужно отрисовать гейт в текущем состоянии. Плюс
это чинит «re-entry восстанавливает тайлы» (loose/гейты).
Дизайн (предложение — уточнить в реализации):
- Массив `room_modif[24][30]` (или lazy per-visited-room) — modifier каждого
тайла каждой комнаты. Инициализируется из bg уровня при первой загрузке
комнаты; далее ЖИВЁТ (не перезагружается). ~720 Б — влезает в W2 (или в
EMM-страницу уровня рядом с данными: остаётся >10КБ).
- `enter_room` берёт modif из `room_modif[room]`, а fg — из level-страницы
(fg почти не меняется; исключения — loose→empty, надо тоже персистить: либо
отдельный `room_fg_override`, либо флаг «loose упал»).
- Обобщить loose-trob: единый список trob (room,tilepos,type) + диспетчер
`animate_*` по коду тайла. Loose (`pop_loose_*`) — перевести на него.
- Кросс-комнатный trigger: `add_trob` в НЕ текущую комнату меняет
`room_modif[room][tilepos]`; анимация продвигается даже для невидимой комнаты
(в оригинале — да; можно упростить: для невидимой комнаты гейт сразу в
финальном состоянии, анимировать только при видимости — решить при реализации).
### Фаза S — Пики (самодостаточно; отладит HP/смерть)
Порядок:
1. `room_modif` для пик (из P0 или временно локально).
2. `check_spike_below` (порт seg006:1199) — в `pop_phys_tick`.
3. `start_anim_spike` + `animate_spike` (порт seg007) — состояние в modif.
4. `is_spike_harmful` + `check_spiked` (порт seg006:0968).
5. **HP/смерть**: ввести `hitp_curr` (старт напр. 3); `take_hp`; при пиках —
мгновенная смерть; seq смерти (`seq_22_crushed`/`frame_177_spiked`..185);
анимация смерти; респавн (kid_init на старте или чек-поинт).
6. Отрисовка: кадры выдвижения пик. В pop_bg есть `SPIKES_FRAM_RIGHT` — нужны
pop-out кадры (spikes_fram по modif) + fore над Кидом.
### Фаза B — Кнопки + гейты (нужен P0)
Порядок:
1. Доступ к LINKLOC/LINKMAP из pop_level (добавить геттеры doorlink1/2[i] с
маппингом W0 или скопировать таблицы в W2 при load — 512 Б).
2. `pop_map` детект «Кид встал на кнопку» (check_press-путь) → `trigger_button`.
3. `trigger_button` → таймер + `do_trigger_list` (обход цепочки) →
`trigger_gate` для целей → изменить `room_modif[целевой]`.
4. `animate_button` (кнопка отжимается) + `animate_door` (гейт откр/закр +
авто-закрытие).
5. Отрисовка гейта (кадры по modif) через ЛЕВЫЙ шов (гейт room8 в room6) +
при входе в room8. Расширить `draw_gate_back`/добавить `draw_gate`.
6. Коллизия: закрытый гейт = стена (порог по высоте открытия); открытый —
проход. Учесть кросс-комнатный гейт на шве (проход влево room6→room8).
**Порядок фаз:** P0 → S → B. S в основном независим (кроме HP/trob-каркаса),
но проще и отладит смерть/анимацию; B требует P0 (кросс-комнатное состояние).
---
## 5. Открытые вопросы / грабли
- Персистентность `fg` для loose (тайл→empty): решить в P0 (override-массив или
флаг), иначе упавший loose «вернётся».
- Анимация trob в НЕВИДИМОЙ комнате: упростить (финальное состояние сразу) или
портировать честно.
- Двоебуфер: любой транзиент (пики/гейт/кнопка) финализировать перерисовкой
«покоя» на ОБЕИХ страницах (урок из loose — см. memory `pop_loose_floors`).
- Дверь уровня (lvldoor room9) + переход на level 2 — ОТДЕЛЬНО, отложено.
- L3-**вверх** (climb-up в комнату сверху) — ещё не сделан; можно закрыть до
объектов или параллельно.