From ffb59b3470d26fb293602718dbe0a39648eebef0 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 25 Aug 2026 15:06:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D1=82=D1=83=D1=81-=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA=D0=B0:=20=D0=B2=D1=80=D0=B5=D0=BC?= =?UTF-8?q?=D1=8F,=20=D0=BE=D1=82=D1=81=D1=87=D1=91=D1=82=20##:##=20=D0=B2?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BB=D0=B0=D0=B4=D0=BE=D1=87=D0=BD=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D0=BE=D1=81=D0=B5,=20=D1=81=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D1=82=D1=8C=20=D0=BA=D0=B0=D0=BA=20=D0=B2=20=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B3=D0=B8=D0=BD=D0=B0=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Порт show_time() целиком: «N MINUTES LEFT», «N SECONDS LEFT» (у единственной секунды свой total=12, как у оригинала), «TIME HAS EXPIRED!». Флаг pop_show_time — это is_show_time: взводит и само ядро таймера (круглые пятёрки, каждая секунда последней минуты), и старт уровня, и читы времени. Значение 2 = «перебить текущую строку», как оригинал делает в последнюю минуту и после читов. * Отладочная полоса показывает оставшееся время ##:## у правого края. На табло уходит minutes-1: rem_min у оригинала — НОМЕР идущей минуты, а не остаток целых (старт 60 при rem_tick 719 = «почти 60:00»). Секунды считаются делением раз в 12 кадров, а не каждый кадр — это единственное деление на кадровом пути. * Смерть Кида по образцу оригинала: строка «Press Button to Continue» с 7-го кадра (Kid.alive > 6), 288 тиков = 24 секунды, последние 72 тика мигание с периодом 12 и звуком 38 на появлении. Промолчал — start_game (title), нажал Enter/Shift — рестарт уровня. Авто-респавна по таймеру больше нет. Отпускания клавиш оригинал не ждёт, а мы ждём: Shift у нас клавиша действия — см. impl_diff.md. * POP_APP_PLAYING принимает EV_RESTART_INTRO: start_game зовётся прямо из геймплея, раньше это событие принимал только pause menu. * expired() загейчен уровнем 13 (seg000:910). Время на уровне Джафара идёт до открытия двери, но КОНЧИТЬСЯ игра там уже не может — у нас этого гейта не было, и партия завершалась. Бюджет: _CODE 23979, куча 263 Б. Host-тесты: 15 наборов. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/impl_diff.md | 44 +++++ applications/PoP/docs/status_line_text.md | 18 +- applications/PoP/roomtest/pop_app.c | 5 + applications/PoP/roomtest/pop_status.c | 225 ++++++++++++++++++++-- applications/PoP/roomtest/pop_status.h | 14 ++ applications/PoP/roomtest/pop_timer.c | 17 +- applications/PoP/roomtest/pop_timer.h | 6 + applications/PoP/roomtest/roomtest.c | 18 +- 8 files changed, 320 insertions(+), 27 deletions(-) diff --git a/applications/PoP/docs/impl_diff.md b/applications/PoP/docs/impl_diff.md index 40976b8..09099c6 100644 --- a/applications/PoP/docs/impl_diff.md +++ b/applications/PoP/docs/impl_diff.md @@ -603,3 +603,47 @@ seg008.c:1600). XOR идёт по 24-битному RGB того, что УЖЕ **Что проверять при регрессе.** Что после неудачной операции на экране остаётся именно `NO QUICKSAVE`/`NO QUICKLOAD`, а не первая строка: отказ идёт обычной заявкой и печатается кадровым проходом, то есть на кадр позже. + + +## Смерть Кида: ждём кнопку и перезапускаем УРОВЕНЬ, а не игру + +**Как в оригинале.** `play_kid` (seg006:1383) печатает «Press Button to +Continue» с `text_time_total = 288`. Тик — это логический игровой кадр, +720 тиков = минута, то есть 12 тиков в секунду: 288 тиков = **24 секунды**. +Последние 72 тика (6 секунд) строка мигает с периодом 12 тиков, и на каждом +появлении играет звук 38. Дальше развилок ровно две: + +* игрок молчит все 24 секунды — `draw_game_frame` (seg000:958) зовёт + `start_game()`, и игра начинается ЗАНОВО, с title, а не с уровня; +* игрок нажимает **Enter или Shift** (не любую клавишу!) — seg000:584 + подменяет их на Ctrl+A: `if (rem_min != 0 && Kid.alive > 6 && (control_shift + || key == SDL_SCANCODE_RETURN)) key = SDL_SCANCODE_A | WITH_CTRL;` — и + уровень перезапускается. Условия важны: время не должно быть исчерпано + (иначе отработал `expired()`), а `Kid.alive > 6` даёт трупу улечься. + +**У нас.** Обе развилки сведены к одной: 24-секундного выхода в начало +игры нет вовсе, строка висит бессрочно (`MSG_HOLD`), а перезапускает уровень +ЛЮБАЯ кнопка, а не только Enter/Shift (решение пользователя). +`pop_start_level()` возвращает игрока на уровень. Место возрождения выбирает сам `pop_start_level` — на части +уровней это не старт, а пройденный чекпойнт. Esc за кнопку продолжения не +считается: он открывает pause menu. Логика ожидания живёт в банке +(`pop_dead_prompt`, pop_status.c) — резидент W1 переполнен. + +**Зачем.** Решение пользователя, 2026-08-25: возврат к title после каждой +смерти в отладочной сборке съедает всё время прохода, а прежний вариант +(авто-респавн через 400 кадров либо стрелка вверх) не объяснял игроку, чего +от него ждут. + +**Чем платим.** Двумя вещами. Первое: смерть больше не заканчивает +партию — счёт попыток фактически бесконечен, тогда как оригинал через 24 +секунды бездействия отправляет в title. Второе: любая клавиша вместо +Enter/Shift означает, что случайное нажатие (например, ещё не отпущенная +после боя клавиша) перезапустит уровень — отсюда требование сперва отпустить +всё. Когда дойдёт до «настоящей» игры, обе развилки придётся выбирать +заново: вернуть таймер на 288 тиков со start_game и сузить клавиши до +Enter/Shift — либо оставить как есть уже осознанно. + +**Что проверять при регрессе.** Нажатие принимается только после того, как +отпущено ВСЁ, что игрок держал в момент смерти (иначе зажатая при падении +стрелка перезапускает уровень мгновенно), и не раньше `RESPAWN_SETTLE` +кадров — труп должен успеть лечь. diff --git a/applications/PoP/docs/status_line_text.md b/applications/PoP/docs/status_line_text.md index 4b28649..75cd199 100644 --- a/applications/PoP/docs/status_line_text.md +++ b/applications/PoP/docs/status_line_text.md @@ -151,11 +151,23 @@ display_text_bottom: draw_rect(чёрным) + show_text(halign_center, valign_b молчат, тринадцатый показывается двенадцатым, бесшовный переход 12→13 пропускается и гасит флаг за собой. +* вся группа времени — `N MINUTES LEFT`, `N SECONDS LEFT`, `1 SECOND LEFT`, + `TIME HAS EXPIRED!`. Флаг `pop_show_time` (порт `is_show_time`) взводит + само ядро таймера на круглых пятёрках и каждую секунду последней минуты, + а также старт уровня и читы времени; значение 2 означает «перебить + текущую строку», как оригинал делает в последнюю минуту; +* `Press Button to Continue` — висит бессрочно (`MSG_HOLD`), уровень + перезапускает кнопка. Расхождение с оригиналом, см. `impl_diff.md`. + Пока НЕ печатается: -* вся группа времени — таймер есть (`pop_timer.c`), текста нет; -* `Press Button to Continue` — у нас авто-респавн по таймеру/стрелке вверх; -* номера комнат (`C`/Shift+`C`) — у нас отдельная отладочная строка. +* номера комнат (`C`/Shift+`C`) — у нас отдельная отладочная строка; +* copy protection и SDLPoP-расширения (replay, скриншоты) — не нужны. + +Отладочная строка вдобавок показывает оставшееся время `##:##` у правого +края. На табло уходит `minutes-1`: у оригинала `rem_min` — это НОМЕР идущей +минуты, а не остаток целых (старт 60 при `rem_tick` 719 = «почти 60:00»). +Секунды считаются делением раз в 12 кадров, а не каждый кадр. Нам не нужно: copy protection (уровень 15 исключён из порта — см. `full_game_plan.md`), joystick-режимы, replay, скриншоты. diff --git a/applications/PoP/roomtest/pop_app.c b/applications/PoP/roomtest/pop_app.c index 42a23eb..ce54778 100644 --- a/applications/PoP/roomtest/pop_app.c +++ b/applications/PoP/roomtest/pop_app.c @@ -75,6 +75,11 @@ uint8_t pop_app_dispatch(pop_app_event_t event) __banked next = POP_APP_TIME_EXPIRED; accepted = 1; } else if (event == POP_APP_EV_ENDING_STARTED) { next = POP_APP_ENDING; accepted = 1; + } else if (event == POP_APP_EV_RESTART_INTRO) { + /* start_game() прямо из геймплея: так оригинал уходит в title, + * когда игрок не нажал кнопку за 288 тиков после смерти + * (seg000:958). Раньше это событие принимал только pause menu. */ + next = POP_APP_TITLE; accepted = 1; } break; diff --git a/applications/PoP/roomtest/pop_status.c b/applications/PoP/roomtest/pop_status.c index 2ceaaa5..38704e6 100644 --- a/applications/PoP/roomtest/pop_status.c +++ b/applications/PoP/roomtest/pop_status.c @@ -38,21 +38,27 @@ #include #include #include +#include #include "pop_status.h" #include "pop_ui.h" #include "pop_font.h" #include "pop_bg.h" /* POP_YOFF */ #include "pop_cdraw.h" /* POP_HP_Y — полосу делим с делениями жизней */ #include "pop_settings.h" +#include "pop_app.h" /* start_game после 288 тиков молчания */ #include "pop_state.h" /* pop_immortal */ #include "pop_level.h" /* pop_current_level */ #include "pop_pace.h" /* pop_speed_mode */ +#include "pop_timer.h" /* оставшееся время и флаг «пора показать» */ #include "pop_sfx.h" /* pop_snd_want */ /* ---- нижняя полоса: статус-строка -------------------------------------- */ /* Как у оригинала: 24 тика (text_time_total = 24) — около двух секунд. */ #define MSG_TICKS 24 +/* «Висит, пока не снимут»: у оригинала эту роль играет особое значение + * text_time_total (288 у «Press Button», 1188 у copy protection). */ +#define MSG_HOLD 0xFF /* Базовая линия та же, что у GAME PAUSED (pop_menu.c): строка ниже делений. */ #define MSG_BASE (POP_YOFF + POP_HP_Y + 8) #define MSG_TOP (MSG_BASE - POP_FONT_BIG_ASCENT - 2) @@ -67,7 +73,11 @@ static const char * const status_text[POP_MSG_N] = { "NO QUICKLOAD", "SOUND ON", "SOUND OFF", - "" + "", /* LEVEL N — собирается с номером */ + "", /* N MINUTES LEFT */ + "", /* N SECONDS LEFT */ + "TIME HAS EXPIRED!", + "Press Button to Continue" }; uint8_t pop_status_msg; /* заявка (пишут откуда угодно) */ @@ -90,7 +100,7 @@ static uint8_t st_arg; /* номер уровня для POP_MSG_LEVEL * «Speed:» оставалась одинокая точка. */ #define DBG_BOTTOM 7 #define DBG_LEFT 8 -#define DBG_RIGHT 254 +#define DBG_RIGHT 310 /* Значения ПРИЖАТЫ ВПРАВО к границе своей зоны, а подпись следующего поля * начинается сразу за ней. Так запятая всегда стоит вплотную к значению — @@ -107,6 +117,10 @@ static uint8_t st_arg; /* номер уровня для POP_MSG_LEVEL #define R_VAL_SOUND 202 /* зона 185..201 */ #define X_LBL_IMM 202 /* ", Immortal" 202..243 */ #define R_VAL_IMM 253 /* зона 244..252: цифре нужен зазор в пробел */ +/* Оставшееся время ##:## — отдельным полем у правого края, подписи не + * требует: формат читается сам. */ +#define R_VAL_TIME 310 /* зона 284..309 */ +#define W_VAL_TIME 26 /* Ширина зоны значения — под самый длинный вариант плюс воздух. */ #define W_VAL_NUM 13 /* две цифры */ @@ -120,12 +134,15 @@ enum { /* биты маски «что перерис DBG_F_SPEED = 0x04, DBG_F_SOUND = 0x08, DBG_F_IMM = 0x10, - DBG_F_ALL = 0x1F /* + подписи: см. dbg_labels */ + DBG_F_TIME = 0x20, + DBG_F_ALL = 0x3F /* + подписи: см. dbg_labels */ }; static const char * const speed_text[] = { "NORMAL", "FAST", "FASTEST" }; static uint8_t dbg_lvl, dbg_room, dbg_spd, dbg_snd, dbg_imm; +static uint8_t dbg_tmin, dbg_tsec; /* показанное время ##:## */ +static uint8_t dbg_phase; /* кадры до пересчёта секунд */ static uint8_t dbg_dirty[2]; /* маска по странице; DBG_F_ALL => и подписи */ static uint8_t dbg_on; /* показанное состояние тумблера Settings */ @@ -152,26 +169,61 @@ static void dec2(uint8_t v, char *buf) buf[1] = 0; } +/* То же, но ВСЕГДА две цифры: часы-минуты без ведущего нуля не читаются. */ +static void dec2z(uint8_t v, char *buf) +{ + uint8_t t = 0; + while (v >= 10) { v = (uint8_t)(v - 10); t++; } + buf[0] = (char)('0' + t); + buf[1] = (char)('0' + v); + buf[2] = 0; +} + /* ---- нижняя полоса ----------------------------------------------------- */ -/* «LEVEL N» собирается вручную: printf сюда не тянем, а номер — единственное - * переменное место во всём наборе сообщений. */ -static void level_text(char *buf) +/* Строки с числом собираются вручную: printf сюда не тянем, а число — + * единственное переменное место во всём наборе сообщений. */ +static char *put(char *buf, const char *s) { - buf[0] = 'L'; buf[1] = 'E'; buf[2] = 'V'; buf[3] = 'E'; buf[4] = 'L'; - buf[5] = ' '; - dec2(st_arg, buf + 6); + while (*s) *buf++ = *s++; + return buf; +} + +/* Число ВПЕРЕДИ («12 MINUTES LEFT»), как у оригинала. */ +static void num_text(char *buf, const char *suffix) +{ + dec2(st_arg, buf); + while (*buf) buf++; + *put(buf, suffix) = 0; +} + +static void build_text(char *buf) +{ + if (st_shown == POP_MSG_LEVEL) { + *put(put(buf, "LEVEL "), "") = 0; + dec2(st_arg, buf + 6); + } else if (st_shown == POP_MSG_MINUTES) { + num_text(buf, " MINUTES LEFT"); + } else if (st_arg == 1) { + *put(buf, "1 SECOND LEFT") = 0; + } else { + num_text(buf, " SECONDS LEFT"); + } } static void status_draw(void) { - char buf[9]; + char buf[18]; const char *text; wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM); if (!st_shown) return; - if (st_shown == POP_MSG_LEVEL) { level_text(buf); text = buf; } - else { text = status_text[st_shown]; } + if (st_shown >= POP_MSG_LEVEL && st_shown <= POP_MSG_SECONDS) { + build_text(buf); + text = buf; + } else { + text = status_text[st_shown]; + } gfx_set_bank(GFX_BANK_TRANSPARENT); pop_text_map(); pop_text_center_mapped(POP_TEXT_BIG, (POP_STATUS_L + POP_STATUS_R) / 2, @@ -179,9 +231,24 @@ static void status_draw(void) pop_text_unmap(); } +/* Заявка сообщения С ЧИСЛОМ. Обычная (байтовая) заявка сравнивает только + * номер сообщения, а здесь меняться может одно лишь число — «12 SECONDS + * LEFT» и «11 SECONDS LEFT» это один и тот же POP_MSG_SECONDS, и по номеру + * их не различить. Поэтому ставим состояние сразу и сами взводим грязь. */ +static void msg_set(uint8_t msg, uint8_t arg, uint8_t ticks) +{ + pop_status_msg = msg; + st_shown = msg; + st_arg = arg; + pop_status_ticks = ticks; + st_dirty = 3; +} + /* Порт show_level (seg008:25A8). Демо-уровень 0 и номера от 14 не * показываются вовсе, тринадцатый представляется двенадцатым, а бесшовный - * переход 12->13 молчит — и, как в оригинале, гасит флаг за собой. */ + * переход 12->13 молчит — и, как в оригинале, гасит флаг за собой. + * Следом оригинал просит показать и время: за «LEVEL 1» идёт «60 MINUTES + * LEFT», когда первая строка отвисит своё. */ void pop_status_level(void) __banked { uint8_t n = pop_current_level; @@ -190,8 +257,29 @@ void pop_status_level(void) __banked pop_seamless = 0; if (quiet || n == 0 || n >= POP_LEVEL_LAST) return; if (n == 13) n = 12; - st_arg = n; - pop_status_msg = POP_MSG_LEVEL; + msg_set(POP_MSG_LEVEL, n, MSG_TICKS); + pop_show_time = 1; +} + +/* Вторая половина show_time (seg008:1812): собственно печать. Флаг + * pop_show_time == 2 означает «перебить текущую строку немедленно» — + * так оригинал ведёт себя в последнюю минуту и после читов времени. */ +static void time_msg(void) +{ + if (!pop_show_time) return; + if (pop_show_time == 2) pop_status_ticks = 0; + else if (pop_status_ticks) return; /* ждём, пока освободится строка */ + pop_show_time = 0; + + if (pop_timer_minutes == 0) { + msg_set(POP_MSG_EXPIRED, 0, MSG_TICKS); + } else if (pop_timer_minutes == 1) { + /* Секунда — 12 тиков; +1 потому, что ticks уже уменьшен. */ + uint8_t sec = (uint8_t)((pop_timer_ticks + 1) / 12); + msg_set(POP_MSG_SECONDS, sec, (uint8_t)(sec == 1 ? 12 : MSG_TICKS)); + } else { + msg_set(POP_MSG_MINUTES, pop_timer_minutes, MSG_TICKS); + } } /* Показать сообщение НЕМЕДЛЕННО, не дожидаясь конца кадра. @@ -253,7 +341,7 @@ static void dbg_labels(void) static void dbg_draw(uint8_t mask) { - char buf[3]; + char buf[6]; /* Полная перерисовка стирает всю полосу разом — значит зоны значений * стирать по второму разу не надо, но нарисовать их всё равно нужно. */ @@ -276,6 +364,12 @@ static void dbg_draw(uint8_t mask) buf[1] = 0; dbg_value(R_VAL_IMM, W_VAL_IMM, buf); } + if (mask & DBG_F_TIME) { + dec2z(dbg_tmin, buf); + buf[2] = ':'; + dec2z(dbg_tsec, buf + 3); + dbg_value(R_VAL_TIME, W_VAL_TIME, buf); + } } /* Снимок ДО сравнения — обход sdcc_z80_cmp_store_a_bug: `if (n != s) s = n;` @@ -284,8 +378,26 @@ static void dbg_scan(uint8_t room) { uint8_t prev_lvl = dbg_lvl, prev_room = dbg_room, prev_spd = dbg_spd; uint8_t prev_snd = dbg_snd, prev_imm = dbg_imm; + uint8_t prev_tmin = dbg_tmin, prev_tsec = dbg_tsec; uint8_t mask = 0; + /* Часы-минуты: у оригинала rem_min — это НОМЕР идущей минуты, а не + * остаток целых минут (старт 60 при rem_tick 719 = «почти 60:00»), + * поэтому на табло уходит minutes-1 и секунды из тиков. + * + * Секунды — единственное деление на этом пути, поэтому считаем их не + * каждый кадр, а раз в двенадцать (тик секунды у SDLPoP) и вдобавок + * сразу, если минуты изменились (чит времени, загрузка снимка). */ + { + uint8_t mm = (uint8_t)(pop_timer_minutes ? pop_timer_minutes - 1 : 0); + if (dbg_phase) dbg_phase--; + if (!dbg_phase || dbg_tmin != mm) { + dbg_phase = 12; + dbg_tsec = (uint8_t)(pop_timer_minutes ? pop_timer_ticks / 12 : 0); + } + dbg_tmin = mm; + } + dbg_lvl = pop_current_level; dbg_room = room; dbg_spd = pop_speed_mode; @@ -296,12 +408,86 @@ static void dbg_scan(uint8_t room) if (dbg_spd != prev_spd) mask |= DBG_F_SPEED; if (dbg_snd != prev_snd) mask |= DBG_F_SOUND; if (dbg_imm != prev_imm) mask |= DBG_F_IMM; + if (dbg_tmin != prev_tmin || dbg_tsec != prev_tsec) mask |= DBG_F_TIME; if (mask) { dbg_dirty[0] |= mask; dbg_dirty[1] |= mask; } } +/* ---- смерть Кида ------------------------------------------------------- * + * + * Порт play_kid (seg006:1383): вместо авто-респавна по таймеру уровень + * перезапускает сам игрок кнопкой, а строка висит, пока он этого не сделал + * (MSG_HOLD). Авто-рестарт оригинала по истечении 288 тиков нам не нужен: + * он там начинает игру заново, а мы возвращаем на чекпойнт. */ +#define DEAD_SETTLE 7 /* Kid.alive > 6: труп успел лечь */ +#define DEAD_TICKS 288 /* text_time_total у «Press Button» */ +#define DEAD_BLINK 72 /* последние 72 тика строка мигает */ +#define SND_BLINK 38 /* sound_38_blink на каждом появлении */ + +static uint8_t dead_armed; /* всё отпущено — можно принимать нажатие */ + +uint8_t pop_dead_prompt(uint16_t frames) __banked +{ + uint16_t rem; + + if (frames < DEAD_SETTLE) return 0; + if (frames == DEAD_SETTLE) { + msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD); + pop_show_time = 0; /* иначе поверх ляжет время (seg006:1365) */ + dead_armed = 0; + return 0; + } + + /* Свой отсчёт, а не pop_status_ticks: тот 8-битный, а здесь нужно 288. + * Кадры смерти считает вызывающий, так что хватает вычитания. */ + rem = (uint16_t)(frames - DEAD_SETTLE); + rem = (uint16_t)(rem >= DEAD_TICKS ? 0 : DEAD_TICKS - rem); + if (rem == 0) { + /* Игрок промолчал все 24 секунды — оригинал зовёт start_game(), + * то есть уходит в title, а НЕ перезапускает уровень. Событие + * шлём отсюда: pop_app живёт в этом же банке, и резиденту остаётся + * один break вместо вызова с проверкой. */ + msg_set(POP_MSG_NONE, 0, 0); + (void)pop_app_dispatch(POP_APP_EV_RESTART_INTRO); + return 2; + } + + /* Мигание последних шести секунд: 4 тика видно, 8 нет; на кадре + * появления оригинал перепечатывает строку и играет звук 38. */ + if (rem < DEAD_BLINK) { + uint8_t phase = (uint8_t)(rem % 12); + if (phase > 3) { + if (st_shown) msg_set(POP_MSG_NONE, 0, 0); + } else if (phase == 3 && !st_shown) { + msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD); + pop_sfx_play(SND_BLINK); + } + } + + /* Рестарт уровня — Enter или Shift (seg000:584 подменяет их на Ctrl+A), + * а не любая клавиша. Esc сюда не попадает: он открывает меню и + * разобран раньше в этом же кадре. + * + * Отпускания оригинал не ждёт, а мы ждём: Shift у нас — клавиша + * действия, и зажатый в момент смерти он перезапустил бы уровень + * мгновенно, не дав прочитать строку. */ + { + uint8_t any = (uint8_t)(kbd_raw_down(KBD_ENTER) || + kbd_raw_down(KBD_LSHIFT) || + kbd_raw_down(KBD_RSHIFT)); + if (!dead_armed) { + dead_armed = (uint8_t)!any; + return 0; + } + if (!any) return 0; + } + dead_armed = 0; + msg_set(POP_MSG_NONE, 0, 0); + return 1; +} + /* ---- кадровая точка ---------------------------------------------------- */ void pop_status_invalidate(void) __banked @@ -322,11 +508,14 @@ void pop_status_frame(uint8_t room) __banked saved = gfx_get_bank(); /* --- нижняя полоса: заявка, таймер, перерисовка --- */ + time_msg(); if (pop_status_msg != st_shown) { st_shown = pop_status_msg; - pop_status_ticks = (uint8_t)(st_shown ? MSG_TICKS : 0); + pop_status_ticks = (uint8_t)(!st_shown ? 0 : + (st_shown == POP_MSG_PRESS_BUTTON ? MSG_HOLD + : MSG_TICKS)); st_dirty = 3; - } else if (pop_status_ticks) { + } else if (pop_status_ticks && pop_status_ticks != MSG_HOLD) { pop_status_ticks--; if (!pop_status_ticks) { /* Оригинал по истечении зовёт erase_bottom_text(1) — снимаем и diff --git a/applications/PoP/roomtest/pop_status.h b/applications/PoP/roomtest/pop_status.h index dbfd659..eb1a61a 100644 --- a/applications/PoP/roomtest/pop_status.h +++ b/applications/PoP/roomtest/pop_status.h @@ -27,6 +27,10 @@ enum { POP_MSG_SOUND_ON, /* Ctrl+S */ POP_MSG_SOUND_OFF, POP_MSG_LEVEL, /* «LEVEL N» — строка собирается с номером */ + POP_MSG_MINUTES, /* «N MINUTES LEFT» */ + POP_MSG_SECONDS, /* «N SECONDS LEFT» / «1 SECOND LEFT» */ + POP_MSG_EXPIRED, /* «TIME HAS EXPIRED!» */ + POP_MSG_PRESS_BUTTON, /* Кид мёртв — ждём кнопку (висит бессрочно)*/ POP_MSG_N }; @@ -61,6 +65,16 @@ extern uint8_t pop_status_ticks; * пока ничего не менялось, не рисует вовсе. */ void pop_status_frame(uint8_t room) __banked; +/* Кид мёртв: показать «Press Button to Continue» и ждать кнопку. frames — + * сколько кадров прошло со смерти (труп должен успеть лечь). Возвращает 1 + * ОДИН раз, когда игрок нажал — вызывающему остаётся перезапустить уровень. + * Логика живёт в банке, потому что резидент W1 переполнен, а зовут её лишь + * в кадрах смерти. + * + * Возвращает: 0 — ждём дальше; 1 — игрок нажал Enter/Shift, перезапустить + * УРОВЕНЬ; 2 — 288 тиков истекли, уйти в title (порт start_game). */ +uint8_t pop_dead_prompt(uint16_t frames) __banked; + /* Заставить перерисовать ОБЕ страницы: звать после того, как полосы * затёрли чужим содержимым (вход в комнату, выход из меню). */ void pop_status_invalidate(void) __banked; diff --git a/applications/PoP/roomtest/pop_timer.c b/applications/PoP/roomtest/pop_timer.c index 60bd8c4..58ed531 100644 --- a/applications/PoP/roomtest/pop_timer.c +++ b/applications/PoP/roomtest/pop_timer.c @@ -12,11 +12,13 @@ uint8_t pop_timer_minutes; uint16_t pop_timer_ticks; +uint8_t pop_show_time; void pop_timer_new_game(void) __banked { pop_timer_minutes = POP_TIMER_START_MINUTES; pop_timer_ticks = POP_TIMER_START_TICKS; + pop_show_time = 0; } uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run) __banked @@ -24,10 +26,21 @@ uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run) __banked if (!enabled || !may_run || pop_timer_minutes == 0) return 0; --pop_timer_ticks; - if (pop_timer_ticks != 0) return 0; + if (pop_timer_ticks != 0) { + /* Последняя минута: оригинал напоминает о времени КАЖДУЮ секунду + * (rem_tick % 12 == 0) и специально гасит текущую строку, чтобы + * новая встала немедленно, не дожидаясь её 24 тиков. */ + if (pop_timer_minutes == 1 && pop_timer_ticks % 12 == 0) + pop_show_time = 2; /* 2 = «показать, перебив текущую» */ + return 0; + } pop_timer_ticks = POP_TIMER_START_TICKS; --pop_timer_minutes; + /* Круглые пятёрки и весь финальный отсчёт: seg008:1795. */ + if (pop_timer_minutes != 0 && + (pop_timer_minutes <= 5 || pop_timer_minutes % 5 == 0)) + pop_show_time = 1; return (uint8_t)(pop_timer_minutes == 0); } @@ -36,6 +49,7 @@ void pop_timer_cheat_subtract(void) __banked /* Это буквально правило SDLK_KP_MINUS: последнюю минуту читом не * отнимаем, иначе не получится отличить чит от настоящего expiry. */ if (pop_timer_minutes > 1) --pop_timer_minutes; + pop_show_time = 2; /* оригинал показывает результат сразу */ } void pop_timer_cheat_add(void) __banked @@ -43,6 +57,7 @@ void pop_timer_cheat_add(void) __banked /* SDLPoP хранит signed short, у нас достаточно byte: 255 минут — * практический максимум и насыщение не даёт тихо свернуться в нуль. */ if (pop_timer_minutes != 0xFF) ++pop_timer_minutes; + pop_show_time = 2; } void pop_timer_qsave(pop_qs_io_t *io) __banked diff --git a/applications/PoP/roomtest/pop_timer.h b/applications/PoP/roomtest/pop_timer.h index 7a90fd0..b17c97b 100644 --- a/applications/PoP/roomtest/pop_timer.h +++ b/applications/PoP/roomtest/pop_timer.h @@ -22,6 +22,12 @@ extern uint8_t pop_timer_minutes; extern uint16_t pop_timer_ticks; +/* «Пора показать оставшееся время» — порт is_show_time (seg008). Взводит + * и сам таймер на круглых минутах, и внешние события: старт уровня, клавиша + * Space, читы +/-, смерть Джафара. Гасит его тот, кто напечатал строку + * (pop_status.c) — ядро таймера про экран по-прежнему ничего не знает. */ +extern uint8_t pop_show_time; + /* Начать новую игру: ровно исходные 60:00 SDLPoP (60 минут, tick 719). */ void pop_timer_new_game(void) __banked; diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 3ba2359..55214c0 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -28,6 +28,7 @@ #include "roomtest_cold.h" /* холодная половина цикла — в банке */ #include "pop_qsave.h" #include "pop_status.h" +#include "pop_timer.h" #include "pop_menu.h" /* pause menu — банк 9, команды наружу */ #include "pop_settings.h" /* единая модель скорости/звука/читов */ #include "pop_app.h" /* автомат приложения: игровой цикл = PLAYING */ @@ -132,8 +133,6 @@ static void kbd_idle(void) #define KBD_QUIT 0x09 /* F10 — отладочный немедленный выход (был на Esc, * пока меню не готово рисовать — см. план MS4) */ -#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ -#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ /* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного * ряда) — следующая/предыдущая комната уровня ПО НОМЕРУ (1..24, с обёрткой), @@ -581,7 +580,12 @@ int main(void) if (!is_demo && pop_timer_tick(pop_settings.time_limit_enabled, (uint8_t)(Kid.alive < 0 && (pop_current_level < 13 || - (pop_current_level == 13 && pop_leveldoor_open == 0))))) { + (pop_current_level == 13 && pop_leveldoor_open == 0)))) && + pop_current_level < 13) { + /* Время ИДЁТ и на уровне Джафара (до открытия двери), но + * КОНЧИТЬСЯ игра там уже не может: expired() у оригинала стоит + * под отдельным `current_level < 13` (seg000:910). На 13-м + * истечение оставляет только строку «TIME HAS EXPIRED!». */ if (!pop_app_dispatch(POP_APP_EV_TIME_EXPIRED)) failed = 1; break; } @@ -811,8 +815,12 @@ int main(void) } if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */ dead_frames++; - if (dead_frames > RESPAWN_SETTLE && - (kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) { + /* Просьба нажать кнопку, мигание и таймаут — в банке + * (pop_status.h). Место возрождения выбирает pop_start_level: + * на части уровней это НЕ старт, а уже пройденный чекпойнт. */ + uint8_t dr = pop_dead_prompt(dead_frames); + if (dr == 2) break; /* 288 тиков молчания -> title */ + if (dr) { dead_frames = 0; pop_start_level(); gfx_set_draw_page(back);