/* * 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 #define TILE_POTION 0x0A #define TILE_TORCH 0x13 #define TILE_TORCH_DEBRIS 0x1E /* ---- Персистентное 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]; /* Сколько тайлов ждут отложенного редроя (spike_rest/button_rest). Без * него цикл по 30 тайлам крутился КАЖДЫЙ кадр впустую: замер в MAME * (2026-07-27) — служебная часть pop_process_trobs ~50 000 тактов при * кадре 430 080, то есть ~12 % кадра на пустой проход. */ static uint8_t rest_pending; /* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и * выбора следующего кадра пламени. */ static unsigned long trob_seed; static uint16_t trob_prandom(uint16_t maxv) { trob_seed = trob_seed * 214013UL + 2531011UL; return (uint16_t)((uint16_t)(trob_seed >> 16) % (uint16_t)(maxv + 1)); } 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; } rest_pending = 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]) { uint8_t i; pop_level_room_bg(room, room_modif[room - 1]); /* seg009 load_level: у ЗЕЛЬЯ модификатор сдвигается влево на 3 — * старшие биты = тип зелья, младшие 3 = фаза пузырька. */ pop_level_access_begin(); for (i = 0; i < POP_ROOMTILES; i++) if ((pop_level_tile_raw(room, i) & 0x1F) == TILE_POTION) room_modif[room - 1][i] = (uint8_t)(room_modif[room - 1][i] << 3); 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) { 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; } rest_pending = 0; } /* 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) { uint8_t *mod = pop_trob_modif(room); uint8_t i, code; if (!mod) return; pop_level_access_begin(); 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); } } 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; } 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; } static uint8_t torch_tick; /* делитель скорости анимации пламени */ void pop_process_trobs(uint8_t cur_room) { uint8_t i, w = 0; torch_tick++; /* Перерисовка «покоя» законченных пик/кнопок (обе страницы). * row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC * выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее * подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет * активных пик/кнопок (тот же случай, что был в pop_loose_tick). */ if (rest_pending) { uint8_t row = 0, col = 0, left = 0; for (i = 0; i < POP_ROOMTILES; i++) { if (spike_rest[i]) { pop_spike_redraw(row, col); if (--spike_rest[i]) left++; } if (button_rest[i]) { pop_button_redraw(row, col); if (--button_rest[i]) left++; } if (++col == 10) { col = 0; row++; } } rest_pending = left; } 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; case TILE_TORCH: case TILE_TORCH_DEBRIS: /* animate_torch (seg007:03C1): кадр пламени; анимация вечная — * type остаётся 1, пока комната отрисована. Смена кадра — через * ЛОГИЧЕСКИЙ кадр (torch_tick): у нас кадр короче игрового тика * оригинала, и пламя «кипело» вдвое быстрее нужного. */ if (room == cur_room && (torch_tick & 1) == 0) 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; default: type = -1; break; } trobs[i].type = type; if (room == cur_room && (code == TILE_TORCH || code == TILE_TORCH_DEBRIS || code == TILE_POTION)) { /* Пламя/пузырёк анимируются КАЖДЫЙ кадр и в фон не запечены — * рисуем поверх (сами делают heal своей области). */ uint8_t trow = tp / 10, tcol = tp % 10; if (code == TILE_POTION) pop_potion_draw(trow, tcol, mod[tp]); else pop_torch_draw(trow, tcol, mod[tp]); } if (room == cur_room && code == TILE_SPIKE) { /* ПИКИ — редрой КАЖДЫЙ кадр, пока trob жив: animate_spike * (seg007:0353) зовёт redraw_21h БЕЗУСЛОВНО, а не по смене кадра. * Это не «лишняя работа оригинала»: выдвинутая пика живёт только в * видео-ОЗУ (банк SPRITE, в фон не запечена), а kid_heal каждый кадр * возвращает под спрайтом Кида ПЕЧЁНЫЙ фон — то есть стирает те * части остриёв, что попали под его прямоугольник. При редрое * по смене кадра пика держится 15 кадров (modif 0x8F..0x81) с * неизменной сигнатурой, и стёртое уже не возвращается: рядом с * Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной * странице дабл-буфера из двух). */ uint8_t trow = tp / 10, tcol = tp % 10; pop_spike_redraw(trow, tcol); /* Последний кадр (пика убралась, trob умирает) — на ВТОРУЮ страницу * его уже никто не положит: ставим отложенный редрой. */ if (type < 0) { spike_rest[tp] = 1; rest_pending++; } } if (room == cur_room && (code == TILE_OPENER || code == TILE_CLOSER)) { /* КНОПКА — редрой только при смене видимого состояния (нажата = * таймер связи >1): пока Кид её держит, картинка стабильна. * Рисуем ТЕКУЩУЮ страницу сразу + =1 (вторая страница * дабл-буфера следующим кадром через rest-цикл). tp/10,tp%10 * считаем только в момент редроя (редко), а не каждый кадр. */ uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1); if (sig != trob_drawn[tp]) { uint8_t trow = tp / 10, tcol = tp % 10; pop_button_redraw(trow, tcol); button_rest[tp] = 1; rest_pending++; 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; }