Звуковые эффекты PoP: оцифровка оригинала через CBL
Набор digisnd1..3.dat приведён упаковщиком к 10 937,5 Гц (частота железа), склеен в 8 EMM-страниц с выравниванием каждого звука на 128 — размер блока запроса CBL, поэтому ни один блок не пересекает границу страницы и проигрыватель не знает слова «стык». Насос (pop_sfx.c) резидентный: его зовут из прерывания CBL, из горячих мест физики и из play_seq. Тишину льём свою (первый блок набора), а не через CBL_UNDERRUN_SILENCE с его malloc — куча в резиденте W2 тесная. Открытие CBL разведено с загрузкой (pop_sfx_start отдельно от pop_sfx_init): пока ESTEX читает файлы, насос не успевает долить блок и железо крутит хвост буфера — на слух скрежет. Разведены все места play_sound() SDLPoP, у которых есть оцифровка (id 0..23, 44..49): посадки, падение, удары о стену, зацеп, тряска и провал плит, ворота, дверь уровня, пики, чомпер, кнопки, боёвка, меч, зеркало, скелет, зелье. Таблица соответствий — docs/sound_plan.md §8. Музыкальные id остаются с нулевой длиной до фазы музыки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ MEMORY ?= huge # small-раскладка + банки кода в W
|
||||
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
|
||||
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
|
||||
ALLOCS ?= 3000
|
||||
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c $(PROF_FLAGS)
|
||||
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c --bank 8=pop_sfx_cold.c $(PROF_FLAGS)
|
||||
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
|
||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||
@@ -57,7 +57,7 @@ POS ?= 2
|
||||
ifneq ($(strip $(ROOM)),)
|
||||
PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS)
|
||||
endif
|
||||
EXTRA_SRCS := pop_vflip.c pop_pace.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c
|
||||
EXTRA_SRCS := pop_vflip.c pop_pace.c pop_sfx.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c
|
||||
|
||||
BG_DIR := $(CURDIR)/../poc/res/bg
|
||||
KID_DIR := $(CURDIR)/../poc/res/kid
|
||||
@@ -120,8 +120,13 @@ SHADOW_DIR := $(CURDIR)/../poc/res/shadow
|
||||
SHADOW_DATA := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(SHADOW_DIR)/sk$(n).atl) \
|
||||
$(foreach n,0 1 2 3,$(SHADOW_DIR)/sf$(n).atl)
|
||||
|
||||
# ЗВУКОВЫЕ ЭФФЕКТЫ — оригинальная оцифровка, приведённая к частоте CBL
|
||||
# (10 937,5 Гц, 8 бит моно). 8 страниц по 16 КБ; см. ../docs/sound_plan.md.
|
||||
SND_DIR := $(CURDIR)/../poc/res/sound
|
||||
SND_DATA := $(foreach n,0 1 2 3 4 5 6 7,$(SND_DIR)/s$(n).bin)
|
||||
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
|
||||
$(VIZIER_DATA) $(SHADOW_DATA) $(LVL_DATA)
|
||||
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(LVL_DATA)
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
|
||||
@@ -139,6 +144,7 @@ hdd: $(EXAMPLE).exe
|
||||
$(foreach f,$(SKEL_DATA),SKEL:$(f)) \
|
||||
$(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \
|
||||
$(foreach f,$(SHADOW_DATA),SHADOW:$(f)) \
|
||||
$(foreach f,$(SND_DATA),SND:$(f)) \
|
||||
$(foreach f,$(LVL_DATA),LEVELS:$(f))
|
||||
|
||||
TC := $(PROJ_ROOT)/applications/PoP/toolchain
|
||||
@@ -156,6 +162,9 @@ $(VIZIER_DATA): $(TC)/pop_pack_guard.py
|
||||
$(SHADOW_DATA) pop_shadow_atlas.h: $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py
|
||||
cd $(TC) && python3 pop_pack_shadow.py
|
||||
|
||||
$(SND_DATA) pop_sound_tbl.h: $(TC)/pop_pack_sound.py
|
||||
cd $(TC) && python3 pop_pack_sound.py
|
||||
|
||||
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
|
||||
cd $(TC) && python3 pop_pack_kid.py
|
||||
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный,
|
||||
* публичный — pop_sfx.h). Горячая половина живёт в резиденте
|
||||
* (pop_sfx.c), холодная в банке (pop_sfx_cold.c); делят они таблицу
|
||||
* звуков, массив физических страниц и насос.
|
||||
*/
|
||||
#ifndef _POP_SFX_INTERNAL_H
|
||||
#define _POP_SFX_INTERNAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "pop_sound_tbl.h" /* pop_snd_tbl, POP_SND_* — генерит упаковщик */
|
||||
|
||||
/* Физические страницы набора, по индексу из pop_snd_tbl[].page. */
|
||||
extern uint8_t pop_snd_page[POP_SND_PAGES];
|
||||
/* 1 — набор поднят и CBL открыт; 0 — играем молча. */
|
||||
extern uint8_t pop_snd_ok;
|
||||
|
||||
/* Насос: отдать n байт в CBL. Резидентный — его адрес отдаётся в
|
||||
* cbl_open() холодной половиной. */
|
||||
int pop_sfx_fill(uint16_t n);
|
||||
|
||||
/* Прочитать файл целиком в ФИЗИЧЕСКУЮ страницу (через W3). Резидентный:
|
||||
* банковый код, переключив W3, вырезал бы сам себя. */
|
||||
int pop_page_read(const char *path, uint8_t phys, uint16_t want);
|
||||
|
||||
#endif
|
||||
@@ -16,6 +16,7 @@
|
||||
* дальше работает общий pop_control().
|
||||
*/
|
||||
#include "pop_guard.h"
|
||||
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
|
||||
#include "pop_kid.h"
|
||||
#include "pop_ctrl.h"
|
||||
#include "pop_map.h"
|
||||
@@ -189,6 +190,7 @@ void pop_check_skel(void) __banked
|
||||
Char.charid = CHARID_4_SKELETON;
|
||||
Char.fall_x = Char.fall_y = 0;
|
||||
pop_char_set_seq(SEQ_88_SKEL_WAKE_UP);
|
||||
pop_sfx_play(44); /* seg002:106D — скелет оживает */
|
||||
play_seq();
|
||||
pop_saveshad();
|
||||
|
||||
@@ -989,7 +991,9 @@ static void hurt_by_sword(void)
|
||||
}
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
Char.fall_y = 0;
|
||||
play_seq(); /* TODO: звук 12/13 */
|
||||
/* seg002:0C1F: у Кида свой звук боли, у соперника свой. */
|
||||
pop_sfx_play((uint8_t)(Char.charid == CHARID_0_KID ? 13 : 12));
|
||||
play_seq();
|
||||
}
|
||||
|
||||
/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника.
|
||||
@@ -1010,6 +1014,8 @@ static void check_hurting(void)
|
||||
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
|
||||
/* Соперник НЕ парирует. */
|
||||
if (cf == FRAME_154_POKING) {
|
||||
/* seg002:0DAE — свист клинка мимо цели. */
|
||||
pop_sfx_play(11);
|
||||
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
|
||||
distance = pop_char_opp_dist();
|
||||
if (distance >= (int16_t)min_range && distance < 29)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "pop_state.h"
|
||||
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
|
||||
#include "pop_kid.h"
|
||||
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
|
||||
#include "pop_map.h"
|
||||
#include "pop_guard.h" /* charid, состояние меча, seq стража */
|
||||
|
||||
@@ -405,7 +406,8 @@ static void draw_sword(void)
|
||||
uint8_t seq_id = SEQ_55_DRAW_SWORD;
|
||||
control_forward = control_shift2 = release_arrows();
|
||||
if (Char.charid == CHARID_0_KID) {
|
||||
offguard = 0; /* TODO: play_sound(19) — звука пока нет */
|
||||
offguard = 0;
|
||||
pop_sfx_play(19); /* seg005:945 — меч из ножен */
|
||||
} else if (Char.charid != CHARID_1_SHADOW) {
|
||||
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
#include "pop_kid.h"
|
||||
#include "pop_sfx.h" /* звуковые эффекты: опкод SOUND в play_seq */
|
||||
#include "pop_trob.h" /* pop_start_chompers — смена ряда будит чомперы */
|
||||
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
|
||||
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
|
||||
@@ -323,14 +324,26 @@ void play_seq(void)
|
||||
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
|
||||
knock = -1;
|
||||
break;
|
||||
case 0xF2: /* SOUND (seg006:0611): звука у нас нет, но опкод несёт
|
||||
* ВТОРОЙ смысл — «Кид нашумел», и его терять нельзя.
|
||||
* Шумом считаются id 0..2 (SILENT/FOOTSTEP/BUMP); id 0
|
||||
* назван silent потому, что НЕ звучит, а стражи его всё
|
||||
* равно замечают (стоит, например, в seq доставания меча).
|
||||
* DRINK(3)/LEVEL(4) — не шум. См. BUG-GUARD-DEAF-1. */
|
||||
if (SEQ(Char.curr_seq) < 3) is_guard_notice = 1;
|
||||
Char.curr_seq++;
|
||||
case 0xF2: /* SOUND (seg006:0627). Аргумент — НЕ номер звука игры, а
|
||||
* одно из пяти событий; отображение взято оттуда же:
|
||||
* 0 SILENT — не звучит, но стражи Кида ЗАМЕЧАЮТ
|
||||
* (стоит, например, в seq доставания меча);
|
||||
* 1 FOOTSTEP — звук 23, и шум;
|
||||
* 2 BUMP — звук 8, и шум;
|
||||
* 3 DRINK — звук 18, не шум;
|
||||
* 4 LEVEL — МУЗЫКА конца уровня (32 на 4-м, иначе 41,
|
||||
* на 13/15 молчок). Оцифровки у неё нет,
|
||||
* это путь A на AY — пока тишина.
|
||||
* Второй смысл опкода («Кид нашумел») терять нельзя даже
|
||||
* без звука, см. BUG-GUARD-DEAF-1. */
|
||||
{
|
||||
uint8_t ev = SEQ(Char.curr_seq);
|
||||
Char.curr_seq++;
|
||||
if (ev < 3) is_guard_notice = 1;
|
||||
if (ev == 1) pop_sfx_play(23);
|
||||
else if (ev == 2) pop_sfx_play(8);
|
||||
else if (ev == 3) pop_sfx_play(18);
|
||||
}
|
||||
break;
|
||||
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
|
||||
* В оригинале это `++next_level`, а главный цикл
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "pop_map.h"
|
||||
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
||||
#include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */
|
||||
#include "pop_sfx.h" /* звуковые эффекты (sound_plan.md) */
|
||||
#include "pop_geom.h" /* общая геометрия (x_bump/y_land/y_to_row) */
|
||||
#include "pop_tile.h" /* POP_TILE_DIV/MOD — деление на ширину тайла таблицей */
|
||||
#include "pop_redraw.h" /* пометки перерисовки (порт set_redraw_*) */
|
||||
@@ -143,6 +144,11 @@ uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заб
|
||||
uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */
|
||||
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
|
||||
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
|
||||
|
||||
/* is_screaming (seg005:38): крик падения звучит ОДИН раз за полёт, а не
|
||||
* каждый кадр. Сбрасывается там же, где у оригинала: приземление (land),
|
||||
* зацеп за карниз (check_grab) и постановка на старт (set_start_pos). */
|
||||
static uint8_t is_screaming;
|
||||
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
|
||||
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
|
||||
/* hitp_beg_lev (seg003 init_game/play_level_2): HP, с которым НАЧАТ уровень.
|
||||
@@ -666,6 +672,7 @@ static void land(void)
|
||||
* control_crouched(), а при `sword == sword_2_drawn` управление уходит в
|
||||
* control_with_sword и до него не доходит — Кид садится в присед
|
||||
* НАВСЕГДА (BUG-LAND-SWORD-1). */
|
||||
is_screaming = 0; /* seg005:116 */
|
||||
if (Char.fall_y < 22) {
|
||||
soft_land:
|
||||
if (Char.charid >= CHARID_2_GUARD || Char.sword == SWORD_2_DRAWN) {
|
||||
@@ -674,8 +681,8 @@ static void land(void)
|
||||
} else {
|
||||
seq = SEQ_17_SOFT_LAND;
|
||||
}
|
||||
/* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */
|
||||
if (Char.charid == CHARID_0_KID) is_guard_notice = 1;
|
||||
/* seg005:185 — мягкое приземление слышно И стражам, и игроку. */
|
||||
if (Char.charid == CHARID_0_KID) { pop_sfx_play(17); is_guard_notice = 1; }
|
||||
} else {
|
||||
uint8_t deadly = (uint8_t)(Char.fall_y >= 33); /* СНЯТЬ до обнуления */
|
||||
if (!deadly && Char.charid == CHARID_1_SHADOW) goto soft_land; /* seg005:189 */
|
||||
@@ -683,13 +690,14 @@ static void land(void)
|
||||
Char.fall_x = Char.fall_y = 0;
|
||||
if (deadly || pop_take_hp(1)) { /* 3+ этажа или последнее HP */
|
||||
pop_take_hp(100);
|
||||
if (Char.charid == CHARID_0_KID) pop_sfx_play(0); /* разбился */
|
||||
pop_char_set_seq(SEQ_22_CRUSHED);
|
||||
play_seq();
|
||||
determine_col();
|
||||
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
|
||||
return;
|
||||
}
|
||||
if (Char.charid == CHARID_0_KID) is_guard_notice = 1; /* seg005:195 */
|
||||
if (Char.charid == CHARID_0_KID) { pop_sfx_play(16); is_guard_notice = 1; } /* seg005:195 */
|
||||
seq = SEQ_20_MEDIUM_LAND;
|
||||
}
|
||||
Char.fall_x = Char.fall_y = 0;
|
||||
@@ -811,7 +819,9 @@ static void check_grab(void)
|
||||
pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
|
||||
play_seq();
|
||||
determine_col();
|
||||
grab_timer = 12; /* (TODO: sound grab) */
|
||||
grab_timer = 12;
|
||||
pop_sfx_play(9); /* seg006 check_grab */
|
||||
is_screaming = 0; /* seg006:1219 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,6 +892,10 @@ static uint8_t check_grab_run_jump(void)
|
||||
static void do_fall(void)
|
||||
{
|
||||
uint8_t nrow = (uint8_t)(Char.curr_row + 1);
|
||||
if (!is_screaming && Char.fall_y >= 31) {
|
||||
pop_sfx_play(1); /* seg005:39 — крик падения */
|
||||
is_screaming = 1;
|
||||
}
|
||||
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
|
||||
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
|
||||
check_grab(); /* ещё летит — попытка зацепа */
|
||||
@@ -1355,6 +1369,8 @@ void pop_proc_get_object(void) __banked
|
||||
* ставили вспышку руками, то есть красили экран дважды.
|
||||
* Спецсобытие уровня зелий: там синие склянки забирают ПОЛОВИНУ
|
||||
* запаса, а не одну единицу. */
|
||||
pop_sfx_stop(); /* seg006:1893 — stop_sounds */
|
||||
pop_sfx_play(13);
|
||||
if (pop_current_level == POP_POTIONS_LEVEL)
|
||||
hitp_delta = (int8_t)(-((hitp_max + 1) >> 1));
|
||||
else
|
||||
@@ -1508,8 +1524,9 @@ static void bumped_fall(void)
|
||||
play_seq();
|
||||
}
|
||||
/* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о
|
||||
* стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел»
|
||||
* взводить обязаны — см. BUG-GUARD-DEAF-1. */
|
||||
* стену слышен и в свободном падении. Флаг «Кид нашумел» взводится
|
||||
* вместе со звуком — см. BUG-GUARD-DEAF-1. */
|
||||
pop_sfx_play(8);
|
||||
is_guard_notice = 1;
|
||||
}
|
||||
|
||||
@@ -1560,7 +1577,8 @@ static void bumped_floor(void)
|
||||
else
|
||||
pop_char_set_seq(SEQ_47_BUMP);
|
||||
play_seq();
|
||||
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */
|
||||
pop_sfx_play(8); /* bumped_sound (seg004:05F1) */
|
||||
is_guard_notice = 1;
|
||||
}
|
||||
|
||||
/* bump_into_opponent (seg003:08AA). Зовётся из play_kid_frame (seg000:1238)
|
||||
@@ -1593,6 +1611,7 @@ static void bump_into_opponent(void)
|
||||
if (distance > 15) return;
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
Char.fall_y = 0;
|
||||
pop_sfx_play(16); /* seg003:0654 — толчок о стража */
|
||||
pop_char_set_seq(SEQ_47_BUMP);
|
||||
play_seq();
|
||||
}
|
||||
@@ -2367,6 +2386,28 @@ static void check_press(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Звук дрожащей плиты (loose_shake, seg007:0E55) ------------------ *
|
||||
* Три сэмпла 20/21/22 вперемешку, но НЕ на каждый кадр фазы: гейт-таблица
|
||||
* loose_sound (data:2734) даёт звук на фазах 1,2,3,5,8, а на остальных
|
||||
* молчит — иначе тряска трещит без пауз. Повтор подряд одного и того же
|
||||
* сэмпла оригинал запрещает (цикл do/while), иначе слышно «заедание».
|
||||
*
|
||||
* Сид СВОЙ, а не общий с trob/боёвкой: домены prandom у нас разведены
|
||||
* (../docs/impl_diff.md), и звук не должен сдвигать чужие розыгрыши. */
|
||||
static const uint8_t loose_sound[12] = { 0,1,1,1,0,1,0,0,1,0,0,0 };
|
||||
static pop_rnd_t loose_seed;
|
||||
static uint8_t last_loose_snd;
|
||||
|
||||
static void loose_shake(uint8_t force, uint8_t modif)
|
||||
{
|
||||
uint8_t id;
|
||||
if (!force && !loose_sound[modif & 0x7F]) return;
|
||||
do { id = (uint8_t)(pop_prandom(&loose_seed, 2) + 20); }
|
||||
while (id == last_loose_snd);
|
||||
last_loose_snd = id;
|
||||
pop_sfx_play(id);
|
||||
}
|
||||
|
||||
/* do_knock (seg007:0FE0) + loose_make_shake (seg007:0FB4): сотрясение от
|
||||
* приземления/удара трясёт ВСЕ loose-полы в ряду tile_row (не роняя их —
|
||||
* modif 0x80..0x83 = чистая тряска, сбрасывается в pop_loose_tick). Трясём
|
||||
@@ -2718,11 +2759,13 @@ void pop_loose_tick(void) __banked
|
||||
/* ждём молча */
|
||||
} else if (m >= 0x84) { /* конец тряски: сброс + покой */
|
||||
*lm = 0;
|
||||
loose_shake(1, 0); /* последний удар — без гейта */
|
||||
/* modif уже 0 → кадр покоя обоих тайлов (col и col+1:
|
||||
* правая грань loose живёт в соседе). ОБЕ страницы —
|
||||
* иначе на одной застывает дрожащий кадр → мерцание. */
|
||||
pop_set_redraw(pos, POP_RD_LOOSE, 2);
|
||||
} else {
|
||||
loose_shake(0, m);
|
||||
pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */
|
||||
}
|
||||
} else if (m >= 11 && pop_loose_fell) {
|
||||
@@ -2753,6 +2796,7 @@ void pop_loose_tick(void) __banked
|
||||
pop_loose_mob_spawn(row, col); /* отрыв падающего куска */
|
||||
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */
|
||||
} else { /* отсчёт 1..10 — дрожащий кадр */
|
||||
loose_shake(0, m);
|
||||
pop_set_redraw(pos, POP_RD_LOOSE, 1);
|
||||
}
|
||||
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
|
||||
@@ -2774,9 +2818,10 @@ void pop_loose_tick(void) __banked
|
||||
* цикл, а не соседний. */
|
||||
if (pop_current_level == POP_LOOSE_TILES_LEVEL) {
|
||||
/* ждём молча */
|
||||
} else if (m >= 0x84) { *lm = 0;
|
||||
} else if (m >= 0x84) { *lm = 0; loose_shake(1, 0);
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
|
||||
else pop_set_redraw_above(col, POP_RDA_CEIL, 1);
|
||||
else { loose_shake(0, m);
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 1); }
|
||||
} else if (m >= 11 && pop_ceil_fell) {
|
||||
*lm = 10; /* сигнал занят — кадр подождём */
|
||||
} else if (m >= 11) { /* loose_floor_delay — плита рушится */
|
||||
@@ -2786,6 +2831,7 @@ void pop_loose_tick(void) __banked
|
||||
pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */
|
||||
pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */
|
||||
} else {
|
||||
loose_shake(0, m);
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 1); /* отсчёт 1..10 */
|
||||
}
|
||||
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
|
||||
@@ -3005,9 +3051,10 @@ static void start_anim_spike(uint8_t tilepos)
|
||||
if (!mod) return;
|
||||
old = (int8_t)mod[tilepos];
|
||||
if (old <= 0) {
|
||||
if (old == 0)
|
||||
pop_add_trob(g_room, tilepos, 1); /* (TODO: sound_49_spikes) */
|
||||
else if ((uint8_t)old != 0xFF)
|
||||
if (old == 0) {
|
||||
pop_add_trob(g_room, tilepos, 1);
|
||||
pop_sfx_play(49); /* seg007:08F6 — пики пошли */
|
||||
} else if ((uint8_t)old != 0xFF)
|
||||
mod[tilepos] = 0x8F;
|
||||
}
|
||||
}
|
||||
@@ -3032,7 +3079,8 @@ static void spiked(uint8_t tilepos)
|
||||
Char.x = (uint8_t)(pop_x_bump[Char.curr_col + FIRST_ONSCREEN_COLUMN] + 10);
|
||||
Char.x = (uint8_t)char_dx_forward(8);
|
||||
Char.fall_y = 0;
|
||||
pop_take_hp(100); /* (TODO: sound_48_spiked) */
|
||||
pop_sfx_play(48);
|
||||
pop_take_hp(100);
|
||||
pop_char_set_seq(SEQ_51_SPIKED);
|
||||
play_seq();
|
||||
determine_col();
|
||||
@@ -3104,7 +3152,8 @@ static void chomped(uint8_t tilepos, int8_t col)
|
||||
Char.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX);
|
||||
Char.x = (uint8_t)char_dx_forward(7 - !Char.direction);
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
pop_take_hp(100); /* (TODO: sound_46_chomped) */
|
||||
pop_sfx_play(46);
|
||||
pop_take_hp(100);
|
||||
pop_char_set_seq(SEQ_54_CHOMPED);
|
||||
play_seq();
|
||||
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
|
||||
@@ -3312,7 +3361,7 @@ static void jump_through_mirror(void)
|
||||
(void)mirror_image();
|
||||
pop_jumped_mirror = 0;
|
||||
Char.charid = CHARID_1_SHADOW;
|
||||
/* play_sound(45) — звука в порте пока нет */
|
||||
pop_sfx_play(45); /* seg003:0617 — прыжок в зеркало */
|
||||
pop_saveshad();
|
||||
guardhp_max = guardhp_curr = hitp_max;
|
||||
hitp_curr = 1;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include "pop_bg.h"
|
||||
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
|
||||
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
||||
#include "_pop_bg.h" /* контракт с горячей половиной (pop_bg.c, банк 2) */
|
||||
#include "pop_tile.h" /* общие листья слоя фона (резидент): блит, тайлы, таблица */
|
||||
@@ -1493,6 +1494,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
* КНОПКА под куском обязаны сработать — сигналим главному циклу
|
||||
* комнатой и тайлом (loose_land, seg007:11E8, работает с
|
||||
* curmob.room, а не с отрисованной). */
|
||||
pop_sfx_play(2); /* tile crashing, seg007:1041 */
|
||||
pop_loose_exit_room = mt_m->room;
|
||||
pop_loose_exit = (uint8_t)(mt_m->row * 10 + mt_m->col + 1);
|
||||
/* clean = 2 даже для чужой комнаты: пока кусок летел, он мог
|
||||
@@ -1504,6 +1506,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
* подержим кусок на границе и приземлим следующим кадром */
|
||||
mt_m->y = MOB_Y_BOUND[mt_m->row + 1];
|
||||
} else {
|
||||
pop_sfx_play(2); /* tile crashing, seg007:1041 */
|
||||
mt_m->y = MOB_Y_BOUND[mt_m->row + 1];
|
||||
pop_loose_landed = (uint8_t)(mt_m->row * 10 + mt_m->col + 1);
|
||||
mt_m->active = 0; mt_m->clean = 2; /* кусок исчез — на его месте debris */
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* pop_sfx.c — ГОРЯЧАЯ часть звуковых эффектов: насос в прерывании CBL,
|
||||
* запуск/останов и таблица звуков. Зачем всё это и в каком виде лежат
|
||||
* данные — в шапке pop_sfx.h и ../docs/sound_plan.md.
|
||||
*
|
||||
* ПОЧЕМУ МОДУЛЬ РЕЗИДЕНТНЫЙ (в EXTRA_SRCS, а не в банке). Три причины,
|
||||
* и каждой хватило бы одной:
|
||||
* - callback насоса зовётся ИЗ ПРЕРЫВАНИЯ; трамплин перед вызовом
|
||||
* ставит базовую страницу W1, значит хендлер обязан быть в базе;
|
||||
* - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг
|
||||
* Кида, физика, боёвка) — трамплин там был бы чистой потерей;
|
||||
* - pop_page_read маппит W3 на время чтения, а банковый код сам живёт
|
||||
* в W3 и вырезал бы себя из-под ног (та же причина, по которой
|
||||
* резидентен pop_level_read_file, см. _pop_level.h).
|
||||
* Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter.h>
|
||||
#include <cbl.h>
|
||||
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
|
||||
#include "pop_sfx.h"
|
||||
#include "_pop_sfx.h"
|
||||
|
||||
/* Физические страницы набора; заполняет холодная половина. */
|
||||
uint8_t pop_snd_page[POP_SND_PAGES];
|
||||
uint8_t pop_snd_ok;
|
||||
|
||||
/* Курсор проигрывания. volatile: пишет главный цикл, читает прерывание.
|
||||
* Всё в 8/16 бит — 32-битной арифметики на Z80 избегаем (см. memory
|
||||
* avoid_32bit_arith_z80); поэтому и в таблице смещение уже разобрано на
|
||||
* страницу и адрес в окне. */
|
||||
static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */
|
||||
static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */
|
||||
static volatile uint16_t sfx_left; /* сколько байт осталось (кратно 128) */
|
||||
|
||||
/* ---- насос ---------------------------------------------------------- *
|
||||
* Зовётся из ISR CBL раз в ~11,7 мс за очередными 128 байтами.
|
||||
*
|
||||
* ОКНО W0 БЕРЁМ ВЗАЙМЫ. Фон в этот момент может держать в W0 страницу
|
||||
* атласа (pop_blit_b делает map ... blit ... unmap), поэтому сохраняем
|
||||
* порт, подставляем свою страницу и возвращаем как было. Для фона
|
||||
* подмена невидима: прерывание для него атомарно. Тот же приём, что у
|
||||
* трамплина с W1 (_irq_tramp.c).
|
||||
*
|
||||
* Заплатки ISR-стаба в 0x38/0x66 нашим страницам НЕ нужны (в отличие от
|
||||
* страницы уровня, pop_level_read_file): пока страница замаплена, мы
|
||||
* внутри прерывания и IFF сброшен — прерваться некому.
|
||||
*
|
||||
* ТИШИНУ ЛЬЁМ СВОЮ. Когда эффект кончился, отдавать в CBL всё равно надо:
|
||||
* иначе железо крутит по кругу хвост своего буфера и это слышно как
|
||||
* жужжание. Готовый блок 0x80 лежит первым в наборе (упаковщик), поэтому
|
||||
* libc-шный CBL_UNDERRUN_SILENCE с его malloc нам не нужен — а куча в
|
||||
* тесном резиденте W2 отказала бы и уронила весь звук. */
|
||||
int pop_sfx_fill(uint16_t n)
|
||||
{
|
||||
uint8_t saved, pg;
|
||||
uint16_t ptr;
|
||||
if (sfx_left) { pg = sfx_pg; ptr = sfx_ptr; }
|
||||
else { pg = POP_SND_SILENCE_PAGE; ptr = POP_SND_SILENCE_OFF; }
|
||||
saved = _io_page_w0;
|
||||
_io_page_w0 = pop_snd_page[pg];
|
||||
cbl_push_otir((const void *)ptr, n);
|
||||
_io_page_w0 = saved;
|
||||
if (!sfx_left) return 1; /* играли тишину — курсор не двигаем */
|
||||
|
||||
sfx_ptr += n;
|
||||
if (sfx_ptr >= 0x4000) { /* граница страницы — ровно по блоку */
|
||||
sfx_ptr = 0;
|
||||
sfx_pg++;
|
||||
}
|
||||
sfx_left -= n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- запуск / останов ------------------------------------------------ */
|
||||
|
||||
void pop_sfx_play(uint8_t id)
|
||||
{
|
||||
uint8_t pg;
|
||||
uint16_t off, len;
|
||||
if (!pop_snd_ok || id >= POP_SND_COUNT) return;
|
||||
pg = pop_snd_tbl[id].page;
|
||||
off = pop_snd_tbl[id].off;
|
||||
len = pop_snd_tbl[id].len;
|
||||
/* Курсор из трёх полей, а читает его прерывание — обновляем под DI,
|
||||
* иначе насос может застать наполовину обновлённое состояние и уехать
|
||||
* в чужие данные. */
|
||||
IRQ_DISABLE();
|
||||
sfx_pg = pg;
|
||||
sfx_ptr = off;
|
||||
sfx_left = len; /* len == 0 — просто глушим */
|
||||
IRQ_ENABLE();
|
||||
}
|
||||
|
||||
void pop_sfx_stop(void)
|
||||
{
|
||||
IRQ_DISABLE();
|
||||
sfx_left = 0;
|
||||
IRQ_ENABLE();
|
||||
}
|
||||
|
||||
/* ---- чтение файла в ФИЗИЧЕСКУЮ страницу ------------------------------ *
|
||||
* Резидентно по причине из шапки: маппим W3, читаем, возвращаем окно.
|
||||
* Ровно то же делает atlas_load, поэтому вызов из банка безопасен.
|
||||
* 0 — прочитано ожидаемое число байт, -1 — нет. */
|
||||
int pop_page_read(const char *path, uint8_t phys, uint16_t want)
|
||||
{
|
||||
uint8_t saved_w3;
|
||||
int fd, n;
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
saved_w3 = _io_page_w3;
|
||||
sprinter_page_w3(phys);
|
||||
n = read(fd, (uint8_t *)0xC000, want);
|
||||
_io_page_w3 = saved_w3;
|
||||
close(fd);
|
||||
return (n == (int)want) ? 0 : -1;
|
||||
}
|
||||
@@ -21,11 +21,20 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Поднять звук: занять 8 EMM-страниц, вычитать в них набор, открыть CBL.
|
||||
* 0 — OK, -1 — не получилось (тогда pop_sfx_play молча ничего не делает,
|
||||
* игра идёт без звука). Звать один раз при старте. */
|
||||
/* Поднять набор: занять 8 EMM-страниц и вычитать в них данные. ВЫВОД НЕ
|
||||
* ВКЛЮЧАЕТ — см. pop_sfx_start. 0 — OK, -1 — не получилось (тогда всё
|
||||
* дальнейшее молча ничего не делает, игра идёт без звука). */
|
||||
int pop_sfx_init(void) __banked;
|
||||
|
||||
/* Включить вывод (открыть CBL). Звать ПОСЛЕ всей загрузки: пока идёт
|
||||
* чтение файлов, ESTEX уходит в диск надолго, насос не успевает долить
|
||||
* блок, и железо крутит хвост своего буфера — на слух сплошной скрежет.
|
||||
* Идемпотентно. */
|
||||
int pop_sfx_start(void) __banked;
|
||||
|
||||
/* Заглушить вывод на время загрузки (закрывает CBL). Парная к start. */
|
||||
void pop_sfx_pause(void) __banked;
|
||||
|
||||
/* Закрыть CBL и отпустить страницы; звать на выходе. */
|
||||
void pop_sfx_close(void) __banked;
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* pop_sfx_cold.c — ХОЛОДНАЯ половина звука: поднять набор и открыть CBL.
|
||||
* Отрабатывает один раз за запуск, поэтому живёт в банке 8; горячая
|
||||
* половина (насос, запуск эффекта) — в резиденте, см. pop_sfx.c.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include <cbl.h>
|
||||
#include "pop_sfx.h"
|
||||
#define POP_SND_FILES /* имена файлов нужны ТОЛЬКО здесь, см. pop_sound_tbl.h */
|
||||
#include "_pop_sfx.h"
|
||||
|
||||
static uint8_t snd_blk; /* блок EMM под весь набор */
|
||||
|
||||
int pop_sfx_init(void) __banked
|
||||
{
|
||||
uint8_t i, blk;
|
||||
|
||||
if (pop_snd_ok) return 0;
|
||||
|
||||
/* Одним блоком: страницы блока идут подряд по логическому индексу, и
|
||||
* таблица звуков адресует их именно так (page = смещение >> 14). */
|
||||
blk = mem_alloc_pages(POP_SND_PAGES);
|
||||
if (!blk) return -1;
|
||||
for (i = 0; i < POP_SND_PAGES; i++) {
|
||||
pop_snd_page[i] = mem_get_page(blk, i);
|
||||
if (!pop_snd_page[i] ||
|
||||
pop_page_read(pop_snd_files[i], pop_snd_page[i], 16384) != 0) {
|
||||
mem_free_block(blk);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
snd_blk = blk;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ЗАПУСТИТЬ ВЫВОД — ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика.
|
||||
*
|
||||
* Пока идёт чтение файлов, CBL держать открытым НЕЛЬЗЯ: ESTEX уходит в
|
||||
* диск надолго, насос не успевает долить очередные 128 байт, и железо
|
||||
* крутит по кругу хвост своего 256-байтового буфера. На слух это
|
||||
* сплошной скрежет (поймано пользователем на старте, 2026-08-20).
|
||||
* Поэтому загрузка страниц и открытие CBL разведены: грузим молча,
|
||||
* включаем звук последним действием, а на время загрузки уровня глушим
|
||||
* (pop_sfx_pause).
|
||||
*
|
||||
* Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один
|
||||
* (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит.
|
||||
* UNDERRUN_APP, а не SILENCE: тишину насос льёт СВОЮ (первый блок
|
||||
* набора), и просить у libc буфер через malloc не надо — куча в
|
||||
* резиденте W2 тесная, и её отказ ронял бы весь звук. */
|
||||
int pop_sfx_start(void) __banked
|
||||
{
|
||||
if (pop_snd_ok) return 0;
|
||||
if (!snd_blk) return -1; /* набор не загружен */
|
||||
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
|
||||
CBL_UNDERRUN_APP, pop_sfx_fill) != 0)
|
||||
return -1;
|
||||
pop_snd_ok = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_sfx_pause(void) __banked
|
||||
{
|
||||
if (!pop_snd_ok) return;
|
||||
pop_sfx_stop();
|
||||
cbl_close();
|
||||
pop_snd_ok = 0;
|
||||
}
|
||||
|
||||
void pop_sfx_close(void) __banked
|
||||
{
|
||||
pop_sfx_pause();
|
||||
if (snd_blk) { mem_free_block(snd_blk); snd_blk = 0; }
|
||||
}
|
||||
@@ -20,35 +20,38 @@
|
||||
#define POP_SND_PAGES 8
|
||||
#define POP_SND_COUNT 57
|
||||
#define POP_SND_BLOCK 128
|
||||
/* Первый блок страницы 0 — тишина для насоса (см. упаковщик). */
|
||||
#define POP_SND_SILENCE_PAGE 0
|
||||
#define POP_SND_SILENCE_OFF 0x0000
|
||||
|
||||
typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;
|
||||
|
||||
/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */
|
||||
static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
|
||||
{ 0, 0x0000, 1664 }, /* 0 */
|
||||
{ 0, 0x0680, 12032 }, /* 1 */
|
||||
{ 0, 0x3580, 6272 }, /* 2 */
|
||||
{ 1, 0x0E00, 1280 }, /* 3 */
|
||||
{ 1, 0x1300, 1152 }, /* 4 */
|
||||
{ 1, 0x1780, 1408 }, /* 5 */
|
||||
{ 1, 0x1D00, 5504 }, /* 6 */
|
||||
{ 1, 0x3280, 896 }, /* 7 */
|
||||
{ 1, 0x3600, 1664 }, /* 8 */
|
||||
{ 1, 0x3C80, 2816 }, /* 9 */
|
||||
{ 2, 0x0780, 4992 }, /* 10 */
|
||||
{ 2, 0x1B00, 1280 }, /* 11 */
|
||||
{ 2, 0x2000, 1280 }, /* 12 */
|
||||
{ 2, 0x2500, 3840 }, /* 13 */
|
||||
{ 2, 0x3400, 7552 }, /* 14 */
|
||||
{ 3, 0x1180, 17664 }, /* 15 */
|
||||
{ 4, 0x1680, 4736 }, /* 16 */
|
||||
{ 4, 0x2900, 4224 }, /* 17 */
|
||||
{ 4, 0x3980, 6912 }, /* 18 */
|
||||
{ 5, 0x1480, 2816 }, /* 19 */
|
||||
{ 5, 0x1F80, 1408 }, /* 20 */
|
||||
{ 5, 0x2500, 1152 }, /* 21 */
|
||||
{ 5, 0x2980, 1280 }, /* 22 */
|
||||
{ 5, 0x2E80, 1408 }, /* 23 */
|
||||
{ 0, 0x0080, 1664 }, /* 0 */
|
||||
{ 0, 0x0700, 12032 }, /* 1 */
|
||||
{ 0, 0x3600, 6272 }, /* 2 */
|
||||
{ 1, 0x0E80, 1280 }, /* 3 */
|
||||
{ 1, 0x1380, 1152 }, /* 4 */
|
||||
{ 1, 0x1800, 1408 }, /* 5 */
|
||||
{ 1, 0x1D80, 5504 }, /* 6 */
|
||||
{ 1, 0x3300, 896 }, /* 7 */
|
||||
{ 1, 0x3680, 1664 }, /* 8 */
|
||||
{ 1, 0x3D00, 2816 }, /* 9 */
|
||||
{ 2, 0x0800, 4992 }, /* 10 */
|
||||
{ 2, 0x1B80, 1280 }, /* 11 */
|
||||
{ 2, 0x2080, 1280 }, /* 12 */
|
||||
{ 2, 0x2580, 3840 }, /* 13 */
|
||||
{ 2, 0x3480, 7552 }, /* 14 */
|
||||
{ 3, 0x1200, 17664 }, /* 15 */
|
||||
{ 4, 0x1700, 4736 }, /* 16 */
|
||||
{ 4, 0x2980, 4224 }, /* 17 */
|
||||
{ 4, 0x3A00, 6912 }, /* 18 */
|
||||
{ 5, 0x1500, 2816 }, /* 19 */
|
||||
{ 5, 0x2000, 1408 }, /* 20 */
|
||||
{ 5, 0x2580, 1152 }, /* 21 */
|
||||
{ 5, 0x2A00, 1280 }, /* 22 */
|
||||
{ 5, 0x2F00, 1408 }, /* 23 */
|
||||
{ 0, 0x0000, 0 }, /* 24 */
|
||||
{ 0, 0x0000, 0 }, /* 25 */
|
||||
{ 0, 0x0000, 0 }, /* 26 */
|
||||
@@ -69,14 +72,14 @@ static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
|
||||
{ 0, 0x0000, 0 }, /* 41 */
|
||||
{ 0, 0x0000, 0 }, /* 42 */
|
||||
{ 0, 0x0000, 0 }, /* 43 */
|
||||
{ 5, 0x3400, 2688 }, /* 44 */
|
||||
{ 5, 0x3E80, 4224 }, /* 45 */
|
||||
{ 6, 0x0F00, 4608 }, /* 46 */
|
||||
{ 6, 0x2100, 5120 }, /* 47 */
|
||||
{ 6, 0x3500, 5120 }, /* 48 */
|
||||
{ 7, 0x0900, 4480 }, /* 49 */
|
||||
{ 5, 0x3480, 2688 }, /* 44 */
|
||||
{ 5, 0x3F00, 4224 }, /* 45 */
|
||||
{ 6, 0x0F80, 4608 }, /* 46 */
|
||||
{ 6, 0x2180, 5120 }, /* 47 */
|
||||
{ 6, 0x3580, 5120 }, /* 48 */
|
||||
{ 7, 0x0980, 4480 }, /* 49 */
|
||||
{ 0, 0x0000, 0 }, /* 50 */
|
||||
{ 7, 0x1A80, 4864 }, /* 51 */
|
||||
{ 7, 0x1B00, 4864 }, /* 51 */
|
||||
{ 0, 0x0000, 0 }, /* 52 */
|
||||
{ 0, 0x0000, 0 }, /* 53 */
|
||||
{ 0, 0x0000, 0 }, /* 54 */
|
||||
@@ -84,6 +87,11 @@ static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
|
||||
{ 0, 0x0000, 0 }, /* 56 */
|
||||
};
|
||||
|
||||
/* Имена файлов видит ТОЛЬКО тот, кто попросил POP_SND_FILES (холодная
|
||||
* половина): иначе копия строк уезжает в каждый включивший заголовок
|
||||
* модуль, а в резиденте W1/W2 каждая сотня байт на счету. Тот же
|
||||
* приём, что у POP_BG_ATLAS_NAMES в pop_bg_atlas.h. */
|
||||
#ifdef POP_SND_FILES
|
||||
static const char *const pop_snd_files[POP_SND_PAGES] = {
|
||||
"SND\\s0.bin",
|
||||
"SND\\s1.bin",
|
||||
@@ -94,5 +102,6 @@ static const char *const pop_snd_files[POP_SND_PAGES] = {
|
||||
"SND\\s6.bin",
|
||||
"SND\\s7.bin",
|
||||
};
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* См. pop_trob.h.
|
||||
*/
|
||||
#include "pop_trob.h"
|
||||
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
|
||||
#include "pop_state.h" /* pop_leveldoor_open */
|
||||
#include "pop_level.h"
|
||||
#include "pop_geom.h"
|
||||
@@ -265,6 +266,7 @@ static void animate_chomper(uint8_t *m, int8_t *type,
|
||||
frame = (uint8_t)((*m & 0x7F) + 1);
|
||||
if (frame > POP_CHOMPER_SPEED) frame = 1;
|
||||
*m = (uint8_t)(blood | frame);
|
||||
if (frame == 2) pop_sfx_play(47); /* щелчок челюстей, seg007:297 */
|
||||
if ((room != drawn_room || (int8_t)TP_ROW[tp] != Kid.curr_row ||
|
||||
(Kid.alive >= 0 && blood == 0)) && frame >= 6)
|
||||
*type = -1;
|
||||
@@ -463,8 +465,9 @@ void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
||||
set_doorlink_timer(modifier, POP_DBG_GATE_HOLD);
|
||||
if (link_timer < 2) {
|
||||
pop_add_trob(room, tilepos, 1); /* кнопка нажимается */
|
||||
/* seg007:766: щелчок кнопки — шум, страж оборачивается. Звука у
|
||||
* нас нет, флаг обязан быть (BUG-GUARD-DEAF-1). */
|
||||
/* seg007:766: щелчок кнопки — шум, и страж оборачивается
|
||||
* (флаг обязателен даже без звука, BUG-GUARD-DEAF-1). */
|
||||
pop_sfx_play(3);
|
||||
is_guard_notice = 1;
|
||||
}
|
||||
do_trigger_list(modifier, button_type);
|
||||
@@ -523,10 +526,13 @@ static void animate_leveldoor(uint8_t *m, int8_t *type, uint8_t cur_room)
|
||||
uint8_t sp;
|
||||
if (*type < 7) (*type)++;
|
||||
sp = leveldoor_close_speeds[*type - 3];
|
||||
if (sp >= *m) { *m = 0; *type = -1; }
|
||||
else *m = (uint8_t)(*m - sp);
|
||||
if (sp >= *m) { *m = 0; *type = -1; pop_sfx_play(14); } /* seg007:437 */
|
||||
else {
|
||||
*m = (uint8_t)(*m - sp);
|
||||
if (*type == 4) pop_sfx_play(15); /* seg007:443 */
|
||||
}
|
||||
} else {
|
||||
if (*m < 43) (*m)++;
|
||||
if (*m < 43) { (*m)++; pop_sfx_play(15); } /* seg007:465 */
|
||||
if (*m >= 43) {
|
||||
*m = 43; *type = -1;
|
||||
/* Условие оригинала — переход ИЗ 0 или 2, то есть ровно один раз
|
||||
@@ -546,7 +552,7 @@ static void animate_leveldoor(uint8_t *m, int8_t *type, uint8_t cur_room)
|
||||
static const uint8_t gate_close_speeds[9] = {0, 0, 0, 20, 40, 60, 80, 100, 120};
|
||||
static const int8_t door_delta[3] = {-1, 4, 4};
|
||||
|
||||
static void animate_door(uint8_t *m, int8_t *type)
|
||||
static void animate_door(uint8_t *m, int8_t *type, uint8_t audible)
|
||||
{
|
||||
int8_t anim_type = *type;
|
||||
uint8_t mod = *m;
|
||||
@@ -555,18 +561,23 @@ static void animate_door(uint8_t *m, int8_t *type)
|
||||
if (anim_type < 8) { anim_type++; *type = anim_type; }
|
||||
{
|
||||
int new_mod = (int)mod - gate_close_speeds[anim_type];
|
||||
if (new_mod < 0) { mod = 0; *type = -1; }
|
||||
if (new_mod < 0) { mod = 0; *type = -1; pop_sfx_play(6); }
|
||||
else mod = (uint8_t)new_mod;
|
||||
}
|
||||
} else if (mod != 0xFF) {
|
||||
mod = (uint8_t)(mod + door_delta[anim_type]);
|
||||
if (anim_type == 0) { /* закрытие */
|
||||
if (mod == 0) *type = -1; /* gate_stop */
|
||||
/* seg007:376: скрип закрывающейся решётки — раз в четыре
|
||||
* шага модификатора и ТОЛЬКО если комната на экране
|
||||
* (play_door_sound_if_visible), иначе на 13-м уровне
|
||||
* скрипели бы решётки из соседних комнат. */
|
||||
else if (mod < 188 && (mod & 3) == 3 && audible) pop_sfx_play(4);
|
||||
} else { /* открытие */
|
||||
if (mod >= 188) {
|
||||
if (anim_type < 2) { mod = 238; *type = 0; } /* открыт → потом закрывать */
|
||||
if (anim_type < 2) { mod = 238; *type = 0; pop_sfx_play(7); }
|
||||
else { mod = 0xFF; *type = -1; } /* насовсем */
|
||||
}
|
||||
} else if ((mod & 7) == 0) pop_sfx_play(5); /* seg007:386 */
|
||||
}
|
||||
} else {
|
||||
*type = -1;
|
||||
@@ -672,7 +683,7 @@ void pop_process_trobs(uint8_t cur_room) __banked
|
||||
animate_button(mod[tp], &type); /* mod[tp] = индекс LINKLOC кнопки */
|
||||
break;
|
||||
case TILE_GATE:
|
||||
animate_door(&mod[tp], &type);
|
||||
animate_door(&mod[tp], &type, (uint8_t)(room == cur_room));
|
||||
break;
|
||||
case TILE_LOOSE:
|
||||
animate_loose(&mod[tp], &type, room, tp, cur_room);
|
||||
|
||||
@@ -282,59 +282,9 @@ int main(void)
|
||||
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
|
||||
if (kbd_raw_down(0x16)) frozen = 1;
|
||||
if (kbd_raw_down(0x1E)) frozen = 0;
|
||||
{ /* Читы (seg000: switch под if (cheats_enabled)). Все — по
|
||||
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
|
||||
* K — убить стража;
|
||||
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
|
||||
* иначе боёвку проверять получается ровно три раза);
|
||||
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
|
||||
* берётся с пола (pop_have_sword ставит подбор);
|
||||
* Shift+L — следующий уровень (seg000:698, ветка next_level =
|
||||
* current_level + 1). Единственный вменяемый способ
|
||||
* тестировать уровни 2..15. NB: L без Shift — отладочный
|
||||
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
|
||||
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
|
||||
* что конфликта на практике нет;
|
||||
* [ / ] — подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
|
||||
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
|
||||
* теряет нажатия при быстрой отправке, и подогнать Кида в
|
||||
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
|
||||
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
|
||||
* персонажа в этом порте задаёт не диспетчер (грабли
|
||||
* L1-EXIT). Колонку не трогаем — её пересчитает физика
|
||||
* следующего кадра. */
|
||||
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
|
||||
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
|
||||
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
|
||||
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
|
||||
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
|
||||
uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE);
|
||||
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
|
||||
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
|
||||
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
|
||||
/* Бессмертие по кругу: 0 выкл -> 1 «только бой» -> 2 «бой и
|
||||
* мелкий урон» -> 0. Что означают уровни — pop_state.h. */
|
||||
if (pop_cheats && i && !immo_prev)
|
||||
pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1);
|
||||
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
|
||||
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
|
||||
pop_next_level = (uint8_t)(pop_current_level + 1);
|
||||
/* U — переворот экрана (seg000:0793 — чит и в оригинале).
|
||||
* Тот же путь, что у зелья типа 4: флаг + пометка «перевернулись». */
|
||||
if (pop_cheats && up && !upsd_prev) { /* тот же путь, что у зелья */
|
||||
pop_upside_want = (uint8_t)!pop_upside_want;
|
||||
pop_upside_dirty = 1;
|
||||
}
|
||||
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
|
||||
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
|
||||
cheat_prev = k;
|
||||
immo_prev = i;
|
||||
sword_prev = s;
|
||||
nlvl_prev = nl;
|
||||
upsd_prev = up;
|
||||
xdec_prev = xd;
|
||||
xinc_prev = xi;
|
||||
}
|
||||
/* Читы — В БАНКЕ (roomtest_cold.c): они дёргаются раз в кадр, а места
|
||||
* в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */
|
||||
pop_cheat_tick();
|
||||
if (frozen) {
|
||||
PROF(0); /* чёрный: ждём vsync */
|
||||
/* Заморозка — тоже полноценный логический кадр: иначе якорь
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "pop_tune.h"
|
||||
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
||||
#include "pop_shadow.h" /* запечённый набор Тени */
|
||||
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
||||
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
||||
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
||||
#include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */
|
||||
@@ -308,6 +309,74 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/* ---- ЧИТЫ (seg000: switch под if (cheats_enabled)) -------------------- *
|
||||
* Переехали из главного цикла в банк 8: дёргаются раз в кадр, то есть
|
||||
* один трамплин на кадр, а места в резиденте W1/W2 не осталось (звук,
|
||||
* атлас Тени и пейсинг съели остаток; см. TASKS_OPEN, MEM).
|
||||
*
|
||||
* Флаги «фронта» стали статиками модуля — раньше они были локалями main.
|
||||
* Смысл прежний: одно нажатие = одно срабатывание.
|
||||
*/
|
||||
static uint8_t cheat_prev, immo_prev, sword_prev, nlvl_prev;
|
||||
static uint8_t upsd_prev, xdec_prev, xinc_prev;
|
||||
|
||||
void pop_cheat_tick(void) __banked
|
||||
{
|
||||
/* Читы (seg000: switch под if (cheats_enabled)). Все — по
|
||||
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
|
||||
* K — убить стража;
|
||||
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
|
||||
* иначе боёвку проверять получается ровно три раза);
|
||||
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
|
||||
* берётся с пола (pop_have_sword ставит подбор);
|
||||
* Shift+L — следующий уровень (seg000:698, ветка next_level =
|
||||
* current_level + 1). Единственный вменяемый способ
|
||||
* тестировать уровни 2..15. NB: L без Shift — отладочный
|
||||
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
|
||||
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
|
||||
* что конфликта на практике нет;
|
||||
* [ / ] — подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
|
||||
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
|
||||
* теряет нажатия при быстрой отправке, и подогнать Кида в
|
||||
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
|
||||
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
|
||||
* персонажа в этом порте задаёт не диспетчер (грабли
|
||||
* L1-EXIT). Колонку не трогаем — её пересчитает физика
|
||||
* следующего кадра. */
|
||||
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
|
||||
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
|
||||
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
|
||||
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
|
||||
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
|
||||
uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE);
|
||||
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
|
||||
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
|
||||
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
|
||||
/* Бессмертие по кругу: 0 выкл -> 1 «только бой» -> 2 «бой и
|
||||
* мелкий урон» -> 0. Что означают уровни — pop_state.h. */
|
||||
if (pop_cheats && i && !immo_prev)
|
||||
pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1);
|
||||
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
|
||||
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
|
||||
pop_next_level = (uint8_t)(pop_current_level + 1);
|
||||
/* U — переворот экрана (seg000:0793 — чит и в оригинале).
|
||||
* Тот же путь, что у зелья типа 4: флаг + пометка «перевернулись». */
|
||||
if (pop_cheats && up && !upsd_prev) { /* тот же путь, что у зелья */
|
||||
pop_upside_want = (uint8_t)!pop_upside_want;
|
||||
pop_upside_dirty = 1;
|
||||
}
|
||||
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
|
||||
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
|
||||
cheat_prev = k;
|
||||
immo_prev = i;
|
||||
sword_prev = s;
|
||||
nlvl_prev = nl;
|
||||
upsd_prev = up;
|
||||
xdec_prev = xd;
|
||||
xinc_prev = xi;
|
||||
}
|
||||
|
||||
/* ---- ОТЛАДКА: состояние прогона в верхнем борте ---------------------- *
|
||||
*
|
||||
* Четыре блока ПАЛОЧКАМИ, каждый своим цветом (outtextxy не годится: текст
|
||||
@@ -766,6 +835,7 @@ int pop_boot(void) __banked
|
||||
pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */
|
||||
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
||||
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */
|
||||
pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */
|
||||
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
|
||||
puts("kid atlas load failed");
|
||||
return -1;
|
||||
@@ -830,6 +900,7 @@ int pop_level_switch(void) __banked
|
||||
* дальше, а урон — нет (do_startpos дольёт до потолка). */
|
||||
hitp_beg_lev = hitp_max;
|
||||
pop_checkpoint = 0; /* уровень пройден (seg003:413) */
|
||||
pop_sfx_pause(); /* загрузка уровня — ESTEX надолго, звук глушим */
|
||||
if (pop_level_load_num(pop_next_level) != 0) {
|
||||
pop_next_level = pop_current_level; /* файла нет — остаёмся */
|
||||
} else {
|
||||
@@ -861,6 +932,7 @@ int pop_level_switch(void) __banked
|
||||
* и делать это в момент глотка нельзя. Звать можно откуда
|
||||
* угодно — функция идемпотентна. */
|
||||
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
|
||||
pop_sfx_start(); /* ...и обратно, когда всё прочитано */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -872,6 +944,7 @@ void pop_shutdown(void) __banked
|
||||
closegraph();
|
||||
pop_bg_free();
|
||||
pop_kid_free();
|
||||
pop_sfx_close();
|
||||
pop_shadow_free();
|
||||
pop_guard_free();
|
||||
pop_kid_data_free();
|
||||
|
||||
@@ -66,6 +66,10 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked;
|
||||
* звать можно каждый кадр. */
|
||||
void pop_dbg_room_label(uint8_t room) __banked;
|
||||
|
||||
/* Читы по фронту нажатия (K/I/S/Shift+L/U/[/]). В банке: раз в кадр,
|
||||
* а резидент W1/W2 переполнен — см. шапку функции. */
|
||||
void pop_cheat_tick(void) __banked;
|
||||
|
||||
/* Кто рисуется ПОЗЖЕ — Кид или соперник (порядок обхода тайлов оригинала,
|
||||
* set_objtile_at_char + sort_curr_objs). 1 = соперник поверх. Зовётся раз
|
||||
* в кадр, поэтому живёт в банке: трамплин на кадр дешевле полукилобайта
|
||||
|
||||
Reference in New Issue
Block a user