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 b1c5df5..0ec6ece 100644 Binary files a/applications/PoP/poc/res/bg/pop_env1.atl and b/applications/PoP/poc/res/bg/pop_env1.atl differ diff --git a/applications/PoP/poc/res/bg/pop_env2.atl b/applications/PoP/poc/res/bg/pop_env2.atl index 45d4520..2470cba 100644 Binary files a/applications/PoP/poc/res/bg/pop_env2.atl and b/applications/PoP/poc/res/bg/pop_env2.atl differ diff --git a/applications/PoP/poc/res/bg/pop_env4.atl b/applications/PoP/poc/res/bg/pop_env4.atl index 395c5d0..ad5c6c3 100644 Binary files a/applications/PoP/poc/res/bg/pop_env4.atl and b/applications/PoP/poc/res/bg/pop_env4.atl differ 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 0000000..2d1a7b9 Binary files /dev/null and b/third_party/16x16-RPG-characters/preview.png differ 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 0000000..b2cc1f7 Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/new-style/chibi-layered.aseprite differ diff --git a/third_party/16x16-RPG-characters/sprites/new-style/chibi-layered.png b/third_party/16x16-RPG-characters/sprites/new-style/chibi-layered.png new file mode 100644 index 0000000..ed9be61 Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/new-style/chibi-layered.png differ diff --git a/third_party/16x16-RPG-characters/sprites/old-style/00-avatars.png b/third_party/16x16-RPG-characters/sprites/old-style/00-avatars.png new file mode 100644 index 0000000..0f1da2e Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/old-style/00-avatars.png differ diff --git a/third_party/16x16-RPG-characters/sprites/old-style/00-guide.png b/third_party/16x16-RPG-characters/sprites/old-style/00-guide.png new file mode 100644 index 0000000..bde23f1 Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/old-style/00-guide.png differ diff --git a/third_party/16x16-RPG-characters/sprites/old-style/01-generic.png b/third_party/16x16-RPG-characters/sprites/old-style/01-generic.png new file mode 100644 index 0000000..419b8a2 Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/old-style/01-generic.png differ diff --git a/third_party/16x16-RPG-characters/sprites/old-style/02-bard.png b/third_party/16x16-RPG-characters/sprites/old-style/02-bard.png new file mode 100644 index 0000000..98bf1c7 Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/old-style/02-bard.png differ 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 0000000..a6d6408 Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/old-style/03-soldier.png differ 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 0000000..dfbf05b Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/old-style/04-scout.png differ 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 0000000..6e9db7f Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/old-style/05-devout.png differ 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 0000000..296c434 Binary files /dev/null and b/third_party/16x16-RPG-characters/sprites/old-style/06-conjurer.png differ