diff --git a/applications/PoP/poc/res/bg/pal_env4.atl b/applications/PoP/poc/res/bg/pal_env4.atl
index 7c1ec1d..b813f03 100644
Binary files a/applications/PoP/poc/res/bg/pal_env4.atl and b/applications/PoP/poc/res/bg/pal_env4.atl differ
diff --git a/applications/PoP/poc/res/bg/pal_fore.atl b/applications/PoP/poc/res/bg/pal_fore.atl
index 1d039fe..6fd76e5 100644
Binary files a/applications/PoP/poc/res/bg/pal_fore.atl and b/applications/PoP/poc/res/bg/pal_fore.atl differ
diff --git a/applications/PoP/poc/res/bg/pop_env4.atl b/applications/PoP/poc/res/bg/pop_env4.atl
index af610f3..683fcdc 100644
Binary files a/applications/PoP/poc/res/bg/pop_env4.atl and b/applications/PoP/poc/res/bg/pop_env4.atl differ
diff --git a/applications/PoP/poc/res/bg/pop_fore.atl b/applications/PoP/poc/res/bg/pop_fore.atl
index 712de61..87f32bb 100644
Binary files a/applications/PoP/poc/res/bg/pop_fore.atl and b/applications/PoP/poc/res/bg/pop_fore.atl differ
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]