Files
Sprinter-SDCC/applications/PoP/roomtest/pop_trob.c
T
snark13 5a42b2d245 P6a: указатель модификаторов комнаты кэшируется между trob'ами
pop_trob_modif объявлен __banked, а звался он на КАЖДЫЙ trob внутри
цикла pop_process_trobs — при том что комната у них в подавляющем
большинстве кадров одна (чужие появляются только у брошенных плит
соседней комнаты).  Тот же паттерн «трамплин в цикле», что дал -23 784 на
луче видимости (P2b) и -14 118 на guard_over_kid (P16).

Замер 11/15: цикл trobs 78 726 -> 74 964, работа кадра 438 324 -> 437 484.

ОЖИДАНИЕ НЕ ОПРАВДАЛОСЬ: в реестре стояло -20 000 на пару P6a/P6b, а
вышло -840.  Причина простая — trob'ов в комнате всего несколько, и кэш
экономит два-три вызова, а не двадцать.  Оценка была построена на
аналогии с лучом видимости, где вызовов было девять на КАЖДЫЙ кадр.

Проверено в MAME на чистом запуске: факелы, чомпер и страж рисуются
правильно, хост-тесты зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:17:38 +03:00

847 lines
49 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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_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 ((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) __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). */
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; }
else *m = (uint8_t)(*m - sp);
} else {
if (*m < 43) (*m)++;
if (*m >= 43) {
*m = 43; *type = -1;
/* Условие оригинала — переход ИЗ 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};
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) __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
#if TORCH_ANIM_DIV > 1
static uint8_t torch_tick;
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
#else
#define TORCH_ANIM_NOW 1
#endif
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;
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);
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;
}