Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aaa480d0f2 | |||
| 8548aa9132 | |||
| 400f5cba63 | |||
| cb995bf0fc | |||
| 38fb3c03bb |
@@ -133,9 +133,15 @@ Makefile склеивает архивы прямо в `assets/packed/<КАТА
|
|||||||
(memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
|
(memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
|
||||||
Пересобрал образ → MAME ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
|
Пересобрал образ → MAME ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
|
||||||
|
|
||||||
|
**Игра лежит на образе в `D:\GAMES\SPRPOP\`**, а НЕ в корне диска (так с
|
||||||
|
обобщения HDD-сборки, коммит ea8efdb — один образ рассчитан на несколько
|
||||||
|
приложений). `dir D:\` показывает только каталог `GAMES`; запуск из корня
|
||||||
|
отвечает `Bad command or file name`.
|
||||||
|
|
||||||
**Тайминги моста** (не ждать дольше, см. `docs/mame-autotest.md` §10):
|
**Тайминги моста** (не ждать дольше, см. `docs/mame-autotest.md` §10):
|
||||||
старт `run_bridge.sh` → 6 с → `go` → 8 с → `keyseq d:{ENTER}` +
|
старт `run_bridge.sh` → 6 с → `go` → 8 с → `keyseq d:{ENTER}` +
|
||||||
`keyseq sprpop{ENTER}` → 5 с → программа работает.
|
`keyseq cd games\sprpop{ENTER}` + `keyseq sprpop{ENTER}` → 5 с →
|
||||||
|
программа работает.
|
||||||
|
|
||||||
Отладочные тумблеры в живой сессии (`src/sprpop.c`): **1** — заморозить
|
Отладочные тумблеры в живой сессии (`src/sprpop.c`): **1** — заморозить
|
||||||
кадр, **2** — продолжить (разбор позы/окклюзии); **ESC** — выход. Читы
|
кадр, **2** — продолжить (разбор позы/окклюзии); **ESC** — выход. Читы
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
|
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
|
||||||
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
||||||
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
||||||
| [CLIMB-VS-GUARD](#climb-vs-guard) | Кид подтягивается к стражу этажом выше: у нас удар засчитывается и убивает, в оригинале Кид просто срывается без урона; страж при этом способен провалиться сквозь пол вслед за Кидом | бой/физика | открыт: цепочка удара сверена — совпадает, расходятся входные данные (2026-08-31) |
|
| [CLIMB-VS-GUARD](#climb-vs-guard) | Кид подтягивается к стражу этажом выше: у нас удар засчитывается и убивает, в оригинале Кид просто срывается без урона; страж при этом способен провалиться сквозь пол вслед за Кидом | бой/физика | открыт: цепочка удара сверена — совпадает, расходятся входные данные; толкование звука ИСПРАВЛЕНО 2026-09-02 (звук 11 = промах, а не «любой укол») — главный подозреваемый снова ПОРОГ ДИСТАНЦИИ |
|
||||||
| [HP-BAR-RESTART](#hp-bar-restart) | после гибели и Ctrl+A на ОДНОЙ из двух страниц остаётся полоса HP по результатам боя | дабл-буфер/UI | КОРЕНЬ НАЙДЕН, фикс есть, ждёт проверки (2026-08-31) |
|
| [HP-BAR-RESTART](#hp-bar-restart) | после гибели и Ctrl+A на ОДНОЙ из двух страниц остаётся полоса HP по результатам боя | дабл-буфер/UI | КОРЕНЬ НАЙДЕН, фикс есть, ждёт проверки (2026-08-31) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1374,23 +1374,63 @@ uint16_t pop_music_left(void) { uint16_t a, b;
|
|||||||
В той же сцене оригинал играет ВЗМАХ (звук 11, «клинок движется»), а у нас
|
В той же сцене оригинал играет ВЗМАХ (звук 11, «клинок движется»), а у нас
|
||||||
слышен звук, похожий на упор Кида в стену (звук 8, `bumped`).
|
слышен звук, похожий на упор Кида в стену (звук 8, `bumped`).
|
||||||
|
|
||||||
Почему это важно. Звук 11 оригинал играет в `check_hurting` СРАЗУ на
|
|
||||||
кадре укола — ДО проверки расстояния и до отметки «ранен». То есть он
|
|
||||||
звучит при ЛЮБОМ уколе, попал тот или нет. Значит его отсутствие
|
|
||||||
означает не «промахнулись», а «до укола дело вообще не дошло»: страж не в
|
|
||||||
кадре укола, либо разбор вышел раньше (меч не вынут / ряды не совпали).
|
|
||||||
А звучащий вместо него упор в стену говорит, что у нас сработало
|
|
||||||
СТОЛКНОВЕНИЕ, а не атака.
|
|
||||||
|
|
||||||
Набор звуков проверен и НЕ виноват: в `assets/packed/SND/snd.idx` слот 11
|
Набор звуков проверен и НЕ виноват: в `assets/packed/SND/snd.idx` слот 11
|
||||||
на месте и содержит собственный короткий сэмпл (1280 Б), слот 8 — другой
|
на месте и содержит собственный короткий сэмпл (1280 Б), слот 8 — другой
|
||||||
(1664 Б). Раскладка не сдвинута.
|
(1664 Б). Раскладка не сдвинута.
|
||||||
|
|
||||||
Отсюда рабочая версия: в этой связке оригинал ведёт стража по ветке
|
**ПОПРАВКА 2026-09-02 — прежнее толкование звука было ОШИБОЧНЫМ.**
|
||||||
«атака с промахом», а мы — по ветке «столкновение с персонажем»
|
|
||||||
(`bump_into_opponent` / `check_bumped`). Это же объясняет и провалившегося
|
Здесь стояло, будто оригинал играет звук 11 «сразу на кадре укола, ДО
|
||||||
сквозь пол стража: столкновение двигает его координату, а не атака.
|
проверки расстояния», то есть при ЛЮБОМ уколе, попал тот или нет. Это
|
||||||
Проверять надо ветку выбора действия стража, а не только дистанцию.
|
описание НАШЕГО кода, а не оригинала: ровно так звук стоял у нас, внутри
|
||||||
|
ветки «не парировано» и до присвоения `Opp.action = 99_hurt`. В SDLPoP
|
||||||
|
(seg002:1039..1044) он стоит в ХВОСТЕ `check_hurting` и защищён условием
|
||||||
|
`Opp.action != actions_99_hurt`:
|
||||||
|
|
||||||
|
if (Char.direction == dir_56_none) return; // Fix looping "sword moving" sound.
|
||||||
|
if (Char.frame == frame_154_poking && Opp.frame != frame_161_parry &&
|
||||||
|
Opp.action != actions_99_hurt)
|
||||||
|
play_sound(sound_11_sword_moving);
|
||||||
|
|
||||||
|
То есть звук 11 в оригинале — это индикатор **ПРОМАХА**, а не укола: на
|
||||||
|
попадании его нет (играет боль), на парировании нет (`Opp.frame` уже 161).
|
||||||
|
Расхождение найдено и исправлено 2026-09-02 по симптому «непрерывный взмах
|
||||||
|
клинка у безоружного Кида на подъёме» (звук опознан в живой сессии по
|
||||||
|
`snd_curr == 11`; указатели насоса при этом сошлись с `snd.idx` байт в
|
||||||
|
байт, то есть тракт звука был исправен). Заодно приехал и пропущенный
|
||||||
|
ранний выход по `dir_56_none` — тот самый, что автор SDLPoP подписал
|
||||||
|
«Fix looping sword moving sound».
|
||||||
|
|
||||||
|
**Что из прежних выводов уцелело, а что нет.**
|
||||||
|
|
||||||
|
* УЦЕЛЕЛО: наблюдение «в оригинале слышен 11, у нас 8» — это данные, и они
|
||||||
|
остаются. Арифметика приоритетов их подкрепляет: `snd_prio[11] = 0x12`
|
||||||
|
(18) против `snd_prio[8] = 0x4B` (75), меньше значит важнее, поэтому
|
||||||
|
взмах не мог быть заглушён упором в стену. При ТОГДАШНЕМ коде звук 11
|
||||||
|
звучал на любом уколе, значит его отсутствие действительно означало, что
|
||||||
|
до кадра укола дело не дошло.
|
||||||
|
* ОТПАЛО: вывод «оригинал ведёт стража атакой с промахом, а мы —
|
||||||
|
столкновением» опирался на неверную посылку и больше не следует из звука
|
||||||
|
сам по себе.
|
||||||
|
* УСИЛИЛОСЬ: то, что в оригинале в этой сцене слышен ИМЕННО 11, теперь
|
||||||
|
доказывает, что укол у стража СОСТОЯЛСЯ и ПРОМАЗАЛ. А это ровно версия
|
||||||
|
про ПОРОГ ДИСТАНЦИИ (см. выше про пару пикселей и нижний порог 8), а не
|
||||||
|
про потерянную ветку. Прежняя «версия про столкновение» её вытеснила
|
||||||
|
зря — возвращаем дистанцию в главные подозреваемые.
|
||||||
|
|
||||||
|
**ВНИМАНИЕ тому, кто вернётся к этому багу: диагностика по звуку с
|
||||||
|
2026-09-02 ЗНАЧИТ ДРУГОЕ.** Старые заметки прогона будут вводить в
|
||||||
|
заблуждение — переснимать сцену заново. Новая таблица:
|
||||||
|
|
||||||
|
| слышно | что это значит |
|
||||||
|
|---|---|
|
||||||
|
| 11 (взмах) | укол состоялся и ПРОМАЗАЛ — поведение оригинала в этой сцене |
|
||||||
|
| 13 (боль Кида) | укол ПОПАЛ, `Opp.action = 99_hurt` |
|
||||||
|
| 8 (упор в стену) | укола не было вовсе, сработало столкновение |
|
||||||
|
| тишина | `check_hurting` вышел раньше: меч не вынут / ряды не совпали / кадр не 153-154 |
|
||||||
|
|
||||||
|
На саму механику урона правка 2026-09-02 НЕ влияет (тронут только звук),
|
||||||
|
поэтому расхождение «удар убивает» остаётся открытым как было.
|
||||||
|
|
||||||
**Решение пользователя:** отложено на будущее (2026-08-31) — поведение в
|
**Решение пользователя:** отложено на будущее (2026-08-31) — поведение в
|
||||||
этой связке расходится широко, чинить нужно целиком, а не по одному
|
этой связке расходится широко, чинить нужно целиком, а не по одному
|
||||||
|
|||||||
@@ -1102,8 +1102,6 @@ static void check_hurting(void)
|
|||||||
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
|
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
|
||||||
/* Соперник НЕ парирует. */
|
/* Соперник НЕ парирует. */
|
||||||
if (cf == FRAME_154_POKING) {
|
if (cf == FRAME_154_POKING) {
|
||||||
/* seg002:0DAE — свист клинка мимо цели. */
|
|
||||||
pop_sfx_play(11);
|
|
||||||
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
|
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
|
||||||
distance = pop_char_opp_dist();
|
distance = pop_char_opp_dist();
|
||||||
if (distance >= (int16_t)min_range && distance < 29)
|
if (distance >= (int16_t)min_range && distance < 29)
|
||||||
@@ -1116,6 +1114,29 @@ static void check_hurting(void)
|
|||||||
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
|
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
|
||||||
play_seq();
|
play_seq();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* СВИСТ КЛИНКА МИМО ЦЕЛИ (seg002:1039..1044) — в оригинале он стоит в
|
||||||
|
* ХВОСТЕ функции, после ОБЕИХ веток, а не внутри ветки «не парировано».
|
||||||
|
* Мы его туда и переносим; раньше он звучал раньше времени и без двух
|
||||||
|
* условий оригинала, отчего на подъёме Кида шёл непрерывный взмах
|
||||||
|
* клинка у безоружного (наблюдение пользователя 2026-09-02, звук
|
||||||
|
* опознан по snd_curr == 11 в живой сессии).
|
||||||
|
*
|
||||||
|
* Три отличия от прежнего кода, все из оригинала:
|
||||||
|
*
|
||||||
|
* 1. Ранний выход по dir_56_none — это ИМЕННО анти-зацикливание, автор
|
||||||
|
* SDLPoP так его и подписал («Fix looping sword moving sound»).
|
||||||
|
* Направление dir_56_none означает «персонаж выключен» (clear_char),
|
||||||
|
* и махать клинком ему нечем.
|
||||||
|
* 2. Условие `Opp.action != ACTION_99_HURT`: попали — играет звук боли,
|
||||||
|
* а не свист мимо. Поэтому звук обязан идти ПОСЛЕ присвоения выше.
|
||||||
|
* 3. Кадры перечитываются у персонажей, а не берутся из cf/of: ветка
|
||||||
|
* парирования только что записала Opp.frame, а play_seq в ней —
|
||||||
|
* Char.frame, и снимки выше уже устарели. */
|
||||||
|
if (Char.direction == DIR_56_NONE) return;
|
||||||
|
if (Char.frame == FRAME_154_POKING && Opp.frame != FRAME_161_PARRY &&
|
||||||
|
Opp.action != ACTION_99_HURT)
|
||||||
|
pop_sfx_play(11);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
|
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
|
||||||
|
|||||||
@@ -771,15 +771,26 @@ static void overlay_mid_tile(int row, int col)
|
|||||||
pop_env_b(42, x, pop_tile_table[1].right_y + dmy);
|
pop_env_b(42, x, pop_tile_table[1].right_y + dmy);
|
||||||
{ /* draw_tile_base (seg008:0A8E) — ЦЕЛИКОМ, вместе с подстановками id:
|
{ /* draw_tile_base (seg008:0A8E) — ЦЕЛИКОМ, вместе с подстановками id:
|
||||||
* у loose верх плиты берётся из loose_fram_left (в pop_tile_table base_id=0),
|
* у loose верх плиты берётся из loose_fram_left (в pop_tile_table base_id=0),
|
||||||
* у opener'а без пола слева — 148. Раньше здесь стоял голый base_id, и
|
* у opener'а без пола слева — 148, НО только в ПОДЗЕМЕЛЬЕ. Условие
|
||||||
|
* `tbl_level_type[current_level] == 0` в оригинале стоит третьим
|
||||||
|
* (seg008:628) и у нас было потеряно ИМЕННО ЗДЕСЬ: статическая
|
||||||
|
* отрисовка (pop_room.c, draw_tile_base) его имеет, а оверлей — нет.
|
||||||
|
* Отсюда симптом «кнопка, на которую лезет Кид, залита кирпичом»:
|
||||||
|
* пока тайл рисует статика, кнопка правильная, но как только Kid
|
||||||
|
* встаёт под кромку, оверлей перерисовывает её поверх него уже
|
||||||
|
* спрайтом 148, а во дворце это другая картинка (кладку палас
|
||||||
|
* вообще рисует заливками, а не спрайтами). Уровень 4, комната 18,
|
||||||
|
* кнопка (0,4) — найдено пользователем 2026-09-02.
|
||||||
|
*
|
||||||
|
* Раньше здесь стоял голый base_id, и
|
||||||
* верх loose-плиты в оверлей не попадал: передняя грань ложилась поверх
|
* верх loose-плиты в оверлей не попадал: передняя грань ложилась поверх
|
||||||
* Kid (foretable), а сама плита — нет, и подтягивающийся Kid рисовался
|
* Kid (foretable), а сама плита — нет, и подтягивающийся Kid рисовался
|
||||||
* поверх её верхней плоскости (комната 12). */
|
* поверх её верхней плоскости (комната 12). */
|
||||||
uint8_t base_id = t->base_id;
|
uint8_t base_id = t->base_id;
|
||||||
if (code == 11 && row >= 0)
|
if (code == 11 && row >= 0)
|
||||||
base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])];
|
base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])];
|
||||||
else if (code == 0x0F && lcode == 0)
|
else if (code == 0x0F && lcode == 0 && !pop_palace)
|
||||||
base_id = 148;
|
base_id = 148; /* ТОЛЬКО ПОДЗЕМЕЛЬЕ (seg008:628) */
|
||||||
if (base_id)
|
if (base_id)
|
||||||
pop_env_b(base_id, x, t->base_y + dmy);
|
pop_env_b(base_id, x, t->base_y + dmy);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -839,30 +839,35 @@ static uint8_t can_grab_front_above(void)
|
|||||||
/* check_grab (seg006:0A28): в падении при зажатом Shift — зацепиться за
|
/* check_grab (seg006:0A28): в падении при зажатом Shift — зацепиться за
|
||||||
* уступ спереди-сверху (seq_15), если скорость падения ещё мала и высота
|
* уступ спереди-сверху (seq_15), если скорость падения ещё мала и высота
|
||||||
* подходящая. Выравнивает передний край по кромке, гасит fall_y, взводит
|
* подходящая. Выравнивает передний край по кромке, гасит fall_y, взводит
|
||||||
* grab_timer (блок climb-up на несколько кадров). */
|
* grab_timer (блок climb-up на несколько кадров).
|
||||||
static void check_grab(void)
|
*
|
||||||
|
* ВОЗВРАТ: 1 — зацеп СОСТОЯЛСЯ на этом кадре. Нужен вызывающему (do_fall):
|
||||||
|
* дальше по кадру идёт glide_through_wall_guard, и ему нельзя трогать
|
||||||
|
* персонажа, которого мы только что повесили на кромку (см. там же). */
|
||||||
|
static uint8_t check_grab(void)
|
||||||
{
|
{
|
||||||
uint8_t old_x;
|
uint8_t old_x;
|
||||||
if (!pop_ctrl_shift_held()) return; /* Shift не зажат */
|
if (!pop_ctrl_shift_held()) return 0; /* Shift не зажат */
|
||||||
if ((uint8_t)Char.fall_y >= 32) return; /* падает слишком быстро */
|
if ((uint8_t)Char.fall_y >= 32) return 0; /* падает слишком быстро */
|
||||||
if ((uint16_t)pop_y_land[Char.curr_row + 1] > (uint16_t)(Char.y + 25)) return;
|
if ((uint16_t)pop_y_land[Char.curr_row + 1] > (uint16_t)(Char.y + 25)) return 0;
|
||||||
old_x = Char.x;
|
old_x = Char.x;
|
||||||
Char.x = (uint8_t)char_dx_forward(-8);
|
Char.x = (uint8_t)char_dx_forward(-8);
|
||||||
determine_col();
|
determine_col();
|
||||||
if (!can_grab_front_above()) {
|
if (!can_grab_front_above()) {
|
||||||
Char.x = old_x; /* не за что — назад */
|
Char.x = old_x; /* не за что — назад */
|
||||||
determine_col();
|
determine_col();
|
||||||
} else {
|
return 0;
|
||||||
Char.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight());
|
|
||||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
|
||||||
Char.fall_y = 0;
|
|
||||||
pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
|
|
||||||
play_seq();
|
|
||||||
determine_col();
|
|
||||||
grab_timer = 12;
|
|
||||||
pop_sfx_play(9); /* seg006 check_grab */
|
|
||||||
is_screaming = 0; /* seg006:1219 */
|
|
||||||
}
|
}
|
||||||
|
Char.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight());
|
||||||
|
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||||
|
Char.fall_y = 0;
|
||||||
|
pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
|
||||||
|
play_seq();
|
||||||
|
determine_col();
|
||||||
|
grab_timer = 12;
|
||||||
|
pop_sfx_play(9); /* seg006 check_grab */
|
||||||
|
is_screaming = 0; /* seg006:1219 */
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void make_loose_fall(int pos, uint8_t modifier);
|
static void make_loose_fall(int pos, uint8_t modifier);
|
||||||
@@ -972,8 +977,16 @@ static void do_fall(void)
|
|||||||
}
|
}
|
||||||
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
|
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
|
||||||
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
|
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
|
||||||
check_grab(); /* ещё летит — попытка зацепа */
|
/* ЗАЦЕП ОТМЕНЯЕТ GUARD. seq_15 — «grab ledge MIDAIR», то есть после
|
||||||
glide_through_wall_guard(); /* и не сквозь кладку (см. выше) */
|
* удачного зацепа Char.action == 3, персонаж уже подтянут вплотную к
|
||||||
|
* кромке, а под кромкой в оригинальных комнатах стоит КЛАДКА. Guard,
|
||||||
|
* позванный следом, видел ровно её (t == TILE_WALL, d == 10), считал
|
||||||
|
* это «пролётом сквозь стену», отбрасывал Кида на 5 пикселей назад и
|
||||||
|
* гасил fall_x — зацеп рисовался и тут же срывался. Ловилось на
|
||||||
|
* длинном прыжке уровня 3 (комната 7 -> кромка комнаты 2) и в
|
||||||
|
* attract-демо; регресс-набор — tests/host/t_glide.c. */
|
||||||
|
if (!check_grab()) /* ещё летит — попытка зацепа */
|
||||||
|
glide_through_wall_guard(); /* и не сквозь кладку (см. выше) */
|
||||||
} else if (Char.curr_row <= 2) {
|
} else if (Char.curr_row <= 2) {
|
||||||
if (get_tile_at_char() == TILE_WALL)
|
if (get_tile_at_char() == TILE_WALL)
|
||||||
in_wall();
|
in_wall();
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
|
|||||||
OBJS_grab := $(OBJS_phys)
|
OBJS_grab := $(OBJS_phys)
|
||||||
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
|
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
|
||||||
OBJS_wall := $(OBJS_phys)
|
OBJS_wall := $(OBJS_phys)
|
||||||
|
# t_hang — зацеп обязан ДЕРЖАТЬСЯ (регресс glide_through_wall_guard).
|
||||||
|
OBJS_hang := $(OBJS_phys)
|
||||||
# t_gate — открытая решётка не должна считаться стеной у осторожного шага
|
# t_gate — открытая решётка не должна считаться стеной у осторожного шага
|
||||||
# (нужны pop_map + физика, то есть тот же состав).
|
# (нужны pop_map + физика, то есть тот же состав).
|
||||||
OBJS_gate := $(OBJS_phys)
|
OBJS_gate := $(OBJS_phys)
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* t_hang.c — ЗАЦЕП ОБЯЗАН ДЕРЖАТЬСЯ, а не срываться на следующем кадре.
|
||||||
|
*
|
||||||
|
* Регресс 2026-09-02. glide_through_wall_guard() (порт fix_glide_through_wall,
|
||||||
|
* взят в билд 2026-08-31) звался в do_fall СРАЗУ ПОСЛЕ check_grab. Беда в
|
||||||
|
* том, что seq_15 называется grab_ledge_MIDAIR: после удачного зацепа
|
||||||
|
* Char.action == 3 (в воздухе), персонаж уже подтянут вплотную к кромке — а
|
||||||
|
* под кромкой в комнатах оригинала стоит КЛАДКА. Guard видел ровно её
|
||||||
|
* (t == TILE_WALL, d == 10), считал это «пролётом сквозь стену», отбрасывал
|
||||||
|
* персонажа на 5 пикселей назад и гасил fall_x. Зацеп РИСОВАЛСЯ и тут же
|
||||||
|
* срывался — уровень 3 (комната 7, длинный прыжок через четыре пролёта) и
|
||||||
|
* attract-демо (комната 2, прыжок с места).
|
||||||
|
*
|
||||||
|
* ПОЧЕМУ ПРЕЖНИЕ НАБОРЫ ЭТОГО НЕ ПОЙМАЛИ. t_grab судит по «действие стало
|
||||||
|
* вис» — а вис-то наступал, он просто не жил. Отсюда мера здесь другая:
|
||||||
|
* вис обязан ДЕРЖАТЬСЯ подряд несколько кадров. Разница между «зацепился»
|
||||||
|
* и «зацепился и держится» — это и есть разница между багом и нормой.
|
||||||
|
*
|
||||||
|
* СЦЕНА (геометрия уровня 3, комната 7, приведённая внутрь одной комнаты,
|
||||||
|
* чтобы шов не примешивался): кромка на (0,0), под ней кладка (1,0), четыре
|
||||||
|
* пролёта (0,1..4), толчок с плиты (0,5), разбег справа.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "tcheck.h"
|
||||||
|
#include "scene.h"
|
||||||
|
#include "stubs.h"
|
||||||
|
#include "pop_kid.h"
|
||||||
|
#include "pop_map.h"
|
||||||
|
|
||||||
|
#define E 0x00
|
||||||
|
#define F 0x01
|
||||||
|
#define G 0x04
|
||||||
|
#define L 0x0B
|
||||||
|
#define T 0x13
|
||||||
|
#define W 0x14
|
||||||
|
|
||||||
|
static uint8_t room[30] = {
|
||||||
|
F, E, E, E, E, L, F, T, F, T,
|
||||||
|
W, E, E, E, E, W, W, W, W, W,
|
||||||
|
W, E, E, E, E, W, W, W, W, W,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define ACT_HANG_CLIMB 2
|
||||||
|
#define ACT_HANG_STRAIGHT 6
|
||||||
|
#define HOLD_FRAMES 3
|
||||||
|
|
||||||
|
/* Вис ДЕРЖИТСЯ: действие «вис» стоит HOLD_FRAMES кадров подряд. */
|
||||||
|
static uint8_t hang_holds(uint8_t ledge, uint8_t nrun)
|
||||||
|
{
|
||||||
|
uint8_t i, run = 0;
|
||||||
|
room[0] = ledge;
|
||||||
|
sc_room(room, 7);
|
||||||
|
sc_kid_at(9, 0, -1); /* лицом влево, у правого края */
|
||||||
|
sc_run(SC_L, nrun); /* разбег */
|
||||||
|
sc_run(SC_L | SC_U, 6); /* толчок в прыжок */
|
||||||
|
sc_trace_clear();
|
||||||
|
sc_run(SC_SHIFT, 30); /* полёт, зацеп и жизнь после него */
|
||||||
|
for (i = 0; i < sc_len; i++) {
|
||||||
|
uint8_t a = sc_trace[i].action;
|
||||||
|
if (a == ACT_HANG_CLIMB || a == ACT_HANG_STRAIGHT) {
|
||||||
|
if (++run >= HOLD_FRAMES) return 1;
|
||||||
|
} else run = 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Сколько фаз разбега из 41 дают ЖИВОЙ вис. До регресса — все, кроме
|
||||||
|
* первой (слишком близкий толчок), то есть 34; при сорванном зацепе — ноль. */
|
||||||
|
static uint8_t window(uint8_t ledge)
|
||||||
|
{
|
||||||
|
uint8_t k, n = 0;
|
||||||
|
for (k = 6; k <= 46; k++) if (hang_holds(ledge, k)) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(long_jump_hang_survives_wall_under_ledge)
|
||||||
|
{
|
||||||
|
/* Кромка-пол: под ней кладка — та самая, на которую срабатывал guard. */
|
||||||
|
TC_TRUE(window(F) >= 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(long_jump_hang_survives_on_gate)
|
||||||
|
{
|
||||||
|
/* Кромка-решётка: тайл другой, кладка под ним та же. Проверяется
|
||||||
|
* отдельно, потому что зацеп за решётку идёт своей веткой can_grab. */
|
||||||
|
TC_TRUE(window(G) >= 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
sc_init();
|
||||||
|
TC_RUN(long_jump_hang_survives_wall_under_ledge);
|
||||||
|
TC_RUN(long_jump_hang_survives_on_gate);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user