SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной папки. Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT, по умолчанию ../..). applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся архивом закрытых задач, багов и исполненных планов. Скопировано из applications/PoP/roomtest@4b74478. Перенос проверен побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита, все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host- тесты зелёные (15/15). Раскладка: src/ рукописный C (roomtest.c -> sprpop.c) gen/ генерируемые заголовки, в репозитории assets/orig/ оригинальные данные игры, вне репозитория (копирайт) assets/packed/ то, что ложится на диск, в раскладке диска tools/ конверторы; все пути — в одном tools/paths.py build/ выход: exe, каталоги ресурсов, hdd/, промежуточные atl/ Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что пересчитывается каждым make. Автоматика построена на ОТСУТСТВИИ файла, а не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по времени превращалось бы в лотерею. Недостающий ресурс или заголовок чинится сам, рекурсивным вызовом в ветку генерации. Музыка собирается из любого из четырёх наборов записей (make music-mp3, music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама делает музыку устаревшей. Длины реплик больше не захардкожены: упаковщик печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них — иначе mt32 (реплики на 6% длиннее) молча ломал катсцену. Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR), HDD_IMG стал ?=; команда сборки roomtest не изменилась. Корневой make host-tests переключён на SprPoP. Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики приходила раньше молнии. Это обход, а не лечение; разбор с замерами — docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,929 @@
|
||||
/*
|
||||
* 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_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
|
||||
#include "pop_state.h" /* pop_leveldoor_open */
|
||||
#include "pop_level.h"
|
||||
#include "pop_geom.h"
|
||||
#include "pop_redraw.h" /* пометки перерисовки тайлов (вместо прямых вызовов) */
|
||||
/* pop_bg нужен ТОЛЬКО для пламени факела и пузырька зелья: это не тайловая
|
||||
* перерисовка, а покадровый оверлей поверх фона (в оригинале — свои
|
||||
* add_backtable/add_foretable внутри draw_tile). Всё остальное идёт через
|
||||
* пометки, поэтому pop_process_trobs — единственная функция модуля, которую
|
||||
* нельзя звать из __banked-кода. */
|
||||
#include "pop_bg.h"
|
||||
#include "pop_tile.h" /* pop_palace — тип уровня как модификатор дыры */
|
||||
#include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */
|
||||
#include "pop_kid.h" /* Kid.curr_row/alive — условие остановки чомпера */
|
||||
#include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */
|
||||
|
||||
/* Коды тайлов (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
|
||||
#define TILE_POTION 0x0A
|
||||
#define TILE_LOOSE 0x0B /* проваливающийся пол (фаза тряски в modif) */
|
||||
#define TILE_SWORD 0x16 /* меч-предмет на полу */
|
||||
#define TILE_LEVELDOOR 0x10 /* левая половина двери уровня (на ней modif) */
|
||||
#define TILE_CHOMP 0x12 /* чомпер (челюсти) */
|
||||
#define TILE_WALL 0x14 /* стена (в модификаторе — флаг «без полоски») */
|
||||
#define TILE_TORCH 0x13
|
||||
#define TILE_TORCH_DEBRIS 0x1E
|
||||
#define TILE_EMPTY 0x00 /* remove_loose: на месте провалившейся плиты */
|
||||
|
||||
/* ---- Персистентное 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 30 /* как в оригинале (types.h) */
|
||||
static trob_t trobs[TROBS_MAX];
|
||||
static uint8_t trobs_count;
|
||||
static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */
|
||||
/* Код ПРАВОГО соседа — заполняется только у факелов (0 у остальных). Нужен
|
||||
* потому, что пламя рисуется в ячейке соседа и запекается в фон: если у
|
||||
* соседа своя anim-графика, её надо вернуть ПОВЕРХ огня. Читается в том же
|
||||
* префетче, чтобы не свапать W0 второй раз за кадр. */
|
||||
static uint8_t trob_rcode[TROBS_MAX];
|
||||
|
||||
/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button:
|
||||
* признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её
|
||||
* изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под
|
||||
* стоящим Кидом перерисовывалась КАЖДЫЙ кадр (2×draw_tile ≈ 50% кадра в
|
||||
* статике), т.к. её trob висит, пока Кид стоит (это верно, как в SDLPoP —
|
||||
* расхождение было именно в безусловном редрое). */
|
||||
static uint8_t trob_drawn[POP_ROOMTILES];
|
||||
|
||||
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
|
||||
* выбора следующего кадра пламени. */
|
||||
static pop_rnd_t trob_seed;
|
||||
|
||||
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
|
||||
|
||||
|
||||
void pop_trob_reset(void) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
|
||||
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
|
||||
pop_redraw_reset();
|
||||
trobs_count = 0;
|
||||
}
|
||||
|
||||
uint8_t *pop_trob_modif(uint8_t room) __banked
|
||||
{
|
||||
if (room < 1 || room > POP_NROOMS) return 0;
|
||||
if (!room_seen[room - 1]) {
|
||||
uint8_t i;
|
||||
pop_level_room_bg(room, room_modif[room - 1]);
|
||||
/* load_alter_mod (seg008:198E): модификатор В ФАЙЛЕ уровня и
|
||||
* модификатор В РАНТАЙМЕ — разные величины, оригинал переводит их
|
||||
* при загрузке. Порт того, что нас касается:
|
||||
*
|
||||
* ворота 1 = «открыты» (Table 8 спецификации DAT) -> 188, то есть
|
||||
* рабочая высота подъёма; ЛЮБОЕ другое значение -> 0
|
||||
* (закрыты). Без этого перевода ворота, которые в
|
||||
* уровне открыты, стартуют закрытыми, и кнопка-closer
|
||||
* рядом с ними теряет смысл (ур. 2, комн. 13).
|
||||
* loose -> 0: фаза тряски всегда начинается с нуля.
|
||||
* зелье -> <<3: старшие биты = тип, младшие 3 = фаза пузырька.
|
||||
* стена -> <<7: единственный бит уровня («без полоски», см. ниже)
|
||||
* переезжает в СТАРШИЙ, где его и ищет отрисовка.
|
||||
*
|
||||
* СВЯЗИ кладки (какая грань у стены слева/справа) из этого перевода
|
||||
* намеренно выкинуты: у нас их считает pop_wall_modifier по типам
|
||||
* СОСЕДЕЙ прямо при отрисовке. Но второй смысл модификатора стены —
|
||||
* авторский флаг «не рисовать синюю полоску на моей правой кромке»
|
||||
* (noblue) — из соседей не выводится, он проставлен вручную в данных
|
||||
* уровня. Без перевода отрисовка видела сырую единицу вместо 0x80,
|
||||
* проверка `(mod & 0x80) == 0` проходила, и во дворце вылезал кусок
|
||||
* узора на стене (BUG BLUELINE-NOBLUE, ур. 6 к. 1 тайл (2,3)). */
|
||||
pop_level_access_begin();
|
||||
for (i = 0; i < POP_ROOMTILES; i++) {
|
||||
uint8_t *m = &room_modif[room - 1][i];
|
||||
switch (pop_level_tile_raw(room, i) & 0x1F) {
|
||||
case TILE_GATE: *m = (uint8_t)((*m == 1) ? 188 : 0); break;
|
||||
case TILE_LOOSE: *m = 0; break;
|
||||
case TILE_POTION: *m = (uint8_t)(*m << 3); break;
|
||||
case TILE_WALL: *m = (uint8_t)(*m << 7); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
pop_level_access_end();
|
||||
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) __banked
|
||||
{
|
||||
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) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
|
||||
pop_redraw_reset();
|
||||
}
|
||||
|
||||
/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её
|
||||
* тайлов: факелам — случайная стартовая фаза пламени (start_anim_torch,
|
||||
* seg007:0847: modif = prandom(8)), зельям — случайная фаза пузырька
|
||||
* (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */
|
||||
void pop_trob_anim_room(uint8_t room) __banked
|
||||
{
|
||||
uint8_t *mod = pop_trob_modif(room);
|
||||
uint8_t i, code;
|
||||
if (!mod) return;
|
||||
pop_level_access_begin();
|
||||
/* Освободить список от «декоративных» trob ДРУГИХ комнат: факел, зелье и
|
||||
* блеск меча анимируются только в ОТРИСОВАННОЙ комнате, а место в списке
|
||||
* занимают. Оригинал их копит и на 30-й выдаёт «Trobs Overflow»; у нас
|
||||
* отладочный обход всех 24 комнат упирается в лимит за пару минут, после
|
||||
* чего новые анимации молча не заводятся. Механику (ворота/пики/кнопки,
|
||||
* в т.ч. кросс-комнатные) чистка не трогает. */
|
||||
{
|
||||
uint8_t k, wr = 0;
|
||||
for (k = 0; k < trobs_count; k++) {
|
||||
uint8_t c = (uint8_t)(pop_level_tile_raw(trobs[k].room,
|
||||
trobs[k].tilepos) & 0x1F);
|
||||
if (trobs[k].room != room &&
|
||||
(c == TILE_TORCH || c == TILE_TORCH_DEBRIS ||
|
||||
c == TILE_POTION || c == TILE_SWORD))
|
||||
continue;
|
||||
trobs[wr++] = trobs[k];
|
||||
}
|
||||
trobs_count = wr;
|
||||
}
|
||||
for (i = 0; i < POP_ROOMTILES; i++) {
|
||||
code = (uint8_t)(pop_level_tile_raw(room, i) & 0x1F);
|
||||
if (code == TILE_TORCH || code == TILE_TORCH_DEBRIS) {
|
||||
mod[i] = (uint8_t)trob_prandom(8);
|
||||
pop_add_trob(room, i, 1);
|
||||
} else if (code == TILE_POTION) {
|
||||
mod[i] = (uint8_t)((mod[i] & 0xF8) | (trob_prandom(6) + 1));
|
||||
pop_add_trob(room, i, 1);
|
||||
} else if (code == TILE_SWORD) {
|
||||
/* start_anim_sword (seg007:087C): случайная фаза счётчика блеска. */
|
||||
mod[i] = (uint8_t)(trob_prandom(0xFF) & 0x1F);
|
||||
/* trob_drawn обязан описывать то, что НАРИСУЕТ полная отрисовка
|
||||
* комнаты, а она идёт ПОСЛЕ этой функции и берёт ЖИВОЙ modif.
|
||||
* Обнулять его (как делает pop_trob_room_changed для всех тайлов)
|
||||
* тут нельзя: если случайная фаза попала ровно на 1, комната
|
||||
* рисуется С БЛЕСКОМ, а trob_drawn говорит «блеска нет» — и на
|
||||
* следующем кадре счётчик уходит с 1, sig снова 0, СОВПАДАЕТ со
|
||||
* стоячим trob_drawn, и перерисовка НЕ помечается. Блеск остаётся
|
||||
* запечённым в фон до конца периода: animate_sword считает вниз и
|
||||
* выдаёт новый период 0x28..0x67, то есть до 103 логических кадров
|
||||
* ≈ 6 секунд. Вероятность ровно 1/32 на вход в комнату — отсюда
|
||||
* «редко воспроизводится» (найдено пользователем 2026-08-17,
|
||||
* уровень 1 комната 15). Проверено ПРИНУДИТЕЛЬНО: со временной
|
||||
* подменой фазы на mod[i] = 1 баг ловится на КАЖДОМ входе в
|
||||
* комнату, а с этой строкой не ловится ни на одном.
|
||||
*
|
||||
* У КНОПКИ той же дыры нет, хотя механизм общий: её признак —
|
||||
* ДЛИТЕЛЬНОЕ состояние (нажата/нет), и рассогласование само
|
||||
* исправляется на первой же смене. У блеска признак — одиночная
|
||||
* вспышка в ОДНОМ кадре из 40..103, и пропущенная смена стоит
|
||||
* целого периода. */
|
||||
trob_drawn[i] = (uint8_t)(mod[i] == 1);
|
||||
pop_add_trob(room, i, 1);
|
||||
}
|
||||
}
|
||||
pop_level_access_end();
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
/* get_torch_frame (seg007:06CD): случайный кадр 0..8, иначе следующий. */
|
||||
static uint8_t get_torch_frame(uint8_t curr)
|
||||
{
|
||||
uint8_t next = (uint8_t)trob_prandom(255);
|
||||
if (next != curr && next < 9) return next;
|
||||
next = (uint8_t)(curr + 1);
|
||||
if (next >= 9) next = 0;
|
||||
return next;
|
||||
}
|
||||
|
||||
/* bubble_next_frame (seg007:0759): фазы пузырька 1..7 по кругу. */
|
||||
static uint8_t bubble_next_frame(uint8_t curr)
|
||||
{
|
||||
uint8_t next = (uint8_t)(curr + 1);
|
||||
return (next >= 8) ? 1 : next;
|
||||
}
|
||||
|
||||
/* Разбор номера тайла 0..29 на ряд/колонку без деления (`tp / 10`, `tp % 10`
|
||||
* — это __divuchar/__moduchar, а в animate_chomper они звались на каждого
|
||||
* чомпера каждый кадр). Определены ЗДЕСЬ, а не перед pop_process_trobs, где
|
||||
* лежали раньше: чомперы выше по файлу и оригинала не видели. */
|
||||
static const uint8_t TP_ROW[30] = {
|
||||
0,0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,1,1, 2,2,2,2,2,2,2,2,2,2
|
||||
};
|
||||
static const uint8_t TP_COL[30] = {
|
||||
0,1,2,3,4,5,6,7,8,9, 0,1,2,3,4,5,6,7,8,9, 0,1,2,3,4,5,6,7,8,9
|
||||
};
|
||||
|
||||
/* animate_chomper (seg007:0448). Модификатор: младшие 7 бит — фаза 1..N,
|
||||
* старший — «перемололо кого-то» (кровь). Фаза крутится по кругу до
|
||||
* POP_CHOMPER_SPEED (у оригинала 15), то есть челюсти смыкаются раз в N
|
||||
* кадров; фаза 2 — момент удара (в оригинале там звук sound_47).
|
||||
*
|
||||
* Остановка (иначе чомперы щёлкали бы всю игру): если Кид ушёл из этой
|
||||
* комнаты, или ушёл из этого РЯДА, или умер не в этом чомпере — trob
|
||||
* снимается, но только после фазы 6, чтобы челюсти успели разомкнуться.
|
||||
* Запускает их обратно start_chompers на смене ряда. Аргументы room/tp
|
||||
* нужны ровно для этой проверки. */
|
||||
static void animate_chomper(uint8_t *m, int8_t *type,
|
||||
uint8_t room, uint8_t tp, uint8_t drawn_room)
|
||||
{
|
||||
uint8_t blood, frame;
|
||||
if (*type >= 0) {
|
||||
blood = (uint8_t)(*m & 0x80);
|
||||
frame = (uint8_t)((*m & 0x7F) + 1);
|
||||
if (frame > POP_CHOMPER_SPEED) frame = 1;
|
||||
*m = (uint8_t)(blood | frame);
|
||||
if (frame == 2) pop_sfx_play(47); /* щелчок челюстей, seg007:297 */
|
||||
if ((room != drawn_room || (int8_t)TP_ROW[tp] != Kid.curr_row ||
|
||||
(Kid.alive >= 0 && blood == 0)) && frame >= 6)
|
||||
*type = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* next_chomper_timing (seg007:0F9A): 15,12,9,6,13,10,7,14,11,8 по кругу —
|
||||
* чомперы одного ряда стартуют вразнобой, а не хлопают в унисон. */
|
||||
static uint8_t next_chomper_timing(uint8_t timing)
|
||||
{
|
||||
timing -= 3;
|
||||
if (timing < 6) timing += 10;
|
||||
return timing;
|
||||
}
|
||||
|
||||
/* start_anim_chomper (seg007:08C7): запустить, только если стоит (0) или
|
||||
* уже прошёл фазу размыкания (>=6). */
|
||||
static void start_anim_chomper(uint8_t *mod, uint8_t room, uint8_t tp,
|
||||
uint8_t modifier)
|
||||
{
|
||||
uint8_t old = (uint8_t)(mod[tp] & 0x7F);
|
||||
if (old == 0 || old >= 6) {
|
||||
mod[tp] = modifier;
|
||||
pop_add_trob(room, tp, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* start_chompers (seg007:0F13): все чомперы РЯДА активного персонажа.
|
||||
* Зовётся отовсюду, где персонаж сменил ряд (SEQ_UP/SEQ_DOWN, start_fall,
|
||||
* приземление) и на входе в комнату. */
|
||||
void pop_start_chompers(uint8_t room, int8_t row) __banked
|
||||
{
|
||||
uint8_t timing = 15, col, tp;
|
||||
uint8_t *mod;
|
||||
if ((uint8_t)row >= 3) return; /* вне комнаты — нечего */
|
||||
/* pop_trob_modif ДО открытия окна: при первом обращении к комнате он
|
||||
* сам лезет за её bg в страницу уровня, то есть перемапит W0 и закроет
|
||||
* его — а мы внутри окна читаем тайлы. */
|
||||
mod = pop_trob_modif(room);
|
||||
pop_level_access_begin();
|
||||
for (col = 0; col < 10; col++) {
|
||||
tp = (uint8_t)(row * 10 + col);
|
||||
if ((pop_level_tile_raw(room, tp) & 0x1F) == TILE_CHOMP) {
|
||||
uint8_t m = (uint8_t)(mod[tp] & 0x7F);
|
||||
if (m == 0 || m >= 6) {
|
||||
start_anim_chomper(mod, room, tp,
|
||||
(uint8_t)(timing | (mod[tp] & 0x80)));
|
||||
timing = next_chomper_timing(timing);
|
||||
}
|
||||
}
|
||||
}
|
||||
pop_level_access_end();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* animate_loose (seg007:0CBA) — для плиты в НЕ отрисованной комнате.
|
||||
*
|
||||
* ЗАЧЕМ. В оригинале loose-пол — обычный trob, а do_trobs крутит весь
|
||||
* список независимо от drawn_room: расшатанная плита доваливается, даже если
|
||||
* Кид уже убежал в соседнюю комнату. У нас фаза живёт в pop_loose_modif[30]
|
||||
* — массиве ТЕКУЩЕЙ комнаты, — и смена комнаты её обнуляла: плита,
|
||||
* пробежав по которой Кид уходил вправо, оставалась целой (уровень 10,
|
||||
* комната 2, тайл 2,9).
|
||||
*
|
||||
* Разделение обязанностей: пока комната отрисована, фазой владеет
|
||||
* pop_loose_tick (ему нужны перерисовка, звук, mob-порядок), и trob здесь
|
||||
* сразу снимается. На выходе из комнаты pop_loose_leave_room (pop_map.c)
|
||||
* сбрасывает фазу в ПЕРСИСТЕНТНЫЙ room_modif и заводит trob — дальше плита
|
||||
* досчитывает здесь, а на входе обратно pop_loose_reset забирает фазу
|
||||
* назад. Персистентный модификатор и есть curr_room_modif оригинала, так
|
||||
* что расхождения с SDLPoP нет — только двухуровневое хранение.
|
||||
*
|
||||
* Рисовать тут нечего (комната не на экране), поэтому от оригинала остаётся
|
||||
* ровно счёт фазы и провал: тайл в EMPTY + отрыв падающего куска. */
|
||||
static void animate_loose(uint8_t *m, int8_t *type, uint8_t room, uint8_t tp,
|
||||
uint8_t cur_room)
|
||||
{
|
||||
uint8_t ph;
|
||||
if (room == cur_room) { *type = -1; return; } /* хозяин — pop_loose_tick */
|
||||
ph = (uint8_t)(*m + 1);
|
||||
if (ph == 0) ph = 1; /* перескок нуля, см. pop_loose_tick */
|
||||
if (ph & 0x80) { /* тряска (do_knock): 0x80..0x83 */
|
||||
/* Спецкейс уровня 13 (seg007:823) — тот же, что в pop_loose_tick:
|
||||
* отложенный старт от check_fall_flo живёт в старшем бите, и гасить
|
||||
* его нельзя. Здесь он тоже нужен: гряда лежит в комнате СВЕРХУ, и
|
||||
* пока Кид не вошёл в комнату под ней, счёт идёт заочно. */
|
||||
if (pop_current_level == POP_LOOSE_TILES_LEVEL) { /* ждём молча */ }
|
||||
else if (ph >= 0x84) { ph = 0; *type = -1; }
|
||||
} else if (ph >= 11) { /* loose_floor_delay — провал */
|
||||
/* remove_loose (seg007:0EB8): тайл → EMPTY, модификатором пустой
|
||||
* клетки становится ТИП УРОВНЯ (по нему draw_tile_right выбирает
|
||||
* blueline у дыры). Тот же порядок, что в pop_loose_tick. */
|
||||
pop_level_set_tile(room, tp, TILE_EMPTY);
|
||||
/* Комната не на экране, но её колонка 9 ВИДНА в шве текущей и лежит
|
||||
* в кромочных снимках — попросить главный цикл их перечитать. */
|
||||
pop_neigh_dirty = 1;
|
||||
pop_loose_mob_spawn_at(room, TP_ROW[tp], TP_COL[tp]);
|
||||
ph = pop_palace;
|
||||
*type = -1;
|
||||
}
|
||||
*m = ph;
|
||||
}
|
||||
|
||||
/* ---- Кнопки/ворота (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);
|
||||
if (target_type == TILE_LEVELDOOR) {
|
||||
/* trigger_1 (seg007:0999): дверь уровня открывается ОДИН раз — если
|
||||
* створка уже тронулась (modif != 0), кнопка ничего не делает. */
|
||||
uint8_t *m = pop_trob_modif(room);
|
||||
if (m && m[tilepos] == 0) return 1;
|
||||
return -1;
|
||||
}
|
||||
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 snd) __banked
|
||||
{
|
||||
uint8_t link_timer = get_doorlink_timer(modifier);
|
||||
if (link_timer != 0x1F) { /* не заклинено */
|
||||
/* Оригинал ставит 5; вынесено в pop_tune.h, потому что проверять
|
||||
* руками проход в закрывающуюся решётку при таком окне почти
|
||||
* невозможно (см. BUG-GATE-PASS-1). */
|
||||
set_doorlink_timer(modifier, POP_DBG_GATE_HOLD);
|
||||
if (link_timer < 2) {
|
||||
pop_add_trob(room, tilepos, 1); /* кнопка нажимается */
|
||||
/* seg007:766: щелчок кнопки — шум, и страж оборачивается
|
||||
* (флаг обязателен даже без звука, BUG-GUARD-DEAF-1).
|
||||
*
|
||||
* Звук НЕ у всех вызовов: у оригинала первый параметр playsound,
|
||||
* и в трёх местах он нулевой — вход на уровень (seg003:170),
|
||||
* выход Джаффара (seg002:520) и зелье «открыть» (seg006:1890,
|
||||
* у нас пока не портировано). Там кнопка нажимается служебно,
|
||||
* щёлкать нечему. */
|
||||
if (snd) pop_sfx_play(3);
|
||||
is_guard_notice = 1;
|
||||
}
|
||||
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_leveldoor (seg007:05F1): створка двери уровня. type 0..2 —
|
||||
* открытие (modif 0 -> 43, по пикселю за кадр), type >= 3 — быстрое
|
||||
* закрытие со скоростями {0,5,17,99} (после выхода из уровня). Готово ->
|
||||
* type = -1. */
|
||||
static const uint8_t leveldoor_close_speeds[5] = {0, 5, 17, 99, 0};
|
||||
|
||||
/* ---- Спецсобытие уровня 4: ПОЯВЛЕНИЕ ЗЕРКАЛА ------------------------ *
|
||||
*
|
||||
* seg007:0457, хвост animate_leveldoor: когда дверь выхода ДОРИСОВАЛА
|
||||
* открытие и leveldoor_open переходит 0/2 -> 1, оригинал ставит тайл
|
||||
* зеркала прямо в данные комнаты:
|
||||
*
|
||||
* get_tile(mirror_room, mirror_column, mirror_row);
|
||||
* curr_room_tiles[curr_tilepos] = tiles_13_mirror;
|
||||
*
|
||||
* Ставится он в ЧУЖУЮ комнату (4), а не в ту, где дверь. В статике уровня
|
||||
* зеркала НЕТ НИ В ОДНОМ файле (проверено перебором всех 15 res200N.bin),
|
||||
* поэтому его спрайты 75/77 пришлось добавить в атлас явным списком —
|
||||
* MIRROR_ENV_IDS в pop_pack_bg.py (грабли memory pop_atlas_dynamic_ids). */
|
||||
#define TILE_MIRROR 0x0D /* MIRROR_* — в pop_guard.h (нужны и ИИ тени) */
|
||||
|
||||
static void place_mirror(uint8_t cur_room)
|
||||
{
|
||||
if (pop_current_level != MIRROR_LEVEL) return;
|
||||
pop_level_set_tile(MIRROR_ROOM, MIRROR_TILEPOS, TILE_MIRROR);
|
||||
/* Комната зеркала обычно НЕ та, где дверь выхода, — игрок войдёт в неё
|
||||
* позже и она отрисуется целиком. Но если она уже на экране, тайл надо
|
||||
* вернуть: зеркало ДОБАВЛЯЕТ графику поверх пустого места, поэтому
|
||||
* хватает обычной пометки (wipe пола + полный draw_tile). */
|
||||
if (cur_room == MIRROR_ROOM)
|
||||
pop_set_redraw(MIRROR_TILEPOS, POP_RD_FLOOR, 2);
|
||||
}
|
||||
|
||||
static void animate_leveldoor(uint8_t *m, int8_t *type, uint8_t cur_room)
|
||||
{
|
||||
if (*type < 0) return;
|
||||
if (*type >= 3) {
|
||||
uint8_t sp;
|
||||
if (*type < 7) (*type)++;
|
||||
sp = leveldoor_close_speeds[*type - 3];
|
||||
if (sp >= *m) { *m = 0; *type = -1; pop_sfx_play(14); } /* seg007:437 */
|
||||
else {
|
||||
*m = (uint8_t)(*m - sp);
|
||||
/* seg007:442: створку НА ЗАКРЫТИИ перебивать можно. */
|
||||
if (*type == 4) { pop_sfx_slide_intr = 1; pop_sfx_play(15); }
|
||||
}
|
||||
} else {
|
||||
/* seg007:464: на ОТКРЫТИИ створка неперебиваема — иначе съезд
|
||||
* обрывается посреди и «конца» не слышно. */
|
||||
if (*m < 43) { (*m)++; pop_sfx_slide_intr = 0; pop_sfx_play(15); }
|
||||
if (*m >= 43) {
|
||||
*m = 43; *type = -1;
|
||||
/* seg007:455: дверь открылась — оригинал глушит всё. Без этого
|
||||
* неперебиваемый съезд (1,6 с) продолжал бы блокировать очередь. */
|
||||
pop_sfx_stop();
|
||||
/* Условие оригинала — переход ИЗ 0 или 2, то есть ровно один раз
|
||||
* за открытие; иначе зеркало ставилось бы заново каждый кадр,
|
||||
* пока дверь стоит открытой. */
|
||||
if (pop_leveldoor_open == 0 || pop_leveldoor_open == 2) {
|
||||
pop_leveldoor_open = 1;
|
||||
place_mirror(cur_room);
|
||||
}
|
||||
pop_leveldoor_open = 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};
|
||||
|
||||
/* play_door_sound_if_visible (seg007:1239): СЛЫШНА ли решётка.
|
||||
*
|
||||
* Правило не «решётка в текущей комнате», как можно подумать, а такое:
|
||||
* - решётка в комнате СЛЕВА и стоит в колонке 9 — слышна (это тайл вплотную
|
||||
* к левому краю экрана, игрок фактически видит её створку в шве);
|
||||
* - решётка в ОТРИСОВАННОЙ комнате и НЕ в колонке 9 — слышна (колонка 9
|
||||
* уезжает за правый край);
|
||||
* - особый случай оригинала: уровень 3, комната 2 — слышна всегда.
|
||||
* Всё остальное молчит, иначе на 13-м уровне скрипели бы решётки со всей
|
||||
* карты.
|
||||
*
|
||||
* Берём вариант ПОД FIX_GATE_SOUNDS (условия через ИЛИ, а не тернарником):
|
||||
* в SDLPoP этот фикс включён по умолчанию, а сравниваемся мы именно с ним.
|
||||
* Отличие только в вырожденном случае room_L == cur_room. */
|
||||
static uint8_t door_audible(uint8_t room, uint8_t tp,
|
||||
uint8_t room_L, uint8_t cur_room)
|
||||
{
|
||||
uint8_t col = TP_COL[tp];
|
||||
if (pop_current_level == 3 && room == 2) return 1;
|
||||
if (room == room_L && col == 9) return 1;
|
||||
if (room == cur_room && col != 9) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void animate_door(uint8_t *m, int8_t *type, uint8_t audible)
|
||||
{
|
||||
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; pop_sfx_play(6); }
|
||||
else mod = (uint8_t)new_mod;
|
||||
}
|
||||
} else if (mod != 0xFF) {
|
||||
mod = (uint8_t)(mod + door_delta[anim_type]);
|
||||
if (anim_type == 0) { /* закрытие */
|
||||
/* seg007:376: скрип закрывающейся решётки — раз в четыре шага
|
||||
* модификатора; лязг «встала» (7) — в конце. Оба через
|
||||
* гейт слышимости (gate_stop, seg007:05E3, тоже через него). */
|
||||
if (mod != 0) {
|
||||
if (mod < 188 && (mod & 3) == 3 && audible) pop_sfx_play(4);
|
||||
} else {
|
||||
*type = -1;
|
||||
if (audible) pop_sfx_play(7); /* gate_stop */
|
||||
}
|
||||
} else { /* открытие */
|
||||
if (mod >= 188) {
|
||||
/* Лязг после ОБЫЧНОГО открытия оригинал даёт без гейта
|
||||
* (seg007:395 — прямой play_sound), а после открытия
|
||||
* насовсем — через gate_stop, то есть с гейтом. */
|
||||
if (anim_type < 2) { mod = 238; *type = 0; pop_sfx_play(7); }
|
||||
else {
|
||||
mod = 0xFF; *type = -1;
|
||||
if (audible) pop_sfx_play(7);
|
||||
}
|
||||
} else if ((mod & 7) == 0) pop_sfx_play(5); /* seg007:386 */
|
||||
}
|
||||
} else {
|
||||
*type = -1;
|
||||
if (audible) pop_sfx_play(7); /* gate_stop, seg007:404 */
|
||||
}
|
||||
}
|
||||
*m = mod;
|
||||
}
|
||||
|
||||
uint8_t pop_trob_room_active(uint8_t room) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < trobs_count; i++)
|
||||
if (trobs[i].room == room) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Темп анимации пламени в ЛОГИЧЕСКИХ кадрах (один логический кадр = 3
|
||||
* vsync и соответствует игровому тику оригинала):
|
||||
* 1 — кадр пламени каждый тик, как в оригинале; кадры при этом ровные по
|
||||
* тактам, что нужно для счёта бюджета;
|
||||
* 2 — вдвое реже (так было до 2026-07-30; пламя шло на половинной
|
||||
* скорости, а бюджет кадра «прыгал» на 6.2 % от того, попал кадр на
|
||||
* анимацию или нет). */
|
||||
#define TORCH_ANIM_DIV 1
|
||||
|
||||
static uint8_t torch_tick;
|
||||
#if TORCH_ANIM_DIV > 1
|
||||
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
|
||||
#else
|
||||
#define TORCH_ANIM_NOW 1
|
||||
#endif
|
||||
|
||||
void pop_trob_qsave(pop_qs_io_t *io) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
pop_qs_bytes(io, room_modif, sizeof(room_modif));
|
||||
pop_qs_bytes(io, room_seen, sizeof(room_seen));
|
||||
pop_qs_u8(io, &trobs_count);
|
||||
for (i = 0; i < TROBS_MAX; i++) {
|
||||
pop_qs_u8(io, &trobs[i].room);
|
||||
pop_qs_u8(io, &trobs[i].tilepos);
|
||||
pop_qs_i8(io, &trobs[i].type);
|
||||
}
|
||||
pop_qs_u16(io, &trob_seed.lo);
|
||||
pop_qs_u16(io, &trob_seed.hi);
|
||||
pop_qs_u8(io, &torch_tick);
|
||||
if (io->reading && io->apply) {
|
||||
if (trobs_count > TROBS_MAX) { io->failed = 1; return; }
|
||||
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
|
||||
pop_redraw_reset();
|
||||
}
|
||||
}
|
||||
|
||||
void pop_process_trobs(uint8_t cur_room) __banked
|
||||
{
|
||||
uint8_t i, w = 0;
|
||||
#if TORCH_ANIM_DIV > 1
|
||||
torch_tick++;
|
||||
#endif
|
||||
|
||||
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
|
||||
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
|
||||
* выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее
|
||||
* подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет
|
||||
* активных пик/кнопок (тот же случай, что был в pop_loose_tick). */
|
||||
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++) {
|
||||
uint8_t tp = trobs[k].tilepos;
|
||||
uint8_t c = pop_level_tile_raw(trobs[k].room, tp);
|
||||
trob_code[k] = c;
|
||||
trob_rcode[k] = 0;
|
||||
if ((c == TILE_TORCH || c == TILE_TORCH_DEBRIS) && TP_COL[tp] < 9)
|
||||
trob_rcode[k] = (uint8_t)(pop_level_tile_raw(trobs[k].room,
|
||||
(uint8_t)(tp + 1)) & 0x1F);
|
||||
}
|
||||
pop_level_access_end();
|
||||
}
|
||||
|
||||
pop_dbg_m8(); /* ЗАМЕР: префетч кодов тайлов сделан */
|
||||
|
||||
/* Указатель на модификаторы КОМНАТЫ кэшируется между итерациями.
|
||||
*
|
||||
* pop_trob_modif объявлен __banked, и вызов его на КАЖДЫЙ trob — это
|
||||
* трамплин из банка 6 в банк 6 же... нет: из горячего цикла в банковую
|
||||
* функцию, то есть полный переход через резидент. А комната у trob'ов
|
||||
* в подавляющем большинстве кадров ОДНА (все они из cur_room, чужие
|
||||
* появляются лишь у брошенных плит соседней комнаты). Тот же паттерн
|
||||
* «трамплин в цикле», что дал −23 784 на луче видимости стража
|
||||
* (P2b) и −14 118 на guard_over_kid (P16).
|
||||
*
|
||||
* Сбрасывается на каждом кадре: room_seen внутри pop_trob_modif живёт
|
||||
* дольше, но указатель на строку room_modif может смениться при
|
||||
* перезагрузке комнаты, поэтому кэш локальный для одного прохода. */
|
||||
{
|
||||
uint8_t mod_room = 0; /* 0 = «ещё не брали» (комнаты 1..24) */
|
||||
uint8_t *mod = 0;
|
||||
/* Сосед СЛЕВА нужен гейту слышимости решёток (door_audible). Берём один
|
||||
* раз на проход: pop_room_link маппит страницу уровня, а решёток в
|
||||
* списке может быть несколько. */
|
||||
uint8_t room_L = pop_room_link(cur_room, 0 /* L */);
|
||||
|
||||
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];
|
||||
int8_t type = trobs[i].type;
|
||||
|
||||
if (room != mod_room) { mod = pop_trob_modif(room); mod_room = room; }
|
||||
|
||||
switch (code) {
|
||||
case TILE_SPIKE:
|
||||
animate_spike(&mod[tp], &type);
|
||||
break;
|
||||
case TILE_CHOMP:
|
||||
animate_chomper(&mod[tp], &type, room, tp, cur_room);
|
||||
break;
|
||||
case TILE_OPENER:
|
||||
case TILE_CLOSER:
|
||||
animate_button(mod[tp], &type); /* mod[tp] = индекс LINKLOC кнопки */
|
||||
break;
|
||||
case TILE_GATE:
|
||||
animate_door(&mod[tp], &type, door_audible(room, tp, room_L, cur_room));
|
||||
break;
|
||||
case TILE_LOOSE:
|
||||
animate_loose(&mod[tp], &type, room, tp, cur_room);
|
||||
break;
|
||||
case TILE_LEVELDOOR:
|
||||
animate_leveldoor(&mod[tp], &type, cur_room);
|
||||
break;
|
||||
case TILE_TORCH:
|
||||
case TILE_TORCH_DEBRIS:
|
||||
/* animate_torch (seg007:03C1): анимация вечная — type остаётся
|
||||
* 1, пока комната отрисована. Темп — TORCH_ANIM_DIV. */
|
||||
if (room == cur_room && TORCH_ANIM_NOW)
|
||||
mod[tp] = get_torch_frame(mod[tp]);
|
||||
break;
|
||||
case TILE_POTION:
|
||||
/* animate_potion (seg007:03E9): крутим младшие 3 бита (фаза
|
||||
* пузырька), старшие 5 — тип зелья. */
|
||||
if (room == cur_room)
|
||||
mod[tp] = (uint8_t)((mod[tp] & 0xF8) | bubble_next_frame(mod[tp] & 7));
|
||||
break;
|
||||
case TILE_SWORD:
|
||||
/* animate_sword (seg007:0425): счётчик вниз; на 0 — новый период
|
||||
* 0x28..0x67. Блеск рисуется РОВНО на modif==1 (draw_tile берёт
|
||||
* кадр (modif==1)+10), т.е. одиночная вспышка раз в 40..103 тика. */
|
||||
if (room == cur_room) {
|
||||
if (--mod[tp] == 0)
|
||||
mod[tp] = (uint8_t)((trob_prandom(255) & 0x3F) + 0x28);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
type = -1;
|
||||
break;
|
||||
}
|
||||
trobs[i].type = type;
|
||||
|
||||
if (room == cur_room &&
|
||||
(code == TILE_TORCH || code == TILE_TORCH_DEBRIS || code == TILE_POTION)) {
|
||||
/* Пламя ЗАПЕКАЕТСЯ в фон (GFX_BANK_NORMAL внутри pop_torch_draw),
|
||||
* пузырёк зелья — нет, ему нужен heal. Порядок «пламя раньше
|
||||
* перерисовок тайлов» обязателен: heal чомпера-соседа вернёт
|
||||
* запечённый огонь из ОЗУ-копии, а его собственная графика ляжет
|
||||
* ПОВЕРХ — ровно как в оригинале, где draw_tile_anim_right (пламя
|
||||
* левого соседа) идёт до draw_tile_anim (сам чомпер). Выносить
|
||||
* отрисовку ПОСЛЕ pop_redraw_needed нельзя: пламя окажется поверх
|
||||
* челюстей. */
|
||||
/* Таблицей, а не tp/10 и tp%10: у Z80 деления нет, SDCC зовёт
|
||||
* __divuchar + __moduchar — на два факела это четыре вызова за
|
||||
* кадр (замер MAME). Тайл всегда 0..29. */
|
||||
uint8_t trow = TP_ROW[tp], tcol = TP_COL[tp];
|
||||
if (code == TILE_POTION) {
|
||||
pop_potion_draw(trow, tcol, mod[tp]);
|
||||
} else {
|
||||
pop_torch_draw(trow, tcol, mod[tp]);
|
||||
/* Порт set_redraw_anim_right (seg007:0101): animate_torch метит
|
||||
* ПРАВОГО СОСЕДА, и redraw_needed рисует его слой anim в
|
||||
* порядке draw_tile_anim_topright -> draw_tile_anim_right
|
||||
* (пламя) -> draw_tile_anim (СВОЯ графика тайла). То есть в
|
||||
* оригинале челюсти чомпера возвращаются поверх огня КАЖДЫЙ
|
||||
* кадр факела, а не только пока жива их собственная анимация.
|
||||
*
|
||||
* У нас пламя запекается напрямую (pop_torch_draw), минуя
|
||||
* механизм пометок, поэтому у ЗАСТЫВШЕГО чомпера возвращать
|
||||
* его было нечем — огонь накрывал челюсти (баг найден на
|
||||
* уровне 4). Метим сосед-тайл сами, ОДНУ страницу: факел
|
||||
* анимируется каждый кадр, значит обе страницы получат свою
|
||||
* перерисовку по очереди. Порядок сходится: этот блок идёт
|
||||
* ДО pop_redraw_needed.
|
||||
*
|
||||
* РАСХОЖДЕНИЕ С ОРИГИНАЛОМ (осознанное): он метит соседа
|
||||
* безусловно, мы — только когда там чомпер. Безусловная
|
||||
* пометка = перерисовка тайла каждый кадр на каждый факел,
|
||||
* а слой anim рисует ещё пики/зелье/меч (seg008:644). Их
|
||||
* случай «застыл справа от факела» в BUGS_OPEN.md как открытый:
|
||||
* пики и меч рядом с факелом на уровнях 1-4 не встретились. */
|
||||
if (trob_rcode[i] == TILE_CHOMP)
|
||||
pop_set_redraw((uint8_t)(tp + 1), POP_RD_CHOMP_ANIM, 1);
|
||||
}
|
||||
}
|
||||
if (room == cur_room && code == TILE_SPIKE) {
|
||||
/* ПИКИ — редрой КАЖДЫЙ кадр, пока trob жив: animate_spike
|
||||
* (seg007:0353) зовёт redraw_21h БЕЗУСЛОВНО, а не по смене кадра.
|
||||
* Это не «лишняя работа оригинала»: выдвинутая пика живёт только в
|
||||
* видео-ОЗУ (банк SPRITE, в фон не запечена), а heal персонажа каждый кадр
|
||||
* возвращает под спрайтом Кида ПЕЧЁНЫЙ фон — то есть стирает те
|
||||
* части остриёв, что попали под его прямоугольник. При редрое
|
||||
* по смене кадра пика держится 15 кадров (modif 0x8F..0x81) с
|
||||
* неизменной сигнатурой, и стёртое уже не возвращается: рядом с
|
||||
* Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной
|
||||
* странице дабл-буфера из двух). */
|
||||
/* Последний кадр (пика убралась, trob умирает) — пометить ОБЕ
|
||||
* страницы: живой trob метит каждый кадр по одной. */
|
||||
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
|
||||
}
|
||||
if (room == cur_room && code == TILE_CHOMP) {
|
||||
/* ТОЛЬКО ПОКА ФАЗА < 6 — как в оригинале (animate_chomper,
|
||||
* seg007:0448 заканчивается `if ((curr_modifier & 0x7F) < 6)
|
||||
* redraw_at_trob();`).
|
||||
*
|
||||
* Это не оптимизация оригинала, а точное следствие таблицы поз:
|
||||
* CHOMP_FRAM1 = {3,2,0,1,4,3,3}, то есть с фазы 5 и до конца
|
||||
* круга (POP_CHOMPER_SPEED = 15) поза одна и та же — 3.
|
||||
* Перерисовывать её десять кадров подряд значит рисовать ровно
|
||||
* ту же картинку, а стоит это 190 260 тактов НА КАДР: полный
|
||||
* draw_tile плюс heal 32x64, 24 % работы кадра в 11/15
|
||||
* (замер 2026-08-19).
|
||||
*
|
||||
* На фазе 5 метим ОБЕ страницы дабл-буфера: она последняя
|
||||
* рисуемая, и её поза обязана лечь на обе, иначе на второй
|
||||
* останется поза фазы 4 и чомпер будет дрожать через кадр.
|
||||
* Сходится и по кадрам: вторую страницу эта пометка догоняет в
|
||||
* кадре фазы 6, где поза та же самая (CHOMP_FRAM1[6] == 3).
|
||||
*
|
||||
* Про type < 0 отдельной ветки больше нет: trob снимается сам
|
||||
* только при frame >= 6 (см. animate_chomper выше), то есть
|
||||
* когда перерисовки уже не идут, а на экране с фазы 5 лежит
|
||||
* финальная поза на обеих страницах. */
|
||||
uint8_t ph = (uint8_t)(mod[tp] & 0x7F);
|
||||
if (ph < 6)
|
||||
pop_set_redraw(tp, POP_RD_CHOMP, (uint8_t)(ph == 5 ? 2 : 1));
|
||||
}
|
||||
if (room == cur_room && code == TILE_GATE) {
|
||||
/* draw_trob (seg007:01E6), которым заканчивается animate_door:
|
||||
* пока trob жив — решётка едет каждый кадр, помечаем ТЕКУЩУЮ
|
||||
* страницу; последний кадр (type<0, ворота встали) — ОБЕ, иначе
|
||||
* на второй странице дабл-буфера застынет предыдущее положение
|
||||
* и решётка дрожит через кадр. Тайлпос — САМИ ворота, соседа
|
||||
* справа выбирает pop_gate_redraw. */
|
||||
pop_set_redraw(tp, POP_RD_GATE, (uint8_t)(type < 0 ? 2 : 1));
|
||||
}
|
||||
if (room == cur_room && code == TILE_LEVELDOOR) {
|
||||
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
|
||||
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
|
||||
* heal персонажа вернул бы из фона закрытую створку. Куски двери
|
||||
* непрозрачны, поэтому wipe не нужен. */
|
||||
/* Створку рисует ПРАВАЯ половина двери; последний кадр анимации
|
||||
* помечаем на обе страницы, иначе дверь дрожит через кадр. */
|
||||
if (TP_COL[tp] < 9)
|
||||
pop_set_redraw((uint8_t)(tp + 1), POP_RD_LEVELDOOR,
|
||||
(uint8_t)(type < 0 ? 2 : 1));
|
||||
}
|
||||
if (room == cur_room && code == TILE_SWORD) {
|
||||
/* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1):
|
||||
* спрайт запечён в фон (draw_tile), поэтому перерисовываем тайл
|
||||
* той же схемой, что и кнопку — текущая страница сразу, вторая
|
||||
* следующим кадром через rest-цикл. */
|
||||
uint8_t sig = (uint8_t)(mod[tp] == 1);
|
||||
if (sig != trob_drawn[tp]) {
|
||||
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
|
||||
trob_drawn[tp] = sig;
|
||||
}
|
||||
}
|
||||
if (room == cur_room && (code == TILE_OPENER || code == TILE_CLOSER)) {
|
||||
/* КНОПКА — редрой только при смене видимого состояния (нажата =
|
||||
* таймер связи >1): пока Кид её держит, картинка стабильна.
|
||||
* Рисуем ТЕКУЩУЮ страницу сразу + <rest>=1 (вторая страница
|
||||
* дабл-буфера следующим кадром через rest-цикл). tp/10,tp%10
|
||||
* считаем только в момент редроя (редко), а не каждый кадр. */
|
||||
uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
|
||||
if (sig != trob_drawn[tp]) {
|
||||
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
|
||||
trob_drawn[tp] = sig;
|
||||
}
|
||||
}
|
||||
}
|
||||
} /* конец блока с кэшем mod/mod_room */
|
||||
|
||||
/* compact: удалить завершённые (type < 0). */
|
||||
for (i = 0; i < trobs_count; i++)
|
||||
if (trobs[i].type >= 0) trobs[w++] = trobs[i];
|
||||
trobs_count = w;
|
||||
}
|
||||
Reference in New Issue
Block a user