Files
Sprinter-SDCC/applications/PoP/roomtest/pop_gdraw.c
T
snark13 6673279cef PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат
Скелет (L3-SKEL, ассеты + механика):
- pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета
  poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3
  curr_guard_color = 0, оригинал палитру не подменяет);
- pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ
  УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом
  стража и был невидим;
- load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень —
  только в кадрах 150..189.  Пока ветка была одна (charid_2_guard), скелет
  получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался;
- check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в
  комнате 3 при падении (seg002:252), autocontrol_skeleton;
- leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня.

Цвета стражей (BUG-GUARD-COLOR-1, закрыт):
- все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по
  guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257).
  Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP
  меняется вместе со стражем.  Грабля: gfx_pal_load отдаёт указатель в BIOS,
  а тот читает только #4000-#BFFF — таблицу из банка копируем в стек.

Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт):
- pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19.
  Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж
  после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать
  обратно.  +48 байт W2.

Окклюзия соперника:
- pop_fore_over_char получил проход other_overlay_tile (порядок midtable,
  seg008:1B06) и расширение перебора объединённым прямоугольником
  «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх
  кладки и верхней грани пола;
- клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка
  (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP;
- ROOMNAV после смерти Кида делает честный pop_start_level: телепорт
  «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с
  вернувшейся кучей костей.

Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком
(101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) —
render_room анимированные тайлы пропускает, и в атласе не было ни одного.
Число EMM-страниц не изменилось.

Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок
(резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен
гард «код наехал на данные» (DATA_LOC).

Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с
разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех
Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3:
level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT).

В roomtest.c временно оставлен автостоп на падении соперника (отладка
падений скелета) — помечен ВРЕМЕННО.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:11:03 +03:00

372 lines
21 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_gdraw.c — ОТРИСОВКА стража. Резидент окна W3 (--w3 pop_gdraw.c).
*
* Почему отдельным файлом, а не внутри pop_guard.c: --w3 забирает МОДУЛЬ
* целиком (код+rodata в 0xC000), а состояние стража и его логика обязаны
* остаться в W1/W2 — из банка (guards.c) резидент W3 недостижим ни прямо,
* ни транзитивно (docs/layout_plan_v2.md §2 R2). Значит граница проходит
* ровно между «что рисует» (зовёт только главный цикл → резидент) и «что
* знает» (зовёт и банк → W1/W2).
*
* Писучие статики резидента (--dataseg не трогаем) остаются в обычном
* _DATA, то есть в W2 — сюда переезжает КОД, не данные.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_guard.h"
#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */
#include "pop_level.h" /* тип соперника уровня (набор спрайтов) */
#include "kid_data.h"
#include "pop_guard_pal.h" /* таблица палитр стражей (сгенерена упаковщиком) */
/* Цвет стража (set_chtab_palette, seg003:257). Оригинал держит ОДИН набор
* спрайтов и подменяет 16 цветов палитры chtab_5 по level.guards_color —
* так на уровне 2 стражи комнат 4/7/11/15/24 разного цвета (1/3/1/1/6).
* Полоса HP стража рисуется тем же атласом, поэтому меняется заодно.
* color = 0 — палитру не трогаем (оригинал в этом случае set_chtab_palette
* не зовёт: тип уровня не «обычный страж»).
* Грузим В ОБЕ палитры: у страниц дабл-буфера они раздельные. */
void pop_guard_set_palette(uint8_t color) __banked
{
/* Таблица — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а
* gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS
* читает только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем
* запись в локальный буфер — стек гарантированно в W2. Без копии BIOS
* забирал мусор с чужой страницы и страж становился невидимым. */
uint8_t buf[64];
const uint8_t *p;
uint8_t i;
if (color == 0 || color > POP_GUARD_NPAL) return;
p = pop_guard_pal[color - 1];
for (i = 0; i < 64; i++) buf[i] = p[i];
gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
}
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
#define GUARD_PAGES 5
static atlas_t gp[GUARD_PAGES];
static uint8_t g_ok;
/* Прямоугольник последней отрисовки ПО СТРАНИЦЕ дабл-буфера (как у Kid:
* heal обязан стирать спрайт именно той страницы, в которую рисуем). */
static int gx_l[2], gy_l[2];
static uint16_t gw_l[2], gh_l[2];
static uint8_t gvalid[2];
/* Клинок — ОТДЕЛЬНЫМ прямоугольником, не объединением со стражем: объединение
* почти вдвое больше суммы двух (клинок уходит вперёд-вверх), а heal стоит
* ровно по площади. Объединение считаем только для окна fore-клипа. */
static int sx_l[2], sy_l[2];
static uint16_t sw_l[2], sh_l[2];
static uint8_t svalid[2];
/* Брызги удара — тоже свой прямоугольник, по той же причине: спрайт мелкий и
* уезжает вперёд-вверх от стража. Живут они ОДИН кадр, но стереть их обязана
* та же страница, в которую рисовали (иначе остаток виден через кадр). */
static int qx_l[2], qy_l[2];
static uint16_t qw_l[2], qh_l[2];
static uint8_t qvalid[2];
/* Ширина спрайта последнего кадра стража — нужна set_char_collision
* (char_width_half) при вычислении его ТАЙЛА ОБЪЕКТА. Не по страницам:
* это свойство кадра, а не буфера. Отставание на кадр допустимо — так же
* работает kid_fp_width() у Кида. */
static uint16_t g_last_w;
uint16_t pop_guard_fp_width(void) __banked { return g_last_w; }
/* Набор спрайтов соперника выбирает ТИП УРОВНЯ (load_lev_spr, seg000:1092):
* tbl_guard_type = {0 guard, 1 fat, 2 skeleton, 3 vizier, 4 shadow, 1 нет}.
* У нас пока два набора: обычный страж (уровни 1-2) и скелет (уровень 3);
* у скелета и кадров меньше (28 против 34), поэтому пятую страницу он не
* грузит. Уровень БЕЗ соперников (type < 0) не грузит ничего. */
int pop_guard_load(void) __banked
{
static const char *const guard_names[GUARD_PAGES] = {
"GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl",
"GUARD\\g3.atl", "GUARD\\g4.atl"
};
static const char *const skel_names[GUARD_PAGES] = {
"SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl",
"SKEL\\g3.atl", 0
};
const char *const *names;
int8_t type = pop_level_guard_type(pop_current_level);
uint8_t i;
if (type < 0) return 0; /* соперников на уровне нет */
names = (type == 2) ? skel_names : guard_names;
for (i = 0; i < GUARD_PAGES; i++) {
if (names[i] == 0) break;
if (atlas_load(&gp[i], names[i]) != 0) return -1;
}
g_ok = 1;
/* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется
* из res10.bin по guards_color — это делает pop_guard_set_palette при
* входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */
if (type == 2) {
uint8_t buf[64], k;
for (k = 0; k < 64; k++) buf[k] = pop_skel_pal[k]; /* BIOS: буфер в W2 */
gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
}
return 0;
}
void pop_guard_free(void) __banked
{
uint8_t i;
if (!g_ok) return;
for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]);
g_ok = 0;
}
/* Стирание прошлого кадра стража и его клинка. pop_heal_fast — по той же
* причине, что в kid_heal: стираем ровно то, что рисовали, а рисовали в
* большинстве кадров noclip-ядром. Два прямоугольника, а не объединение
* (см. комментарий к sx_l выше). */
void pop_guard_heal(void) __banked
{
uint8_t p = gfx_get_draw_page() & 1;
if (gvalid[p]) pop_heal_fast(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
if (svalid[p]) pop_heal_fast(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]);
if (qvalid[p]) pop_heal_fast(qx_l[p], qy_l[p] + POP_YOFF, qw_l[p], qh_l[p]);
gvalid[p] = svalid[p] = qvalid[p] = 0;
}
void pop_guard_draw(void) __banked
{
int obj_x, obj_y, top, fwd, fp_x;
uint8_t page, idx, p, flip;
const uint8_t *img;
uint16_t w, h;
/* Окно fore-клипа — по ОБЪЕДИНЁННОМУ габариту «страж + клинок»; сам
* футпринт (pop_fore_over_char) считается по спрайту стража, поэтому
* w/h ниже НЕ трогаем. */
int clip_x = 0, clip_y = 0; uint16_t clip_w = 0, clip_h = 0;
if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return;
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
fwd = (int)Guard.x + ((Guard.direction < 0) ? -pop_gframe.dx : pop_gframe.dx);
obj_x = (fwd << 1) - 116;
obj_y = (int)Guard.y + pop_gframe.dy;
if ((int8_t)(pop_gframe.flags ^ Guard.direction) >= 0) obj_x++;
fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — футпринт */
obj_x = obj_x * 8 / 7; /* calc_screen_x_coord */
page = (uint8_t)(pop_gframe.image >> 3);
idx = (uint8_t)(pop_gframe.image & 7);
if (page >= GUARD_PAGES) return;
img = (const uint8_t *)atlas_image(&gp[page], idx);
gfx_w0_map(gp[page].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
int bx = obj_x;
/* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507:
* obj_clip_top = 0, obj_clip_bottom = 192). Персонаж, вышедший за
* него (скелет, падающий в пропасть), обязан обрезаться: ниже поля
* идёт полоса HP, и без клипа спрайт лез прямо на неё. */
int skip = 0, rows;
top = obj_y - (int)h + 1;
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
if (flip) bx -= (int)w;
rows = (int)h;
if (top < 0) { skip = -top; rows -= skip; top = 0; }
if (top + rows > 192) rows = 192 - top;
if (rows > 0) {
p = gfx_get_draw_page() & 1;
gfx_set_bank(GFX_BANK_SPRITE);
if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip,
(uint8_t)skip, (uint8_t)rows);
else
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, skip, rows);
gfx_set_bank(GFX_BANK_NORMAL);
/* heal — ровно по НАРИСОВАННОМУ (обрезанному) прямоугольнику. */
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = (uint16_t)rows;
gvalid[p] = 1;
clip_x = bx; clip_y = top; clip_w = w; clip_h = (uint16_t)rows;
}
g_last_w = w; /* для char_width_half, см. выше */
}
/* Брызги удара (draw_hurt_splash, seg006:16CE) — ровно здесь, между
* стражем и клинком: draw_guard кладёт их в objtable СРАЗУ после стража
* и ДО клинка (seg008:22C9), а порядок записей задаёт порядок рисования.
*
* Спрайт — chtab_5_guard `obj_id = 1`, то есть image 1 нашего атласа
* стража (страница 0, индекс 1): у add_midtable аргумент `obj_id + 1`, а
* get_image вычитает единицу обратно (тот же off-by-one, что расписан в
* шапке pop_pack_guard.py). У Кида это image 218 своего chtab.
*
* Смещения от кадра персонажа — по трём веткам оригинала; у стража
* подъём на 11 (у Кида 15: `((charid == kid) << 2) + 11`). */
if (pop_guard_hurt && Guard.frame != 178 /* chomped: брызг нет */) {
int qx = fp_x, qy = obj_y;
const uint8_t *qimg = (const uint8_t *)atlas_image(&gp[0], 1);
uint16_t qw, qh;
int fw = (Guard.direction < 0) ? -5 : 5; /* obj_dx_forward(5) */
if (Guard.frame == 185 || /* мёртв */
(Guard.frame >= 106 && Guard.frame < 111)) { /* падение+посадка */
qy += 4;
} else if (Guard.frame == 177) { /* напоролся на пики */
fw = -fw; /* obj_dx_forward(-5) */
} else {
qy -= 11;
}
qx = (qx + fw) * 8 / 7; /* calc_screen_x_coord */
gfx_w0_map(gp[0].page);
qw = (uint16_t)(qimg[0] | ((uint16_t)qimg[1] << 8));
qh = (uint16_t)(qimg[2] | ((uint16_t)qimg[3] << 8));
if (qw && qh) {
int qtop = qy - (int)qh + 1;
uint8_t qflip = (uint8_t)(Guard.direction >= 0);
int qskip = 0, qrows = (int)qh; /* клип полем 192, как у спрайта */
if (qflip) qx -= (int)qw;
if (qtop < 0) { qskip = -qtop; qrows -= qskip; qtop = 0; }
if (qtop + qrows > 192) qrows = 192 - qtop;
if (qrows <= 0) goto splash_done;
qh = (uint16_t)qrows;
gfx_set_bank(GFX_BANK_SPRITE);
if (pop_onscreen_cols(qx, qtop + POP_YOFF, qw, qh))
gfx_blit_cols_part_noclip(qx, qtop + POP_YOFF, qimg, qflip,
(uint8_t)qskip, (uint8_t)qrows);
else
gfx_blit_cols_part(qx, qtop + POP_YOFF, qimg, qflip, qskip, qrows);
gfx_set_bank(GFX_BANK_NORMAL);
p = gfx_get_draw_page() & 1;
qx_l[p] = qx; qy_l[p] = qtop; qw_l[p] = qw; qh_l[p] = qh;
qvalid[p] = 1;
if (!clip_w) {
clip_x = qx; clip_y = qtop; clip_w = qw; clip_h = qh;
} else {
int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h;
if (qx < clip_x) clip_x = qx;
if (qtop < clip_y) clip_y = qtop;
if (qx + (int)qw > x1) x1 = qx + (int)qw;
if (qtop + (int)qh > y1) y1 = qtop + (int)qh;
clip_w = (uint16_t)(x1 - clip_x);
clip_h = (uint16_t)(y1 - clip_y);
}
}
splash_done: ;
}
/* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable,
* seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник
* heal; окно fore-клипа — объединение (без него клинок полез бы поверх
* переднего столба). */
{
int sx, sy; uint16_t sw, sh;
if (pop_sword_draw(&Guard, &pop_gframe, obj_x, obj_y,
&sx, &sy, &sw, &sh)) {
p = gfx_get_draw_page() & 1;
sx_l[p] = sx; sy_l[p] = sy; sw_l[p] = sw; sh_l[p] = sh;
svalid[p] = 1;
if (!clip_w) {
clip_x = sx; clip_y = sy; clip_w = sw; clip_h = sh;
} else {
int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h;
if (sx < clip_x) clip_x = sx;
if (sy < clip_y) clip_y = sy;
if (sx + (int)sw > x1) x1 = sx + (int)sw;
if (sy + (int)sh > y1) y1 = sy + (int)sh;
clip_w = (uint16_t)(x1 - clip_x);
clip_h = (uint16_t)(y1 - clip_y);
}
}
}
gfx_w0_unmap();
/* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c,
* fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов. */
pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h);
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
* midtable, а foretable рисуется после всех midtable-записей, поэтому
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
* столб. */
if (w && h)
pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction, Guard.sword,
clip_x, clip_y + POP_YOFF, clip_w, clip_h,
Guard.action, Guard.curr_col, Guard.curr_row);
}
/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- *
* Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри
* тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража
* справа налево от x = 314. Пустые деления у Кида — свой спрайт (chtab_2
* id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы
* просто не печатаем — полоса и так на чёрном фоне.
*
* Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через
* pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */
#define HP_Y 194
#define HP_STEP 7
#define HP_KID_FULL 216
#define HP_KID_EMPTY 217
#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */
/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ
* страницах дабл-буфера (иначе на второй останется старое значение и его
* будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг.
*
* Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
* sdcc_z80_cmp_store_a_bug). */
static uint8_t hp_todo; /* сколько страниц ещё обновить */
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
* комнату (фон перерисован целиком и стёр её) и при старте. */
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
void pop_hp_draw(void) __banked
{
uint8_t i, n;
uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0);
uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0);
{ /* Изменилось хоть одно число — обновить обе страницы. */
uint8_t k = hitp_curr, km = hitp_max;
uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev ||
gd != hp_gd_prev || gm != hp_gdmax_prev);
hp_kid_prev = k; hp_kidmax_prev = km;
hp_gd_prev = gd; hp_gdmax_prev = gm;
if (changed) hp_todo = 2;
}
if (hp_todo == 0) return;
hp_todo--;
/* Стереть прошлую полосу: фон под ней — чёрный борт. */
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
/* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */
n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max);
for (i = hitp_curr; i < n; i++)
pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF);
for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++)
pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF);
/* Страж: только полные деления, справа налево. Скелет полосы не имеет. */
if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) {
const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0);
uint16_t w, h;
gfx_w0_map(gp[0].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
/* Атлас стража тоже column-major — блит колоночный. */
n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd);
for (i = 0; i < n; i++)
gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP,
HP_Y + POP_YOFF, img, 0, 0, 0);
}
gfx_w0_unmap();
}
}