From 1b2111f2a04e15795f563712f8bb626e15ccd716 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 10 Aug 2026 21:26:58 +0300 Subject: [PATCH] =?UTF-8?q?L4-MIRROR=20=D1=88=D0=B0=D0=B3=D0=B8=201-2:=20?= =?UTF-8?q?=D0=B7=D0=B5=D1=80=D0=BA=D0=B0=D0=BB=D0=BE=20=D0=B2=20=D0=B0?= =?UTF-8?q?=D1=82=D0=BB=D0=B0=D1=81=D0=B5=20+=20=D0=BF=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=82=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B4=D0=B2=D0=B5=D1=80=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Задача заведена активной на доске по решению пользователя (palace был отложен 2026-08-04, но уровень 4 уже гоняется в MAME и зеркало — единственное, что мешает пройти его сюжетно). Механика целиком сверена по SDLPoP, таблица соответствий в TASKS_OPEN. Шаг 1 — АТЛАС. tile_table[0x0D] = база 75, фронт 77 (наша таблица совпадает с SDLPoP байт в байт). Тайла 13 НЕТ НИ В ОДНОМ уровне статически: перебор всех 15 res200N.bin даёт ноль попаданий (санити разбора: ур.1 без чомпера, ур.3 с 18, ур.4 с паласными 25..29). Значит render_room эти id не увидит и на месте зеркала был бы чёрный провал — грабли memory pop_atlas_dynamic_ids. Добавлены MIRROR_ENV_IDS = {75,77} в pop_pack_bg.py, 77 ещё и в FORE_ENV_IDS. Оба набора переупакованы: fore 17 -> 18 спрайтов, все страницы EMM в пределах 16 КБ. Шаг 2 — ПОСТАНОВКА. place_mirror() в pop_trob.c по переходу pop_leveldoor_open 0/2 -> 1 (условие оригинала, seg007:0457 — иначе тайл ставился бы заново каждый кадр открытой двери). Пишет тайл 13 в комнату 4, колонку 4, ряд 0; если комната уже на экране — POP_RD_FLOOR на обе страницы. Банк 6 3450 -> 3518. НЕ ПРОВЕРЕНО В MAME: нужно нажать плиту выхода на уровне 4 и дойти до комнаты 4. Отдельный вопрос к проверке — не устареет ли g_fg коллизии, если игрок окажется в комнате 4 в момент постановки. Дальше по плану: 4 (прыжок сквозь + рождение тени), 5 (клип тени), 3 (отражение — косметика, самое дорогое). Co-Authored-By: Claude Opus 5 --- applications/PoP/poc/res/bg/pal_env4.atl | Bin 5052 -> 6976 bytes applications/PoP/poc/res/bg/pal_fore.atl | Bin 12308 -> 13632 bytes applications/PoP/poc/res/bg/pop_env4.atl | Bin 5068 -> 6992 bytes applications/PoP/poc/res/bg/pop_fore.atl | Bin 12359 -> 13683 bytes applications/PoP/roomtest/TASKS_OPEN.md | 54 ++++++++++++++++++++++ applications/PoP/roomtest/pop_trob.c | 46 ++++++++++++++++-- applications/PoP/toolchain/pop_pack_bg.py | 16 ++++++- 7 files changed, 111 insertions(+), 5 deletions(-) diff --git a/applications/PoP/poc/res/bg/pal_env4.atl b/applications/PoP/poc/res/bg/pal_env4.atl index 7c1ec1da96e017083693fedf9e7e104693997d48..b813f035b0fd5c49f8651b9002e1ae5aea83dc77 100644 GIT binary patch literal 6976 zcmeHKOLE;d5Tv41$|}c>tKx;d;}Mj-%T>8SNP?hL&yR28W8@fFW|70>AX(%P*)iQS z_yeRZ+o^aVNFs>o>6xAZ`5rX=^7BtWbpQSMyL)+ab=7shU+HYV(fRvpo&UUkCHNoT z_Rs0}{=5De=O<5IzYyMy&bLqdYp#ED^FL4fH(dXP64LqCAN#jlbG+_Bd;IAji|hNlyTO?@Z-Y~`3G41&b9fK1B-Su~VcnTVr}Tju6PDA6I$gCa zpFQ|2-5=EG6)=(BYgTm4_0fC8K1RQ&F=c5D578a>I>{FFcw4HGwuftsEU%GKd}ZGD zs`-51-l~rLdW(@;xT3YTtF^RyYi^I$)SgcZ;ao~QN$Nq&g11qhKk-g(U9>GR zwQfy5Dwn={_I2+e>I^;Bn_`y^_Rv+E@T>Z12zZV9J)jFw*LCOU_n?i2XpS4!12_Ei-_&dE02u+v-+3=56DiI3H<1>r(evylV4f>l@AAZGFR6O?`LWl`l`R zw#B+!+94km{v~o0LGz4Q_1znJw$x9a*>PMTeOcyZ9+%m5KoDMXw8Tr)7~jDrcUt7o zNS|RWxiF5?#G{d2j;IekAPR;iYfl_((g74>aJDnN5B$fVO3xXLs0O{PZ6Jk1mUzNy+DFL1RSJ8pX-? z@iWw&X(L{siW`2ukzb(>$H&`JqVDvr|FoCcn0^^4aTimC73cG#%HF-@37 z58T6_BQA*txru50`&T?&*uVV?5$y2p#kiBp-aHtbAI8z_&Evr17)~!cvms#dKJU#0 z0E1zg0aX@l7fSQ7OPY-GfXm*6AkHm0X0oQfC*wvoL2~Vr@ zkjO+=3)p&`-*4i9i=e>pd$x{Gd=(GEXW_CC+(~@kt2zL_z-$0qE>IhAbpQ;zHAoBx zPAPxgD9phHF8p<1+(1wDGe7taXJBMc;wztmDUZtX`jk(c`lWt41bvYY{p=lolloFV zV8!Xe{%il>qVbjQt`1N5LcU0Z7GHT6H~59#@qfk9>;9Chg{DUUZ2n z)A|`s>QjC6kvpl6-+dgcpYaLG4R&Kp{$U_a&~f2(J}XnNvLUcKZh>n2ebnI;fRRTI h@efep?{XOdZ_G|)AHSVfQSw`=JPJJPMzs68e*xs<$#DPx delta 49 rcmX?Lwnu%!0jUQJ3=E8n3=HV>5AKb(6u6n}7$!Fg%Ws}9ypSCLGDZwG diff --git a/applications/PoP/poc/res/bg/pal_fore.atl b/applications/PoP/poc/res/bg/pal_fore.atl index 1d039febb359e764dd95b9d6cdff07f69f646380..6fd76e5be969df37f80789db76a6d182da87bb79 100644 GIT binary patch delta 782 zcmc(dze)o^5XLu`6^TV6A%rAsNR*%=3Q;ayKnn{y&1L_zDMbR}1BezrfX9k9K7?{$ zDORbitW){`_UFv(8ZY_)4(xrmzx#G(clK^aucwc1f!)a0;_1HNm1;kiW9*PKmep>( zwik~D7p>)fJnr!6YP%4Rw>rF2B>(Zc(}zu`AKOmDog(-Tx>sS)#C$JdP)Gd?gEFe< z1qIaIUa)|Q(PoRx4tOg$kiHpHYsIIm05#kPf0brW}=; zYT()kA19bMO^2`xGlPz$QtLk~^^_KS9|@KF-qGv>l=IBqG=zO_RP6-DJB+)3924Q{ zPecVSfO-q(^6iAp1M`y~N=%VgV!QB+o@weWSPi_)n9*p7q*CC9d`f{q(>g3ht(}9| uTf|u-78$}t{9^EjfD^+f#8MF8#E|xBZG~4^pCzMGGjESd*U3fszWD_Y#8Wx| delta 150 zcmX?*H6>xg1*VA)A|_v8l3-yGk+7ebBg67RQov?njta{bSq09CIXWsgbl>rbavn9{Onrizph^k|Kr>K z1?_(Qu7A$?=H|^y@w$>dk_xq^PiyEewml|yDHHYz9??wCcx*xBR)qH%7#>>~p7#fL_ zuI2OUPJJ}lVjqLn?qje(XJd^)OEqfU+I`dwFiuae`{Qe51!=ECJ&)Cxyh4qB&tzq~ zhH-jby}$47G)sQG!^jn0QQhjQmU>WgJ*if=YSMEx=%uQ9-@T|jyM6_uld)ZoD4x3F z-pPCq?oj2?mkBoUGrHjgEqI{F8-|G=Xh1SYUZpX9gL+=2S_I;IYKn5`jK2&tE6Q9< z`7lIJ`|f#r57$+U;u^~nAFY)AHEC-PePj(#@%ND%JfpG4A&gSft^i~%e0gbjLE|2% zNlw!q)Tpsdc}-C{H&-s-v1uCKbT2zGFonib>uJxc_mp$vFjy<@5C`NEM0wD=Ki#?Gjc?ilrtYvkHm^h;#xZa=ZzvwRm|uRYLd{W4+|BcbT7TtOZ}es2d`58fGlD0xAZJHFrOzoDMv^|q9h^7*Sz%}G&bdK3=sMH54%?d&BG4gG!EZ3 zjoA^*c%EA`fqc=mW;aV{B&pAv+&iXqi`_gJbate>B+Y0wreW|L_k(0(z)QVftB25_}d&uPKhBK0?-PPCC2-dEJq>2#h? z^Klq)w|WbSOnRDu=fL^K*FBgI{i4T5K1K=}c2XH>4+Q`)a z82apx7zUiI|7m4m9v58vPdmnq?zVpBhrj0xjO-@9`mt}pIIl*oW=6@K-3B50jjz{&>qihIN?^{rf p-=?0k0E|3xjDLZOf76!{@WeDChxqlpO6gy#Dp2sO8%Ymm{{l+W&$9pk delta 49 rcmca$c1C@|0jUQJ3=E8n3=HTr2hYY^2HZ?`43j4c%WqyUypSCLDHjWu diff --git a/applications/PoP/poc/res/bg/pop_fore.atl b/applications/PoP/poc/res/bg/pop_fore.atl index 712de619d402a55c690e242869bff67edb2f7926..87f32bbfe77c3f82a61f8ba22928bfb0d8beb9e1 100644 GIT binary patch delta 1486 zcmc(fu}T9$5QaDBio_y;#3Te0O)wY`5rqVs0~-Z9$=>c>Ya^l@=_94uV!pEIMCU6*No+Ae z9g=~_FlDIJG{XzXuK^a{s%40r&!+c-!k8vt)9yh%?wQY)%x7X4>2!63MuEkN9?30NdRlYXATM diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 6409b65..81eca73 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -55,6 +55,7 @@ | 2 | [L3-CHOMP](#l3-chomp) | **СЛЕДУЮЩАЯ**: чомперы (5 шт) | прохождение ур. 3 | | — | [L3-SKEL](#l3-skel) | скелет ур. 3 — **сделан 2026-08-07**, ждёт финальной приёмки | — | | 3 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели | +| 4 | [L4-MIRROR](#l4-mirror) | **АКТИВНА с 2026-08-10 по решению пользователя**: зеркало уровня 4 + тень | прохождение ур. 4 | | — | [DRAW-COST](#draw-cost) | **кадр уложился в бюджет 2026-08-09**: 470 964 -> 425 600 тактов, период цикла 4 растровых кадра -> **3** (игра быстрее на треть). Дальнейшее — запас, не срочность | плавность на ВСЕХ уровнях | | — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент | | — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности | @@ -720,6 +721,59 @@ loose-плиту (7, кол 4, ряд 0). --- +### L4-MIRROR. Зеркало уровня 4 и тень — **АКТИВНА с 2026-08-10** + +Приоритет переставлен пользователем: palace-уровни были отложены решением +2026-08-04, но уровень 4 уже гоняется в MAME, и зеркало — единственное, что +мешает пройти его сюжетно. + +**Как это работает в оригинале** (SDLPoP, всё сверено по коду): + +| что | где | суть | +|---|---|---| +| постановка тайла | `animate_leveldoor`, seg007:0457 | дверь выхода ДОРИСОВАЛА открытие (`modif >= 43`, `leveldoor_open` 0/2 -> 1) -> в комнату 4, колонку 4, ряд 0 пишется `tiles_13_mirror` | +| отражение | `check_mirror`, seg003:0798 | пока Кид на тайле зеркала — каждый кадр рисуется зеркальная копия: `Char.x = (xpos<<1) - Char.x`, `direction` инвертируется, копия идёт в objtable типом 4 с клипом `left = (curr_col<<5)+9`, `top = y_clip[row+1]` | +| прыжок сквозь | seg004:0240 | засчитывается ТОЛЬКО `run-jump` СПРАВА НАЛЕВО (`frame 39..43`, `direction < 0`) -> `modif = 0x56` (разбитое), `jumped_through_mirror = -1` | +| рождение тени | `jump_through_mirror`, seg003:080A | Кид копируется в слот Guard как `charid_1_shadow`; `guardhp = hitp_max`, у Кида `hitp_curr = 1` | +| поведение тени | `autocontrol_shadow_level4`, seg002:1131 | в зеркальной комнате при `x < 80` исчезает (`clear_char`), иначе бежит вперёд. Боя нет | +| клип тени | seg008:1699 | `obj_clip_left = 137 + (mirror_column-4)*32` — тень видна только СПРАВА от зеркала | +| коллизия | `wall_type`, seg006:1632 | зеркало = 2 «стена слева» — **уже портировано** (`pop_map.c`), как и спецкейс `can_climb_up` | + +**Шаги.** + +1. **АТЛАС — СДЕЛАНО 2026-08-10.** `tile_table[0x0D]` = база 75, фронт 77 + (наша таблица совпадает с SDLPoP такт в такт). Тайла 13 **нет ни в одном + уровне статически** — проверено перебором всех 15 `res200N.bin`, ноль + попаданий; санити-проверка разбора: ур.1 без чомпера, ур.3 с 18, ур.4 с + паласными 25..29. Значит `render_room` эти id не увидит и на месте + зеркала был бы чёрный провал (memory `pop_atlas_dynamic_ids`). Добавлены + `MIRROR_ENV_IDS = {75, 77}` в `pop_pack_bg.py`, 77 — ещё и в + `FORE_ENV_IDS`. Оба набора переупакованы: fore 17 -> 18 спрайтов, все + страницы EMM в пределах 16 КБ. +2. **ПОСТАНОВКА ТАЙЛА — СДЕЛАНО 2026-08-10, НЕ ПРОВЕРЕНО В MAME.** + `place_mirror()` в `pop_trob.c`, вызывается из `animate_leveldoor` по + переходу `pop_leveldoor_open` 0/2 -> 1 (условие оригинала — иначе тайл + ставился бы заново каждый кадр открытой двери). Если комната зеркала уже + на экране, ставится `POP_RD_FLOOR` на обе страницы. + **Что проверить:** нажать плиту выхода на ур. 4, дойти до комнаты 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). + +Порядок: 1 -> 2 -> 4 -> 5 -> 3. После 4 и 5 уровень уже проходится, потому +что сюжетно достаточно прыгнуть сквозь зеркало. + +--- + ## P1 — берётся в любой момент ### L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01) diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 6e73c35..b1e153b 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -409,7 +409,37 @@ static void animate_button(uint8_t idx, int8_t *type) * type = -1. */ static const uint8_t leveldoor_close_speeds[5] = {0, 5, 17, 99, 0}; -static void animate_leveldoor(uint8_t *m, int8_t *type) +/* ---- Спецсобытие уровня 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 MIRROR_LEVEL 4 +#define MIRROR_ROOM 4 +#define MIRROR_TILEPOS 4 /* row 0, col 4 */ +#define TILE_MIRROR 0x0D + +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) { @@ -420,7 +450,17 @@ static void animate_leveldoor(uint8_t *m, int8_t *type) else *m = (uint8_t)(*m - sp); } else { if (*m < 43) (*m)++; - if (*m >= 43) { *m = 43; *type = -1; pop_leveldoor_open = 1; } + 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; + } } } @@ -538,7 +578,7 @@ void pop_process_trobs(uint8_t cur_room) __banked animate_door(&mod[tp], &type); break; case TILE_LEVELDOOR: - animate_leveldoor(&mod[tp], &type); + animate_leveldoor(&mod[tp], &type, cur_room); break; case TILE_TORCH: case TILE_TORCH_DEBRIS: diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py index 322bee6..713ce3c 100644 --- a/applications/PoP/toolchain/pop_pack_bg.py +++ b/applications/PoP/toolchain/pop_pack_bg.py @@ -43,8 +43,9 @@ OUT_DIR = os.path.join(HERE, "..", "poc", "res", "bg") # Спрайты FORE-слоя (add_foretable в seg008 — «перед персонажем»): фронты # столбов, ворот, дебриса. См. tile_table.fore_id + анализ pop_background_strategy. +# 119..123 — фронт чомпера и его кровь; 77 — фронт ЗЕРКАЛА (MIRROR_ENV_IDS). FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100} | {106, 107, 108, 109, 110} \ - | {119, 120, 121, 122, 123} # фронт чомпера + его кровь + | {119, 120, 121, 122, 123} | {77} # Пол-оверлей подъёма (floor_left_overlay, draw_floor_overlay seg008): рисуется # рантаймом поверх нижней части Kid при подтягивании на пол у кромки — НЕ @@ -177,6 +178,16 @@ SRC_DIRS = None # ставит main() по выбранному н # добавляем явно — иначе при переключении набора в этих местах будет дыра. PALACE_ENV_IDS = {78, 80, 81, 82, 83, 84, 145} +# ЗЕРКАЛО (tile_table[0x0D] = база 75, фронт 77; 42/43 — общие грани пола). +# Тайла 13 НЕТ НИ В ОДНОМ уровне статически (проверено перебором всех 15 +# res200N.bin): оригинал СТАВИТ его в рантайме, когда дверь выхода уровня 4 +# дорисовала открытие — animate_leveldoor, seg007:0457: +# curr_room_tiles[curr_tilepos] = tiles_13_mirror; +# Значит в used от render_room эти id не попадают и без явного добавления на +# месте зеркала был бы чёрный провал (те же грабли, что у нажатой кнопки и +# дрожащей плиты — memory pop_atlas_dynamic_ids). +MIRROR_ENV_IDS = {75, 77} + ENV_SHIFT = 4 # env фон: страница = id>>SHIFT, idx = id&MASK. # Было 5: паласный набор добавляет куски верха # двери (78..84), и страница 2 переваливала за @@ -492,7 +503,8 @@ def main(): | CHOMPER_BLOOD_FORE_IDS | OPENER_NOFLOOR_ENV_IDS # + opener без пола слева | LEVELDOOR_ENV_IDS # + дверь уровня (створка/лестница) - | PALACE_ENV_IDS) # + куски, что рисует только палас + | PALACE_ENV_IDS # + куски, что рисует только палас + | MIRROR_ENV_IDS) # + зеркало (ставится в рантайме) wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL) env_bg = [i for i in env_ids if i not in FORE_ENV_IDS]