From a3aaa30e53ee896b106799c7e18b4cea59750392 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 31 Aug 2026 20:23:35 +0300 Subject: [PATCH] =?UTF-8?q?SprPoP:=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20?= =?UTF-8?q?=D1=81=D0=BC=D0=B5=D1=80=D1=82=D0=B8=20=D0=BE=D1=82=20=D0=BC?= =?UTF-8?q?=D0=B5=D1=87=D0=B0=20(=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=B4=D0=BE=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D0=BA=2012/13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit КОД ИГРЫ НЕ МЕНЯЛСЯ. Новый набор t_death — 9 проверок, всего наборов 16. Заведён ПЕРЕД правкой находок 12/13, чтобы поймать деградацию: правка 13 в изоляции уже ломала смерть (мёртвый оставался с ненулевой скоростью падения, проваливался за нижнюю границу, игра уходила на рестарт, не показав тела). Поэтому проверяется ровно то, на что эти правки влияют: * удар не в боевой стойке смертелен независимо от запаса HP; * удар с мечом снимает одно HP, на последнем — убивает; * ПЕРЕЖИВШИЙ удар ставится на пол своего ряда с нулевой скоростью падения (в оригинале это единственная ветка, где координата трогается); * тело после смерти остаётся в своём ряду — и на ровном полу, и у самого обрыва (целевая сцена находки 12; после правки ожидание изменится осознанно). По дороге тест дважды показал не баг движка, а мои ошибки в самой сцене: обвязка выставляет признак «жив» только стражу, а урон применяется не сразу — удар выставляет дельту, и HP меняет отдельный шаг кадра, как в оригинале. Оба раза чинился тест. VANILLA/BUGFIXED: записано, что переключатель уже существует в настройках и зафиксирован в положении VANILLA, второй заводить не нужно. Отмечено главное следствие — наш «ванильный» билд УЖЕ не чистая ваниль (часть ванильных багов пофикшена), поэтому при разделении режимов придётся пройтись по сделанным отступлениям и распределить их; отдельные фиксы (падение сквозь стену) могут быть сделаны и в нынешнем билде. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY --- .../SprPoP/docs/vanilla_vs_bugfixed.md | 24 +++- applications/SprPoP/tests/host/Makefile | 3 + applications/SprPoP/tests/host/t_death.c | 126 ++++++++++++++++++ 3 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 applications/SprPoP/tests/host/t_death.c diff --git a/applications/SprPoP/docs/vanilla_vs_bugfixed.md b/applications/SprPoP/docs/vanilla_vs_bugfixed.md index 16c5e65..f45b9cc 100644 --- a/applications/SprPoP/docs/vanilla_vs_bugfixed.md +++ b/applications/SprPoP/docs/vanilla_vs_bugfixed.md @@ -67,12 +67,28 @@ То есть когда появится BUGFIXED, этому тесту понадобится ожидание, зависящее от режима, — и он готов стать первым таким. +## Переключатель уже есть — второго не нужно + +Уточнено 2026-08-31: в настройках игры ПЕРЕКЛЮЧАТЕЛЬ VANILLA/ENHANCED +СУЩЕСТВУЕТ (`docs/menu_settings_plan.md`), просто сейчас он жёстко +зафиксирован в положении VANILLA. Отдельную сущность заводить не надо — +эта задача про то, чтобы наполнить смыслом уже имеющееся положение +ENHANCED. + +**Наш «ванильный» билд уже не чистая ваниль.** Часть ванильных багов у +нас пофикшена по ходу портирования. Значит: + +* нельзя считать текущее поведение эталоном ванили — оно смешанное; +* при разделении режимов придётся пройтись по уже сделанным фиксам и + решить по каждому, остаётся он в VANILLA или уезжает в ENHANCED; +* и наоборот: отдельные исправления (например, падение сквозь стену) + вполне могут быть сделаны прямо в нынешнем «ванильном» билде, если + сочтём, что играбельность важнее буквальности. + ## Что предстоит решить -1. **Как переключать.** Ключ сборки (два разных `.exe`) или настройка в - `POP.CFG` с проверкой в рантайме. У нас уже есть VANILLA/ENHANCED в - меню настроек (`docs/menu_settings_plan.md`) — возможно, это то же - самое измерение и стоит объединить, а не заводить второе. +1. **Что считать ванилью на практике.** Составить список уже сделанных + отступлений от оригинала и распределить их между режимами. 2. **Цена рантайм-проверки.** Условия попадают в физику и `play_seq`, то есть в горячий путь. Если переключать в рантайме, проверка должна быть дешевле самого фикса: флаг в резиденте, а не вызов через банк. diff --git a/applications/SprPoP/tests/host/Makefile b/applications/SprPoP/tests/host/Makefile index 28b1542..6dec110 100644 --- a/applications/SprPoP/tests/host/Makefile +++ b/applications/SprPoP/tests/host/Makefile @@ -65,6 +65,9 @@ OBJS_mouse := $(OBJS_phys) build/eng_guards.rel OBJS_shadow := $(OBJS_phys) build/eng_guards.rel # t_jaffar — спецсобытия уровня 13 (встреча, победа, выход, падающая гряда). OBJS_jaffar := $(OBJS_phys) build/eng_guards.rel +# t_death — смерть от меча: HP, признак смерти и, главное, координата с +# остаточной скоростью падения (находки 12/13 в docs/sdlpop_audit.md). +OBJS_death := $(OBJS_phys) build/eng_guards.rel # t_cfg — весь codec POP.CFG из единственного прикладного модуля. OBJS_cfg := build/eng_pop_config.rel build/bank_stub.rel # pop_app — чистая таблица переходов, без DSS/графики. diff --git a/applications/SprPoP/tests/host/t_death.c b/applications/SprPoP/tests/host/t_death.c new file mode 100644 index 0000000..dc3580d --- /dev/null +++ b/applications/SprPoP/tests/host/t_death.c @@ -0,0 +1,126 @@ +/* + * t_death — смерть от меча: что происходит с координатой и состоянием. + * + * Набор заведён ПЕРЕД правкой находок 12/13 (docs/sdlpop_audit.md), чтобы + * зафиксировать нынешнее поведение и поймать деградацию. История вопроса: + * правка 13 в изоляции уже ломала смерть — мёртвый Кид оставался с + * ненулевой скоростью падения, проваливался за нижнюю границу и игра + * уходила на рестарт, не показав тела. + * + * Проверяем ровно то, на что эти правки влияют: + * - HP и признак смерти; + * - КООРДИНАТУ по Y и остаточную скорость падения (в этом вся суть); + * - что смерть наступает при ударе не в боевой стойке (порт оригинала: + * «ранение вне боевой стойки означает смерть»). + */ +#include "tcheck.h" +#include "scene.h" +#include "stubs.h" +#include "pop_kid.h" +#include "pop_map.h" +#include "pop_guard.h" + +#define E 0 +#define F 1 +#define W 20 + +/* Ровный пол во всю ширину: ряд 2 — пол, выше пусто. Комната без краёв, + * чтобы смерть не смешивалась с падением. */ +static const uint8_t room_flat[30] = { + E, E, E, E, E, E, E, E, E, E, + E, E, E, E, E, E, E, E, E, E, + F, F, F, F, F, F, F, F, F, F, +}; + +/* Площадка, обрывающаяся справа: колонки 0-4 — пол, 5-9 пусто. Кид у + * самого края спиной к обрыву — целевая сцена находки 12 (в оригинале + * такой удар отправляет в отдельную последовательность падения замертво). */ +static const uint8_t room_ledge[30] = { + E, E, E, E, E, E, E, E, E, E, + E, E, E, E, E, E, E, E, E, E, + F, F, F, F, F, E, E, E, E, E, +}; + +/* Поставить сцену «страж бьёт Кида» и нанести удар. */ +static void strike_kid(const uint8_t *room, uint8_t col, uint8_t hp, uint8_t sword) +{ + sc_room(room, 1); + sc_kid_at(col, 2, 1 /* лицом вправо */); + hitp_curr = hp; + Kid.sword = sword; + pop_kid_dead = 0; + /* Признак «жив» обвязка ставит только стражу, поэтому Киду задаём его + * сами: обработчик удара первым делом отсекает уже мёртвого. */ + Kid.alive = -1; + /* Удар оформляется действием 99 «ранен» — так его помечает боевая + * проверка оригинала, а разбирает pop_check_sword_hurt. */ + Kid.action = 99; + pop_check_sword_hurt(); + /* Удар выставляет ДЕЛЬТУ, а HP меняет отдельный шаг кадра — как в + * оригинале. Без него hitp_curr остался бы прежним, и тест мерил бы + * не то. */ + pop_do_delta_hp(); +} + +TC_TEST(death_unarmed_hit_is_lethal) +{ + strike_kid(room_flat, 4, 3 /* HP */, 0 /* без меча */); + /* Удар не в боевой стойке смертелен независимо от запаса HP. */ + TC_EQ(hitp_curr, 0); +} + +TC_TEST(death_armed_hit_costs_one_hp) +{ + strike_kid(room_flat, 4, 3, 2 /* меч вынут */); + TC_EQ(hitp_curr, 2); +} + +TC_TEST(death_armed_last_hp_kills) +{ + strike_kid(room_flat, 4, 1, 2); + TC_EQ(hitp_curr, 0); +} + +/* --- КООРДИНАТА ПОСЛЕ УДАРА: суть находок 12 и 13 -------------------- */ + +TC_TEST(survivor_is_placed_on_floor) +{ + /* Пережил удар — оригинал ставит его на пол своего ряда и гасит + * скорость падения. Это единственная ветка, где он так делает. */ + strike_kid(room_flat, 4, 3, 2); + TC_EQ(Kid.fall_y, 0); + TC_EQ(Kid.y, pop_y_land[Kid.curr_row + 1]); +} + +TC_TEST(death_on_flat_floor_keeps_body_in_place) +{ + /* Смерть на ровном полу: тело обязано остаться в своём ряду. Если + * координата или скорость падения уедут, физика утащит труп вниз — + * ровно так ломалась игра при правке 13 в одиночку. */ + strike_kid(room_flat, 4, 1, 2); + TC_EQ(Kid.curr_row, 2); + TC_EQ(Kid.fall_y, 0); +} + +TC_TEST(death_at_ledge_keeps_body_in_place) +{ + /* Смерть у самого обрыва — целевая сцена находки 12. Сейчас мы + * ставим «заколот на месте» и удерживаем тело; в оригинале здесь + * своя последовательность падения замертво. Тест фиксирует НЫНЕШНЕЕ + * поведение: после правки 12+13 ожидание изменится осознанно. */ + strike_kid(room_ledge, 4, 1, 2); + TC_EQ(Kid.curr_row, 2); + TC_EQ(Kid.fall_y, 0); +} + +int main(void) +{ + sc_init(); + TC_RUN(death_unarmed_hit_is_lethal); + TC_RUN(death_armed_hit_costs_one_hp); + TC_RUN(death_armed_last_hp_kills); + TC_RUN(survivor_is_placed_on_floor); + TC_RUN(death_on_flat_floor_keeps_body_in_place); + TC_RUN(death_at_ledge_keeps_body_in_place); + return 0; +}