Files
Sprinter-SDCC/applications/PoP/roomtest/pop_trob.c
T
snark13 c127a4b0d2 PoP: факелы и зелья (chtab_1) + быстрый блит фона gfx_blit_noclip
Графика chtab_1 (flame/sword/potion) — новый атлас pop_pot.atl:
- склянка зелья берётся из chtab_1 (seg008 draw_tile_fore: id 12 малая /
  13 большая при типах 2..4), а НЕ chtab_6 id 12 — res212 в VDUNGEON нет,
  каскад падал на VPALACE и рисовал кусок дворцовой декорации («мусорный
  объект» в комнате 5);
- пакуем chtab_1 целиком (id 1..23): пламя, склянки, кадры пузырька;
- MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из
  ОБЩЕЙ 16-цветной палитры экрана — она добавлена в 0x30..0x3F, палитра
  chtab_1 в 0x40..0x4F; пузырёк пакуется силуэтом цвета 12 (красный),
  его маска — цвета 0;
- в env-атлас добавлено основание факела (env 146, seg008:489 — рисует
  правый сосед); в статический render_room оно не попадало.

Анимация (seg007 animate_torch/animate_potion + seg000:0B12
anim_tile_modif): при входе в комнату факелам и зельям задаётся случайная
стартовая фаза и заводится trob; факел — get_torch_frame, пламя в ячейке
ПРАВОГО соседа (xh = draw_xh+1, y = draw_main_y−40); зелье —
bubble_next_frame по младшим 3 битам модификатора (старшие 5 = тип, при
загрузке уровня modif <<= 3, seg009).  Пламя и пузырёк не запекаются в
фон: heal своей области + кадр поверх.  Скорость пламени /2 — наш
логический кадр короче игрового тика оригинала.

Скорость отрисовки (профиль в MAME, кадр = 430 080 тактов):
- замер показал, что цена блита почти НЕ зависит от размера — 13 288
  тактов на спрайт 32×3 против 4 617 у линейного спрайтового ядра без
  клипа; платим за проход gfx_blit → gfx_blit_part → _gfx_blit_full;
- в libbgi добавлен gfx_blit_noclip() — блит без клипа в ТЕКУЩЕМ банке
  (putsprite не годится: навязывает GFX_BANK_SPRITE, фону нужен
  TRANSPARENT ради ОЗУ-копии); pop_bg.blit_b уходит на него, когда
  спрайт целиком на экране и не нужен g_clip_top → ~2.9× на блит;
- W3-скобку ставит САМА libbgi: из модуля с --w3 её вызывать нельзя —
  после _bgi_begin окно W3 занято видеобанком и код вызывающего исчезает
  (проявлялось белым экраном);
- редрой шва больше не перерисовывает полосу потолка (bar начинается с
  POP_YOFF+3) — это удваивало стоимость блока при анимации решётки;
- docs/size_optimization_plan.md §7–§8: замеры, сделанное и запас
  (батчинг W3-скобки, решётка одним спрайтом, лишние блиты).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:53:04 +03:00

413 lines
18 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_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];
/* 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; }
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; }
}
/* 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). */
{
uint8_t row = 0, col = 0;
for (i = 0; i < POP_ROOMTILES; i++) {
if (spike_rest[i]) {
pop_spike_redraw(row, col);
spike_rest[i]--;
}
if (button_rest[i]) {
pop_button_redraw(row, col);
button_rest[i]--;
}
if (++col == 10) { col = 0; row++; }
}
}
if (trobs_count == 0) return;
/* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не
* pop_level_tile() на каждый trob (тот свапал W0 каждый вызов = N свапов
* за кадр даже без анимации). Дальше цикл (с редроями) идёт с
* размапленным W0 — как раньше. */
{
uint8_t k;
pop_level_access_begin();
for (k = 0; k < trobs_count; k++)
trob_code[k] = pop_level_tile_raw(trobs[k].room, trobs[k].tilepos);
pop_level_access_end();
}
for (i = 0; i < trobs_count; i++) {
uint8_t room = trobs[i].room;
uint8_t tp = trobs[i].tilepos;
uint8_t code = trob_code[i];
uint8_t *mod = pop_trob_modif(room);
int8_t type = trobs[i].type;
switch (code) {
case TILE_SPIKE:
animate_spike(&mod[tp], &type);
break;
case TILE_OPENER:
case TILE_CLOSER:
animate_button(mod[tp], &type); /* mod[tp] = индекс LINKLOC кнопки */
break;
case TILE_GATE:
animate_door(&mod[tp], &type);
break;
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 || code == TILE_OPENER || code == TILE_CLOSER)) {
/* Редрой ТОЛЬКО при изменении видимого состояния (redraw_11h из
* SDLPoP). spike: draw-сигнатура = modifier; button: = «нажата»
* (таймер связи >1). Пока Кид держит кнопку — сигнатура стабильна
* → редроя нет. При изменении рисуем ТЕКУЩУЮ страницу сразу +
* <rest>=1 (вторая страница дабл-буфера следующим кадром через
* rest-цикл). tp/10,tp%10 считаем только в момент редроя (редко),
* а не каждый кадр. */
/* Сигнатура = ВИДИМЫЙ кадр, а не сырой modifier. Для пики кадр =
* (mod&0x80)?5:mod (как в draw_tile): весь ретракт-hold 0x80..0x8F
* (16 значений modifier) рисуется одним кадром 5 → сырой modifier
* дал бы 16 ложных редроёв «выдвинутой» неподвижной пики. */
uint8_t sig = (code == TILE_SPIKE)
? (uint8_t)((mod[tp] & 0x80) ? 5 : mod[tp])
: (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
if (sig != trob_drawn[tp]) {
uint8_t trow = tp / 10, tcol = tp % 10;
if (code == TILE_SPIKE) { pop_spike_redraw(trow, tcol); spike_rest[tp] = 1; }
else { pop_button_redraw(trow, tcol); button_rest[tp] = 1; }
trob_drawn[tp] = sig;
}
}
}
/* compact: удалить завершённые (type < 0). */
for (i = 0; i < trobs_count; i++)
if (trobs[i].type >= 0) trobs[w++] = trobs[i];
trobs_count = w;
}