From 86d761584163ca154840c19e67d635f1956b463b Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 23 Jul 2026 19:38:23 +0300 Subject: [PATCH] =?UTF-8?q?PoP:=20roomtest=20=E2=80=94=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=8A=D0=B5=D0=BA=D1=82=D1=8B/=D0=BE=D0=B1=D0=BB=D0=BE=D0=BC?= =?UTF-8?q?=D0=BA=D0=B8/=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4=D1=8B?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=BD=D0=B0=D1=82=20+=20=D0=B0=D1=80?= =?UTF-8?q?=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Порт Prince of Persia (applications/PoP/roomtest): развитие уровня, объекты (loose-полы/обломки), переходы между комнатами, фон-упаковка; планы (room_model/size_optimization), bug_list, pop_trob. Арт third_party/16x16-RPG-characters. Co-Authored-By: Claude Opus 4.8 --- applications/PoP/docs/room_model_plan.md | 68 ++++ .../PoP/docs/size_optimization_plan.md | 258 +++++++++++++ applications/PoP/poc/res/bg/pop_env1.atl | Bin 11873 -> 12449 bytes applications/PoP/poc/res/bg/pop_env2.atl | Bin 10614 -> 10798 bytes applications/PoP/poc/res/bg/pop_env4.atl | Bin 1684 -> 8498 bytes applications/PoP/roomtest/Makefile | 6 +- applications/PoP/roomtest/bug_list.md | 333 ++++++++++++++++ applications/PoP/roomtest/pop_bg.c | 202 +++++++++- applications/PoP/roomtest/pop_bg.h | 18 + applications/PoP/roomtest/pop_ctrl.c | 13 +- applications/PoP/roomtest/pop_kid.c | 9 +- applications/PoP/roomtest/pop_kid.h | 8 + applications/PoP/roomtest/pop_level.c | 53 +++ applications/PoP/roomtest/pop_level.h | 23 ++ applications/PoP/roomtest/pop_map.c | 305 +++++++++++++-- applications/PoP/roomtest/pop_map.h | 17 + applications/PoP/roomtest/pop_trob.c | 324 ++++++++++++++++ applications/PoP/roomtest/pop_trob.h | 61 +++ applications/PoP/roomtest/roomtest.c | 160 +++++++- applications/PoP/toolchain/pop_pack_bg.py | 19 +- third_party/16x16-RPG-characters/LICENSE | 359 ++++++++++++++++++ third_party/16x16-RPG-characters/README.md | 27 ++ third_party/16x16-RPG-characters/preview.png | Bin 0 -> 3387 bytes .../sprites/new-style/chibi-layered.aseprite | Bin 0 -> 3645 bytes .../sprites/new-style/chibi-layered.png | Bin 0 -> 1742 bytes .../sprites/old-style/00-avatars.png | Bin 0 -> 3276 bytes .../sprites/old-style/00-guide.png | Bin 0 -> 1607 bytes .../sprites/old-style/01-generic.png | Bin 0 -> 8196 bytes .../sprites/old-style/02-bard.png | Bin 0 -> 7830 bytes .../sprites/old-style/03-soldier.png | Bin 0 -> 8203 bytes .../sprites/old-style/04-scout.png | Bin 0 -> 7222 bytes .../sprites/old-style/05-devout.png | Bin 0 -> 8706 bytes .../sprites/old-style/06-conjurer.png | Bin 0 -> 8565 bytes 33 files changed, 2199 insertions(+), 64 deletions(-) create mode 100644 applications/PoP/docs/room_model_plan.md create mode 100644 applications/PoP/docs/size_optimization_plan.md create mode 100644 applications/PoP/roomtest/bug_list.md create mode 100644 applications/PoP/roomtest/pop_trob.c create mode 100644 applications/PoP/roomtest/pop_trob.h create mode 100644 third_party/16x16-RPG-characters/LICENSE create mode 100644 third_party/16x16-RPG-characters/README.md create mode 100644 third_party/16x16-RPG-characters/preview.png create mode 100644 third_party/16x16-RPG-characters/sprites/new-style/chibi-layered.aseprite create mode 100644 third_party/16x16-RPG-characters/sprites/new-style/chibi-layered.png create mode 100644 third_party/16x16-RPG-characters/sprites/old-style/00-avatars.png create mode 100644 third_party/16x16-RPG-characters/sprites/old-style/00-guide.png create mode 100644 third_party/16x16-RPG-characters/sprites/old-style/01-generic.png create mode 100644 third_party/16x16-RPG-characters/sprites/old-style/02-bard.png create mode 100644 third_party/16x16-RPG-characters/sprites/old-style/03-soldier.png create mode 100644 third_party/16x16-RPG-characters/sprites/old-style/04-scout.png create mode 100644 third_party/16x16-RPG-characters/sprites/old-style/05-devout.png create mode 100644 third_party/16x16-RPG-characters/sprites/old-style/06-conjurer.png diff --git a/applications/PoP/docs/room_model_plan.md b/applications/PoP/docs/room_model_plan.md new file mode 100644 index 0000000..7e5f41b --- /dev/null +++ b/applications/PoP/docs/room_model_plan.md @@ -0,0 +1,68 @@ +# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4) + +Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате, +пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP. + +## Факты из SDLPoP (подтверждено чтением исходника) + +- `Char.room` (реальная комната персонажа) ≠ `drawn_room` (отрисованная) — + штатное состояние. +- **Коллизия через ±140:** `xpos_in_drawn_room()` (seg004:0405) сдвигает + xpos на `±TILE_SIZEX*SCREEN_TILECOUNTX = ±140`, когда `curr_room` колонки + (`curr_row_coll_room[col]`) ≠ `drawn_room` (room_L/room_BL → −140, + room_R/room_BR → +140). Т.е. коллизия строится по РЕАЛЬНЫМ тайлам соседей. +- **Смена экрана:** `check_the_end()` (seg000:0FBD): `if (next_room!=0 && + next_room!=drawn_room) { drawn_room=next_room; load_room_links; redraw }`. + `next_room` ставится в `exit_room()` (= `Char.room` ПОСЛЕ успешного + `leave_room`). Значит drawn_room следует за Char.room, но Char.room меняется + ТОЛЬКО при реальном пересечении шва (leave_room, seg002:0504) на «легальном» + кадре/действии (не turn/climb/standup). +- **Отрисовка левого соседа:** только `load_leftroom()` (col9 левого соседа в + левую кромку); правый сосед НЕ рисуется (изометрия). Окклюзия ворот на шве — + только левая (seg008:696). +- **Ceiling-полоса:** `draw_room` рисует доп. ряд из `room_A` (row2, draw_main_y + =-1). (Уже реализовано, баг #3.) + +## Текущее состояние нашего движка (до #4) + +`cur_room` (=drawn_room) ВСЕГДА == комната Kid. Шов подделан: Kid остаётся в +drawn_room с `curr_col=-1/10` + снапшоты соседей `g_lcol/g_rcol` (коллизия ±1 +кол) / `lcol_bg` (openness ворот). Уход из комнаты — `pop_leave_dir`/`enter_room` +МГНОВЕННО при пересечении порога `char_x`. Отсюда #4: экран переключается +раньше, чем в оригинале (Kid должен «отступить» за кромку, оставив старую +комнату). + +## План (инкременты, каждый проверяется в MAME) + +### S1. Данные + рендер-смещение Kid +- Ввести `kid_room` (реальная комната Kid) отдельно от `cur_room`(=drawn_room). +- `kid_x_offset()` = разница комнат: kid_room == left(drawn) → лог. x Kid −140 + (рисуется за левой кромкой); right → +140; равны → 0. (порт + xpos_in_drawn_room). +- `kid_draw`/heal/fore используют смещение (Kid рисуется частично за кромкой). +- Проверка: Kid у шва рисуется со сдвигом, экран не дёргается. + +### S2. Коллизия по kid_room +- Коллизионный контекст (`g_fg`/edges/`g_room`/modif в pop_map) следует за + `kid_room`, а не за drawn_room. Когда kid_room≠drawn_room — грузим + соседа как коллизионную комнату (curr_col 0..9 в кадре kid_room). +- Отрисовка (room_fg и т.п.) остаётся по drawn_room. +- Порт `curr_row_coll_room[]`/`xpos_in_drawn_room` можно упростить: держим + ОДИН коллизионный room (kid_room) + существующие снапшоты кромок для ±1 кол. + +### S3. Отложенная смена drawn_room +- Уход (`check_leave`/`check_leave_below`): ставит `kid_room=сосед`, + репроецирует Kid (x∓140, col∓10) — но drawn_room НЕ меняет сразу. +- `check_the_end`-эквивалент в главном цикле: `if (kid_room != drawn_room && + <условие коммита>) enter_room(kid_room)`. Условие коммита — по SDLPoP: + как только Char.room сменилась легальным leave (не bumped/turn). Для + «bumped назад за кромку» drawn_room остаётся (симптом #4). +- Проверка сценариев #4/#5 в MAME. + +### S4. Полировка +- Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край. + +## Связанные баги (bug_list.md) +BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке), +BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней +колонны). Memory: `pop_seam_room_model`. diff --git a/applications/PoP/docs/size_optimization_plan.md b/applications/PoP/docs/size_optimization_plan.md new file mode 100644 index 0000000..60ab8ed --- /dev/null +++ b/applications/PoP/docs/size_optimization_plan.md @@ -0,0 +1,258 @@ +# roomtest — план оптимизации по размеру + переход на huge/banking + +Статус: **план для отдельной сессии** (2026-07-21). Документ самодостаточный +(рассчитан на старт с пустого контекста). Цель — освободить место: сейчас +`applications/PoP/roomtest` в режиме `small` почти упёрся в потолок 32 КБ. + +Правило проекта (`applications/PoP/CLAUDE.md`): механику/раскладку памяти +сверять с исходником и с memory (`sprinter_memory_modes`, `sdcc_banking`, +`bank_local_data_pattern`, `pop_banking_architecture`). Перед оптимизацией — +`make size-check`-подобный замер до/после (здесь — руками по `.map`). + +--- + +## 0. Как мерить + +- Сборка: `cd applications/PoP/roomtest && make roomtest.exe` (режим `small`, + `--gfx 256`). Карта символов — `.sprinter-cc-roomtest/roomtest.map` + (адреса сдвигаются при каждой пересборке!). +- Размеры областей — из `.map` (`_CODE`, `_DATA`, `_BSS`). +- Вклад модулей в `_CODE` — атрибуция диапазонов между символами по модулю + (скрипт-однострочник на python в истории; группировать символы `.map` по + 3-й колонке-модулю и суммировать `addr[i+1]-addr[i]`). +- MAME-проверка после изменений раскладки ОБЯЗАТЕЛЬНА (режимы памяти — + типовой источник «молча ломается», см. `sprinter_memory_modes`). + +## 1. ТЕКУЩЕЕ СОСТОЯНИЕ (замер 2026-07-21) + +Режим `small` = единое пространство **W1+W2 = 0x4000..0xBFFF (32 КБ)**; CODE с +0x4100, DATA/BSS/heap цепляются ЗА CODE автоматически (`--data-loc 0` = +linker chains), стек — вверху W2. + +| Область | Размер | Диапазон | +|---------|--------|----------| +| `_CODE` | ~27 250 Б (0x6A6F) | 0x4100–0xAB6F | +| `_HOME` | 227 Б | 0xAB6F | +| `_DATA` | 3 449 Б (0x0D79) | 0xAC78–0xB9F1 | +| `_BSS` | 290 Б | | + +**Образ ≈ 31.2 КБ; до верха W2 (0xBFFF) остаётся ≈ 1.3 КБ на кучу+стек.** +Куча в roomtest почти не используется (атласы/уровень — в EMM-страницах), +но запас критично мал. + +### Вклад модулей в _CODE (по .map, приблизительно) +``` + 7003 pop_bg (вся отрисовка тайлов/слоёв/wall_pattern) + 6326 pop_kid (из них ~3745 Б — СТАТ. ТАБЛИЦЫ kid_data.h, см. ниже) + 3762 pop_map (коллизия/физика/пики) + 1455 pop_trob (кнопки/ворота/пики-каркас) + 1224 pop_level (загрузка уровня, doorlink) + 914 roomtest (главный цикл) + ~7000 libc/libbgi (gfx_blit*, atlas_load, kbd_raw, open/read, irq, div/mul…) +``` + +### Крупные СТАТИЧЕСКИЕ данные (сейчас в _CODE как `const`) +- **`kid_data.h` — самый большой кусок, ~3.7 КБ**, живёт в _CODE (атрибутируется + pop_kid): + - `kid_seqtbl[2310]` — байткод последовательностей (play_seq). + - `kid_frames[241]` × 5 Б = 1205 Б — таблица кадров (image,dx,dy,flags,sword). + - `kid_seq_off[115]` × 2 Б = 230 Б — смещения seq. +- `pop_bg`: `tile_table[31]`×12 = 372 Б + ~20 мелких const-таблиц (COL_XH, + WALL_FRAM_*, SPIKES_FRAM_{RIGHT,LEFT,FORE}, LOOSE_FRAM_*, DOOR_FRAM_SLICE, + BLUELINE_*, LPOS/RPOS, FLOOR_LEFT_OVERLAY) — суммарно ~0.5–0.7 КБ. +- `pop_map`: `x_bump[20]`, `y_land[5]`, `wall_dl/dr`, `dir_front/behind` — ~100 Б. +- В `_DATA` (W2, не CODE): `room_modif[24][30]`=720 Б + копии LINKLOC/LINKMAP=512 Б + (pop_trob/pop_level) + рабочие массивы roomtest. + +--- + +## 2. ПУТЬ A — оптимизация КОДА (без смены модели) + +1. **Компиляторные флаги** (`bin/sprinter-cc`): попробовать `--opt-code-size` + у SDCC и подобрать `--max-allocs` (сейчас дефолт 100000; меньше = мельче код, + но медленнее компиляция; см. `mdview2_size_budget` — там `--max-allocs` + давал −1.4 КБ). Замерить каждый модуль отдельно. +2. **Дедуп подстановки нажатой кнопки**: логика `opener→floor / closer→stuck` + по таймеру связи ПРОДУБЛИРОВАНА в `draw_tile` и `fore_tile` (pop_bg.c). + Вынести в `static inline`/helper `subst_pressed_button(code,mod)`. +3. **wall_pattern / prandom** (pop_bg): 32-битный LCG (`unsigned long`) — + пользователь не любит 32-бит (см. `avoid_32bit_arith_z80`); но это PRNG + оригинала (нужен для совпадения раскладки стен) — трогать осторожно, только + если найдётся 16-битный эквивалент, дающий ТУ ЖЕ последовательность. +4. **Ревизия дублей**: `y_to_row` определён в pop_bg И pop_map; мелкие + геометрические хелперы дублируются — свести в один internal-модуль. +5. `/simplify`-проход по последним правкам Фазы B (pop_trob/pop_bg). + +Ожидаемый выигрыш пути A: единицы–первые сотни байт на пункт; в сумме, +оптимистично, ~1–2 КБ. Недостаточно как единственная мера. + +--- + +## 3. ПУТЬ B — вынос СТАТ. ДАННЫХ в EMM-страницы (с атласами / с level) + +**Идея (по замечанию пользователя):** EMM-страницы атласов и уровня +использованы лишь частично (страница 16 КБ, данных меньше), в «хвосте» — +свободное место. Часть `const`-таблиц можно хранить ТАМ, а не в _CODE/_DATA, +если таблица читается ИМЕННО ТОГДА, когда нужная страница уже в W0. + +**Механика W0:** атласы блитятся из W0 (`_gfx_w0_state`: `_gfx_w0_cur` — +спрайт-страница в W0; ISR-стаб `_gfx_w0_isr` возвращает её после прерывания). +Уровень (pop_level) маппит свою страницу в W0 на время извлечения +(`gfx_w0_map`/`gfx_w0_unmap`). → пока страница в W0, CPU может читать и +данные из неё по адресам 0x0000..0x3FFF. + +**Категоризация таблиц по W0-контексту (задача сессии — уточнить по каждой):** + +- **(a) Читается, когда в W0 АТЛАС** → хранить в свободном хвосте атлас-страницы. + Кандидаты — таблицы, которые нужны В МОМЕНТ блита конкретного атласа. + ГРАБЛИ: `draw_tile` читает `tile_table`/`COL_XH` ДО блита (чтобы решить, какой + спрайт/куда) — в этот момент в W0 может быть ДРУГАЯ страница (DSS/предыдущий + атлас). Т.е. большинство draw-таблиц читаются ВНЕ W0-атлас-контекста → + «в лоб» не переносятся. Нужен аудит КАЖДОГО чтения: гарантирована ли нужная + страница в W0 в этот тик. +- **(b) Читается, когда в W0 LEVEL** → хранить с уровнем (в его странице; там + ~13.7 КБ свободно из 16). Кандидаты: константы декода doorlink, разбор + комнат — всё, что pop_level делает под `gfx_w0_map(lvl_page)`. +- **(c) Нужна и там, и там** → дублировать в обеих страницах ЛИБО оставить + резидентной (если дубли дороже экономии). +- **(d) Читается в чистой ЛОГИКЕ (W0 не важен)** → перенос требует ЯВНОГО + `gfx_w0_map` на каждое чтение (дорого, особенно в горячих циклах) → как + правило оставить резидентной. + +**Отдельно `kid_data.h` (3.7 КБ — самый жирный кандидат):** +- `kid_frames`/`kid_seqtbl` читаются в `play_seq` (ЧИСТАЯ логика, каждый тик) И + в `kid_draw` (блит из kid-атласа, kid-страница в W0). Т.е. частично (a), + частично (d). Перенос всей таблицы в kid-атлас-страницу заставит `play_seq` + делать `gfx_w0_map` на каждый шаг байткода → замерить стоимость (может убить + бюджет спрайтов, см. `sprite_engine_perf`). Вариант: держать в EMM отдельной + страницей данных Kid и маппить один раз на кадр вокруг kid_tick+kid_draw. +- Это самый большой одиночный выигрыш (−3.7 КБ из _CODE), но и самый рискованный + по скорости — приоритетный к ПРОТОТИПИРОВАНИЮ и замеру. + +**Паттерн переноса writable/const данных в банк/страницу:** см. memory +`bank_local_data_pattern` (--codeseg/--constseg/--dataseg BANKn + trampoline-fix ++ mkexe -p 0) и `sdcc_static_storage_gotcha`. + +### 3.1 Свободное место в страницах (замер 2026-07-21, страница = 16384 Б) +``` +BG-атласы: размер свободно + pop_env0.atl 10578 5806 + pop_env1.atl 12449 3935 <- САМАЯ ТЕСНАЯ из bg + pop_env2.atl 10798 5586 + pop_env3.atl 5032 11352 <- много места + pop_env4.atl 8498 7886 + pop_wall.atl 11543 4841 + pop_fore.atl 7763 8621 +Kid-атласы (28 стр): free min=6161 max=15452 avg=9722 +Level (res2001.bin): данные 2305, свободно ~13823 (16384 − 0x100 стаб − 2305) +``` + +**Выводы по вместимости:** +- **Макс. данных в ОДНОМ атлас-банке = свободный хвост ЭТОЙ страницы** (см. + таблицу). Связывающее ограничение — самая тесная нужная страница (env1 = + 3935 Б; не перегружать её). +- Если страница будет маппиться в **W0** — минус ~0x100 Б на ISR-стаб (как + level). Атлас-страницы стаб УЖЕ содержат (atlas_load патчит) → данные класть + в хвост ПОСЛЕ атласа. +- **`kid_data.h` (3.7 КБ) влезает в kid-страницу** (min free 6161) или в + отдельную выделенную страницу данных Kid — предпочтительно отдельную (маппить + раз на кадр, не конфликтуя с kid-атласами блита). +- **Level-таблицы** — вагон места в level-странице (~13.8 КБ). +- **BG draw-таблицы** (~0.7 КБ) влезут в env3/fore/env4 (много free), НО см. + граблю W0-контекста в §3(a) — читаются ли они, когда нужная страница в W0. +- **Выделенная страница ТОЛЬКО под данные** (не делить с атласом) = до ~16 КБ + (−0x100 стаб при W0-маппинге). EMM-бюджет это позволяет (см. + `sprinter_emm_budget`: 215/3440 КБ free на старте). +- **Принудительно уменьшать макс. атлас (репак мельче) — КРАЙНИЙ случай:** это + резко поднимет число атлас-банков (сейчас 5 env-страниц адресуются как id>>5; + дробление ломает эту адресацию и множит страницы). Сначала использовать + СУЩЕСТВУЮЩИЙ свободный хвост и отдельные data-страницы. + +--- + +## 4. ПУТЬ C — переход на huge (banked code) + +### 4.1 Что такое huge сейчас (`bin/sprinter-cc`, `runtime/crt0_banked`) +- `--memory huge`: `MODE_CODE_LOC=0x4100`, **`MODE_DATA_LOC=0x8000` (ФИКС.)**, + banked code в W3. crt0_banked, как crt0_small, авто-детектит W2. + Помечено `[TODO]` — не обкатано. +- Отличие от small: small цепляет DATA сразу за CODE (`--data-loc 0`); huge + ФИКСИРУЕТ DATA на 0x8000. + +### 4.2 ТРЕБОВАНИЕ (по пользователю): huge должен переносить DATA динамически +Сейчас huge жёстко кладёт DATA на 0x8000. Если РЕЗИДЕНТНЫЙ CODE вылезет за +0x8000 (W1 = только 0x4000..0x7FFF ≈ 16 КБ; резидент > 16 КБ лезет в W2) → +коллизия с DATA. **Надо научить huge класть DATA динамически ЗА резидентным +CODE (как small: `--data-loc 0` + crt0 считает старт), а не на фикс 0x8000.** +Тогда huge = «small-раскладка резидента (W1+W2, DATA за CODE) + ДОП. код в +банках W3». Это первый пункт работ по huge. + +### 4.3 КОНФЛИКТ: графика тоже хочет W3 (ключевой риск) +`pop_banking_architecture` прямо говорит: **графику нельзя в W3** (блиты/атласы +используют окна; см. §4.5). Поэтому в банки W3 можно выносить ТОЛЬКО +НЕ-графические блоки, и такой банк НЕ должен во время своего исполнения держать +графику в W3. Если W3-банкованная функция ЗОВЁТ графику (которой нужен W3), +трамплин обязан сохранить/восстановить банк вокруг вызова (проверить, что +banking-ABI это делает — `sdcc_banking`). Альтернатива без этого риска — +**big + BANK_W1** (банк кода в W1, не W3), рекомендованная в +`pop_banking_architecture` именно из-за W3-графики. Сессия должна выбрать: +huge(W3) с аккуратным save/restore ИЛИ big(BANK_W1). + +### 4.4 Какие блоки МОЖНО вынести (не работают с графикой напрямую) +Замер graphics-ref по модулям (grep `gfx_|blit|env_b|wall_b|fore_b|setfillstyle| +bar(|GFX_BANK|initgraph`): +``` +pop_bg.c : 83 — РЕЗИДЕНТ (вся отрисовка) +roomtest.c : 23 — РЕЗИДЕНТ (главный цикл + флип страниц) +pop_level.c : 17 — использует gfx_w0_map (W0, не W3-блиты) — ПОГРАНИЧНЫЙ +pop_kid.c : 12 — kid_draw = графика; НО play_seq — чистая логика (можно split) +pop_ctrl.c : 0 — КАНДИДАТ В БАНК (ввод/диспетчер control) +pop_map.c : 0 — КАНДИДАТ В БАНК (коллизия/физика, ~3.8 КБ) — лучший по объёму +pop_trob.c : 0 — КАНДИДАТ В БАНК (кнопки/ворота/пики-логика) +``` +- **Лучшие кандидаты в W3-банк(и): pop_map + pop_trob + pop_ctrl** (нет прямой + графики; вместе ~5.3 КБ CODE). Освобождают резидент → он влезает в W1. +- **Осторожно с межбанковыми вызовами:** pop_map/pop_trob ЗОВУТ pop_bg + (перерисовка loose/пик/кнопок/шва) и pop_kid (play_seq/kid_set_seq). Это + кросс-банк вызовы через трамплин (`sdcc_banking`: стек +3 байта, виртуальный + 24-битный адрес). Правило `pop_banking_architecture`: «один файл = один банк + = прямые вызовы», main резидентен. Проверить, что трамплин сохраняет W3 + вокруг вызова в графический pop_bg (см. §4.3). +- **pop_kid split** (по желанию): вынести play_seq/seqtbl-интерпретатор + (логика + таблицы kid_data.h) в банк, оставить kid_draw/kid_heal резидентными. + Даёт и −код, и −данные из резидента, но требует аккуратного разделения TU + (1 функция = 1 модуль, см. `libc_one_function_per_module`). +- **pop_level: пограничный** — не блитит, но маппит уровень в W0; банковать + можно, если W0-логика совместима с трамплином (проверить ISR-стаб взаимодействие). + +### 4.5 Почему графику нельзя в W3 (контекст) +Блиттер держит спрайт-страницу атласа в **W0** (`_gfx_w0_state`, +`_gfx_w0_isr`). Ускоритель/адресация видео — отдельная тема (см. +`sprinter_accelerator`, `sprinter_graphics`). W3 в banked-раскладке — окно +кода-банка; смешивать с окном, которое графика перемапливает, нельзя без +save/restore. Детально — `pop_banking_architecture`, `graphics_constraints`. + +--- + +## 5. РЕКОМЕНДУЕМЫЙ ПОРЯДОК РАБОТ (для след. сессии) + +1. **Замер-базлайн** (CODE/DATA/BSS + per-module) — зафиксировать до. +2. **Путь A** дешёвые пункты (флаги, дедуп кнопки, дедуп y_to_row) — быстрый −1..2 КБ. +3. **huge §4.2**: научить huge класть DATA динамически (как small) — инфраструктурный + пререквизит, без него банкинг не даст гибкости. Обкатать в MAME на текущем + резиденте (пока без выноса — просто huge-раскладка = small + пустой W3). +4. **huge §4.4**: вынести pop_map (+pop_trob, +pop_ctrl) в W3-банк(и); проверить + кросс-банк вызовы в pop_bg (§4.3) в MAME. ЛИБО выбрать big+BANK_W1. +5. **Путь B** (по остатку нужды): прототип выноса `kid_data.h` в EMM-страницу + Kid с маппингом раз на кадр; замерить скорость (`sprite_engine_perf`). + Затем аудит draw-таблиц по W0-контексту (§3 a/b/c/d). + +## 6. Ссылки +- `bin/sprinter-cc` (§162+ — резолв memory-mode → CODE_LOC/DATA_LOC). +- `runtime/crt0_small.*`, `runtime/crt0_banked.*`, `runtime/bank.s`. +- memory: `sprinter_memory_modes`, `memory_modes_implemented`, + `setwin2_for_w2_alloc`, `sdcc_banking`, `bank_local_data_pattern`, + `pop_banking_architecture`, `avoid_32bit_arith_z80`, + `libc_one_function_per_module`, `sprite_engine_perf`, `mdview2_size_budget`. +- `applications/PoP/roomtest/bug_list.md` — открытые баги Фазы B (не блокируют + оптимизацию, но держать в уме при рефакторе pop_map/pop_bg). diff --git a/applications/PoP/poc/res/bg/pop_env1.atl b/applications/PoP/poc/res/bg/pop_env1.atl index b1c5df54fd6a39a8f23fea448acf1d88ac2561f8..0ec6ece77b37a40149b01d711441b7664ab9c506 100644 GIT binary patch delta 293 zcmaDDvoMi0IKa_RW};yDL!5U!>h}4$g z0V%B25#R#RinPfJHXjOd)RuH{VUxEchvl>W%Eete}U;^=54I~&h zi!l1?vPdwnF-*RwI|0aGWteQMTgAk}Fqu&=0mx@&n4GKY#l*xg*-*~_$Y*4jJXco& E0HjMJkpKVy delta 195 zcmZ3O_%MbwIKa_RY@%TJLqgH;Nn2qz;W149B3gJ>Bh1wIh{fLVbTM7OX?3V>({b}2BuheLrG#8=>) zxGSu_g;RkA#N*&numRCMf>L%M8fMxZaejLcf1>s3!9Ne}sWxUH>D|#DKc~Bddex0*V18 iKzc#Cgd)J&Kpc>u0s|kz;HRYk delta 157 zcmZ1%@-1jW!^8^$S}BYQe2k0?3>wS|yde4vi=+UEPGFM)(<~ec%piUS$HZM>5ob6Q zSU|i0P6ZnfeMLaZ4y+o;2GJ_w{PrOFrj#HXvbeJ}ME!9khWdrg!Q9a_ttC diff --git a/applications/PoP/poc/res/bg/pop_env4.atl b/applications/PoP/poc/res/bg/pop_env4.atl index 395c5d04afb9d3c539cde87b029d81d2f9f64d57..ad5c6c3f2377a38d8d393a1fddbe061dfcf43a48 100644 GIT binary patch literal 8498 zcmeHM&2Ah;5FQgOBrBT%94D)d@h&$GTsU*$4Wi9P-h(eZ0uo1Fg@@oNcmyO~X6X9= zs=H^_#!ldnk+NO&ef3pUSNAB{&AYch{jeTB{PX?U`r_`SuO1zkjja z(E8`a4X=N#HY52zkLS;5{dHWi|Kpb9q^wz^SyBCPBw>z%;+x8Xrd$+sdl_s+%^V5aIrkm#|qdXvM?#dEZ)Z5|B!|zPd#U`|eyfkzMve%^h$h41`_JH%^ zJ!K$!EZ`{pcsNbPZNIKIK)Nx{Uv~rgv}aLTw`Xdk3uUg-=2BO3TSa~^8pJ&{(!zUI zo`N>Yqr~EDC$VRD$ug~V^pI3-Ud%v|lhIx^Qj`~I^XWx`d6XfeSL_UAk3#)cvs0^u zwwR8Z1PyI~6gSTg*Z`%qrqNM^EpwfwKKT(ud8IA71axVY_Gu8inwv|hBn3ijqd1ED zJeO(7=FL^E%*k2p&HXmot9r~d-Tb>Z#m@>-h53|h&Xv8mV^aH3Y&D(wWMX$@Ewd+H z4{pE1kmW@Aj{1v>r&NfVP>N$r1qUI zvWc|RUTcw+#vs-!IyP;FaX8F9`x#=t?=uBQ#%;!@sbmvq&o+7GpaIj&o7E_DHDlim6bAHsIFEnRb0zA%Jjn)u zW#O3P@p@Rf9wnRuJ|4&Q)jA5O5~htQ5U?bj4vX<9&)mw_9g{L|xNRo2UpU0z5f#0Eng-c{()48V%Kv){6)bn594?U#2D8 zT2r;VxGMrrMnlt{L*yfD3+;Sxo}jYEx?MNSdTHHOB6V!g_(uau0V>1DvcNd?f%3qN~ERsY>R9p;rPmx9J}F#-8@g~ z#yk0X>wY$rJsdKup0Ia^yDslqs?g{Y+wc`z@y3?A_CS7PjpEh$F6;R_tQ28=%kti- zK;e~zrl1330ZQik(Z!Y8J5ZQao0Aw@|t^fc4 diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 8a589c7..655347d 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -6,8 +6,8 @@ EXAMPLE := roomtest # small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе # CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge). MEMORY ?= small -EXTRA_FLAGS ?= --gfx 256 -EXTRA_SRCS := pop_bg.c pop_kid.c pop_ctrl.c pop_map.c pop_level.c +EXTRA_FLAGS ?= --gfx 256 --w3 pop_trob.c --w3 pop_map.c --w3 pop_bg.c +EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c BG_DIR := $(CURDIR)/../poc/res/bg KID_DIR := $(CURDIR)/../poc/res/kid @@ -33,4 +33,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py kid_data.h: $(TC)/pop_extract_kid_data.py cd $(TC) && python3 pop_extract_kid_data.py -$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h kid_data.h $(BG_DATA) $(KID_DATA) $(LVL_DATA) +$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(LVL_DATA) diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md new file mode 100644 index 0000000..953318d --- /dev/null +++ b/applications/PoP/roomtest/bug_list.md @@ -0,0 +1,333 @@ +# roomtest — список известных багов (Фаза B и смежное) + +Найдено при тестировании Фазы B (кнопки/ворота) в MAME. **НЕ решаем сейчас** +— задача следующего этапа. Правило проекта: механику сверять с +`applications/PoP/SDLPoP/src/` ДО кодинга. + +Статус на момент записи: 2026-07-21. Тестовый старт — комната 6 (`START_ROOM` +в `roomtest.c`). Кнопка room6(0,2, opener) → ворота room8(0,9), видны в левом +шве room6. + +--- + +## BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — **Critical** + +**Симптом:** при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату (через шов) +Kid оказывается на ряду **row 1** вместо row 0 — «проваливается» на этаж ниже +(стоит в дверном проёме на неправильной высоте). + +**Где:** `pop_map.c` `check_leave()` (порт seg002 leave_room) — боковой уход +`pop_leave_dir`; репроекция координат `Kid.x ∓= 140` есть, но Y/`curr_row` +при боковом переходе НЕ репроецируются (в отличие от `check_leave_below`, +где `Kid.y -= 189; curr_row = y_to_row`). При входе в соседнюю комнату у +шва Kid, видимо, попадает в позицию, где `determine_col`/физика следующего +кадра сбрасывают его на row 1 (нет пола на row 0 у входа, либо неверный y). + +**Сверить с SDLPoP:** `goto_other_room` (seg002) — как именно +пересчитываются `Char.curr_row`/`Char.y` при side-переходе (left/right). +Для вертикального перехода мы это сделали, для бокового — нет. + +## BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — **Critical** + +**Симптом:** после перехода в соседнюю комнату попытка сразу вернуться +обратно приводит к тому, что Kid снова закидывается в ту же комнату — +переход «пинг-понгует», выйти нельзя. + +**Где:** `pop_map.c` `check_leave()`. После перехода Kid репроецирован к +ПРОТИВОПОЛОЖНОЙ кромке новой комнаты (`x ∓ 140`). На СЛЕДУЮЩЕМ же кадре +передний край снова за порогом ухода в обратную сторону → `pop_leave_dir` +взводится опять → мгновенный обратный переход. Нужен guard «только что +перешёл» (подавить повторный leave на N кадров / пока Kid не отойдёт от +кромки), как в оригинале (`next_room`/leave обрабатываются раз за кадр + +Kid входит достаточно внутрь). + +**Сверить с SDLPoP:** порядок `leave_room`/`goto_other_room` + на какой +X ставится персонаж при входе (должен быть НЕ на самой кромке, иначе +мгновенный обратный триггер). + +## BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия (Kid поверх кнопки) — **Critical** + +**Симптом:** когда Kid подтягивается (climb-up) на тайл, верх которого — +КНОПКА (opener/closer), а не обычный пол, отрисовка неверная: Kid рисуется +ПОВЕРХ кнопки, хотя передняя грань пола/кнопки должна перекрывать нижнюю +часть Kid во время подъёма. + +**Где:** `pop_bg.c` `climb_overlay_tile()` — floor-overlay при подъёме +рисуется только для floor-подобных тайлов `code ∈ {1,3,5,19}`. Кнопка +(opener 0x0F / closer 0x06) в этот набор НЕ входит → пол-оверлей кнопки не +рисуется поверх Kid. Нужно: для нажатой кнопки (draw как floor/stuck) +трактовать её как floor-тайл в climb-overlay (учесть подстановку из +`get_tile_to_draw`), и/или добавить opener/closer в набор. + +**Сверить с SDLPoP:** `draw_floor_overlay` (seg008) + `get_tile_to_draw` +(нажатая кнопка → tiles_1_floor / tiles_5_stuck) — оверлей должен идти по +ПОДСТАВЛЕННОМУ типу тайла. + +--- + +## Оптимизации (отложено) + +### OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22 + +**Возможность:** `pop_room_redraw_seam_left()` (pop_bg.c) на каждое изменение +openness рисует `bar(BLACK)` по всему col0 + **полный `draw_tile(0,0)`** +(стены, `topright`, `wall_pattern` с `prandom()` — десятки блитов). Реально +анимируются только бары решётки — `draw_gate_back` (~9 `env_b`). + +**Стоимость:** seam-блок (синий io_border) занимает ~30-50% кадрового периода, +**но только пока openness меняется** — т.е. во время открытия и медленного +авто-закрытия (~5 сек после схода с кнопки). В покое (ворота открыты-держим +кнопку / закрыты / FF) — 0%. Замерено в MAME: брейк на +`_pop_room_redraw_seam_left` (0x5A54) срабатывает ⟺ сегмент дорогой. + +**Приём** (как у пик, но сложнее — heal НЕ годится: печёные бары устаревшие, +поэтому и стоит `bar(BLACK)`+redraw): `bar(BLACK)` только по полосе баров +(x=0, gate_top..gate_bot) + `draw_gate_back(lmod,...)` + дорисовать статику +тайла (0,0), задетую полосой. Ожидаемо срезает бóльшую часть 30-50%. +**Пиксель-чувствительно** — полоса баров пересекается со статикой col0, +обязательна выверка в MAME по кромкам. + +**Решение (2026-07-22):** оставляем как есть — стоимость транзиентная, в бюджет +помещаемся. Делать, если упрёмся в кадровый бюджет на сценах с воротами. + +--- + +## Исправлено + +- **Шов ворот жёг 50% кадра в покое (закрытая решётка)** — ИСПРАВЛЕНО + 2026-07-22. Причина — **баг кодогенератора SDCC z80** (memory + `sdcc_z80_cmp_store_a_bug`): `if (m[9] != seam_sig) seam_sig = m[9];` + компилировался в `sub (seam_sig)` (A ← разность) + `ld (seam_sig),a` — + сохранял РАЗНОСТЬ `m[9]-seam_sig`, не `m[9]`. `seam_sig` осциллировала + (напр. 25↔231), `m[9]!=seam_sig` истинно каждый кадр → `draw_tile(0,0)` + каждый кадр даже у неподвижной решётки. Фикс: store-до-сравнения + (`seam_sig = g;` из чистого `g` ДО `sub`), подтверждён в .asm. Прочёс всех + модулей PoP: других случайных compare-then-store нет. + +- **Пики: 2×полный `draw_tile` на кадр анимации** → хирургический редрой, + 2026-07-22. `pop_spike_redraw`: `heal_off` уже возвращает всю печёную + статику (база 127, пол, грани); поверх анимируются только два острия + (`SPIKES_FRAM_LEFT` в своей ячейке + `SPIKES_FRAM_RIGHT` в соседней). + Заменили 2×`draw_tile` на 2×`env_b` — пиксель-в-пиксель тот же результат, + в разы дешевле (шахта из нескольких пик больше не съедает полный кадр). + +- **Отрисовка нажатой кнопки (0,2)/(0,3)** — ИСПРАВЛЕНО. Причина: `fore_tile` + (pop_bg.c, fore-слой поверх Kid) рисовал переднюю грань КНОПКИ (bottom_id + 149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки». + Фикс: `fore_tile` применяет ту же подстановку нажатой кнопки, что и + `draw_tile` (opener→floor / closer→stuck при таймере связи >1). Плюс + `pop_button_redraw` — wipe своей ячейки + правой грани (дальний угол в + 0,3), низ строго yb+64 (не залезать в стену ряда 1). + +## Заметки + +- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо + (эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в + сторону = разворот (как в оригинале safe_step), движение со второго. +- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`). +- `START_ROOM=6` — временный тестовый старт; вернуть на + `pop_level_start_room()` после отладки объектов. + +--- + +## BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — **Medium** + +**Симптом (2026-07-22):** при прыжке вверх (SEQ up, кадры 67..79) руки/голова +Kid заходят в полосу кладки у потолка (row -1) и рисуются ПОВЕРХ неё — потолок +должен окклюдить руки (как передняя грань). + +**Где:** `pop_bg.c` — полоса потолка (`draw_tile(-1)`) сейчас только фон +(запечена в pop_room_draw), не окклюдер поверх Kid. Нужен ceiling-overlay в +`pop_fore_over_kid` (перерисовать срез полосы над Kid поверх спрайта), по +аналогии с fore-тайлами. Инфраструктура `g_clip_top`/`gfx_blit_part` уже есть. + +## BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row -1) не работает — **Medium** + +**Симптом (2026-07-22):** loose-плита в ряду 2 верхнего соседа (room5 (2,5) → +потолок room6 над (0,5)) при прыжках Kid на (0,5) НЕ трясётся и не разбивается. + +**Где:** верхний сосед не «живой» — `above_fg` берётся из статики уровня, +loose-состояние соседней комнаты не тянем (per-room loose/modif не персистятся, +P0). check_press/knock работают только по текущей комнате. Нужно: (а) пробой +loose сверху через границу комнаты, (б) персистентное loose-состояние соседа + +его отрисовка в полосе. Пока полоса рисует loose статически (кадр покоя). + +## BUG-OCCL-1. Тень дальней колонны перекрывает Kid в (0,4)-(0,5) — **Medium** + +**Симптом (2026-07-22):** Kid, стоя/прыгая у (0,4)-(0,5), частично перекрыт +тёмной штриховкой — это ТЕНЬ/боковая грань ДАЛЬНЕЙ колонны (сзади по глубине), +которая НЕ должна окклюдить персонажа. Over-occlusion fore-слоя. + +**Где:** `pop_bg.c` `pop_fore_over_kid`/`fore_tile` — рисует fore футпринт-тайлов +без учёта глубины (какая грань РЕАЛЬНО перед Kid). Порт seg003 redraw_at_char +окклюдит только передние грани; дальняя колонна (за Kid) не должна. Сверить с +SDLPoP redraw_at_char (какие тайлы/грани идут в fore vs back по y-сортировке). + +## BUG-CEIL-3. Анимация ворот стирает потолок над ними — **Medium** + +**Симптом (2026-07-22):** при опускании/поднятии решётки шва (анимация openness) +полоса кладки у потолка НАД воротами пропадает (см. потолок над левым швом). + +**Где:** `pop_bg.c` `pop_room_redraw_seam_left` (и `pop_button_redraw`/ +`pop_spike_redraw` — общий класс): чёрный `bar` по col0 row0 стирает потолочную +полосу (row-1), а redraw рисует только сам гейт, не восстанавливая полосу. +Нужно: при перерисовке шва/тайла у потолка перерисовать и срез потолочной полосы +(draw_tile(-1,col) с g_clip_top) над затронутыми колонками. + +--- + +# BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — **РЕШЁН 2026-07-22** + +Статус: 2026-07-22, вечер. НАСТОЯЩИЙ корень найден потиковой трассой ЖИВОГО +SDLPoP 1.23 (lldb-брейкпоинты на leave_room/bumped/safe_step с логом Char + +char_x_left/right; fixes выключены = vanilla). Все прежние гипотезы ниже +оставлены для истории — они НЕ были причиной. + +## КОРЕНЬ (подтверждён трассой + исходником) + +`set_char_collision` (seg006:0723): `char_x_right = obj_x/2 + 58`, где +`load_frame_to_obj` (seg008:1728) считает `obj_x = 2*char_dx_forward(dx) - 116` +и **добавляет +1** для кадров «чётного пикселя»: +`if ((sbyte)(cur_frame.flags ^ obj_direction) >= 0) ++obj_x;` +(бит 0x80 флагов кадра XOR направление; вправо: +1 если бит НЕ стоит). +Деление `obj_x/2` — C-усечение К НУЛЮ, поэтому при `e = x+dx <= 57` +(obj_x < 0, зона левого шва) поправка +1 даёт `char_x_right = e+1`, а при +e >= 58 формула сокращается к чистому `e`. + +Итог: Kid, осевший после отскока от ворот шва на x=57 (frame15, флаги 0x43 — +бит 0x80 не стоит), имеет **char_x_right = 58** и порога leave-left (<=57) +НЕ достигает. Наш движок считал передний край как `Kid.x + dx` без поправки +→ 57 → ложный leave → пинг-понг. + +Эталонный цикл SDLPoP (сн normalized по трассе): стойка x=61 → тап вправо → +safe_step(d=0) → step, на первом dx(1) x=62 → bump (edge-триггер) → align 61 → +seq47 dx(-4) → **x=57** (скрыт за кромкой) → кадры 50/51/52 (cxr 61/60/58, у +всех бит 0x80 снят, e>57 — без сдвига) → стойка cxr=58 → leave НЕ срабатывает; +тап → safe_step d=3 → x=60 (1/3 видно); тап → step1 → x=61 (2/3 видно); тап → +bump → 57 … по кругу. Char.room и drawn_room НЕ меняются вообще. + +## Фикс (pop_map.c) + +`char_x_forward_edge()`: `e = char_dx_forward(dx); if (((flags ^ (dir<0 ? +0x80 : 0)) & 0x80) == 0 && e <= 57) e++;` — используется в `char_front_coll` +(коллизия/bump/edge_distance) и в `check_leave` (порог ухода). Плюс порт +doortop-гарда leave-right из leave_room (тайл (9,row) = doortop → правого +выхода нет). `pop_leave_timer` (exit_room_timer) оставлен — он реален в +seg002/seg003. + +## Гипотезы, ОТПАВШИЕ после трассы (история диагноза) + +## Симптом (подтверждён пользователем + инструментально) + +Kid стоит за решёткой закрытых ворот шва (левый сосед room8 виден в кромке +room6). При удержании/нажатии ВПРАВО экран пинг-понгует между двумя +состояниями: +- **A**: показывается room6, Kid у левой кромки за решёткой (спрайт виден на 2/3); +- **B**: показывается room8, Kid у его правой кромки. + +Эталон SDLPoP (описан пользователем): drawn_room **всегда остаётся room6**, +Kid осциллирует у кромки (1/3→2/3→отступил→по кругу), в room8 экран НЕ +переключается. То есть у нас лишний свитч A→B. + +## Тестовый контекст + +START_ROOM=6. Ворота room8(0,9) закрыты, видны в col0 room6. Kid-структура +kframe: см. pop_kid.h (frame,x,y,direction,curr_col,curr_row,action,...). +Адрес _Kid и статиков — в .sprinter-cc-roomtest/roomtest.map (сдвигается от +сборки к сборке). MAME: memory `mame_mcp_bridge`/`mame_hdd_test_disk`. +Отладочные: J/L — короткий шаг влево/вправо (эмуляция Shift+стрелка). + +## Инструментальный диагноз (watchpoint на pop_leave_dir) + +В момент лишнего свитча A→B: `pop_leave_dir=1 (LEFT)`, Kid **frame=15 (СТОЯ, +не transient!), Kid.x=57** (до репроекции +140). То есть: +- `char_x_right = char_dx_forward(kid_cur_dx()) = Kid.x + frame15.dx = 57+0 = 57`. +- Порог leave-left (взгляд вправо): `char_x_right <= 57` → срабатывает РОВНО на 57. +- Грань ворот (где их держит коллизия) = **61** (`wall_dist_from_left[1]=10 + + coll_tile_left_xpos=51`). Между 57 и 61 — **зазор 4px**: Kid НЕ удержан + воротами (d=61−57=4≥0 → check_bumped не бампит), но уже на пороге ухода. +- Kid оседает на 57 из-за recoil отскока: seq_47 = `act(bumped), dx(-4), + frame_50, 51, 52`; SEQ_DX(−4) двигает Char.x на −4 суммарно; frame_50.dx=4 + компенсирует ТОЛЬКО точку коллизии НА кадре 50, но при возврате в стойку + (frame15, dx=0) `char_x_right = Char.x = aligned−4 = 57`. + +## Что ИСКЛЮЧЕНО (сверено с исходниками SDLPoP, НЕ причина) + +- Формула char_x: `char_x_right = obj_x/2+58 = Char.x+frame.dx` (seg006 + set_char_collision) — совпадает с нашим char_dx_forward. +- Позиция грани ворот: `get_left_wall_xpos = wall_dist_from_left[1](10) + + xpos_in_drawn_room(x_bump[9+5])+7 = 10+(184−140)+7 = 61` — совпадает с нашим + (`x_bump[−1+5]=44`, +7, +10 = 61). +- Порог leave: SDLPoP leave_room looking-right `char_x_right<=57` — совпадает. +- Данные кадров: frame_50 (image=49,dx=4,flags=0x67) и frame_15 + (image=14,dx=0,flags=0x43,sword=9) — БАЙТ-В-БАЙТ как в SDLPoP frame_table_kid. +- seq_47 (act bumped, dx(-4), frame 50/51/52) — совпадает. + +## Почему точечные фиксы НЕ работают + +- Гард в check_leave (подавить leave на закрытых воротах) — это ОТСЕБЯТИНА, + не SDLPoP (в leave_room такого нет); откачено. +- `exit_room_timer=2` (порт seg002 exit_room — РЕАЛЬНЫЙ механизм, оставлен как + pop_leave_timer): блокирует leave 2 кадра после входа в комнату. НЕ спасает: + положение Char.x=57 **устойчивое** (Kid стоит), а не transient — после + истечения таймера leave срабатывает. +- По коду SDLPoP raw-рассуждением раз за разом выходит, что leave-left ДОЛЖЕН + сработать и там (Char.x=57), но эталон показывает, что нет. Значит различие + не в этих кусках, а в ДИНАМИКЕ: где именно оседает Char.x у шва И как + вычисляется drawn_room. + +## КОРЕНЬ (гипотеза) + +Наш движок держит `drawn_room == комната_Kid` ВСЕГДА, а шов подделан: +- коллизия — самодельные снапшоты кромок соседа (g_lcol/g_rcol, 1 колонка) + + `x_bump[tcol=-1]`, а НЕ порт `curr_row_coll_room[]`/`get_all_coll_row`/ + `xpos_in_drawn_room` из seg004; +- уход — мгновенный `enter_room(сосед)` при пороге char_x, а НЕ отложенный + drawn_room через `next_room`/`check_the_end`. + +Из-за упрощённой коллизии Char.x у шва оседает иначе, чем в SDLPoP (у нас ровно +57 = порог), и/или drawn_room переключается там, где в оригинале нет. + +## План S2/S3 (порт coll_room + отложенный drawn_room) — НЕ потребовался для +## этого бага (трасса показала: в эталоне Char.room/drawn_room вообще не +## меняются). Остаётся как задел под стражей/двух персонажей в кадре. + +Правило проекта: SDLPoP — источник истины, переводить 1:1, НЕ рассуждать «как +должно быть». Нужно портировать связку целиком: + +1. **Раздельные комнаты.** `Char.room` (реальная комната персонажа) ≠ + `drawn_room` (отрисованная). У нас уже есть `kid_room` (S1) + рендер-смещение + `pop_kid_set_render_dx(∓140)` (порт xpos_in_drawn_room для СПРАЙТА). + +2. **Коллизия по Char.room через coll_room (seg004, S2).** Портировать + `check_collisions` → `get_row_collision_data` (строки curr/below/above) → + `get_left_wall_xpos`/`get_right_wall_xpos` (по `xpos_in_drawn_room(x_bump[ + col+FIRST_ONSCREEN_COLUMN])`, ±140 когда колонка принадлежит соседу) → + массивы `curr_row_coll_room[]`/`_flags[]`, `bump_col_left_of_wall`/ + `bump_col_right_of_wall` → `check_bumped_look_left/right` → `bumped()`. + Тайлы берутся из РЕАЛЬНОЙ комнаты колонки (get_tile(room,col,row) для любой + комнаты), НЕ из снапшота. Именно это определяет, где Char.x оседает у шва. + +3. **Отрисовка по drawn_room, персонаж со сдвигом.** Рисуем drawn_room; Kid + (и будущие стражи) — со смещением ±140, если их комната ≠ drawn_room. + +4. **Отложенная смена drawn_room (seg002/seg000, S3).** Уход = + `leave_room` (seg002:0504, ПОРТ ЦЕЛИКОМ, включая up/down/side, guard'ы + climb/standup/turn, doortop-check для right) → `goto_other_room` + (seg002:0486: Char.room=сосед, x±140/y±189). `exit_room` (seg002:03C7): + `exit_room_timer` + `next_room = Char.room`. `check_the_end` (seg000:0FBD): + `if (next_room != 0 && next_room != drawn_room) drawn_room = next_room; + redraw`. drawn_room меняется ТОЛЬКО так, а не по нашему прямому enter_room. + +Ключевой момент, который надо переводить БУКВАЛЬНО (а не выводить): порядок +вызовов в play_frame (seg000:863 `play_frame`: … play_kid_frame → exit_room → +check_the_end) + set_char_collision перед leave_room в exit_room. + +Карта сегментов: seg002 (leave/goto/exit_room), seg004 (коллизия/coll_room/ +bumped), seg006 (set_char_collision/play_seq/frame_table_kid), seg000 +(play_frame/check_the_end/draw). Наши файлы: pop_map.c (коллизия/физика/leave), +roomtest.c (enter_room/drawn_room/главный цикл), pop_kid.c (кадры/рендер-сдвиг). + +Реализовано на 2026-07-22: S1 (kid_room + render_dx) + pop_leave_timer. +S2/S3 отложены — баг закрыт odd-pixel поправкой (см. КОРЕНЬ выше). +Память: [[pop_seam_room_model]], [[sdlpop_odd_pixel_char_x]]. diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 8440854..63c3268 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -14,6 +14,7 @@ #include #include #include "pop_bg.h" +#include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */ /* ---- Атласы (прямая адресация) ------------------------------------- */ static atlas_t env[5]; @@ -44,20 +45,36 @@ void pop_bg_free(void) atlas_free(&fore_at); } +/* Верхний клип блитов фона (ЭКРАННАЯ y): !=0 — строки кадра с screen_y < + * g_clip_top не рисуются (частичным блитом со сдвигом источника). Нужно + * полосе кладки у потолка (row<0): высокие кадры не должны лезть в верхний + * борт (за пределы игрового поля). 0 — без клипа. */ +static int g_clip_top; + /* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x, - * прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0). */ + * прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0). + * При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */ static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) { const uint8_t *img; uint16_t w, h; + int top; if (idx >= a->count) return; img = (const uint8_t *)atlas_image(a, idx); gfx_w0_map(a->page); w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); - if (w && h) - gfx_blit(x, ybottom - (int)h + 1 + POP_YOFF, img); /* +YOFF: центрирование */ + top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */ + if (w && h) { + if (g_clip_top && top < g_clip_top) { + int sy = g_clip_top - top; /* пропустить верхние строки */ + if (sy < (int)h) + gfx_blit_part(x, g_clip_top, img, 0, sy, (int)w, (int)h - sy); + } else { + gfx_blit(x, top, img); + } + } gfx_w0_unmap(); } @@ -116,6 +133,8 @@ static const uint8_t WALL_FRAM_BOTTOM[4] = {7, 9, 5, 3}; static const uint8_t WALL_FRAM_MAIN[4] = {8, 10, 6, 4}; static const uint8_t DOOR_FRAM_SLICE[9] = {67, 59, 58, 57, 56, 55, 54, 53, 52}; static const uint8_t SPIKES_FRAM_RIGHT[10] = {0, 134, 135, 136, 137, 138, 137, 135, 134, 0}; +static const uint8_t SPIKES_FRAM_LEFT[10] = {0, 128, 129, 130, 131, 132, 131, 129, 128, 0}; +static const uint8_t SPIKES_FRAM_FORE[10] = {0, 139, 140, 141, 142, 143, 142, 140, 139, 0}; static const uint8_t BLUELINE_FRAM1[4] = {0, 124, 125, 126}; static const int8_t BLUELINE_FRAM_Y[4] = {0, -20, -20, 0}; static const uint8_t BLUELINE_FRAM3[4] = {44, 44, 45, 45}; @@ -133,6 +152,7 @@ static const uint8_t RPOS[4] = {52, 42, 31, 21}; /* ---- Текущая комната (globals для tile_code/mod) ------------------- */ static const uint8_t *g_fg, *g_bg, *g_lfg, *g_lbg; static const uint8_t *g_below; /* row0-коды комнаты СНИЗУ (для topright под нижним рядом) */ +static const uint8_t *g_above, *g_abovemod; /* row2 fg/mod комнаты СВЕРХУ (полоса кладки у потолка) */ static uint8_t g_room; /* Задать ряд 0 комнаты снизу (fg-коды) — нужен для draw_tile_topright/ @@ -141,13 +161,22 @@ static uint8_t g_room; * снизу (кромка уровня = стена). */ void pop_room_set_below(const uint8_t *below_row0_fg) { g_below = below_row0_fg; } +/* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки + * у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom + * при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */ +void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) +{ g_above = above_row2_fg; g_abovemod = above_row2_mod; } + static uint8_t tile_code(int row, int col) { if (col < 0) { if (g_lfg && row >= 0 && row < 3) return g_lfg[row]; return 20; /* уровневая кромка = стена */ } - if (row < 0) return 1; /* верхняя кромка = пол */ + if (row < 0) { /* верхняя полоса: ряд 2 комнаты сверху */ + if (row == -1 && g_above && col < 10) return g_above[col]; + return 1; /* нет комнаты сверху → пол (кромка) */ + } if (row > 2 || col > 9) return 20; return g_fg[row * 10 + col]; } @@ -158,6 +187,7 @@ static uint8_t tile_mod(int row, int col) if (g_lbg && row >= 0 && row < 3) return g_lbg[row]; return 0; } + if (row == -1 && g_abovemod && col < 10) return g_abovemod[col]; if (row < 0 || row > 2 || col > 9) return 0; return g_bg[row * 10 + col]; } @@ -213,7 +243,9 @@ static void wall_pattern(int row, int col, int which_part) uint16_t mid_div, mid_off, bot_div, bot_off; uint8_t bg; - rnd_seed = (unsigned long)(g_room + TBL_LINE[row] + col); + /* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху + * (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */ + rnd_seed = (unsigned long)(g_room + TBL_LINE[row < 0 ? 2 : row] + col); prandom(1); /* сброс */ mid_div = prandom(1); mid_off = prandom(4); @@ -321,6 +353,14 @@ static void draw_tile(int row, int col) uint8_t code = tile_code(row, col); uint8_t lcode = tile_code(row, col - 1); uint8_t lmod = tile_mod(row, col - 1); + /* Нажатая кнопка (seg008 get_tile_to_draw): при таймере связи >1 opener + * (0x0F) рисуется как ПОЛ (modif 0), closer (0x06) — как stuck (0x05). + * Модификатор кнопки = индекс LINKLOC. И для самого тайла, и для левого + * соседа (его правая грань). */ + if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; } + else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; } + if (lcode == 0x0F) { if ((pop_doorlink2(lmod) & 0x1F) > 1) { lcode = 1; lmod = 0; } } + else if (lcode == 0x06) { if ((pop_doorlink2(lmod) & 0x1F) > 1) lcode = 5; } int xh = COL_XH[col]; int x = xh * 8; int dby = 63 * row + 65; @@ -367,15 +407,19 @@ static void draw_tile(int row, int col) } } - /* --- draw_tile_anim_right (seg008.c:530) — статичные части --- */ - if (lcode == 4) { /* gate: решётка */ - draw_gate_back(lmod, xh, dby, dmy); - } else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */ - env_b(LOOSE_FRAM_RIGHT[get_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1); - } else if (lcode == 2) { /* spike: статич. кадр */ - uint8_t sf = (lmod & 0x80) ? 5 : lmod; - if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) - env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); + /* --- draw_tile_anim_right (seg008.c:530) — статичные части. Для + * верхней полосы (row<0, draw_tile_aboveroom) НЕ рисуется + защита от + * отрицательного индекса в pop_loose_modif. --- */ + if (row >= 0) { + if (lcode == 4) { /* gate: решётка */ + draw_gate_back(lmod, xh, dby, dmy); + } else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */ + env_b(LOOSE_FRAM_RIGHT[get_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1); + } else if (lcode == 2) { /* spike: статич. кадр */ + uint8_t sf = (lmod & 0x80) ? 5 : lmod; + if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) + env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); + } } /* --- draw_tile_bottom (seg008.c:570) --- */ @@ -387,12 +431,16 @@ static void draw_tile(int row, int col) env_b(t->bottom_id, x, dby); } - /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif) --- */ + /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif). В + * верхней полосе (row<0) loose ряда 2 верхнего соседа рисуем статически + * (кадр покоя 43): анимацию/разбитость соседа мы не тянем. --- */ if (code == 11) - env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(pop_loose_modif[row * 10 + col])], x, dby); + env_b(row >= 0 ? LOOSE_FRAM_BOTTOM[get_loose_frame(pop_loose_modif[row * 10 + col])] + : LOOSE_FRAM_BOTTOM[0], x, dby); - /* --- draw_tile_base (seg008.c:611) --- */ - { + /* --- draw_tile_base (seg008.c:611). Верхняя полоса (row<0) базу НЕ + * рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */ + if (row >= 0) { uint8_t base_id = t->base_id; if (code == 11) /* loose: левая грань (анимир.) */ base_id = LOOSE_FRAM_LEFT[get_loose_frame(pop_loose_modif[row * 10 + col])]; @@ -400,6 +448,17 @@ static void draw_tile(int row, int col) env_b(base_id, x, t->base_y + dmy); } + /* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в + * СВОЕЙ ячейке (анимир. по своему modif). Без него остриё пики + * рисуется только правым соседом со сдвигом → шахта над самой пикой + * пустая. */ + if (row >= 0 && code == 2) { + uint8_t sm = tile_mod(row, col); + uint8_t sf = (sm & 0x80) ? 5 : sm; + if (sf > 0 && sf < 10 && SPIKES_FRAM_LEFT[sf]) + env_b(SPIKES_FRAM_LEFT[sf], x, dmy - 2); + } + /* --- draw_tile_fore (seg008.c:690) --- */ if (code == 20) { wmod = wall_modifier(row, col); @@ -431,6 +490,75 @@ void pop_loose_shake_draw(int row, int col) gfx_set_bank(GFX_BANK_NORMAL); } +/* Перерисовать пики тайла (row,col) на back-странице по ЖИВОМУ modif + * (pop_trob). Кадр выдвижения рисует ПРАВЫЙ сосед (draw_tile ветка lcode==2 + * читает lmod = g_bg[row*10+col] = живой modif пики). g_bg должен указывать + * на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile. + * heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */ +void pop_spike_redraw(int row, int col) +{ + int x = COL_XH[col] * 8; + int dmy = 63 * row + 62; /* как в draw_tile (dby-3) */ + uint8_t sm = g_bg[row * 10 + col]; /* живой modif пики (room_modif) */ + uint8_t sf = (sm & 0x80) ? 5 : sm; /* видимый кадр (hold 0x80..0x8F → 5) */ + /* Шипы выдвигаются выше линии пола (env_b на dmy-7) — heal с запасом + * вверх (63*row+34 .. +74) покрывает и остриё, и сам пол; ширина 64 = + * тайл + правый сосед (остриё рисуется в его ячейке). + * heal вернул из ОЗУ-копии ВЕСЬ печёный статичный фон (база пик 127, пол, + * грани соседей — всё, что рисовал бы полный draw_tile при modif=0). + * Единственное, что реально анимируется — два спрайта острия (LEFT в своей + * ячейке + RIGHT в соседней, оба по sf). Поэтому вместо 2×draw_tile + * (десятки блитов статики поверх идентичного фона = идемпотентно) рисуем + * РОВНО эти два острия — пиксель-в-пиксель тот же результат, во много раз + * дешевле (порт redraw_21h: draw_tile_anim + draw_tile_anim_right). */ + heal_off(x, 63 * row + 34, 64, 40); + gfx_set_bank(GFX_BANK_SPRITE); + if (sf > 0 && sf < 10) { + if (SPIKES_FRAM_LEFT[sf]) /* остриё в своей ячейке */ + env_b(SPIKES_FRAM_LEFT[sf], x, dmy - 2); + if (col + 1 < 10 && SPIKES_FRAM_RIGHT[sf]) /* остриё в ячейке соседа */ + env_b(SPIKES_FRAM_RIGHT[sf], COL_XH[col + 1] * 8, dmy - 7); + } + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* Перерисовать ЛЕВЫЙ шов (col0) на back-странице по живому modif соседа — + * ворота соседней комнаты слева (portcullis room8) видны в col0 текущей + * комнаты через draw_gate_back (draw_tile ветка lcode==4, lmod = g_lbg[0]). + * g_lbg (lcol_bg) должен указывать на живой modif (roomtest обновляет из + * room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake): + * heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары + * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */ +void pop_room_redraw_seam_left(void) +{ + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(0, POP_YOFF, 31, POP_YOFF + 64); /* col0, ряд 0 (бары ворот) */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + draw_tile(0, 0); /* фон col0 + бары ворот (живой modif) */ + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* Перерисовать тайл КНОПКИ (row,col) на back-странице: нажатая/отжатая + * (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный + + * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе + * в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */ +void pop_button_redraw(int row, int col) +{ + int x = COL_XH[col] * 8; + int yb = POP_YOFF + 63 * row; + /* Wipe СВОЕЙ ячейки + правой грани кнопки (заходит в 0,3) + перерисовка + * пола. НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда 1 + * (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */ + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(x, yb + 26, x + 59, yb + 64); + gfx_set_bank(GFX_BANK_TRANSPARENT); + draw_tile(row, col); + if (col + 1 < 10) draw_tile(row, col + 1); + gfx_set_bank(GFX_BANK_NORMAL); +} + /* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — @@ -571,6 +699,14 @@ void pop_room_draw(uint8_t room_num, for (row = 2; row >= 0; row--) /* снизу вверх (2,1,0) */ for (col = 0; col < 10; col++) draw_tile(row, col); + /* Полоса кладки у потолка (seg008 draw_room: доп. ряд из room_A, row2 + * при draw_main_y=-1): нижние/правые грани ряда 2 комнаты сверху. Кадры + * высокие — верх ушёл бы в борт (за поле); клип по верхней границе поля + * (POP_YOFF) рисует ТОЛЬКО срез у потолка, без зачернения борта. */ + g_clip_top = POP_YOFF; + for (col = 0; col < 10; col++) + draw_tile(-1, col); + g_clip_top = 0; gfx_set_bank(GFX_BANK_NORMAL); } @@ -598,7 +734,13 @@ static int8_t y_to_row(int y) /* y_to_row_mod4 (seg006, TILE_SIZEY= static void fore_tile(int row, int col) /* передний слой одного тайла */ { uint8_t code = tile_code(row, col); - const piece *t = &tile_table[code]; + const piece *t; + /* Нажатая кнопка → пол/stuck (как get_tile_to_draw), ИНАЧЕ fore рисует + * переднюю грань КНОПКИ (bottom_id 149) поверх Kid, перекрывая уже + * нарисованную грань пола 43 → «остаток нажатой кнопки» на грани. */ + if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; } + else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; } + t = &tile_table[code]; int x = COL_XH[col] * 8; int dmy = 63 * row + 62; int dby = dmy + 3; /* = 63*row+65 (draw_tile_bottom) */ @@ -611,6 +753,12 @@ static void fore_tile(int row, int col) /* передний слой одно } else { if (t->bottom_id) /* передняя кромка пола (draw_tile_bottom) */ env_b(t->bottom_id, x, dby); + if (code == 2) { /* пики: fore-кадр поверх Kid (напоротый) */ + uint8_t sm = tile_mod(row, col); + uint8_t sf = (sm & 0x80) ? 5 : sm; + if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf]) + env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2); + } if (t->fore_id) /* передние объекты: колонны/ворота/щебень */ fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); } @@ -665,10 +813,11 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, int wh = (int)((w + 1) >> 1); /* char_width_half */ int xl = obj_x / 2 + 58; /* char_x_left (58-база) */ int xr; - int8_t cL, cR, rT, rB, r, c; + int8_t cL, cR, cLraw, rT, rB, r, c; if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ xr = xl + wh; - cL = col_from_x(xl); if (cL < 0) cL = 0; else if (cL > 9) cL = 9; + cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */ + cL = cLraw; if (cL < 0) cL = 0; else if (cL > 9) cL = 9; cR = col_from_x(xr); if (cR < 0) cR = 0; else if (cR > 9) cR = 9; /* Порт set_char_collision (seg006:0723): нижний ряд = y_to_row_mod4(obj_y), * причём обёртка -1 (Kid НИЖЕ комнаты — climbdown/провал, obj_y в полосе @@ -695,6 +844,17 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, for (r = rT; r <= rB; r++) for (c = cL; c <= cR; c++) fore_tile(r, c); + /* Шовная окклюзия ворот (порт redraw_at_char для шва): футпринт зашёл за + * ЛЕВЫЙ шов (cLraw<0) и у соседа слева в этом ряду решётка ворот (код 4) — + * перерисовать её бары ПОВЕРХ Kid. Иначе Kid, стоя за закрытыми воротами + * (col-1), рисуется поверх решётки. Бары = draw_gate_back по живому modif + * соседа (g_lbg = lcol_bg, openness). */ + if (cLraw < 0 && g_lfg) + for (r = rT; r <= rB; r++) + if ((g_lfg[r] & 0x1F) == 4) { + int dby = 63 * r + 65; + draw_gate_back(g_lbg ? g_lbg[r] : 0, COL_XH[0], dby, dby - 3); + } if (frame >= 137 && frame <= 144) { /* подъём: пол назначения поверх Kid */ uint8_t fidx = (uint8_t)(frame - 137); for (r = rT; r <= rB; r++) diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 7c44ff4..6f35739 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -46,6 +46,11 @@ void pop_room_draw(uint8_t room_num, * рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */ void pop_room_set_below(const uint8_t *below_row0_fg); +/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы + * кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет + * комнаты сверху (кромка уровня → пол). */ +void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod); + /* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него — * звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ * спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики @@ -60,6 +65,19 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, void pop_loose_shake_draw(int row, int col); void pop_loose_bake_empty(int row, int col); +/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому + * modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */ +void pop_spike_redraw(int row, int col); + +/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева. + * g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла + * на back-странице, когда сосед слева анимируется. См. pop_bg.c. */ +void pop_room_redraw_seam_left(void); + +/* Кнопка (opener/closer) — перерисовать тайл (row,col) на back-странице: + * нажата/отжата по таймеру связи. Звать из pop_process_trobs. */ +void pop_button_redraw(int row, int col); + /* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла * (row,col) и покадровое продвижение+отрисовка на back-странице. tick * зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */ diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index bfc8e6e..78e3b37 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -20,6 +20,12 @@ #include "pop_kid.h" #include "pop_map.h" +/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в + * MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2 + * scancodes. TODO: убрать после отладки. */ +#define KBD_DBG_STEPL 0x3B /* J */ +#define KBD_DBG_STEPR 0x4B /* L */ + /* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */ #define CONTROL_RELEASED 0 #define CONTROL_IGNORE 1 @@ -314,9 +320,14 @@ static void read_input(void) uint8_t r = kbd_raw_down(KBD_RIGHT); uint8_t u = kbd_raw_down(KBD_UP); uint8_t d = kbd_raw_down(KBD_DOWN); + /* Отладочный осторожный шаг: J/L = Shift+влево/вправо (см. KBD_DBG_STEP*). */ + uint8_t sl = kbd_raw_down(KBD_DBG_STEPL); + uint8_t sr = kbd_raw_down(KBD_DBG_STEPR); + if (sl) l = 1; + if (sr) r = 1; control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED); control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED); - control_shift = (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)) + control_shift = (sl || sr || kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)) ? CONTROL_HELD : CONTROL_RELEASED; } diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index a092b57..292fc39 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -185,6 +185,10 @@ static uint8_t kid_lvalid[2]; * obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */ static int kid_fp_x, kid_fp_y; static uint16_t kid_fp_w, kid_fp_h; +/* Рендер-смещение по ЛОГИЧЕСКОЙ X (straddle у шва, порт xpos_in_drawn_room): + * ∓140 когда комната Kid ≠ отрисованной; 0 обычно. */ +static int kid_render_dx; +void pop_kid_set_render_dx(int dx) { kid_render_dx = dx; } int kid_fp_obj_x(void) { return kid_fp_x; } int kid_fp_obj_y(void) { return kid_fp_y; } uint16_t kid_fp_width(void) { return kid_fp_w; } @@ -209,7 +213,10 @@ void kid_draw(void) if (fr.image == 255) return; /* пустой кадр */ - fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx); + /* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠ + * отрисованной — спрайт за кромкой. Коллизия (Kid.x в pop_map) НЕ + * смещается: она в кадре комнаты Kid; смещение только рендерное. */ + fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx) + kid_render_dx; obj_x = (fwd << 1) - 116; obj_y = (int)Kid.y + fr.dy; if ((int8_t)(fr.flags ^ Kid.direction) >= 0) diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index a7615c8..e982d9c 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -56,6 +56,14 @@ uint8_t kid_cur_flags(void); * точка для pop_ctrl/pop_map (иначе каждый дублировал бы kid_seqtbl ~2.3КБ). */ void kid_set_seq(uint8_t seq_id); +/* Рендер-смещение Kid по ЛОГИЧЕСКОЙ X (порт xpos_in_drawn_room, seg004): + * когда комната Kid ≠ отрисованной (straddle у шва), спрайт рисуется со + * сдвигом ∓140 (левый сосед −140 / правый +140), при этом Kid.x/коллизия + * остаются в кадре комнаты Kid. 0 — комнаты совпадают (обычный случай). + * Влияет на kid_draw и метрики футпринта (fore) — heal берёт запомненный + * прямоугольник, поэтому согласован автоматически. */ +void pop_kid_set_render_dx(int dx); + /* Нарисовать текущий кадр Kid (банк GFX_BANK_SPRITE — видео-ОЗУ, без * теневой копии; запоминает прямоугольник для kid_heal). */ void kid_draw(void); diff --git a/applications/PoP/roomtest/pop_level.c b/applications/PoP/roomtest/pop_level.c index 9b30372..ab37c63 100644 --- a/applications/PoP/roomtest/pop_level.c +++ b/applications/PoP/roomtest/pop_level.c @@ -20,12 +20,16 @@ extern void _gfx_w0_isr(void); /* Смещения блоков blueprnt относительно начала данных (Table 6). */ #define BP_FG 0u #define BP_BG 720u +#define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */ +#define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */ #define BP_LINKS 1952u #define BP_START 2112u #define ROOM_TILES 30u #define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */ static uint8_t lvl_blk, lvl_page, lvl_ok; +/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */ +static uint8_t dl1[256], dl2[256]; int pop_level_load(const char *path) { @@ -47,11 +51,17 @@ int pop_level_load(const char *path) /* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */ n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF); if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */ + uint16_t i; stub = (uint16_t)&_gfx_w0_isr; pg[0x38] = 0xC3; /* JP _gfx_w0_isr */ pg[0x39] = (uint8_t)(stub & 0xFF); pg[0x3A] = (uint8_t)(stub >> 8); pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */ + /* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */ + for (i = 0; i < 256; i++) { + dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i]; + dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i]; + } } sprinter_page_w3(saved_w3); close(fd); @@ -154,6 +164,49 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col) return res; } +uint8_t pop_doorlink1(uint8_t i) { return dl1[i]; } +uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; } +void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; } + +uint8_t pop_level_tile(uint8_t room, uint8_t tilepos) +{ + uint8_t v; + if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0; + gfx_w0_map(lvl_page); + v = (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F); + gfx_w0_unmap(); + return v; +} + +/* Батч-доступ: смаппить страницу уровня в W0 ОДИН раз, прочитать много + * тайлов через pop_level_tile_raw, размаппить один раз. Экономит N свапов + * окна 0 в горячих циклах (pop_process_trobs звал pop_level_tile на каждый + * trob каждый кадр). Между begin/end НЕЛЬЗЯ рисовать/звать код, которому + * нужно окно 0 в исходном состоянии — только сырые чтения уровня. */ +void pop_level_access_begin(void) { if (lvl_ok) gfx_w0_map(lvl_page); } +void pop_level_access_end(void) { if (lvl_ok) gfx_w0_unmap(); } + +uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos) +{ + if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0; + return (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F); +} + +void pop_level_room_bg(uint8_t room, uint8_t *out30) +{ + const uint8_t *rbg; + uint8_t i; + if (!out30) return; + if (!lvl_ok || room < 1 || room > 24) { + for (i = 0; i < ROOM_TILES; i++) out30[i] = 0; + return; + } + gfx_w0_map(lvl_page); + rbg = room_bg_ptr(room); + for (i = 0; i < ROOM_TILES; i++) out30[i] = rbg[i]; + gfx_w0_unmap(); +} + uint8_t pop_room_link(uint8_t room, uint8_t side) { uint8_t v; diff --git a/applications/PoP/roomtest/pop_level.h b/applications/PoP/roomtest/pop_level.h index 2af1aea..aa0c8ef 100644 --- a/applications/PoP/roomtest/pop_level.h +++ b/applications/PoP/roomtest/pop_level.h @@ -41,6 +41,29 @@ int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg, * 0, если стороны нет. Для переходов между комнатами (L2/L3). */ uint8_t pop_room_link(uint8_t room, uint8_t side); +/* Тайл-код (fg & 0x1F) по комнате (1..24) и позиции tilepos (0..29, + * row*10+col); вне диапазона → 0 (empty). Для trob-диспетчера (pop_trob): + * тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */ +uint8_t pop_level_tile(uint8_t room, uint8_t tilepos); + +/* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений). + * Между begin/end — только pop_level_tile_raw, без рисования. */ +void pop_level_access_begin(void); +void pop_level_access_end(void); +uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos); + +/* Скопировать backtable (модификаторы) комнаты room в out[30]. Исходное + * состояние per-room modifier (openness ворот, состояние пик и т.п.) для + * ленивой инициализации room_modif в pop_trob. Вне диапазона → нули. */ +void pop_level_room_bg(uint8_t room, uint8_t *out30); + +/* Таблицы связей дверей (LINKLOC@1440 / LINKMAP@1696, по 256 Б). Копируются + * в W2-RAM при загрузке (LINKMAP мутабельна — таймеры кнопок). i — индекс + * цепочки (модификатор кнопки). Декод — в pop_trob (get_doorlink_*). */ +uint8_t pop_doorlink1(uint8_t i); +uint8_t pop_doorlink2(uint8_t i); +void pop_doorlink2_set(uint8_t i, uint8_t v); + /* Ряд приземления падающего loose-куска в колонке col комнаты room: первый * НЕ-пустой тайл сверху вниз; возвращает его ряд, если это floor (→ debris), * иначе -1 (пролетел/крушение о стену). */ diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 37ecae8..a54c1d5 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -20,6 +20,7 @@ #include "pop_map.h" #include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */ #include "pop_bg.h" /* pop_loose_shake_draw/bake_empty — перерисовка */ +#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */ /* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */ #define TILE_EMPTY 0 @@ -31,6 +32,9 @@ #define TILE_WALL 20 #define TILE_LOOSE 11 /* проваливающийся пол */ #define TILE_DEBRIS 14 /* щебень (упавший loose) */ +#define TILE_SPIKE 2 /* пики */ +#define TILE_CLOSER 6 /* кнопка-закрывалка (DROP) */ +#define TILE_OPENER 15 /* кнопка-открывалка (RAISE, 0x0F) */ #define ACT_STAND 0 #define ACT_RUNJUMP 1 #define ACT_HANGCLIMB 2 @@ -38,6 +42,7 @@ #define ACT_FREEFALL 4 #define ACT_BUMPED 5 #define ACT_HANGSTRAIGHT 6 +#define ACT_TURN 7 #define SEQ_7_FALL 7 /* stepfall (stand/run/step/crouch) */ #define SEQ_17_SOFT_LAND 17 @@ -58,6 +63,13 @@ #define SEQ_23_RELEASE_LEDGE_FALL 23 /* отпустить уступ и падать */ #define SEQ_68_CLIMB_DOWN 68 /* спуск через зацеп (не приседание) */ #define SEQ_73_CLIMB_UP_TO_GATE 73 /* подъём к закрытой калитке */ +#define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */ + +/* Кадры анимации для check_spiked (seg006:0658). */ +#define FRAME_7_RUN 7 +#define FRAME_26_STANDJUMP_LAND 26 +#define FRAME_34_RUNJUMP_START 34 +#define FRAME_43_RUNJUMP_LAND 43 #define FRAME_NEEDS_FLOOR 0x40 #define FRAME_WEIGHT_X 0x1F @@ -86,7 +98,11 @@ static int8_t g_infrontx; /* колонка «перед персонаже /* ---- Карта комнаты ------------------------------------------------- */ uint8_t pop_fell_out; /* 1 = Kid пересёк НИЖНЮЮ границу (см. pop_map.h) */ uint8_t pop_leave_dir; /* уход вбок/вверх: 0=нет; 1=left,2=right,3=up (side+1) */ +uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заблокирован N кадров после входа в комнату */ uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */ +uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */ +uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */ +static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */ static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */ static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */ @@ -100,6 +116,18 @@ static uint8_t g_lcol[3], g_rcol[3]; void pop_map_set(uint8_t *fg) { g_fg = fg; } +void pop_map_set_room(uint8_t room) { g_room = room; } + +void pop_kid_hp_reset(void) { hitp_curr = 3; pop_kid_dead = 0; } + +/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */ +static uint8_t take_hp(uint8_t count) +{ + if (count >= hitp_curr) { hitp_curr = 0; return 1; } + hitp_curr = (uint8_t)(hitp_curr - count); + return 0; +} + void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, const uint8_t *lcol_fg, const uint8_t *rcol_fg) { @@ -123,6 +151,28 @@ static uint8_t get_tile(int8_t col, int8_t row) return g_curr_tile; } +/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и + * идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто + * проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в + * tile_is_floor это ПОЛ), поэтому Kid НЕ падает на них. Раньше get_tile ради + * проходимости подменял ворота шва на empty — но подмену видел и check_on_floor + * → start_fall (Kid проваливался при проходе через ОТКРЫТЫЕ ворота шва). Теперь + * проходимость живёт ТОЛЬКО в bump-логике (как в SDLPoP), не трогая проверку пола. + * Модификатор: шов слева (col=-1) — сосед g_link_l col9; шов справа (col=10) — + * g_link_r col0; ВНУТРИ комнаты (0..9) — текущая комната g_room (иначе после + * перехода Kid встаёт на ту же створку уже как на внутрикомнатную и его отбивает + * обратно). Открыто ⇔ (modif>>2)+6 >= char_height (бары выше головы). */ +static uint8_t gate_passable(int8_t col, int8_t row) +{ + uint8_t gm; + if (row < 0 || row > 2) return 0; + if (col == -1 && g_link_l) gm = pop_trob_modif(g_link_l)[row * 10 + 9]; + else if (col == 10 && g_link_r) gm = pop_trob_modif(g_link_r)[row * 10 + 0]; + else if (col >= 0 && col <= 9) gm = pop_trob_modif(g_room)[row * 10 + col]; + else return 0; + return (uint8_t)((gm >> 2) + 6) >= (uint8_t)kid_fp_height(); +} + static uint8_t tile_is_floor(uint8_t t) { switch (t) { @@ -225,13 +275,31 @@ static int distance_to_edge(int xpos) static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); } /* ---- ТОЧНАЯ коллизия со стеной (seg004) --------------------------- * - * Передний край персонажа char_x_*_coll = char_dx_forward(cur_frame.dx) - * (obj_x/2+58 сокращается ровно к этому; ширина спрайта НЕ нужна), ∓4 для - * THIN-кадров. dist_from_wall_forward — расстояние переднего края до грани - * стены tiletype в колонке tcol: >0 зазор, <=0 достал/зашёл (насколько). */ + * Передний край персонажа = char_x_right (взгляд вправо) / char_x_left + * (влево) из set_char_collision (seg006:0723): obj_x/2 + 58, где obj_x = + * 2*char_dx_forward(cur_frame.dx) - 116 и load_frame_to_obj (seg008:1728) + * добавляет +1 для кадров «чётного пикселя» (бит 0x80 флагов XOR знак + * направления). Деление obj_x/2 в C усечено К НУЛЮ, поэтому в зоне левого + * шва (e<=57, obj_x<0) поправка +1 сдвигает край на пиксель ВПРАВО: e -> e+1. + * Это НЕ мелочь: Kid, осевший после отскока от ворот шва на x=57, имеет + * char_x_right=58 и НЕ пересекает порог leave-left (<=57) — иначе пинг-понг + * drawn_room у закрытых ворот (BUG-SEAM-PINGPONG; подтверждено потиковой + * трассой живого SDLPoP 1.23). При e>=58 формула сокращается к чистому + * char_dx_forward. ∓4 для THIN-кадров поверх. dist_from_wall_forward — + * расстояние переднего края до грани стены tiletype в колонке tcol: >0 + * зазор, <=0 достал/зашёл (насколько). */ +static int char_x_forward_edge(void) +{ + int e = char_dx_forward(kid_cur_dx()); + uint8_t fl = kid_cur_flags(); + if (Kid.direction < 0) fl ^= 0x80; + if (!(fl & 0x80) && e <= 57) e++; /* odd pixel + C-трунк к нулю */ + return e; +} + static int char_front_coll(void) { - int cx = char_dx_forward(kid_cur_dx()); + int cx = char_x_forward_edge(); if (kid_cur_flags() & FRAME_THIN) cx += (Kid.direction < 0) ? 4 : -4; /* THIN: край внутрь */ return cx; @@ -270,10 +338,36 @@ static void in_wall(void) /* ---- Падение ------------------------------------------------------- */ static void inc_curr_row(void) { Kid.curr_row++; } +/* Пики (определены ниже, перед pop_phys_tick) — нужны в land() для смерти + * при ПАДЕНИИ на выдвинутые пики (seg005 loc_5EE6). */ +static int is_spike_harmful(uint8_t tilepos); +static void spiked(uint8_t tilepos); + +/* fell_on_spikes (seg005 loc_5EE6): при приземлении тайл ПОД персонажем = + * пика (или пика СЗАДИ у самой кромки) и вредна → напороться (смерть). + * Возвращает 1, если умер (land прерывается). */ +static uint8_t fell_on_spikes(void) +{ + uint8_t tp; + if (get_tile_at_char() == TILE_SPIKE) { + tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col); + } else if (distance_to_edge_weight() >= 12 && + get_tile_behind_char() == TILE_SPIKE) { + int8_t bcol = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col); + if (bcol < 0 || bcol > 9) return 0; + tp = (uint8_t)(Kid.curr_row * 10 + bcol); + } else { + return 0; + } + if (is_spike_harmful(tp)) { spiked(tp); return 1; } + return 0; +} + static void land(void) { uint8_t seq; Kid.y = (uint8_t)y_land[Kid.curr_row + 1]; + if (fell_on_spikes()) return; /* упал на вредные пики — смерть */ /* к краю пола — чуть назад (как оригинал) */ if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) Kid.x = (uint8_t)char_dx_forward(-3); @@ -618,7 +712,7 @@ static void check_action(void) * выравниваем, без рестарта. В падении/висе не трогаем. */ static void check_bumped(void) { - uint8_t t; int8_t tcol; int d; + uint8_t t, wt; int8_t tcol; int d; /* Не бампим в падении/висе/подтягивании (seg004 check_bumped guard'ы: * action hang_climb/hang_straight + кадры подъёма 135..148 — иначе * climb-up у стены прерывается отскоком). FREEFALL — наш guard: стену @@ -635,8 +729,19 @@ static void check_bumped(void) * underflow X и уход в стену. fell_out ловит do_fall. */ if (Kid.curr_row < 0 || Kid.curr_row > 2) return; t = get_tile_at_char(); tcol = Kid.curr_col; - if (!wall_type(t)) { t = get_tile_infrontof_char(); tcol = g_infrontx; } - if (!wall_type(t)) return; + wt = wall_type(t); + /* seg004: bump проверяется по колонке СБОКУ от стены (bump_col_left/right_ + * of_wall), а НЕ по тайлу ПОД персонажем. Стоя́ на односторонней стене + * (ворота = стена справа / зеркало = стена слева) лицом ПРОЧЬ от её + * блокирующей стороны — она не бампит. Иначе разворот на тайле ворот у + * шва выталкивает Kid к дальней грани ворот (сквозь закрытую решётку в + * соседнюю комнату — баг «телепорта» при развороте). */ + if ((wt == 1 && Kid.direction < 0) || (wt == 2 && Kid.direction >= 0)) + wt = 0; + if (!wt) { t = get_tile_infrontof_char(); tcol = g_infrontx; wt = wall_type(t); } + if (!wt) return; + if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row)) + return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */ d = dist_from_wall_forward(t, tcol); if (d >= 0) return; /* ещё не зашёл за грань */ Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */ @@ -676,11 +781,20 @@ static void check_press(void) make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1); return; } - /* стоя́ние: action turn/bumped или < hang_climb (0/1), с FRAME_NEEDS_FLOOR */ - if (action != ACT_BUMPED && action >= ACT_HANGCLIMB) return; + /* стоя́ние: action turn/bumped или < hang_climb (0/1), с FRAME_NEEDS_FLOOR. + * ACT_TURN обязателен (SDLPoP check_press: actions_7_turn): при развороте + * на кнопке иначе trigger_button не зовётся кадр-два → таймер связи падает + * <2 → кнопка ложно отжимается и жмётся снова. */ + if (action != ACT_BUMPED && action != ACT_TURN && action >= ACT_HANGCLIMB) return; if (!(kid_cur_flags() & FRAME_NEEDS_FLOOR)) return; - if (get_tile_at_char() == TILE_LOOSE) - make_loose_fall(Kid.curr_row * 10 + Kid.curr_col, 1); + { + uint8_t t = get_tile_at_char(); + uint8_t tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col); + if (t == TILE_OPENER || t == TILE_CLOSER) + pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]); + else if (t == TILE_LOOSE) + make_loose_fall(tp, 1); + } } /* do_knock (seg007:0FE0) + loose_make_shake (seg007:0FB4): сотрясение от @@ -729,29 +843,32 @@ void pop_loose_reset(void) void pop_loose_tick(void) { int pos; + /* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80 + * нет аппаратного деления, и SDCC выносил оба деления (__divuchar/ + * __moduchar) в начало КАЖДОЙ итерации (общее подвыражение аргументов + * вызовов), т.е. 30*2=60 вызовов деления за кадр — даже в покое и в + * комнатах без loose-плит (условия if к тому моменту ещё не проверены). + * Инкремент+сравнение считают row/col без деления. */ + uint8_t row = 0, col = 0; for (pos = 0; pos < 30; pos++) { if (loose_bake[pos]) { /* упал: запечь пусто (2 кадра=2 стр.) */ - pop_loose_bake_empty(pos / 10, pos % 10); + pop_loose_bake_empty(row, col); loose_bake[pos]--; - continue; - } - if (loose_rest[pos]) { /* тряска кончилась: перерисовать ПОКОЙ */ + } else if (loose_rest[pos]) { /* тряска кончилась: перерисовать ПОКОЙ */ /* modif уже 0 → shake_draw рисует базовый кадр (покой) обоих тайлов * (col и col+1: правая грань loose живёт в 2,7). 2 кадра = обе * страницы дабл-буфера — иначе на одной остаётся застывший * дрожащий кадр правой грани → мерцание («бесконечная тряска»). */ - pop_loose_shake_draw(pos / 10, pos % 10); + pop_loose_shake_draw(row, col); loose_rest[pos]--; - continue; - } - if (pop_loose_modif[pos] == 0) continue; - { + } else if (pop_loose_modif[pos] != 0) { uint8_t m = ++pop_loose_modif[pos]; if (m & 0x80) { /* тряска (do_knock) */ if (m >= 0x84) { /* конец тряски: сброс + перерисовать покой */ pop_loose_modif[pos] = 0; loose_rest[pos] = 2; /* покой на ОБЕИХ страницах */ - continue; + } else { + pop_loose_shake_draw(row, col); /* дрожащий кадр */ } } else if (m >= 11) { /* loose_floor_delay — падение */ /* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!). @@ -761,14 +878,15 @@ void pop_loose_tick(void) * (стена-вниз от тайла снизу + чёрный фон). */ g_fg[pos] = TILE_EMPTY; pop_loose_modif[pos] = 0; - pop_loose_bake_empty(pos / 10, pos % 10); /* запечь пусто на ТЕКУЩЕЙ странице сразу */ + pop_loose_bake_empty(row, col); /* запечь пусто на ТЕКУЩЕЙ странице сразу */ loose_bake[pos] = 1; /* + вторая страница следующим кадром */ - pop_loose_mob_spawn(pos / 10, pos % 10); /* отрыв падающего куска (L4) */ + pop_loose_mob_spawn(row, col); /* отрыв падающего куска (L4) */ pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению: кусок улетел */ - continue; + } else { /* отсчёт 1..10 — дрожащий кадр */ + pop_loose_shake_draw(row, col); } - pop_loose_shake_draw(pos / 10, pos % 10); /* дрожащий кадр */ } + if (++col == 10) { col = 0; row++; } /* row/col без деления */ } pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */ } @@ -799,9 +917,27 @@ static void check_leave_below(void) * удержит у шва). */ static void check_leave(void) { - int fx = char_dx_forward(kid_cur_dx()); + int fx; + uint8_t f = Kid.frame; + /* seg002 leave_room: комнату НЕ покидаем во время разворота (action turn), + * подъёма-с-зацепа (climb, кадры 135..149) и вставания из приседа + * (110..119). Без этого гарда разворот у правого шва (взгляд флипается + * право→лево, char_x_left проскакивает >=198) телепортирует Kid сквозь + * закрытые ворота в соседнюю комнату (leave_room возвращает −1). */ + if (Kid.action == ACT_TURN || + (f >= 135 && f < 150) || + (f >= 110 && f < 120)) + return; + /* Передний край — ТОЛЬКО через char_x_forward_edge (obj_x-арифметика + * set_char_collision): в зоне шва (<=57) он на пиксель правее чистого + * Kid.x+dx, и Kid, осевший у ворот на x=57, порога leave-left НЕ достигает + * (эталон SDLPoP: экран не переключается, BUG-SEAM-PINGPONG). */ + fx = char_x_forward_edge(); if (Kid.direction >= 0) { /* взгляд вправо: fx = char_x_right */ - if (g_link_r && fx >= 201) { + uint8_t t9 = get_tile(9, Kid.curr_row); + if (g_link_r && fx >= 201 && + t9 != TILE_DOORTOP_FLOOR && t9 != TILE_DOORTOP) { + /* leave_room (seg002): под doortop у правой кромки выхода нет */ Kid.x = (uint8_t)(Kid.x - 140); determine_col(); pop_leave_dir = 2; } else if (g_link_l && fx <= 57) { Kid.x = (uint8_t)(Kid.x + 140); determine_col(); pop_leave_dir = 1; @@ -815,8 +951,105 @@ static void check_leave(void) } } +/* ---- Пики (spike, seg006/seg007) ---------------------------------- * + * Состояние пики в room_modif[g_room][tilepos] (pop_trob), автомат — + * animate_spike (pop_trob). Здесь: триггер выдвижения (check_spike_below), + * решение о смерти (check_spiked) + spiked(). */ + +/* start_anim_spike (seg007:08E3): взвести выдвижение, если пика в покое + * (modif 0 → trob type1) или ушла не полностью (modif<0, кроме 0xFF disabled + * → мгновенно 0x8F «вышли»). */ +static void start_anim_spike(uint8_t tilepos) +{ + uint8_t *mod = pop_trob_modif(g_room); + int8_t old; + if (!mod) return; + old = (int8_t)mod[tilepos]; + if (old <= 0) { + if (old == 0) + pop_add_trob(g_room, tilepos, 1); /* (TODO: sound_49_spikes) */ + else if ((uint8_t)old != 0xFF) + mod[tilepos] = 0x8F; + } +} + +/* is_spike_harmful (seg007:1178): 0/-1 → 0 (безопасно); <0 → 1; 1..4 → 2; + * >=5 → 0. */ +static int is_spike_harmful(uint8_t tilepos) +{ + int8_t m = (int8_t)pop_trob_modif(g_room)[tilepos]; + if (m == 0 || m == -1) return 0; + if (m < 0) return 1; + if (m < 5) return 2; + return 0; +} + +/* spiked (seg005:0888): напоролся — пика становится безвредной (0xFF), + * выровнять по колонке, take_hp(100) = смерть, seq_51 (импейл). */ +static void spiked(uint8_t tilepos) +{ + pop_trob_modif(g_room)[tilepos] = 0xFF; + Kid.y = (uint8_t)y_land[Kid.curr_row + 1]; + Kid.x = (uint8_t)(x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + 10); + Kid.x = (uint8_t)char_dx_forward(8); + Kid.fall_y = 0; + take_hp(100); /* (TODO: sound_48_spiked) */ + kid_set_seq(SEQ_51_SPIKED); + play_seq(); + determine_col(); + pop_kid_dead = 1; +} + +/* check_spiked (seg006:0658): под персонажем пика И вредна И кадр + * бега(7..14)/старта прыжка(34..39) при harmful>=2, ЛИБО кадр приземления + * (43/26) при harmful!=0 → smert'. */ +static void check_spiked(void) +{ + uint8_t frame = Kid.frame, tp; + int h; + if (Kid.curr_col < 0 || Kid.curr_col > 9 || Kid.curr_row < 0 || Kid.curr_row > 2) + return; + if (get_tile_at_char() != TILE_SPIKE) return; + tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col); + h = is_spike_harmful(tp); + if ((h >= 2 && ((frame >= FRAME_7_RUN && frame < 15) || + (frame >= FRAME_34_RUNJUMP_START && frame < 40))) || + ((frame == FRAME_43_RUNJUMP_LAND || frame == FRAME_26_STANDJUMP_LAND) && h != 0)) + spiked(tp); +} + +/* check_spike_below (seg006:1199): для каждой колонки РЕАЛЬНОГО габарита + * спрайта (char_x_left..char_x_right, порт set_char_collision) скан ВНИЗ от + * curr_row через НЕ-floor тайлы; встретил пику → start_anim_spike. Габарит, + * а не «curr_col + колонка спереди»: иначе пики выдвигаются на тайл раньше + * (когда тело Кида ещё не над шахтой). Метрики кадра — kid_fp (габарит + * последнего kid_draw; отставание в 1 кадр несущественно, ошибка в позднюю + * сторону). */ +static void check_spike_below(void) +{ + int wh = ((int)kid_fp_width() + 1) >> 1; /* char_width_half */ + int xl = kid_fp_obj_x() / 2 + 58; /* char_x_left */ + int xr; + int8_t c0, c1, col; + if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ + xr = xl + wh; /* char_x_right */ + c0 = get_tile_div_mod_m7(xl); + c1 = get_tile_div_mod_m7(xr); + for (col = c0; col <= c1; col++) { + int8_t row = Kid.curr_row; + if (col < 0 || col > 9) continue; + for (;;) { + uint8_t t = get_tile(col, row); + if (t == TILE_SPIKE) { start_anim_spike((uint8_t)(row * 10 + col)); break; } + if (!tile_is_floor(t) && row <= 2) { row++; continue; } + break; + } + } +} + void pop_phys_tick(void) { + if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */ if (grab_timer) grab_timer--; fall_accel(); fall_speed(); @@ -824,7 +1057,19 @@ void pop_phys_tick(void) check_bumped(); /* удержать у стены до check_action (порядок PoP) */ check_action(); check_press(); /* seg006: стойка/пробой loose → make_loose_fall */ + check_spike_below(); /* seg006: над колонкой с пиками → выдвинуть пики */ + check_spiked(); /* seg006: напоролся на вредные пики → смерть */ + if (pop_kid_dead) return; /* только что умер — без leave/knock в этот кадр */ pop_check_knock(); /* seg003: сотрясение приземления → тряска loose в ряду */ - check_leave_below(); /* seg002: y>=211 → переход в комнату снизу */ - check_leave(); /* seg002: уход вбок (право) → переход в соседнюю комнату */ + /* exit_room_timer (seg002 exit_room): 2 кадра ПОСЛЕ входа в комнату любой + * leave заблокирован. Без него транзиент отскока о ворота шва (seq_47 + * роняет char_x за порог) сразу же выкидывает Kid в соседнюю комнату + * (ping-pong / баг #4). С таймером первый бамп попадает в окно блокировки, + * Kid оседает у грани (char_x стабильно > порога) — экран не перескакивает. */ + if (pop_leave_timer) { + pop_leave_timer--; + } else { + check_leave_below(); /* seg002: y>=211 → переход в комнату снизу */ + check_leave(); /* seg002: уход вбок → переход в соседнюю комнату */ + } } diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index e19c8f9..3ebc0c3 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -25,11 +25,28 @@ void pop_map_set(uint8_t *fg); void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, const uint8_t *lcol_fg, const uint8_t *rcol_fg); +/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob). + * Звать в enter_room. */ +void pop_map_set_room(uint8_t room); + +/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP. + * pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/ + * респавн). */ +extern uint8_t pop_kid_dead; +extern uint8_t hitp_curr; +void pop_kid_hp_reset(void); + /* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для * pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189). * Приложение переключает комнату по соответствующей связи и снимает флаг. */ extern uint8_t pop_leave_dir; +/* exit_room_timer (порт seg002 exit_room): пока >0, боковой/нижний leave + * заблокирован (декремент в pop_phys_tick). Приложение ставит =2 при входе + * в комнату (enter_room) — 2 кадра защиты от повторного/транзиентного leave + * (ping-pong у шва, транзиент отскока о ворота, баг #4/BUG-2). */ +extern uint8_t pop_leave_timer; + /* Loose floors (проваливающиеся полы). pop_loose_modif[30] — состояние на * тайл (0 покой / 0x80.. тряска / 1..11 отсчёт падения); читает pop_bg при * отрисовке. pop_loose_tick() — анимация + перерисовка изменившихся тайлов diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c new file mode 100644 index 0000000..9bd98a2 --- /dev/null +++ b/applications/PoP/roomtest/pop_trob.c @@ -0,0 +1,324 @@ +/* + * pop_trob.c — P0-каркас: room_modif + список trob + диспетчер анимаций. + * Порт SDLPoP seg007 (process_trobs/animate_tile/add_trob/animate_spike). + * См. pop_trob.h. + */ +#include "pop_trob.h" +#include "pop_level.h" +#include "pop_bg.h" /* pop_spike_redraw */ + +/* Коды тайлов (fg & 0x1F), которые анимируются. */ +#define TILE_SPIKE 0x02 +#define TILE_GATE 0x04 +#define TILE_CLOSER 0x06 +#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */ +#define TILE_OPENER 0x0F + +/* ---- Персистентное per-room modifier-состояние -------------------- */ +static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES]; +static uint8_t room_seen[POP_NROOMS]; /* modif проинициализирован из уровня */ + +/* ---- Список анимируемых тайлов ------------------------------------ */ +typedef struct { uint8_t room, tilepos; int8_t type; } trob_t; +#define TROBS_MAX 24 +static trob_t trobs[TROBS_MAX]; +static uint8_t trobs_count; +static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */ + +/* Отложенная перерисовка «покоя» законченных пик/кнопок текущей комнаты + * (2 кадра = обе страницы дабл-буфера, как loose_rest). */ +static uint8_t spike_rest[POP_ROOMTILES]; +static uint8_t button_rest[POP_ROOMTILES]; + +/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button: + * признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её + * изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под + * стоящим Кидом перерисовывалась КАЖДЫЙ кадр (2×draw_tile ≈ 50% кадра в + * статике), т.к. её trob висит, пока Кид стоит (это верно, как в SDLPoP — + * расхождение было именно в безусловном редрое). */ +static uint8_t trob_drawn[POP_ROOMTILES]; + +void pop_trob_reset(void) +{ + uint8_t i; + for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0; + for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; } + trobs_count = 0; +} + +uint8_t *pop_trob_modif(uint8_t room) +{ + if (room < 1 || room > POP_NROOMS) return 0; + if (!room_seen[room - 1]) { + pop_level_room_bg(room, room_modif[room - 1]); + room_seen[room - 1] = 1; + } + return room_modif[room - 1]; +} + +static int find_trob(uint8_t room, uint8_t tilepos) +{ + uint8_t i; + for (i = 0; i < trobs_count; i++) + if (trobs[i].room == room && trobs[i].tilepos == tilepos) return i; + return -1; +} + +void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) +{ + int f = find_trob(room, tilepos); + if (f >= 0) { trobs[f].type = type; return; } + if (trobs_count >= TROBS_MAX) return; + trobs[trobs_count].room = room; + trobs[trobs_count].tilepos = tilepos; + trobs[trobs_count].type = type; + trobs_count++; +} + +void pop_trob_room_changed(void) +{ + uint8_t i; + for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; } +} + +/* animate_spike (seg007:04D3): автомат по modifier. *m — modifier тайла, + * *type — тип trob (>=0 активен, <0 = удалить). Выдвиг: ++modif (1..4), + * на 5 → 0x8F (полностью вышли, безвредны), на 9 → 0 + конец. Убирание + * (флаг 0x80): --modif; когда 0x7F-часть обнулилась → modif=6 (начать + * визуальный уход 6..8). 0xFF = disabled. */ +static void animate_spike(uint8_t *m, int8_t *type) +{ + uint8_t mod = *m; + if (*type >= 0) { + if (mod == 0xFF) return; /* disabled — не двигать */ + if (mod & 0x80) { + --mod; + if (mod & 0x7F) { *m = mod; return; } + mod = 6; + } else { + ++mod; + if (mod == 5) mod = 0x8F; + else if (mod == 9) { mod = 0; *type = -1; } + } + } + *m = mod; +} + +/* ---- Кнопки/ворота (seg007) --------------------------------------- * + * Таблицы связей LINKLOC/LINKMAP — в pop_level (мутабельная копия в W2). + * doorlink1 (d1) = LINKLOC, doorlink2 (d2) = LINKMAP. */ +static uint8_t get_doorlink_timer(uint8_t i) { return (uint8_t)(pop_doorlink2(i) & 0x1F); } +static void set_doorlink_timer(uint8_t i, uint8_t v) +{ + pop_doorlink2_set(i, (uint8_t)((pop_doorlink2(i) & 0xE0) | (v & 0x1F))); +} +static uint8_t get_doorlink_tile(uint8_t i) { return (uint8_t)(pop_doorlink1(i) & 0x1F); } +static uint8_t get_doorlink_next(uint8_t i) { return (uint8_t)(!(pop_doorlink1(i) & 0x80)); } +static uint8_t get_doorlink_room(uint8_t i) +{ + return (uint8_t)(((pop_doorlink1(i) & 0x60) >> 5) + ((pop_doorlink2(i) & 0xE0) >> 3)); +} + +/* trigger_gate (seg007:092C): изменить openness ворот по типу кнопки. + * Возвращает тип анимации для add_trob (или -1 = ничего). */ +static int8_t trigger_gate(uint8_t room, uint8_t tilepos, uint8_t button_type) +{ + uint8_t *mod = pop_trob_modif(room); + uint8_t modifier = mod[tilepos]; + if (button_type == TILE_OPENER) { + if (modifier == 0xFF) return -1; /* уже насовсем открыт */ + if (modifier >= 188) { mod[tilepos] = 238; return -1; } /* открыт → держать */ + mod[tilepos] = (uint8_t)((modifier + 3) & 0xFC); + return 1; /* обычное открытие */ + } else if (button_type == TILE_DEBRIS) { /* насовсем (died_on_button) */ + if (modifier < 188) return 2; + mod[tilepos] = 0xFF; + return -1; + } else { /* closer */ + if (modifier != 0) return 3; /* быстро закрыть */ + return -1; + } +} + +/* trigger_1 (seg007:0999): цель = ворота → trigger_gate; иначе -1 (leveldoor + * и прочее — отложено). */ +static int8_t trigger_1(uint8_t target_type, uint8_t room, uint8_t tilepos, uint8_t button_type) +{ + if (target_type == TILE_GATE) + return trigger_gate(room, tilepos, button_type); + return -1; +} + +/* do_trigger_list (seg007:09E5): обойти цепочку LINKLOC от index, для каждой + * цели добавить trob (если trigger_1 >= 0). */ +static void do_trigger_list(uint8_t index, uint8_t button_type) +{ + for (;;) { + uint8_t room = get_doorlink_room(index); + uint8_t tilepos = get_doorlink_tile(index); + uint8_t target_type = pop_level_tile(room, tilepos); + int8_t result = trigger_1(target_type, room, tilepos, button_type); + if (result >= 0) pop_add_trob(room, tilepos, result); + if (get_doorlink_next(index) == 0) break; + index++; + } +} + +void pop_trigger_button(uint8_t room, uint8_t tilepos, + uint8_t button_type, uint8_t modifier) +{ + uint8_t link_timer = get_doorlink_timer(modifier); + if (link_timer != 0x1F) { /* не заклинено */ + set_doorlink_timer(modifier, 5); + if (link_timer < 2) + pop_add_trob(room, tilepos, 1); /* кнопка нажимается (+ TODO sound) */ + do_trigger_list(modifier, button_type); + } +} + +/* animate_button (seg007:0D3A): отсчитать таймер связи; <2 → кнопка + * отжимается (trob кончился). idx = модификатор кнопки (индекс LINKLOC). */ +static void animate_button(uint8_t idx, int8_t *type) +{ + if (*type >= 0) { + uint8_t timer = get_doorlink_timer(idx); + timer = (uint8_t)(timer ? timer - 1 : 0); + set_doorlink_timer(idx, timer); + if (timer < 2) *type = -1; + } +} + +/* animate_door (seg007:0522): openness по типу анимации. *m — modif ворот, + * *type — 0 закрытие / 1 открытие / 2 насовсем / 3..8 быстрое закрытие. */ +static const uint8_t gate_close_speeds[9] = {0, 0, 0, 20, 40, 60, 80, 100, 120}; +static const int8_t door_delta[3] = {-1, 4, 4}; + +static void animate_door(uint8_t *m, int8_t *type) +{ + int8_t anim_type = *type; + uint8_t mod = *m; + if (anim_type >= 0) { + if (anim_type >= 3) { /* быстрое закрытие */ + if (anim_type < 8) { anim_type++; *type = anim_type; } + { + int new_mod = (int)mod - gate_close_speeds[anim_type]; + if (new_mod < 0) { mod = 0; *type = -1; } + else mod = (uint8_t)new_mod; + } + } else if (mod != 0xFF) { + mod = (uint8_t)(mod + door_delta[anim_type]); + if (anim_type == 0) { /* закрытие */ + if (mod == 0) *type = -1; /* gate_stop */ + } else { /* открытие */ + if (mod >= 188) { + if (anim_type < 2) { mod = 238; *type = 0; } /* открыт → потом закрывать */ + else { mod = 0xFF; *type = -1; } /* насовсем */ + } + } + } else { + *type = -1; + } + } + *m = mod; +} + +uint8_t pop_trob_room_active(uint8_t room) +{ + uint8_t i; + for (i = 0; i < trobs_count; i++) + if (trobs[i].room == room) return 1; + return 0; +} + +void pop_process_trobs(uint8_t cur_room) +{ + uint8_t i, w = 0; + + /* Перерисовка «покоя» законченных пик/кнопок (обе страницы). + * row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC + * выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее + * подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет + * активных пик/кнопок (тот же случай, что был в pop_loose_tick). */ + { + uint8_t row = 0, col = 0; + for (i = 0; i < POP_ROOMTILES; i++) { + if (spike_rest[i]) { + pop_spike_redraw(row, col); + spike_rest[i]--; + } + if (button_rest[i]) { + pop_button_redraw(row, col); + button_rest[i]--; + } + if (++col == 10) { col = 0; row++; } + } + } + + if (trobs_count == 0) return; + + /* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не + * pop_level_tile() на каждый trob (тот свапал W0 каждый вызов = N свапов + * за кадр даже без анимации). Дальше цикл (с редроями) идёт с + * размапленным W0 — как раньше. */ + { + uint8_t k; + pop_level_access_begin(); + for (k = 0; k < trobs_count; k++) + trob_code[k] = pop_level_tile_raw(trobs[k].room, trobs[k].tilepos); + pop_level_access_end(); + } + + for (i = 0; i < trobs_count; i++) { + uint8_t room = trobs[i].room; + uint8_t tp = trobs[i].tilepos; + uint8_t code = trob_code[i]; + uint8_t *mod = pop_trob_modif(room); + int8_t type = trobs[i].type; + + switch (code) { + case TILE_SPIKE: + animate_spike(&mod[tp], &type); + break; + case TILE_OPENER: + case TILE_CLOSER: + animate_button(mod[tp], &type); /* mod[tp] = индекс LINKLOC кнопки */ + break; + case TILE_GATE: + animate_door(&mod[tp], &type); + break; + default: + type = -1; + break; + } + trobs[i].type = type; + + if (room == cur_room && + (code == TILE_SPIKE || code == TILE_OPENER || code == TILE_CLOSER)) { + /* Редрой ТОЛЬКО при изменении видимого состояния (redraw_11h из + * SDLPoP). spike: draw-сигнатура = modifier; button: = «нажата» + * (таймер связи >1). Пока Кид держит кнопку — сигнатура стабильна + * → редроя нет. При изменении рисуем ТЕКУЩУЮ страницу сразу + + * =1 (вторая страница дабл-буфера следующим кадром через + * rest-цикл). tp/10,tp%10 считаем только в момент редроя (редко), + * а не каждый кадр. */ + /* Сигнатура = ВИДИМЫЙ кадр, а не сырой modifier. Для пики кадр = + * (mod&0x80)?5:mod (как в draw_tile): весь ретракт-hold 0x80..0x8F + * (16 значений modifier) рисуется одним кадром 5 → сырой modifier + * дал бы 16 ложных редроёв «выдвинутой» неподвижной пики. */ + uint8_t sig = (code == TILE_SPIKE) + ? (uint8_t)((mod[tp] & 0x80) ? 5 : mod[tp]) + : (uint8_t)(get_doorlink_timer(mod[tp]) > 1); + if (sig != trob_drawn[tp]) { + uint8_t trow = tp / 10, tcol = tp % 10; + if (code == TILE_SPIKE) { pop_spike_redraw(trow, tcol); spike_rest[tp] = 1; } + else { pop_button_redraw(trow, tcol); button_rest[tp] = 1; } + trob_drawn[tp] = sig; + } + } + } + + /* compact: удалить завершённые (type < 0). */ + for (i = 0; i < trobs_count; i++) + if (trobs[i].type >= 0) trobs[w++] = trobs[i]; + trobs_count = w; +} diff --git a/applications/PoP/roomtest/pop_trob.h b/applications/PoP/roomtest/pop_trob.h new file mode 100644 index 0000000..a0223c6 --- /dev/null +++ b/applications/PoP/roomtest/pop_trob.h @@ -0,0 +1,61 @@ +/* + * pop_trob.h — P0-каркас: персистентное per-room modifier-состояние + + * список анимируемых тайлов (trob). Порт SDLPoP seg007 (process_trobs/ + * add_trob/animate_tile) + curr_room_modif. + * + * ЗАЧЕМ: тайлы-объекты (пики, ворота, кнопки) имеют состояние (modifier), + * которое живёт МЕЖДУ входами в комнату и может меняться КРОСС-КОМНАТНО + * (кнопка в одной комнате открывает ворота в другой). room_modif[room] + * — единственный источник модификаторов для отрисовки (pop_bg draw_tile + * читает его как bg), как curr_room_modif в оригинале. + * + * Список trob хранит (room, tilepos, type) активных анимаций; каждый кадр + * pop_process_trobs продвигает их (animate_spike/door/button по коду тайла), + * пишет результат в room_modif и перерисовывает изменившиеся тайлы ТЕКУЩЕЙ + * комнаты на back-странице (2-page, как loose). Невидимые комнаты + * анимируются в состоянии (без отрисовки) — при входе рисуются статически. + */ +#ifndef POP_TROB_H +#define POP_TROB_H + +#include + +#define POP_NROOMS 24 +#define POP_ROOMTILES 30 + +/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и + * очистить список trob. */ +void pop_trob_reset(void); + +/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт. + * При ПЕРВОМ обращении лениво инициализируется из backtable уровня + * (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне + * диапазона → 0. */ +uint8_t *pop_trob_modif(uint8_t room); + +/* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить + * type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */ +void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type); + +/* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать + * modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице. + * Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */ +void pop_process_trobs(uint8_t cur_room); + +/* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку + * LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button. + * room/tilepos — кнопка; button_type — её тайл-код; modifier — её modif + * (= индекс в LINKLOC). Звать из check_press (pop_map). */ +void pop_trigger_button(uint8_t room, uint8_t tilepos, + uint8_t button_type, uint8_t modifier); + +/* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через + * шов (roomtest не дёргает шов, пока сосед не анимируется). */ +uint8_t pop_trob_room_active(uint8_t room); + +/* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты + * (trob-состояние в room_modif персистентно и НЕ трогается). Звать в + * enter_room. */ +void pop_trob_room_changed(void); + +#endif diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 64b62e0..897f87a 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -14,18 +14,58 @@ #include "pop_ctrl.h" #include "pop_map.h" #include "pop_level.h" +#include "pop_trob.h" + +__sfr __at (0xFE) io_border; /* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */ #define SEQ_STAND 2 #define ROOM_1 1 #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ +/* Тест-старт для отладки пик (Фаза S): комната 6 = тестовая (пики в col3/col4 + * row2, шахта col3). Kid на полу row0 col1, идёт вправо → падает в шахту → + * пики. TODO: вернуть на pop_level_start_room (комната 1) после отладки. */ +#define START_ROOM 6 +#define START_COL 1 +#define START_ROW 0 +#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ +#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ + /* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы * соседей для кромок. Заполняются pop_room_load из уровня. File-scope, * чтобы enter_room переключал комнату без длинного списка параметров. */ static uint8_t room_fg[30], room_bg[30]; static uint8_t lcol_fg[3], lcol_bg[3], rcol_fg[3], rcol_bg[3], below_fg[10]; -static uint8_t cur_room; /* номер текущей комнаты (для переходов) */ +static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */ +static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */ +static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */ + +/* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid + * слева от отрисованной → −140 (спрайт за левой кромкой), справа → +140, та + * же → 0. Звать каждый кадр перед kid_draw. */ +static void update_kid_render_dx(void) +{ + int dx = 0; + if (kid_room != cur_room) { + if (kid_room == pop_room_link(cur_room, 0)) dx = -140; /* left neighbour */ + else if (kid_room == pop_room_link(cur_room, 1)) dx = 140; /* right neighbour */ + } + pop_kid_set_render_dx(dx); +} + +/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый + * draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта + * реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен + * → 0 редроёв (раньше шов рисовался безусловно КАЖДЫЙ кадр, пока сосед активен + * — тот же класс, что был у кнопки). seam_redraw=2 = обе страницы дабл-буфера. */ +static uint8_t seam_sig; /* последняя нарисованная openness гейта в шве */ +static uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */ + +/* Отладочный лейбл номера комнаты — рисуем только при смене (outtextxy тяжёлая): + * sig = последняя показанная комната, redraw = остаток кадров (2 = обе страницы). */ +static uint8_t room_label_sig = 0xFF; +static uint8_t room_label_redraw; /* Персистентные debris-переопределения (упавший loose-кусок приземлился в * комнате снизу → floor стал debris). Минимальный per-room override: живёт @@ -51,6 +91,7 @@ static void add_debris_override(uint8_t room, uint8_t pos) static void enter_room(uint8_t room) { uint8_t pg, i; + uint8_t *room_mod; pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); for (i = 0; i < dbr_cnt; i++) /* применить debris-переопределения */ if (dbr_room[i] == room) room_fg[dbr_pos[i]] = TILE_DEBRIS; @@ -59,14 +100,49 @@ static void enter_room(uint8_t room) pop_room_link(room, 2), pop_room_link(room, 3), lcol_fg, rcol_fg); pop_room_set_below(below_fg); + /* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика + * уровня, mod — живой room_modif (ленивая инициализация). */ + { + uint8_t up = pop_room_link(room, 2), c; + if (up) { + uint8_t *um = pop_trob_modif(up); + pop_level_access_begin(); + for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c)); + pop_level_access_end(); + for (c = 0; c < 10; c++) above_mod[c] = um[20 + c]; + pop_room_set_above(above_fg, above_mod); + } else { + pop_room_set_above(0, 0); /* нет комнаты сверху → пол (кромка) */ + } + } + pop_map_set_room(room); pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ + pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ + /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ + * per-room (room_modif живёт между входами); рисуем и коллидируем по ним, + * а не по свежему backtable. Фаза P0. */ + room_mod = pop_trob_modif(room); + /* Модификаторы швов — из ЖИВОГО room_modif соседей (openness ворот, + * видимых через шов, персистентен: ворота room8(0,9) видны в col0 room6). + * pop_room_load заполнил lcol_bg/rcol_bg статикой уровня — перекрываем. */ + { + uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1); + if (lft) { uint8_t *m = pop_trob_modif(lft); + lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29]; + seam_sig = m[9]; } /* шов рисуется в обе страницы ниже — синхр. сигнатуру */ + if (rgt) { uint8_t *m = pop_trob_modif(rgt); + rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; } + } + seam_redraw = 0; /* шов свежий на обеих страницах — редрой не нужен */ for (pg = 0; pg < 2; pg++) { gfx_set_draw_page(pg); setfillstyle(SOLID_FILL, BLACK); bar(0, 0, 319, 255); - pop_room_draw(room, room_fg, room_bg, lcol_fg, lcol_bg); + pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg); } cur_room = room; + kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */ + pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */ } int main(void) @@ -75,6 +151,7 @@ int main(void) * по умолчанию (без мерцания); SPACE переключает в однобуфер для * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */ uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0; + uint16_t dead_frames = 0; if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */ puts("pop_bg_load failed"); @@ -100,14 +177,17 @@ int main(void) * ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ * страницы, в которую рисуем. Дальше каждый тик восстанавливаем * ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */ - /* Извлечь и отрисовать стартовую комнату 1 (в обе страницы). */ - enter_room(ROOM_1); + /* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить + * trob/room_modif (ленивая инициализация из уровня при первом входе). */ + pop_trob_reset(); + enter_room(START_ROOM); gfx_set_draw_page(0); /* K2b: Kid стоит на уступе (row1), управляется с клавиатуры. * ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход. * Карта коллизии (pop_map_set) уже задана в enter_room. */ - kid_init(SEQ_STAND, 1, 1, 0); + kid_init(SEQ_STAND, START_COL, START_ROW, 0); + pop_kid_hp_reset(); kid_draw(); pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), kid_fp_width(), kid_fp_height(), @@ -122,6 +202,9 @@ int main(void) while (!kbd_raw_down(KBD_ESC)) { /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один * переключ). Выключение возвращает показ на страницу 0. */ + + io_border = 2; /* синий - начало */ + uint8_t sp = kbd_raw_down(KBD_SPACE); if (sp && !sp_prev) { dbuf ^= 1; @@ -138,6 +221,7 @@ int main(void) if (kbd_raw_down(0x16)) frozen = 1; if (kbd_raw_down(0x1E)) frozen = 0; if (frozen) { + io_border = 0; /* черный: ждём vsync */ gfx_wait_vsync(); continue; } @@ -157,9 +241,11 @@ int main(void) enter_room(down); /* координаты Kid уже репроецированы pop_map */ gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */ } else { /* выпал из уровня (нет комнаты снизу) — респавн */ - enter_room(ROOM_1); + pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */ + enter_room(START_ROOM); gfx_set_draw_page(back); - kid_init(SEQ_STAND, 1, 1, 0); + kid_init(SEQ_STAND, START_COL, START_ROW, 0); + pop_kid_hp_reset(); } } else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */ uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1)); @@ -169,18 +255,59 @@ int main(void) gfx_set_draw_page(back); } } + + io_border = 4; /* зеленый */ + pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */ /* — ДО Kid, чтобы Kid всегда был поверх плиты) */ + io_border = 6; /* голубой */ + pop_process_trobs(cur_room); /* пики/ворота: продвинуть + перерисовать (тоже */ + /* слой фона — ДО Kid) */ + io_border = 2; /* синий */ + { /* ворота соседа слева (room8) видны в col0 — */ + uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */ + if (lft) { + uint8_t *m = pop_trob_modif(lft); + uint8_t g = m[9], prev = seam_sig; + seam_sig = g; /* СНАЧАЛА сохранить (обход SDCC z80 sub-store */ + /* бага: `if(g!=s)s=g` эмитит store РАЗНОСТИ g-s, */ + /* не g — sdcc_z80_cmp_store_a_bug) */ + if (g != prev) seam_redraw = 2; /* openness изменился → обе страницы дабл-буфера */ + if (seam_redraw) { + lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29]; + pop_room_redraw_seam_left(); + seam_redraw--; + } + } + } + io_border = 4; /* зеленый */ + if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */ + dead_frames++; + if (dead_frames > RESPAWN_SETTLE && + (kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) { + dead_frames = 0; + pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */ + enter_room(START_ROOM); + gfx_set_draw_page(back); + kid_init(SEQ_STAND, START_COL, START_ROW, 0); + pop_kid_hp_reset(); + } + } + io_border = 6; /* голубой */ if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */ uint8_t fcol = (uint8_t)((pop_loose_fell - 1) % 10); uint8_t down = pop_room_link(cur_room, LINK_DOWN); pop_loose_fell = 0; + io_border = 2; /* синий */ if (down) { int8_t lrow = pop_room_col_landing(down, fcol); if (lrow >= 0) add_debris_override(down, (uint8_t)(lrow * 10 + fcol)); } } + io_border = 4; /* зеленый */ + update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ + io_border = 0; /* черный */ pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), kid_fp_width(), kid_fp_height(), Kid.direction, Kid.frame, Kid.action); @@ -189,6 +316,25 @@ int main(void) /* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже * сделан (скрытая страница), wait_vsync ловит начало кадра, * set_visible_page переключает во время бланка (gfx.h паттерн). */ + + io_border = 0; /* черный: ждём vsync */ + + /* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО + * при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера), + * а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */ + { + uint8_t r = cur_room; + if (r != room_label_sig) { room_label_sig = r; room_label_redraw = 2; } + if (room_label_redraw) { + char roomLabel[] = "Room ##"; + roomLabel[5] = (char)(r / 10 + '0'); + roomLabel[6] = (char)(r % 10 + '0'); + setcolor(WHITE); + outtextxy(100, 0, roomLabel); + room_label_redraw--; + } + } + gfx_wait_vsync(); gfx_wait_vsync(); gfx_wait_vsync(); diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py index 15cd5ca..a8160a6 100644 --- a/applications/PoP/toolchain/pop_pack_bg.py +++ b/applications/PoP/toolchain/pop_pack_bg.py @@ -56,6 +56,21 @@ CLIMB_OVERLAY_ENV_IDS = {32, 150, 151} # попадают, поэтому добавляем явно. Без них дрожащая плита невидима. LOOSE_ANIM_ENV_IDS = {69, 70, 71, 72, 73, 74} +# Кадры ВЫДВИЖЕНИЯ пик (seg008): три части — left (128-132, своя ячейка, +# draw_tile_anim), right (134-138, правый сосед, draw_tile_anim_right), fore +# (139-143, поверх Kid, draw_tile_fore). Рисуются ТОЛЬКО динамикой +# (animate_spike); в уровнях пики статически убраны (modif=0 → кадр 0), +# поэтому в used render_room НЕ попадают. Без них выдвинутые пики невидимы. +SPIKE_ANIM_ENV_IDS = {128, 129, 130, 131, 132, # left + 134, 135, 136, 137, 138, # right + 139, 140, 141, 142, 143} # fore + +# Кадры ОТКРЫТИЯ ворот (portcullis, DOOR_FRAM_SLICE + gate pieces 50/51, +# seg008 draw_gate): 50-59 + 67. В уровнях ворота статически ЗАКРЫТЫ +# (низкий modif → используются только нижние срезы 50-53); срезы более +# открытых состояний (54-59, 67) нужны анимации открытия — добавляем явно. +GATE_ANIM_ENV_IDS = {50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 67} + ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31 ENV_MASK = (1 << ENV_SHIFT) - 1 @@ -248,7 +263,9 @@ def main(): used = collect_used() env_ids = sorted(set(id for ch, id in used if ch != R.CHTAB_WALL) | CLIMB_OVERLAY_ENV_IDS # + пол-оверлей подъёма - | LOOSE_ANIM_ENV_IDS) # + дрожащие кадры loose + | LOOSE_ANIM_ENV_IDS # + дрожащие кадры loose + | SPIKE_ANIM_ENV_IDS # + кадры выдвижения пик + | GATE_ANIM_ENV_IDS) # + кадры открытия ворот wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL) env_bg = [i for i in env_ids if i not in FORE_ENV_IDS] diff --git a/third_party/16x16-RPG-characters/LICENSE b/third_party/16x16-RPG-characters/LICENSE new file mode 100644 index 0000000..604209a --- /dev/null +++ b/third_party/16x16-RPG-characters/LICENSE @@ -0,0 +1,359 @@ +Creative Commons Legal Code + +Attribution-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(f) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined below) for the purposes of this + License. + c. "Creative Commons Compatible License" means a license that is listed + at https://creativecommons.org/compatiblelicenses that has been + approved by Creative Commons as being essentially equivalent to this + License, including, at a minimum, because that license: (i) contains + terms that have the same purpose, meaning and effect as the License + Elements of this License; and, (ii) explicitly permits the relicensing + of adaptations of works made available under that license under this + License or a Creative Commons jurisdiction license with the same + License Elements as this License. + d. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + e. "License Elements" means the following high-level license attributes + as selected by Licensor and indicated in the title of this License: + Attribution, ShareAlike. + f. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + g. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + h. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + i. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + j. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + k. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; and, + iii. Voluntary License Schemes. The Licensor waives the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(c), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(c), as requested. + b. You may Distribute or Publicly Perform an Adaptation only under the + terms of: (i) this License; (ii) a later version of this License with + the same License Elements as this License; (iii) a Creative Commons + jurisdiction license (either this or a later license version) that + contains the same License Elements as this License (e.g., + Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible + License. If you license the Adaptation under one of the licenses + mentioned in (iv), you must comply with the terms of that license. If + you license the Adaptation under the terms of any of the licenses + mentioned in (i), (ii) or (iii) (the "Applicable License"), you must + comply with the terms of the Applicable License generally and the + following provisions: (I) You must include a copy of, or the URI for, + the Applicable License with every copy of each Adaptation You + Distribute or Publicly Perform; (II) You may not offer or impose any + terms on the Adaptation that restrict the terms of the Applicable + License or the ability of the recipient of the Adaptation to exercise + the rights granted to that recipient under the terms of the Applicable + License; (III) You must keep intact all notices that refer to the + Applicable License and to the disclaimer of warranties with every copy + of the Work as included in the Adaptation You Distribute or Publicly + Perform; (IV) when You Distribute or Publicly Perform the Adaptation, + You may not impose any effective technological measures on the + Adaptation that restrict the ability of a recipient of the Adaptation + from You to exercise the rights granted to that recipient under the + terms of the Applicable License. This Section 4(b) applies to the + Adaptation as incorporated in a Collection, but this does not require + the Collection apart from the Adaptation itself to be made subject to + the terms of the Applicable License. + c. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and (iv) , consistent with Ssection 3(b), in the case of an + Adaptation, a credit identifying the use of the Work in the Adaptation + (e.g., "French translation of the Work by Original Author," or + "Screenplay based on original Work by Original Author"). The credit + required by this Section 4(c) may be implemented in any reasonable + manner; provided, however, that in the case of a Adaptation or + Collection, at a minimum such credit will appear, if a credit for all + contributing authors of the Adaptation or Collection appears, then as + part of these credits and in a manner at least as prominent as the + credits for the other contributing authors. For the avoidance of + doubt, You may only use the credit required by this Section for the + purpose of attribution in the manner set out above and, by exercising + Your rights under this License, You may not implicitly or explicitly + assert or imply any connection with, sponsorship or endorsement by the + Original Author, Licensor and/or Attribution Parties, as appropriate, + of You or Your use of the Work, without the separate, express prior + written permission of the Original Author, Licensor and/or Attribution + Parties. + d. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION +OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of the License. + + Creative Commons may be contacted at https://creativecommons.org/. diff --git a/third_party/16x16-RPG-characters/README.md b/third_party/16x16-RPG-characters/README.md new file mode 100644 index 0000000..b3b78a0 --- /dev/null +++ b/third_party/16x16-RPG-characters/README.md @@ -0,0 +1,27 @@ +# 16x16 RPG characters `v3.0` + +16x16px RPG character sprite sheet for up to down games. + +This asset pack has been downloaded from https://route1rodent.itch.io/16x16-rpg-character-sprite-sheet + +Adapted from opengameart.org 's "[NES-Style RPG Characters](https://opengameart.org/content/nes-style-rpg-characters)" and "[More NES-style RPG Characters](https://opengameart.org/content/more-nes-style-rpg-characters)", with additional content made by [@route1rodent](https://route1rodent.itch.io). + +## License + +16x16 RPG character sprite sheet (c) by @route1rodent + +"16x16 RPG character sprite sheet" is licensed under a +Creative Commons Attribution-ShareAlike 3.0 Unported License (CC BY-SA 3.0). + +You should have received a copy of the license along with this +work. If not, see http://creativecommons.org/licenses/by-sa/3.0/. + + +## Credits + +Maintained by @route1rodent: + +- itch.io: https://route1rodent.itch.io +- Twitter: https://twitter.com/route1rodent +- Github: https://github.com/itsjavi +- Blog: https://blog.itsjavi.com diff --git a/third_party/16x16-RPG-characters/preview.png b/third_party/16x16-RPG-characters/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..2d1a7b926918e8e539ea131f7233f44da36c9adc GIT binary patch literal 3387 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 literal 0 HcmV?d00001 diff --git a/third_party/16x16-RPG-characters/sprites/new-style/chibi-layered.aseprite b/third_party/16x16-RPG-characters/sprites/new-style/chibi-layered.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..b2cc1f78a3e181676774e4235c455a9a5ada7f28 GIT binary patch literal 3645 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 z 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 S$U0~S7+g2jE&I(e>i+;CrcmSn literal 0 HcmV?d00001 diff --git a/third_party/16x16-RPG-characters/sprites/old-style/03-soldier.png b/third_party/16x16-RPG-characters/sprites/old-style/03-soldier.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d640875fe0f97d20e9f5e76cd4ccfe7c02aee3 GIT binary patch literal 8203 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 xpQdt=G0`p-_zw-({+h#3qieZ z(EtL}TF<=l>$PaKvs!9n=_PLWHMXD=VD$*6`xa;HR5uZcO?kN;2-ar$={`VD+vrxw IP1wu-0!x*&fB*mh literal 0 HcmV?d00001 diff --git a/third_party/16x16-RPG-characters/sprites/old-style/04-scout.png b/third_party/16x16-RPG-characters/sprites/old-style/04-scout.png new file mode 100644 index 0000000000000000000000000000000000000000..dfbf05bd596fdf8aab64e073463a73e3ffb5f847 GIT binary patch literal 7222 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 AF#rGn literal 0 HcmV?d00001 diff --git a/third_party/16x16-RPG-characters/sprites/old-style/05-devout.png b/third_party/16x16-RPG-characters/sprites/old-style/05-devout.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9db7fd4c7042e372c46fe0a8da9f02ee048388 GIT binary patch literal 8706 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 literal 0 HcmV?d00001 diff --git a/third_party/16x16-RPG-characters/sprites/old-style/06-conjurer.png b/third_party/16x16-RPG-characters/sprites/old-style/06-conjurer.png new file mode 100644 index 0000000000000000000000000000000000000000..296c43436b49eec168f74f97c50259d26bd08c02 GIT binary patch literal 8565 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 literal 0 HcmV?d00001