diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 81eca73..f4769c2 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -750,24 +750,53 @@ loose-плиту (7, кол 4, ряд 0). `MIRROR_ENV_IDS = {75, 77}` в `pop_pack_bg.py`, 77 — ещё и в `FORE_ENV_IDS`. Оба набора переупакованы: fore 17 -> 18 спрайтов, все страницы EMM в пределах 16 КБ. -2. **ПОСТАНОВКА ТАЙЛА — СДЕЛАНО 2026-08-10, НЕ ПРОВЕРЕНО В MAME.** +2. **ПОСТАНОВКА ТАЙЛА — СДЕЛАНО И ПРОВЕРЕНО ПОЛЬЗОВАТЕЛЕМ 2026-08-10.** `place_mirror()` в `pop_trob.c`, вызывается из `animate_leveldoor` по переходу `pop_leveldoor_open` 0/2 -> 1 (условие оригинала — иначе тайл ставился бы заново каждый кадр открытой двери). Если комната зеркала уже на экране, ставится `POP_RD_FLOOR` на обе страницы. - **Что проверить:** нажать плиту выхода на ур. 4, дойти до комнаты 4 — - зеркало должно быть нарисовано. Отдельно посмотреть, не устарел ли - `g_fg` коллизии, если игрок окажется в комнате 4 в момент постановки. + Зеркало появляется, спрайты из атласа читаются. Остался незакрытый + краевой вопрос: не устареет ли `g_fg` коллизии, если игрок окажется в + комнате 4 ровно в момент постановки (в обычном прохождении дверь в другой + комнате). 3. **ОТРАЖЕНИЕ.** Самый дорогой шаг: это ЧЕТВЁРТЫЙ рисуемый Char помимо Кида и соперника, со своим клипом. Отрисовка у нас уже общая (`pop_cdraw.c`, слоты — memory `pop_char_draw_unified`), вопрос в числе слотов и в том, что отражение меняется вместе с Кидом каждый кадр, то есть `pop_char_skip_mask` на нём не сработает. ЧИСТО КОСМЕТИКА — делается последним. -4. **ПРЫЖОК СКВОЗЬ + РОЖДЕНИЕ ТЕНИ.** Слот Guard уже умеет держать - не-стража (скелет ур. 3), `charid_1_shadow` встаёт рядом. - `autocontrol_shadow_level4` — десяток строк. -5. **КЛИП ТЕНИ** (seg008:1699). +4. **ПРЫЖОК СКВОЗЬ + РОЖДЕНИЕ ТЕНИ — СДЕЛАНО 2026-08-10, НЕ ПРОВЕРЕНО В MAME.** + - `is_obstacle` (`pop_map.c`): ветка зеркала (seg004:0239) — Кид, кадры + бегового прыжка 39..43, направление влево -> `modif = 0x56`, + `pop_jumped_mirror = -1`, препятствия нет. + - `mirror_image` / `jump_through_mirror` / `pop_check_mirror` + (`pop_map.c`, порт seg003:0798..08A9). Отражённый Char уходит в слот + Guard как `CHARID_1_SHADOW`; `guardhp = hitp_max`, у Кида `hitp_curr = 1`. + `savekid` НЕ делается — как в оригинале. + - `pop_check_mirror()` зовётся из главного цикла ПЕРЕД отрисовкой + персонажей (в оригинале — первая строка `draw_people`). + - `autocontrol_shadow` + `autocontrol_shadow_level4` + `clear_char` + (`guards.c`, seg002:081D/1131/seg006:1945). Тень идёт СВОЕЙ веткой + целиком: к стражьему ИИ она не сводится, на ур. 4 не дерётся вовсе. + - **АТЛАС ТЕНИ.** Вскрылось при чтении seg006:0532: тень вне боевых + кадров 150..189 ходит по таблице КИДА, и `image` оттуда индексирует + спрайты Кида, а не стража. Выбор атласа в `pop_cdraw` шёл по СЛОТУ — + тень рисовалась бы спрайтами стража. Условие вынесено в + `pop_frame_tbl_is_guard()` (`pop_kid.c`), его теперь читают и + `load_frame`, и отрисовка — разъехаться не могут. + - Звука (`sound_45_jump_through_mirror`) в порте нет, пропущен. + Цена: `_CODE` +18 Б, банк 1 2311 -> 2367, банк 3 10328 -> 10551, + банк 4 8243 -> 8267. + **Что проверить в MAME:** на ур. 4 открыть дверь выхода, дойти до + комнаты 4, разбежаться СПРАВА НАЛЕВО и прыгнуть в зеркало — должна + появиться тень (спрайтами Кида, зеркально) и убежать влево, растворившись + при `x < 80`. У Кида после этого 1 HP, у тени полная полоса. +5. **КЛИП ТЕНИ** (seg008:1699): `obj_clip_left = 137 + (mirror_column-4)*32` + — тень видна только СПРАВА от зеркала. **Не сделано и не однострочник:** + в `pop_cdraw` есть клип сверху, снизу и СПРАВА (`vis_w`), а левого нет + вовсе — `gfx_blit_cols_part_w` умеет ограничить ширину, но не пропустить + колонки слева. Нужен либо новый примитив, либо сдвиг `bx` со срезом + исходных колонок. Пока без него тень будет вылезать левее зеркала. Порядок: 1 -> 2 -> 4 -> 5 -> 3. После 4 и 5 уровень уже проходится, потому что сюжетно достаточно прыгнуть сквозь зеркало. diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index 74e86c8..5833a9e 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -436,6 +436,35 @@ static void autocontrol_guard_active(void) autocontrol_guard_kid_far(); } +/* clear_char (seg006:1945): персонажа со сцены — направление «никуда», + * не жив, действие 0, HP обнулены. Полосу перерисует pop_hp_draw сам. */ +static void clear_char(void) +{ + Char.direction = DIR_56_NONE; + Char.alive = 0; + Char.action = 0; + guardhp_curr = 0; +} + +/* autocontrol_shadow_level4 (seg002:1131). Вся «логика» тени на уровне 4: + * в комнате зеркала она бежит вперёд (то есть ВЛЕВО — её направление + * зеркально Киду) и при x < 80 исчезает. Ни боя, ни преследования: она + * просто убегает за левый край и растворяется. */ +static void autocontrol_shadow_level4(void) +{ + if (Char.room != MIRROR_ROOM) return; + if (Char.x < 80) clear_char(); + else move_1_forward(); +} + +/* autocontrol_shadow (seg002:081D) — по уровням. У нас пока только ур. 4; + * ветки 5 (крадёт зелье), 6 (шагает по плите) и 12 появятся со своими + * уровнями. */ +static void autocontrol_shadow(void) +{ + if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4(); +} + /* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */ static void autocontrol_opponent(void) { @@ -447,6 +476,9 @@ static void autocontrol_opponent(void) /* autocontrol_skeleton (seg002:685): у скелета меч вынут ВСЕГДА — он не * бывает «неактивным», поэтому ветка inactive ему недоступна. */ if (Char.charid == CHARID_4_SKELETON) Char.sword = SWORD_2_DRAWN; + /* ТЕНЬ идёт своей веткой ЦЕЛИКОМ (seg002:0647): к стражьему ИИ она не + * сводится — на уровне 4 не дерётся вовсе. */ + if (Char.charid == CHARID_1_SHADOW) { autocontrol_shadow(); return; } if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive(); else autocontrol_guard_active(); } diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index 8640613..c4b2728 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -483,6 +483,14 @@ void pop_char_draw(uint8_t who) __banked npages = g_ok ? GUARD_PAGES : 0; hurt = pop_guard_hurt; if (Guard.charid == 0) return; /* соперника на сцене нет */ + /* ТЕНЬ (ур. 4) — это зеркальный Кид: вне боевых кадров 150..189 она + * ходит по таблице КИДА (seg006:0532), а `image` из неё индексирует + * спрайты Кида, а не стража. Набор атласов обязан идти за таблицей — + * условие берём из того же места, что load_frame, чтобы они не + * разъехались. */ + if (!pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) { + pages = kidp; npages = kid_npages; + } } if (fr->image == 255) return; /* пустой кадр */ diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index dcb31c6..3e21077 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -69,6 +69,20 @@ #define CHARID_4_SKELETON 4 #define CHARID_5_PRINCESS 5 +/* dir_56_none (types.h:402): «направления нет» — им clear_char убирает + * персонажа со сцены. Значение 0x56 в int8_t = 86, то есть заведомо не + * dir_0_right (0) и не dir_FF_left (-1). */ +#define DIR_56_NONE 0x56 + +/* Спецсобытие «зеркало» (custom->mirror_*, дефолты SDLPoP): уровень 4, + * комната 4, колонка 4, верхний ряд. Здесь, а не в pop_trob.c: константы + * нужны и постановке тайла (банк 6), и ИИ тени (банк 1). */ +#define MIRROR_LEVEL 4 +#define MIRROR_ROOM 4 +#define MIRROR_COL 4 +#define MIRROR_ROW 0 +#define MIRROR_TILEPOS (MIRROR_ROW * 10 + MIRROR_COL) + /* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */ #define NUM_GUARD_SKILLS 12 diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 53ccf33..e855318 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -328,6 +328,17 @@ void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5) /* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в * cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */ +/* Какая ТАБЛИЦА КАДРОВ у персонажа (seg006:0293). Вынесено из load_frame + * отдельной функцией, потому что тот же вопрос решает ОТРИСОВКА: набор + * атласов обязан совпадать с таблицей, иначе image из таблицы Кида ищется + * среди спрайтов стража. Для тени это не теория — она ходит кадрами Кида + * (и рисуется его спрайтами), а дерётся кадрами стража. */ +uint8_t pop_frame_tbl_is_guard(int8_t charid, uint8_t frame) +{ + return (uint8_t)(charid == CHARID_2_GUARD || charid == CHARID_4_SKELETON || + (charid == CHARID_1_SHADOW && frame >= 150 && frame < 190)); +} + static void load_frame(void) { /* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у @@ -341,10 +352,7 @@ static void load_frame(void) * ТЕНЬ (charid_1_shadow) — по таблице стража ТОЛЬКО в кадрах 150..189, * в остальных она ходит кадрами Кида (seg006:532). */ const uint8_t *f; - uint8_t use_guard_tbl = - (Char.charid == CHARID_2_GUARD || Char.charid == CHARID_4_SKELETON || - (Char.charid == CHARID_1_SHADOW && - Char.frame >= 150 && Char.frame < 190)); + uint8_t use_guard_tbl = pop_frame_tbl_is_guard(Char.charid, Char.frame); if (use_guard_tbl) { int16_t idx = (int16_t)Char.frame; if (idx >= 102 && idx < 107) idx += 70; @@ -514,12 +522,17 @@ void play_seq(void) * в оригинале. Раньше вторая половина связки работала только на ветке Кида * (стражу колонку вёл pop_guard_enter/leave) — прямое следствие того, что * физика знала один Kid. */ -void pop_load_fram_det_col(void) +void pop_load_frame(void) { if (!kdat_ok) return; gfx_w0_map(kdat_page); load_frame(); gfx_w0_unmap(); +} + +void pop_load_fram_det_col(void) +{ + pop_load_frame(); pop_determine_col(); } diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index ede4d74..00ab116 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -104,6 +104,16 @@ void pop_guard_play(void); * play_guard_frame (seg000:1209/1246): cur_frame один на всех, и без этого * геометрия одного персонажа считается по кадру другого. */ void pop_load_fram_det_col(void); +/* Только load_frame (seg006:0293), БЕЗ determine_col. Нужен там, где + * оригинал зовёт голый load_frame и пересчёт колонки по x был бы вреден — + * например в jump_through_mirror, где ось отражения берётся из curr_col. */ +void pop_load_frame(void); + +/* Какая таблица кадров у персонажа (seg006:0293): 1 — стража, 0 — Кида. + * Нужна не только load_frame, но и отрисовке — набор атласов обязан + * совпадать с таблицей (тень ходит кадрами и спрайтами КИДА, а дерётся + * кадрами стража). */ +uint8_t pop_frame_tbl_is_guard(int8_t charid, uint8_t frame); /* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования * таблицы во втором TU). */ diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 2e2d356..d07a293 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -1746,7 +1746,19 @@ static uint8_t is_obstacle_at_col(int8_t col) if (t == TILE_CHOMPER) /* закрыт только при modif==2 */ return (uint8_t)(col >= 0 && col <= 9 && pop_trob_modif(g_room)[Char.curr_row * 10 + col] == 2); - /* ветка «прыжок сквозь зеркало» (seg004:239) — уровень 4, не портируем */ + /* Прыжок СКВОЗЬ ЗЕРКАЛО (seg004:0239). Зеркало вообще-то «стена слева» + * (wall_type = 2), но для Кида в БЕГОВОМ ПРЫЖКЕ справа налево оно + * перестаёт быть препятствием: он пролетает насквозь, зеркало «бьётся» + * (modif 0x56) и взводится флаг рождения тени. Условия — все четыре, как + * в оригинале: только Кид, только кадры бегового прыжка 39..43, только + * направление влево. */ + if (t == TILE_MIRROR && Char.charid == CHARID_0_KID && + Char.frame >= 39 && Char.frame < 44 && Char.direction < 0 && + col >= 0 && col <= 9) { + pop_trob_modif(g_room)[Char.curr_row * 10 + col] = 0x56; + pop_jumped_mirror = -1; + return 0; + } return 1; } @@ -2552,3 +2564,60 @@ void pop_guard_phys_tick(void) __banked } pop_saveshad_and_opp(); } + +/* ---- Зеркало уровня 4 (порт seg003:0798..08A9) ---------------------- * + * + * Тайл зеркала ставится в рантайме (pop_trob.c place_mirror), а «стеной + * слева» он уже был — wall_type = 2 портирован давно. Здесь вторая часть: + * что происходит ПОСЛЕ того, как Кид пролетел сквозь него беговым прыжком + * (флаг pop_jumped_mirror взводит is_obstacle выше по файлу). */ + +/* check_mirror_image (seg003:085B). МУТИРУЕТ Char НА МЕСТЕ: x отражается + * относительно оси зеркала, direction инвертируется. Возвращает + * distance_mirror — насколько персонаж вошёл в тайл (оригинал по нему + * решает, рисовать ли отражение; нам это понадобится на шаге «отражение»). + * + * Мутация Char здесь ЗАДУМАНА, а не побочный эффект: оригинал зовёт эту + * функцию из двух мест, и оба на неё рассчитывают — при рисовании отражения + * (в Char временная копия Кида, обратно она не сохраняется) и при рождении + * тени (мутированный Char уходит в слот Guard). */ +static int8_t mirror_image(void) +{ + /* +10 — смещение оси зеркала внутри тайла; в оригинале так и написано + * комментарием («I think 10 is the offset for mirror collision»). */ + int xpos = pop_x_bump[Char.curr_col + FIRST_ONSCREEN_COLUMN] + 10; + int distance = distance_to_edge_weight(); + if (Char.direction >= 0) distance = ~distance + TILE_SIZEX; + Char.x = (uint8_t)((xpos << 1) - (int)Char.x); + Char.direction = (int8_t)~Char.direction; + return (int8_t)(distance - 2); +} + +/* jump_through_mirror (seg003:080A): рождение ТЕНИ. Кид копируется в слот + * Guard уже отражённым; HP меняются местами — у тени полное здоровье Кида, у + * самого Кида остаётся 1. Полосы HP отдельно перерисовывать не надо: + * pop_hp_draw сравнивает числа и смену увидит сам. + * + * savekid здесь НЕТ и в оригинале: собственные x/direction Кида остаются + * прежними, отражается только копия, уходящая в слот Guard. */ +static void jump_through_mirror(void) +{ + pop_loadkid(); + pop_load_frame(); /* mirror_image смотрит на dx/flags КАДРА */ + (void)mirror_image(); + pop_jumped_mirror = 0; + Char.charid = CHARID_1_SHADOW; + /* play_sound(45) — звука в порте пока нет */ + pop_saveshad(); + guardhp_max = guardhp_curr = hitp_max; + hitp_curr = 1; +} + +void pop_check_mirror(void) __banked +{ + if (pop_jumped_mirror == -1) jump_through_mirror(); + /* Ветка ОТРИСОВКИ ОТРАЖЕНИЯ (шаг 3 задачи L4-MIRROR) сюда добавится + * позже: пока Кид стоит на тайле зеркала, оригинал каждый кадр рисует + * его зеркальную копию типом 4 objtable с клипом left = (curr_col<<5)+9 + * и top = y_clip[curr_row+1]. */ +} diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 4fb2036..a54909e 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -219,4 +219,9 @@ uint8_t pop_down_action(void) __banked; /* 1=обработано(спус * seq_70 ставит вызывающий (pop_ctrl) — тайлы наши, управление его. */ uint8_t pop_leveldoor_enter(void) __banked; +/* Зеркало уровня 4 (check_mirror, seg003:0798). Звать раз в кадр ПЕРЕД + * отрисовкой персонажей — в оригинале это первая строка draw_people + * (seg008:228A). Родит тень, если Кид только что пролетел сквозь зеркало. */ +void pop_check_mirror(void) __banked; + #endif diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c index 17dde1b..75f1680 100644 --- a/applications/PoP/roomtest/pop_state.c +++ b/applications/PoP/roomtest/pop_state.c @@ -73,3 +73,6 @@ int pop_leveldoor_ybottom; uint8_t pop_dbg_hits; void pop_dbg_trap(void) { pop_dbg_hits++; } + +/* Зеркало уровня 4 — см. pop_state.h. */ +int8_t pop_jumped_mirror; diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h index c0b7d1c..6347e4e 100644 --- a/applications/PoP/roomtest/pop_state.h +++ b/applications/PoP/roomtest/pop_state.h @@ -53,3 +53,9 @@ extern uint8_t pop_dbg_hits; void pop_dbg_trap(void); #endif + +/* Зеркало уровня 4 (jumped_through_mirror, seg003). −1 — Кид ТОЛЬКО ЧТО + * пролетел сквозь зеркало (ставит is_obstacle в pop_map, банк 3), и + * следующий pop_check_mirror родит тень; 0 — обычное состояние. + * Межбанковый: пишет pop_map, читает pop_map же, но через главный цикл. */ +extern int8_t pop_jumped_mirror; diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index b1e153b..b1b3653 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -422,10 +422,7 @@ static const uint8_t leveldoor_close_speeds[5] = {0, 5, 17, 99, 0}; * зеркала НЕТ НИ В ОДНОМ файле (проверено перебором всех 15 res200N.bin), * поэтому его спрайты 75/77 пришлось добавить в атлас явным списком — * MIRROR_ENV_IDS в pop_pack_bg.py (грабли memory pop_atlas_dynamic_ids). */ -#define MIRROR_LEVEL 4 -#define MIRROR_ROOM 4 -#define MIRROR_TILEPOS 4 /* row 0, col 4 */ -#define TILE_MIRROR 0x0D +#define TILE_MIRROR 0x0D /* MIRROR_* — в pop_guard.h (нужны и ИИ тени) */ static void place_mirror(uint8_t cur_room) { diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index d7afba0..41a499b 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -911,6 +911,11 @@ int main(void) } update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ PROF(6); /* спрайты: страж, Kid, fore поверх */ + /* check_mirror (seg003:0798) — ПЕРВАЯ строка draw_people + * (seg008:228A), до отрисовки кого бы то ни было: если Кид только что + * пролетел сквозь зеркало, тень должна родиться ДО того, как слот + * соперника пойдёт рисоваться, иначе кадр её проглотит. */ + pop_check_mirror(); { /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль * персонажа (см. guard_over_kid). Отрисовка одна на всех Char * (pop_cdraw.c), различает их только слот. */