Compare commits
21 Commits
3dbad6120c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f3e854854 | |||
| 75a51fb1db | |||
| a32b66700f | |||
| 59e51f7e83 | |||
| b22cee3456 | |||
| 2e90eaf7d7 | |||
| 3983fa4513 | |||
| d014a3f577 | |||
| 4f7d9c0596 | |||
| 37fc572cc3 | |||
| 99b430f2ed | |||
| d438a1d3da | |||
| 79ea473910 | |||
| cf06896dbd | |||
| 8bbc6b4d07 | |||
| 565ba98852 | |||
| ac9871c58c | |||
| 1e6c377edc | |||
| 0280b05933 | |||
| f2093e0d89 | |||
| af5f0a4638 |
@@ -0,0 +1,43 @@
|
||||
# Идеи и вопросы «на подумать» (PoP)
|
||||
|
||||
Не план работ, а список того, что осознанно отложено: каждая запись —
|
||||
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
||||
сделать это прямо сейчас.
|
||||
|
||||
## Заменить генератор псевдослучайных чисел
|
||||
|
||||
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
|
||||
совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов),
|
||||
но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в
|
||||
обёртках вызова. Тексты процедур, разбор качества и порядок действий —
|
||||
`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение
|
||||
к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три.
|
||||
|
||||
## Отключать мышь на время игры
|
||||
|
||||
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
|
||||
см. memory `mouse_api`). Игре она не нужна вообще: управление —
|
||||
raw-клавиатура (`<kbd_raw.h>`), которую мы и так забираем у DSS целиком.
|
||||
Значит каждое мышиное прерывание за кадр — украденные такты в бюджете,
|
||||
который у нас и без того занят на 86 %.
|
||||
|
||||
**Откуда взялось (2026-07-30).** При замере бюджета по 100 кадрам три
|
||||
кадра выбились до 552–647 К тактов (1.28–1.51 кадра) при типичных 371 К.
|
||||
Причиной оказалось движение мыши на ХОСТЕ: при неподвижной мыши 225
|
||||
кадров подряд прошли без единого превышения. То есть эффект реальный и
|
||||
измеримый, просто в тесте он был наведён извне.
|
||||
|
||||
**Что проверить.**
|
||||
1. Есть ли у драйвера мыши (RST 30h) функция «выключить/включить» —
|
||||
разобрать список из 14 обёрток; если нет явной, посмотреть, что делает
|
||||
«hide cursor» и снимает ли она обработчик.
|
||||
2. Сколько тактов реально стоит одно мышиное прерывание на нашем железе
|
||||
(замер: breakpoint на входе ISR + totalcycles, при движении мыши).
|
||||
3. Не ломает ли отключение выход в DSS: состояние обязано
|
||||
восстанавливаться при `exit`, включая аварийный (atexit).
|
||||
|
||||
**Почему не сейчас.** Выигрыш проявляется только когда игрок реально
|
||||
двигает мышью, то есть в норме его нет; а риск оставить систему без мыши
|
||||
после выхода — заметный. Делать после того, как закроем стражей и
|
||||
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
||||
возможный возврат `pop_bg` в резидент `--w3`).
|
||||
@@ -18,7 +18,7 @@
|
||||
| 1. `kid_data.h` → EMM-страница + `load_frame`/`cur_frame` | **сделан** | `_CODE` −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) |
|
||||
| 2. `pop_geom.c` (дедуп геометрии + PRNG) | **сделан** | −41 Б `_CODE`, −11 Б W3; ценность — не байты, а bank-safe слой |
|
||||
| 3. `pop_map` без вызовов графики | **сделан** (фазы 1a/1b) | через пометки перерисовки, см. ниже |
|
||||
| 4. Разгрузка/перебалансировка W3 | **сделан** | в резиденте остался ТОЛЬКО `pop_bg`; W3 14 376 → 11 632 (свободно 4 752 Б) |
|
||||
| 4. Разгрузка/перебалансировка W3 | **сделан** | резидент = `pop_bg` + `pop_gdraw` (отрисовка стража, 2026-07-29); W3 14 376 → 12 819 (свободно 3 565 Б) |
|
||||
| 5. Данные (`room_modif`, `dl1/dl2`, `_kbdraw_down`) | не начат | потенциал ~1.5 КБ |
|
||||
| 6. Контракт банка стражей + пробник | **пробник сделан** | `tests/w3bankgfx` — модель подтверждена в MAME, см. ниже |
|
||||
| 7. Дедуп семейства `draw_tile` | отложен по решению пользователя | «мороки много, выгода не так велика» |
|
||||
@@ -27,6 +27,15 @@
|
||||
W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала
|
||||
на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение.
|
||||
|
||||
**Замер 2026-07-29 (после стража).** Появление стража съело кучу до 805 Б;
|
||||
разгрузка — вынос ОТРИСОВКИ стража в резидент (`pop_gdraw.c`, `--w3`), логика
|
||||
и состояние остались в W1/W2, чтобы банк `guards.c` их видел (R2). Итог:
|
||||
`_CODE` 26 149 → 25 703, куча **805 → 1 245 Б**, W3-резидент 11 656 → 12 819
|
||||
(свободно 4 728 → 3 565 Б), банк 1 — 236 / 16 384 Б. Граница «что резидент»
|
||||
теперь формулируется одним правилом: **резидент = только то, что рисует и
|
||||
зовётся исключительно из главного цикла**; всё, что может понадобиться банку,
|
||||
остаётся в W1/W2.
|
||||
|
||||
### Что сделано вместо §5.3 (вынос loose в W3)
|
||||
|
||||
Вместо переноса кода между окнами выбран (по обсуждению с пользователем)
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Генераторы псевдослучайных чисел: запасные варианты
|
||||
|
||||
Что сейчас стоит в порте, какие есть альтернативы и сколько на них реально
|
||||
можно выиграть. Заготовка на случай, если упрёмся в бюджет кадра —
|
||||
**сейчас менять ничего не нужно**.
|
||||
|
||||
## Что стоит сейчас
|
||||
|
||||
`pop_geom.c`, ветка `POP_PRANDOM_EXACT=1` (по умолчанию) — LCG оригинала
|
||||
`s = s*214013 + 2531011`, шаг написан на Z80-ассемблере (единственное такое
|
||||
место в порте). Схема Горнера по разреженной записи константы:
|
||||
|
||||
```
|
||||
214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3
|
||||
```
|
||||
|
||||
17 удвоений, три сложения, одно вычитание; величина `3*s`, нужная в конце,
|
||||
попадается по дороге на втором шаге. Тело — **≈1 020 тактов** по статическому
|
||||
подсчёту. Бит-в-бит совместим с SDLPoP, поэтому по картинке можно сверяться
|
||||
с эталоном.
|
||||
|
||||
Вторая ветка, `POP_PRANDOM_EXACT=0` — xorshift16 + шаг Вейля на C.
|
||||
Совместимость теряется.
|
||||
|
||||
Замер в MAME, комната 3, 175 кадров (медиана кадра):
|
||||
|
||||
| вариант | кадр | prandom → torch_draw |
|
||||
|---|---|---|
|
||||
| C, бит-в-бит (16-битные половины) | 403 632 | 10 933 |
|
||||
| C, xorshift16 + Вейль | 397 986 | 7 927 |
|
||||
| **asm, бит-в-бит (сейчас)** | **400 800** | **9 331** |
|
||||
|
||||
## Вариант A — комбинированный LFSR + LCG, ~148 тактов
|
||||
|
||||
Период > 4 млрд (lcm(65536, 65535) ≈ 4.29e9), младшие биты не вырождены.
|
||||
|
||||
```z80
|
||||
prng16:
|
||||
seed1=$+1
|
||||
ld hl, 9999
|
||||
ld b, h
|
||||
ld c, l
|
||||
add hl, hl
|
||||
add hl, hl
|
||||
inc l
|
||||
add hl, bc
|
||||
ld (seed1), hl
|
||||
seed2=$+1
|
||||
ld hl, 987
|
||||
add hl, hl
|
||||
sbc a, a
|
||||
and 101101b
|
||||
xor l
|
||||
ld l, a
|
||||
ld (seed2), hl
|
||||
add hl, bc
|
||||
ret
|
||||
```
|
||||
|
||||
Устройство: `seed1` — LCG `x = 5x + 1` (по модулю 2^16; `inc l` вместо
|
||||
`inc hl` — экономия байта, на период не влияет). `seed2` — 16-битный
|
||||
LFSR Галуа: сдвиг влево, и если выехала единица, XOR младшего байта с маской
|
||||
`0x2D` (примитивный многочлен `x^16 + x^5 + x^3 + x^2 + 1`). На выходе
|
||||
сумма обоих состояний — она и разрушает регулярность младших бит LCG.
|
||||
|
||||
**Что мешает взять как есть:** сиды зашиты в код (SMC), а нам нужны ДВЕ
|
||||
независимые последовательности — раскладка кладки и анимация тайлов.
|
||||
Пришлось бы передавать состояние через указатель, как сейчас у `pop_prandom`
|
||||
(это +20…40 тактов, не принципиально).
|
||||
|
||||
## Вариант B — xorshift(7,9,8), ~86 тактов
|
||||
|
||||
Самый быстрый, период 65535.
|
||||
|
||||
```z80
|
||||
xrnd:
|
||||
ld hl, 1 ; seed must not be 0
|
||||
ld a, h
|
||||
rra
|
||||
ld a, l
|
||||
rra
|
||||
xor h
|
||||
ld h, a
|
||||
ld a, l
|
||||
rra
|
||||
ld a, h
|
||||
rra
|
||||
xor l
|
||||
ld l, a
|
||||
xor h
|
||||
ld h, a
|
||||
ld (xrnd+1), hl
|
||||
ret
|
||||
```
|
||||
|
||||
**Две оговорки.** Ноль — неподвижная точка, а сид раскладки кладки у нас
|
||||
считается как `номер комнаты + смещение ряда + колонка` и вполне может
|
||||
оказаться нулём: нужен либо guard, либо шаг Вейля поверх. И тот же SMC-сид,
|
||||
что в варианте A.
|
||||
|
||||
## Чего НЕ брать: RND из Apple II
|
||||
|
||||
Оригинальный `Prince-of-Persia-Apple-II`:
|
||||
|
||||
```
|
||||
RNDseed := (5 * RNDseed + 23) mod 256
|
||||
```
|
||||
|
||||
```asm
|
||||
RND
|
||||
lda RNDseed
|
||||
asl
|
||||
asl
|
||||
clc
|
||||
adc RNDseed
|
||||
clc
|
||||
adc #23
|
||||
sta RNDseed
|
||||
rts
|
||||
```
|
||||
|
||||
Полный период 256 (`a ≡ 1 mod 4`, `c` нечётное), и для своего движка он
|
||||
работал. Нам не годится: у LCG по модулю 256 младшие биты вырождены — бит 0
|
||||
просто чередуется. Наши вызовы это увидят: раскладка кладки берёт
|
||||
`prandom(1)` (ОДИН бит) и `prandom(4)`, то есть вместо шума получилась бы
|
||||
аккуратная шахматка.
|
||||
|
||||
## Сколько реально можно выиграть
|
||||
|
||||
Меньше, чем кажется по числам 86/148 против 1 020. Тело генератора — уже не
|
||||
весь расход: остаются обёртка `pop_prandom`, приведение к диапазону
|
||||
`pop_rnd_fit` и ABI вызова. Верхняя граница выигрыша видна из замера выше:
|
||||
между нынешним asm-LCG и самым дешёвым из проверенных вариантов разница
|
||||
**2 814 тактов за кадр (0.65 %)** при двух вызовах за кадр, и это ПОТОЛОК —
|
||||
любой из вариантов A/B ниже него не опустится.
|
||||
|
||||
Порядок действий, если понадобится:
|
||||
|
||||
1. Сначала убрать обёртки: слить `pop_rnd_fit` в ту же asm-процедуру, чтобы
|
||||
на вызов приходился один `call`, а не три. Это ничего не ломает и не
|
||||
трогает совместимость с эталоном.
|
||||
2. И только если этого мало — менять генератор, начиная с варианта A
|
||||
(качество последовательности у него не хуже LCG, в отличие от B).
|
||||
|
||||
Важно помнить: число вызовов вырастет с боёвкой. Сейчас их два за кадр
|
||||
(факелы), а `guard_advance` / `guard_block` / `guard_strike` дёргают
|
||||
`prandom(255)` каждый по разу за кадр боя — то есть при драке станет 5–6, и
|
||||
цена вопроса вырастет во столько же раз.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -37,5 +37,5 @@ static const char *const kid_atl[KID_PAGES] = {
|
||||
};
|
||||
#define KID_PAL_FILE "kid.pal"
|
||||
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
|
||||
#define KID_SWORD_ID0 20 /* индекс в атласе = sword_tbl.id - ID0 */
|
||||
#define KID_SWORD_ID0 0 /* индекс в атласе = sword_tbl.id - ID0 */
|
||||
#endif
|
||||
|
||||
Binary file not shown.
@@ -6,13 +6,22 @@ EXAMPLE := roomtest
|
||||
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
|
||||
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
|
||||
MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
|
||||
# --w3 берёт ОДИН файл на флаг. В W3-резиденте держим ТОЛЬКО pop_bg —
|
||||
# отрисовку тайлов: она большая и её зовёт лишь главный цикл. Всё
|
||||
# остальное живёт в W1/W2, чтобы оставаться достижимым из будущих
|
||||
# __banked-модулей (стражи): из банка резидентная страница W3 не видна —
|
||||
# ни напрямую, ни транзитивно. См. docs/layout_plan_v2.md §2 (R2), §4.
|
||||
EXTRA_FLAGS ?= --gfx 256 --w3 pop_bg.c --bank 1=guards.c
|
||||
EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c pop_map.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c
|
||||
# Резидента --w3 БОЛЬШЕ НЕТ: отрисовка уехала в БАНК 2 (pop_bg + pop_gdraw
|
||||
# в одной странице — вызовы между ними прямые, если функция не помечена
|
||||
# __banked). Так снято главное ограничение резидента: из банка его было
|
||||
# не достать ни прямо, ни транзитивно, а банк→банк работает (трамплин
|
||||
# сохраняет страницу окна на СТЕКЕ, проверено вложенной цепочкой
|
||||
# банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
|
||||
# логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
|
||||
# sdcc_banked_call_rules.
|
||||
EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c $(PROF_FLAGS)
|
||||
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
|
||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||
# MAME = время фазы в растровых строках.
|
||||
PROF ?= 1
|
||||
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
||||
EXTRA_SRCS := pop_state.c pop_kid.c pop_ctrl.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c
|
||||
|
||||
BG_DIR := $(CURDIR)/../poc/res/bg
|
||||
KID_DIR := $(CURDIR)/../poc/res/kid
|
||||
@@ -62,4 +71,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
|
||||
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
|
||||
cd $(TC) && python3 pop_extract_kid_data.py
|
||||
|
||||
$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
|
||||
$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_gdraw.c pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
|
||||
|
||||
@@ -10,13 +10,490 @@
|
||||
* пока исполняется банк, резидентной страницы в адресном пространстве
|
||||
* нет. Поэтому фон/окклюзию за стража рисует главный цикл.
|
||||
*
|
||||
* Сейчас — каркас: тик есть, ИИ и боёвка (порт seg005 control_guard*,
|
||||
* seg004 fight/collision) добавляются сюда.
|
||||
* Порт: seg003 check_can_guard_see_kid, seg002 autocontrol_*, seg000
|
||||
* play_guard_frame, seg006 char_opp_dist. ДИСПЕТЧЕР у соперника не свой:
|
||||
* ИИ выставляет те же глобалы control_* (pop_ctrl.h), что и ввод игрока, а
|
||||
* дальше работает общий pop_control().
|
||||
*/
|
||||
#include "pop_guard.h"
|
||||
#include "pop_kid.h"
|
||||
#include "pop_ctrl.h"
|
||||
#include "pop_map.h"
|
||||
#include "pop_geom.h"
|
||||
#include "pop_trob.h"
|
||||
#include "pop_state.h" /* pop_immortal — чит бессмертия */
|
||||
|
||||
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
|
||||
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
|
||||
|
||||
#define TILE_GATE 4
|
||||
#define TILE_DOORTOP_FLOOR 7
|
||||
#define TILE_LOOSE 11
|
||||
#define TILE_DOORTOP 12
|
||||
#define TILE_CHOMPER 18
|
||||
#define TILE_WALL 20
|
||||
|
||||
/* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и
|
||||
* «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его
|
||||
* через границу банка дороже, чем повторить. */
|
||||
static uint8_t tile_is_floor(uint8_t t)
|
||||
{
|
||||
switch (t) {
|
||||
case 0: case 9: case 12: case 20:
|
||||
case 26: case 27: case 28: case 29:
|
||||
return 0;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid,
|
||||
* seg003:761): xpos − 7 делится на ширину тайла с округлением вниз. */
|
||||
static int8_t tile_col; /* колонка последнего tile_at_kid */
|
||||
|
||||
static uint8_t tile_at_kid(int16_t xpos)
|
||||
{
|
||||
/* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos − 7 − 58)/14),
|
||||
* СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]).
|
||||
* Оригинал берёт её из tile_div_tbl, у нас — делением с округлением
|
||||
* вниз, как в pop_map. */
|
||||
int16_t x = xpos - 7 - SCREENSPACE_X;
|
||||
int16_t col = x / TILE_SIZEX;
|
||||
if (x % TILE_SIZEX < 0) --col;
|
||||
tile_col = (int8_t)col;
|
||||
return pop_tile_at(tile_col, Kid.curr_row);
|
||||
}
|
||||
|
||||
/* check_can_guard_see_kid (seg003:688) — луч видимости по ряду.
|
||||
* Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт /
|
||||
* 2 видит и пойдёт. */
|
||||
void pop_check_can_guard_see_kid(void) __banked
|
||||
{
|
||||
uint8_t kid_frame = Kid.frame;
|
||||
int16_t left_pos, right_pos;
|
||||
uint8_t tile;
|
||||
|
||||
if (!(Guard.charid != 0 &&
|
||||
/* кадры 219..228 — подъём по лестнице: там страж Кида не видит */
|
||||
kid_frame != 0 && (kid_frame < 219 || kid_frame >= 229) &&
|
||||
Kid.alive < 0 && Guard.alive < 0 &&
|
||||
Kid.room == Guard.room && Kid.curr_row == Guard.curr_row)) {
|
||||
can_guard_see_kid = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
can_guard_see_kid = 2;
|
||||
left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||
right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
|
||||
if (left_pos > right_pos) {
|
||||
int16_t t = left_pos; left_pos = right_pos; right_pos = t;
|
||||
}
|
||||
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
|
||||
if (tile_at_kid(left_pos) == TILE_CHOMPER)
|
||||
left_pos += TILE_SIZEX;
|
||||
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
|
||||
if (tile_at_kid(right_pos) == TILE_GATE)
|
||||
right_pos -= TILE_SIZEX;
|
||||
|
||||
while (left_pos <= right_pos) {
|
||||
tile = tile_at_kid(left_pos);
|
||||
/* Сквозь это не видно вовсе. */
|
||||
if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) {
|
||||
can_guard_see_kid = 0;
|
||||
return;
|
||||
}
|
||||
/* Видно, но страж не пойдёт: провалится / попадёт под чомпер /
|
||||
* упрётся в неоткрытые ворота / шагнёт в дыру. */
|
||||
if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
|
||||
can_guard_see_kid = 1;
|
||||
} else if (tile == TILE_GATE &&
|
||||
pop_trob_modif(Kid.room)[Kid.curr_row * 10 + tile_col] < 112) {
|
||||
can_guard_see_kid = 1; /* ворота подняты не до конца */
|
||||
}
|
||||
left_pos += TILE_SIZEX;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Синтетический ввод (seg002:0706..) --------------------------- */
|
||||
static void move_0_nothing(void)
|
||||
{
|
||||
control_shift = control_y = control_x = control_shift2 = 0;
|
||||
control_down = control_up = control_backward = control_forward = 0;
|
||||
}
|
||||
static void move_1_forward(void)
|
||||
{
|
||||
control_x = POP_CONTROL_HELD_FORWARD;
|
||||
control_forward = POP_CONTROL_HELD;
|
||||
}
|
||||
static void move_2_backward(void)
|
||||
{
|
||||
control_backward = POP_CONTROL_HELD;
|
||||
control_x = POP_CONTROL_HELD_BACKWARD;
|
||||
}
|
||||
static void move_3_up(void)
|
||||
{
|
||||
control_y = POP_CONTROL_HELD_UP;
|
||||
control_up = POP_CONTROL_HELD;
|
||||
}
|
||||
static void move_4_down(void)
|
||||
{
|
||||
control_down = POP_CONTROL_HELD;
|
||||
control_y = POP_CONTROL_HELD_DOWN;
|
||||
}
|
||||
static void move_6_shift(void)
|
||||
{
|
||||
control_shift = POP_CONTROL_HELD;
|
||||
control_shift2 = POP_CONTROL_HELD;
|
||||
}
|
||||
static void move_down_back(void)
|
||||
{
|
||||
control_down = POP_CONTROL_HELD;
|
||||
move_2_backward();
|
||||
}
|
||||
static void move_down_forw(void)
|
||||
{
|
||||
control_down = POP_CONTROL_HELD;
|
||||
move_1_forward();
|
||||
}
|
||||
|
||||
/* autocontrol_guard_inactive (seg002:710): страж с убранным мечом.
|
||||
* Пока Кид за спиной — страж не реагирует; исключение — «шум» Кида
|
||||
* (is_guard_notice), от которого страж оборачивается. */
|
||||
static void autocontrol_guard_inactive(void)
|
||||
{
|
||||
int16_t distance;
|
||||
if (Kid.alive >= 0) return;
|
||||
distance = pop_char_opp_dist();
|
||||
/* В оригинале условие записано как (word)distance < (word)-8 —
|
||||
* беззнаковое сравнение, истинное для ВСЕГО, кроме диапазона −8..−1.
|
||||
* То есть: Кид не «вплотную за спиной». */
|
||||
if (Opp.curr_row != Char.curr_row || distance >= 0 || distance < -8) {
|
||||
if (is_guard_notice) {
|
||||
is_guard_notice = 0;
|
||||
if (distance < 0) {
|
||||
if (distance < -4) move_4_down(); /* обернуться на шум */
|
||||
return;
|
||||
}
|
||||
} else if (distance < 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (can_guard_see_kid) {
|
||||
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */
|
||||
move_down_forw();
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Активный страж (меч обнажён), seg002:737.. -------------------- *
|
||||
* Таблицы вероятностей по 12 градациям мастерства (seg002:26..38). Бросок
|
||||
* — prandom(255) с СОБСТВЕННЫМ сидом: у нас последовательности разведены
|
||||
* по доменам (кладка / анимация тайлов / бой), см. pop_geom.h. */
|
||||
static const uint8_t STRIKEPROB[NUM_GUARD_SKILLS] = { 61,100, 61, 61, 61, 40,100,220, 0, 48, 32, 48};
|
||||
static const uint8_t RESTRIKEPROB[NUM_GUARD_SKILLS] = { 0, 0, 0, 5, 5,175, 16, 8, 0,255,255,150};
|
||||
static const uint8_t BLOCKPROB[NUM_GUARD_SKILLS] = { 0,150,150,200,200,255,200,250, 0,255,255,255};
|
||||
static const uint8_t IMPBLOCKPROB[NUM_GUARD_SKILLS] = { 0, 61, 61,100,100,145,100,250, 0,145,255,175};
|
||||
static const uint8_t ADVPROB[NUM_GUARD_SKILLS] = {255,200,200,200,255,255,200, 0, 0,255,100,100};
|
||||
|
||||
/* Бросок «сработало ли»: в оригинале prob > prandom(255). */
|
||||
static uint8_t roll(uint8_t prob)
|
||||
{
|
||||
return (uint8_t)(prob > (uint8_t)pop_prandom(&pop_fight_seed, 255));
|
||||
}
|
||||
|
||||
/* guard_advance (seg002:0AF5): наступать. Мастерство 0 наступает всегда,
|
||||
* остальные — только пока Кид не замахнулся. */
|
||||
static void guard_advance(void)
|
||||
{
|
||||
if (guard_skill == 0 || kid_sword_strike == 0) {
|
||||
if (roll(ADVPROB[guard_skill])) move_1_forward();
|
||||
}
|
||||
}
|
||||
|
||||
/* guard_block (seg002:0B1D): блокировать замах Кида. Если Кид только что
|
||||
* пробил блок (justblocked) — своя, «импровизационная» вероятность. */
|
||||
static void guard_block(void)
|
||||
{
|
||||
uint8_t of = Opp.frame;
|
||||
if (of == FRAME_152_STRIKE_2 || of == FRAME_153_STRIKE_3 ||
|
||||
of == FRAME_162_BLOCK_TO_STRIKE) {
|
||||
if (roll(justblocked ? IMPBLOCKPROB[guard_skill]
|
||||
: BLOCKPROB[guard_skill]))
|
||||
move_3_up();
|
||||
}
|
||||
}
|
||||
|
||||
/* guard_strike (seg002:0B73): бить. По замаху Кида не бьём (кадры 169/151);
|
||||
* из своего парирования — по отдельной вероятности «добить». */
|
||||
static void guard_strike(void)
|
||||
{
|
||||
uint8_t of = Opp.frame, cf = Char.frame;
|
||||
if (of == FRAME_169_BEGIN_BLOCK || of == FRAME_151_STRIKE_1) return;
|
||||
if (cf == FRAME_161_PARRY || cf == FRAME_150_PARRY) {
|
||||
if (roll(RESTRIKEPROB[guard_skill])) move_6_shift();
|
||||
} else {
|
||||
if (roll(STRIKEPROB[guard_skill])) move_6_shift();
|
||||
}
|
||||
}
|
||||
|
||||
/* autocontrol_guard_kid_armed (seg002:0AC1): Кид с мечом. Вплотную и
|
||||
* далеко — наступаем; в «рабочей» дистанции 10..28 сначала пробуем блок, и
|
||||
* только отдышавшись бьём. */
|
||||
static void autocontrol_guard_kid_armed(int16_t distance)
|
||||
{
|
||||
if (distance < 10 || distance >= 29) {
|
||||
guard_advance();
|
||||
} else {
|
||||
guard_block();
|
||||
if (guard_refrac == 0) {
|
||||
if (distance < 12 || distance >= 29) guard_advance();
|
||||
else guard_strike();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* autocontrol_guard_kid_in_sight (seg002:0A93). */
|
||||
static void autocontrol_guard_kid_in_sight(int16_t distance)
|
||||
{
|
||||
if (Opp.sword == SWORD_2_DRAWN) {
|
||||
autocontrol_guard_kid_armed(distance);
|
||||
} else if (guard_refrac == 0) {
|
||||
/* Кид безоружен: подойти и рубить. */
|
||||
if (distance < 29) move_6_shift();
|
||||
else move_1_forward();
|
||||
}
|
||||
}
|
||||
|
||||
/* Тайл перед стражем / через один (порт get_tile_infrontof_char и
|
||||
* get_tile_infrontof2_char, seg004) — но для Char, а не для Kid. */
|
||||
static uint8_t tile_infront(int8_t step)
|
||||
{
|
||||
int8_t d = (int8_t)(Char.direction < 0 ? -step : step);
|
||||
return pop_tile_at((int8_t)(Char.curr_col + d), Char.curr_row);
|
||||
}
|
||||
|
||||
/* autocontrol_guard_kid_far (seg002:09CB): идём, если впереди есть пол хотя
|
||||
* бы через тайл; иначе пятимся (не шагать в пропасть). */
|
||||
static void autocontrol_guard_kid_far(void)
|
||||
{
|
||||
if (tile_is_floor(tile_infront(1)) || tile_is_floor(tile_infront(2)))
|
||||
move_1_forward();
|
||||
else
|
||||
move_2_backward();
|
||||
}
|
||||
|
||||
/* autocontrol_guard_active (seg002:737). */
|
||||
static void autocontrol_guard_active(void)
|
||||
{
|
||||
uint8_t cf = Char.frame;
|
||||
uint8_t of;
|
||||
int16_t distance;
|
||||
|
||||
/* Кадр 166 (неактивная стойка) и всё, что ниже 150, — не боевые позы. */
|
||||
if (cf == FRAME_166_STAND_INACTIVE || cf < 150 || can_guard_see_kid == 1)
|
||||
return;
|
||||
|
||||
if (can_guard_see_kid == 0) {
|
||||
/* TODO: droppedout -> guard_follows_kid_down (seg002:09F8) — вместе
|
||||
* с физикой стража; скелета (charid 4) ветка тоже не берёт. */
|
||||
if (Char.charid != CHARID_4_SKELETON) move_down_back();
|
||||
return;
|
||||
}
|
||||
|
||||
/* can_guard_see_kid == 2 */
|
||||
of = Opp.frame;
|
||||
distance = pop_char_opp_dist();
|
||||
/* Кид падает/приземляется в отскоке — не мешаем ему, ждём. */
|
||||
if (distance >= 12 && of >= 102 && of < 118 && Opp.action == 5)
|
||||
return;
|
||||
|
||||
if (distance < 35) {
|
||||
if ((Char.sword < SWORD_2_DRAWN && distance < 8) || distance < 12) {
|
||||
/* Слишком близко: если смотрим в одну сторону — развернуться. */
|
||||
if (Char.direction == Opp.direction) move_2_backward();
|
||||
else move_1_forward();
|
||||
} else {
|
||||
autocontrol_guard_kid_in_sight(distance);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (guard_refrac != 0) return;
|
||||
if (Char.direction != Opp.direction) {
|
||||
/* Кид разбежался — встречаем ударом, а не шагом. */
|
||||
if (of >= 7 && of < 15) { /* бег */
|
||||
if (distance < 40) move_6_shift();
|
||||
return;
|
||||
} else if (of >= 34 && of < 44) { /* разбег в прыжок */
|
||||
if (distance < 50) move_6_shift();
|
||||
return;
|
||||
}
|
||||
}
|
||||
autocontrol_guard_kid_far();
|
||||
}
|
||||
|
||||
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
|
||||
static void autocontrol_opponent(void)
|
||||
{
|
||||
move_0_nothing();
|
||||
/* Таймеры боя убывают раз в кадр — здесь, как в оригинале. */
|
||||
if (justblocked) --justblocked;
|
||||
if (kid_sword_strike) --kid_sword_strike;
|
||||
if (guard_refrac) --guard_refrac;
|
||||
if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive();
|
||||
else autocontrol_guard_active();
|
||||
}
|
||||
|
||||
/* ---- Урон мечом (seg002:0CD4..0E1F) -------------------------------- */
|
||||
#define ACTION_99_HURT 99
|
||||
#define ACTION_1_RUN 1
|
||||
#define FRAME_154_POKING 154
|
||||
#define SEQ_69_ATTACK_WAS_PARRIED 69
|
||||
#define SEQ_74_HIT_BY_SWORD 74
|
||||
#define SEQ_85_STABBED_TO_DEATH 85
|
||||
|
||||
/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */
|
||||
static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] =
|
||||
{ 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0};
|
||||
|
||||
/* take_hp (seg006): отнять count HP у АКТИВНОГО персонажа через дельту.
|
||||
* Возврат 1 — HP кончились. Чит бессмертия ловим здесь: анимацию удара
|
||||
* Кид получает, а HP не теряет. */
|
||||
static uint8_t take_hp(uint8_t count)
|
||||
{
|
||||
if (Char.charid == CHARID_0_KID) {
|
||||
if (pop_immortal) return 0;
|
||||
if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; }
|
||||
hitp_delta = (int8_t)-(int8_t)count;
|
||||
} else {
|
||||
if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; }
|
||||
guardhp_delta = (int8_t)-(int8_t)count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
|
||||
* Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр
|
||||
* «получил удар».
|
||||
* УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки
|
||||
* меньше 4) не портируем — ей нужны тайловые запросы ОТ Char, а pop_map
|
||||
* пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал
|
||||
* идёт ровно нашей веткой. */
|
||||
static void hurt_by_sword(void)
|
||||
{
|
||||
if (Char.alive >= 0) return;
|
||||
/* Чит бессмертия перекрывает и «безоружного закалывают насмерть»:
|
||||
* без этого Кид гибнет с одного удара мимо стойки, и чит бесполезен
|
||||
* ровно там, где нужен. Анимацию удара оставляем. */
|
||||
if (Char.charid == CHARID_0_KID && pop_immortal) {
|
||||
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
Char.fall_y = 0;
|
||||
play_seq();
|
||||
return;
|
||||
}
|
||||
if (Char.sword != SWORD_2_DRAWN) {
|
||||
take_hp(100);
|
||||
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
|
||||
} else if (Char.charid != CHARID_4_SKELETON && take_hp(1)) {
|
||||
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
|
||||
} else {
|
||||
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
||||
}
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
Char.fall_y = 0;
|
||||
play_seq(); /* TODO: звук 12/13 */
|
||||
}
|
||||
|
||||
/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника.
|
||||
* Бьют кадры 153/154; если соперник в этот момент парирует — удар отбит. */
|
||||
static void check_hurting(void)
|
||||
{
|
||||
int16_t distance;
|
||||
uint8_t cf, of, min_range;
|
||||
|
||||
if (Char.sword != SWORD_2_DRAWN) return;
|
||||
if (Char.curr_row != Opp.curr_row) return;
|
||||
cf = Char.frame;
|
||||
if (cf != FRAME_153_STRIKE_3 && cf != FRAME_154_POKING) return;
|
||||
|
||||
distance = pop_char_opp_dist();
|
||||
of = Opp.frame;
|
||||
if (distance < 0 || distance >= 29 ||
|
||||
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
|
||||
/* Соперник НЕ парирует. */
|
||||
if (cf == FRAME_154_POKING) {
|
||||
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
|
||||
distance = pop_char_opp_dist();
|
||||
if (distance >= (int16_t)min_range && distance < 29)
|
||||
Opp.action = ACTION_99_HURT;
|
||||
}
|
||||
} else {
|
||||
/* Парировано. */
|
||||
Opp.frame = FRAME_161_PARRY;
|
||||
if (Char.charid != CHARID_0_KID) justblocked = 4;
|
||||
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
|
||||
play_seq();
|
||||
}
|
||||
}
|
||||
|
||||
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
|
||||
void pop_check_sword_hurting(void) __banked
|
||||
{
|
||||
uint8_t kf = Kid.frame;
|
||||
if (kf == 0 || (kf >= 219 && kf < 229)) return; /* подъём по лестнице */
|
||||
pop_loadshad_and_opp();
|
||||
check_hurting();
|
||||
pop_saveshad_and_opp();
|
||||
pop_loadkid_and_opp();
|
||||
check_hurting();
|
||||
pop_savekid_and_opp();
|
||||
}
|
||||
|
||||
/* check_sword_hurt (seg002:0CD4): применить помеченные попадания. Если
|
||||
* ранен страж — Киду в этом кадре урон уже не проходит (в оригинале так же:
|
||||
* его action сбрасывается ДО проверки). */
|
||||
void pop_check_sword_hurt(void) __banked
|
||||
{
|
||||
if (Guard.action == ACTION_99_HURT) {
|
||||
if (Kid.action == ACTION_99_HURT) Kid.action = ACTION_1_RUN;
|
||||
pop_loadshad();
|
||||
hurt_by_sword();
|
||||
pop_saveshad();
|
||||
guard_refrac = (int8_t)REFRACTIMER[guard_skill];
|
||||
} else if (Kid.action == ACTION_99_HURT) {
|
||||
pop_loadkid();
|
||||
hurt_by_sword();
|
||||
pop_savekid();
|
||||
}
|
||||
}
|
||||
|
||||
/* play_guard (seg006): ИИ + общий диспетчер. Отдельная ветка «HP кончились»:
|
||||
* пока страж жив (alive < 0) и HP есть — обычный ход; как только HP дошли до
|
||||
* нуля, он переводится в «умирает» (alive = 0), и дальше тот же control()
|
||||
* ставит ему seq_71_dying. Уже умирающий сюда попадает с alive >= 0 и просто
|
||||
* доигрывает последовательность. */
|
||||
static void play_guard(void)
|
||||
{
|
||||
if (Char.alive < 0 && guardhp_curr == 0) {
|
||||
Char.alive = 0;
|
||||
/* on_guard_killed (seg006:1927) — у нас пока только звук победы,
|
||||
* которого нет; для уровня 1 других эффектов там нет. */
|
||||
}
|
||||
autocontrol_opponent();
|
||||
pop_control();
|
||||
}
|
||||
|
||||
/* pop_guard_tick — порт play_guard_frame (seg000:1246) в объёме, который у
|
||||
* нас уже есть: окно Char/Opp вокруг ИИ, диспетчера и интерпретатора
|
||||
* последовательностей. Физика стража (fall_accel/коллизия/бамп/пики) —
|
||||
* отдельным шагом. */
|
||||
void pop_guard_tick(void) __banked
|
||||
{
|
||||
if (Guard.charid == 0) return; /* стража в комнате нет */
|
||||
/* TODO: порт control_guard/fight (seg005/seg004) — состояние в Guard. */
|
||||
pop_loadshad_and_opp(); /* Char = Guard, Opp = Kid */
|
||||
play_guard();
|
||||
/* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
|
||||
* выбирается по Char.charid внутри load_frame (seg006:0293). */
|
||||
play_seq();
|
||||
pop_saveshad();
|
||||
}
|
||||
|
||||
@@ -42,4 +42,31 @@ static const uint16_t kid_seq_off[KID_NSEQ] = {
|
||||
0x21FC,0x2240,0x2257,
|
||||
};
|
||||
|
||||
/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.
|
||||
* Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.
|
||||
* Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),
|
||||
* x — «вперёд» по направлению персонажа. */
|
||||
#define KID_NSWORD 51
|
||||
#define KID_SWORD_ID_INIT { \
|
||||
0xFF,0x00,0x05,0x01,0x02,0x06,0x07,0x03,0x04,0x1E,0x08,0x09, \
|
||||
0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0x13,0x11, \
|
||||
0x11,0x07,0x01,0x1C,0x08,0x04,0x04,0x0A,0x0D,0x0D,0x0D,0x14, \
|
||||
0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0xFF,0xFF,0x1D,0x08,0x1F, \
|
||||
0x20,0x21,0x08, \
|
||||
}
|
||||
#define KID_SWORD_DX_INIT { \
|
||||
0,0,-9,7,17,7,0,17,16,12,13,7, \
|
||||
10,10,22,28,13,0,0,21,14,21,22,7, \
|
||||
15,0,7,14,7,6,9,11,24,19,21,7, \
|
||||
14,14,14,28,28,21,14,14,21,0,8,14, \
|
||||
14,7,8, \
|
||||
}
|
||||
#define KID_SWORD_DY_INIT { \
|
||||
0,-9,-29,-25,-26,-14,-5,-16,-19,-9,-34,-25, \
|
||||
-16,-11,-21,-23,-35,-38,-29,-19,-23,-22,-23,-13, \
|
||||
-18,-8,-27,-28,-27,-23,-21,-18,-23,-23,-23,-32, \
|
||||
-32,-31,-29,-28,-28,-25,-22,-25,-25,-16,-37,-24, \
|
||||
-24,-14,-37, \
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include "pop_bg.h"
|
||||
#include "pop_state.h"
|
||||
#include "pop_geom.h"
|
||||
#include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */
|
||||
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */
|
||||
@@ -32,7 +33,7 @@ static const char *const ENV_ATL[5] = {
|
||||
"BG\\pop_env3.atl", "BG\\pop_env4.atl"
|
||||
};
|
||||
|
||||
int pop_bg_load(void)
|
||||
int pop_bg_load(void) __banked
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 5; i++)
|
||||
@@ -43,7 +44,7 @@ int pop_bg_load(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_bg_free(void)
|
||||
void pop_bg_free(void) __banked
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 5; i++)
|
||||
@@ -62,33 +63,73 @@ static int g_clip_top;
|
||||
/* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x,
|
||||
* прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0).
|
||||
* При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */
|
||||
/* ОКНО FORE-СЛОЯ (экранные координаты, полуинтервал). Пока оно включено,
|
||||
* blit_b рисует только ту часть куска, что попадает в окно.
|
||||
*
|
||||
* Зачем: fore-слой существует РОВНО для того, чтобы вернуть куски тайлов
|
||||
* поверх нарисованного персонажа — за пределами его прямоугольника в
|
||||
* видеопамяти и так лежит правильный фон. А рисовал он тайлы ЦЕЛИКОМ:
|
||||
* стена (код 20) — блок 64×63 = 4032 пикселя, и таких кусков за проход
|
||||
* до девяти. Замер в MAME 2026-07-29 (комната 3, Kid + страж): проход
|
||||
* поверх Kid — 402 816 тактов, поверх стража — 431 964, вместе 78 % всей
|
||||
* работы кадра при бюджете 430 000 тактов на кадр. С окном большинство
|
||||
* кусков отсекается целиком, остальные режутся до пересечения. */
|
||||
static int fclip_x0, fclip_y0, fclip_x1, fclip_y1;
|
||||
static uint8_t fclip_on;
|
||||
|
||||
void pop_fore_set_clip(int x, int y, int w, int h) __banked
|
||||
{
|
||||
fclip_x0 = x; fclip_y0 = y;
|
||||
fclip_x1 = x + w; fclip_y1 = y + h;
|
||||
}
|
||||
|
||||
static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
|
||||
{
|
||||
const uint8_t *img;
|
||||
uint16_t w, h;
|
||||
int top;
|
||||
int top, dx, dy, dw, dh, sx = 0, sy = 0;
|
||||
if (idx >= a->count)
|
||||
return;
|
||||
/* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке
|
||||
* ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать
|
||||
* gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее
|
||||
* 64×64) и, если он не задевает окно, уходим не заплатив ни за
|
||||
* atlas_image, ни за map/unmap. */
|
||||
if (fclip_on &&
|
||||
(x >= fclip_x1 || x + 64 <= fclip_x0 ||
|
||||
ybottom + POP_YOFF + 1 <= fclip_y0 ||
|
||||
ybottom + POP_YOFF - 64 >= fclip_y1))
|
||||
return;
|
||||
img = (const uint8_t *)atlas_image(a, idx);
|
||||
gfx_w0_map(a->page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */
|
||||
if (w && h) {
|
||||
if (g_clip_top && top < g_clip_top) {
|
||||
int sy = g_clip_top - top; /* пропустить верхние строки */
|
||||
if (sy < (int)h)
|
||||
gfx_blit_part(x, g_clip_top, img, 0, sy, (int)w, (int)h - sy);
|
||||
} else if (x >= 0 && top >= 0 && w < 256 && h < 256 &&
|
||||
x + (int)w <= 320 && top + (int)h <= 256) {
|
||||
/* Спрайт целиком на экране — линейное ядро без клипа: ~2.9×
|
||||
* быстрее (замер: 4617 против 13288 тактов на спрайт 32×3;
|
||||
* общее ядро gfx_blit платит за клип/16-бит/split, а не за
|
||||
* пиксели). W3-скобку ставит сама libbgi: из кода, собранного
|
||||
* с --w3, её вызывать нельзя (окно W3 уходит под видеобанк). */
|
||||
gfx_blit_noclip(x, top, img);
|
||||
dx = x; dy = top; dw = (int)w; dh = (int)h;
|
||||
if (g_clip_top && dy < g_clip_top) { /* полоса у потолка */
|
||||
sy = g_clip_top - dy; dy = g_clip_top; dh -= sy;
|
||||
}
|
||||
if (fclip_on) { /* окно fore-слоя */
|
||||
if (dx < fclip_x0) { int d = fclip_x0 - dx; sx += d; dx += d; dw -= d; }
|
||||
if (dy < fclip_y0) { int d = fclip_y0 - dy; sy += d; dy += d; dh -= d; }
|
||||
if (dx + dw > fclip_x1) dw = fclip_x1 - dx;
|
||||
if (dy + dh > fclip_y1) dh = fclip_y1 - dy;
|
||||
}
|
||||
if (w && h && dw > 0 && dh > 0) {
|
||||
/* Целиком на экране — линейное ядро без клипа: ~2.9× быстрее
|
||||
* (замер: 4617 против 13288 тактов на спрайт 32×3; общее ядро
|
||||
* gfx_blit платит за клип/16-бит/split, а не за пиксели).
|
||||
* W3-скобку ставит сама libbgi: из кода, собранного с --w3, её
|
||||
* вызывать нельзя (окно W3 уходит под видеобанк). */
|
||||
if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
|
||||
dx + dw <= 320 && dy + dh <= 256 && w < 256) {
|
||||
if (sx == 0 && sy == 0 && dw == (int)w && dh == (int)h)
|
||||
gfx_blit_noclip(dx, dy, img);
|
||||
else
|
||||
gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy,
|
||||
(uint8_t)dw, (uint8_t)dh);
|
||||
} else {
|
||||
gfx_blit(x, top, img); /* край экрана — с клипом */
|
||||
gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */
|
||||
}
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
@@ -129,7 +170,7 @@ static void ov_mark(int x, int y, int w, int h)
|
||||
ov_pending = 3; /* обе страницы */
|
||||
}
|
||||
|
||||
void pop_fore_heal(void)
|
||||
void pop_fore_heal(void) __banked
|
||||
{
|
||||
uint8_t pg = gfx_get_draw_page() & 1;
|
||||
if (!(ov_pending & (1 << pg))) return;
|
||||
@@ -253,12 +294,12 @@ static uint8_t g_room;
|
||||
* floorright, когда тайл нижнего ряда стал видимым (empty после падения
|
||||
* loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты
|
||||
* снизу (кромка уровня = стена). */
|
||||
void pop_room_set_below(const uint8_t *below_row0_fg) { g_below = below_row0_fg; }
|
||||
void pop_room_set_below(const uint8_t *below_row0_fg) __banked { g_below = below_row0_fg; }
|
||||
|
||||
/* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки
|
||||
* у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
|
||||
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
|
||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod)
|
||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked
|
||||
{ g_above = above_row2_fg; g_abovemod = above_row2_mod; }
|
||||
|
||||
static uint8_t tile_code(int row, int col)
|
||||
@@ -313,7 +354,7 @@ static uint8_t wall_modifier(int row, int col)
|
||||
}
|
||||
|
||||
/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */
|
||||
static unsigned long rnd_seed;
|
||||
static pop_rnd_t rnd_seed;
|
||||
|
||||
#define prandom(maxv) pop_prandom(&rnd_seed, (maxv))
|
||||
|
||||
@@ -338,65 +379,135 @@ static void draw_right_mark(uint16_t dv, int arg1, int row, int col)
|
||||
wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]);
|
||||
}
|
||||
|
||||
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
|
||||
/* КЭШ РАСКЛАДКИ КЛАДКИ. Серия prandom для тайла зависит только от
|
||||
* (комната, ряд, колонка) — пока мы в комнате, это КОНСТАНТА. А
|
||||
* wall_pattern зовётся из fore-слоя КАЖДЫЙ кадр, и на Z80 это самая
|
||||
* дорогая вещь в кадре: замер в MAME 2026-07-29 (комната 3) — 18 вызовов
|
||||
* pop_prandom за один fore-проход, ~10 700 тактов каждый (32-битный LCG:
|
||||
* __mullong ~8 000 + __moduint), то есть 193 К из 341 К всего прохода при
|
||||
* бюджете кадра ~430 К. Кэшируем не сид, а ГОТОВЫЕ РЕШЕНИЯ (что рисовать
|
||||
* и с каким смещением) — тогда в кадре не остаётся ни одного prandom.
|
||||
*
|
||||
* Порядок вызовов prandom воспроизведён один в один, включая то, что
|
||||
* значение метки берётся ТОЛЬКО когда сработало её условие: иначе
|
||||
* последовательность разъедется и кладка станет другой. which_part=0
|
||||
* использует префикс той же серии, поэтому кэш общий.
|
||||
*
|
||||
* Упаковка: wp_a = mid_div | mid_off<<1 | bot_div<<4 | bot_off<<5;
|
||||
* wp_b = rndblk | right<<1 | right_val<<2 | left<<4 | left_val<<5.
|
||||
* Индекс — (row+1)*10+col, ряды −1..2. */
|
||||
#define WP_N 40
|
||||
static uint8_t wp_a[WP_N], wp_b[WP_N], wp_ok[WP_N];
|
||||
|
||||
static void wall_pattern_reset(void) /* смена комнаты — сид другой */
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < WP_N; i++) wp_ok[i] = 0;
|
||||
}
|
||||
|
||||
/* Выходы wall_rnd — в файловых статиках: возвращать девять значений
|
||||
* через указатели на Z80 дороже самой функции. */
|
||||
static uint8_t wr_mid_div, wr_mid_off, wr_bot_div, wr_bot_off;
|
||||
static uint8_t wr_rndblk, wr_right, wr_rightv, wr_left, wr_leftv;
|
||||
|
||||
static void wall_rnd(int row, int col)
|
||||
{
|
||||
/* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс
|
||||
* соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
|
||||
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
|
||||
uint8_t a, b, bg;
|
||||
pop_rnd_t saved;
|
||||
if (idx >= 0 && idx < WP_N && wp_ok[idx]) {
|
||||
a = wp_a[idx]; b = wp_b[idx];
|
||||
wr_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7);
|
||||
wr_bot_div = (uint8_t)((a >> 4) & 1); wr_bot_off = (uint8_t)((a >> 5) & 7);
|
||||
wr_rndblk = (uint8_t)(b & 1);
|
||||
wr_right = (uint8_t)((b >> 1) & 1); wr_rightv = (uint8_t)((b >> 2) & 3);
|
||||
wr_left = (uint8_t)((b >> 4) & 1); wr_leftv = (uint8_t)((b >> 5) & 7);
|
||||
return;
|
||||
}
|
||||
saved = rnd_seed;
|
||||
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
|
||||
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
|
||||
pop_prandom_set(rnd_seed, g_room + TBL_LINE[row < 0 ? 2 : row] + col);
|
||||
prandom(1); /* сброс */
|
||||
wr_mid_div = (uint8_t)prandom(1);
|
||||
wr_mid_off = (uint8_t)prandom(4);
|
||||
wr_bot_div = (uint8_t)prandom(1);
|
||||
wr_bot_off = (uint8_t)prandom(4);
|
||||
wr_rndblk = wr_right = wr_left = 0;
|
||||
wr_rightv = wr_leftv = 0;
|
||||
bg = wall_modifier(row, col);
|
||||
if (bg == 3) { /* WWW */
|
||||
wr_rndblk = (uint8_t)(prandom(4) == 0);
|
||||
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
|
||||
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
|
||||
} else if (bg == 0) { /* SWS */
|
||||
if (prandom(6) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(1); }
|
||||
} else if (bg == 1) { /* SWW */
|
||||
wr_rndblk = (uint8_t)(prandom(4) == 0);
|
||||
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
|
||||
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(3); }
|
||||
} else { /* WWS (bg==2) */
|
||||
if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)(prandom(1) + 2); }
|
||||
if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); }
|
||||
}
|
||||
rnd_seed = saved;
|
||||
if (idx >= 0 && idx < WP_N) {
|
||||
wp_a[idx] = (uint8_t)(wr_mid_div | (wr_mid_off << 1) |
|
||||
(wr_bot_div << 4) | (wr_bot_off << 5));
|
||||
wp_b[idx] = (uint8_t)(wr_rndblk | (wr_right << 1) | (wr_rightv << 2) |
|
||||
(wr_left << 4) | (wr_leftv << 5));
|
||||
wp_ok[idx] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
|
||||
static void wall_pattern(int row, int col, int which_part)
|
||||
{
|
||||
int xh = COL_XH[col];
|
||||
int dby = 63 * row + 65;
|
||||
unsigned long saved = rnd_seed;
|
||||
uint16_t mid_div, mid_off, bot_div, bot_off;
|
||||
uint8_t bg;
|
||||
|
||||
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
|
||||
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
|
||||
rnd_seed = (unsigned long)(g_room + TBL_LINE[row < 0 ? 2 : row] + col);
|
||||
prandom(1); /* сброс */
|
||||
mid_div = prandom(1);
|
||||
mid_off = prandom(4);
|
||||
bot_div = prandom(1);
|
||||
bot_off = prandom(4);
|
||||
bg = wall_modifier(row, col);
|
||||
uint8_t bg = wall_modifier(row, col);
|
||||
|
||||
wall_rnd(row, col);
|
||||
if (bg == 3) { /* WWW */
|
||||
if (which_part) {
|
||||
if (prandom(4) == 0)
|
||||
if (wr_rndblk)
|
||||
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21);
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
|
||||
}
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby);
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
|
||||
if (which_part) {
|
||||
if (prandom(4) == 0)
|
||||
draw_right_mark(prandom(3), mid_off, row, col);
|
||||
if (prandom(4) == 0)
|
||||
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col);
|
||||
if (wr_right)
|
||||
draw_right_mark(wr_rightv, wr_mid_off, row, col);
|
||||
if (wr_left)
|
||||
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
|
||||
}
|
||||
} else if (bg == 0) { /* SWS */
|
||||
if (which_part) {
|
||||
if (prandom(6) == 0)
|
||||
draw_left_mark(prandom(1), mid_off - mid_div, bot_off - bot_div, row, col);
|
||||
}
|
||||
if (which_part && wr_left)
|
||||
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
|
||||
} else if (bg == 1) { /* SWW */
|
||||
if (which_part) {
|
||||
if (prandom(4) == 0)
|
||||
if (wr_rndblk)
|
||||
wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21);
|
||||
if (prandom(4) == 0)
|
||||
draw_right_mark(prandom(3), mid_off, row, col);
|
||||
if (prandom(4) == 0)
|
||||
draw_left_mark(prandom(3), mid_off - mid_div, bot_off - bot_div, row, col);
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
|
||||
if (wr_right)
|
||||
draw_right_mark(wr_rightv, wr_mid_off, row, col);
|
||||
if (wr_left)
|
||||
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
|
||||
}
|
||||
} else { /* WWS (bg==2) */
|
||||
if (which_part)
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21);
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby);
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
|
||||
wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
|
||||
if (which_part) {
|
||||
if (prandom(4) == 0)
|
||||
draw_right_mark((uint16_t)(prandom(1) + 2), mid_off, row, col);
|
||||
if (prandom(4) == 0)
|
||||
draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col);
|
||||
if (wr_right)
|
||||
draw_right_mark(wr_rightv, wr_mid_off, row, col);
|
||||
if (wr_left)
|
||||
draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
|
||||
}
|
||||
}
|
||||
rnd_seed = saved;
|
||||
}
|
||||
|
||||
/* topright тайла снизу-слева (seg008.c:410). */
|
||||
@@ -636,7 +747,7 @@ static void draw_tile(int row, int col)
|
||||
* Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать
|
||||
* ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif
|
||||
* внутри draw_tile). */
|
||||
void pop_loose_shake_draw(int row, int col)
|
||||
void pop_loose_shake_draw(int row, int col) __banked
|
||||
{
|
||||
int x = COL_XH[col] * 8;
|
||||
/* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх —
|
||||
@@ -658,7 +769,7 @@ void pop_loose_shake_draw(int row, int col)
|
||||
* читает lmod = g_bg[row*10+col] = живой modif пики). g_bg должен указывать
|
||||
* на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile.
|
||||
* heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */
|
||||
void pop_spike_redraw(int row, int col)
|
||||
void pop_spike_redraw(int row, int col) __banked
|
||||
{
|
||||
int x = COL_XH[col] * 8;
|
||||
/* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ
|
||||
@@ -685,7 +796,7 @@ void pop_spike_redraw(int row, int col)
|
||||
* room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake):
|
||||
* heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары
|
||||
* оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */
|
||||
void pop_room_redraw_seam_left(void)
|
||||
void pop_room_redraw_seam_left(void) __banked
|
||||
{
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
setfillstyle(SOLID_FILL, BLACK);
|
||||
@@ -707,7 +818,7 @@ void pop_room_redraw_seam_left(void)
|
||||
* после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */
|
||||
#define CEIL_BAND_H 8 /* видимая высота полосы (room-space y 0..7) */
|
||||
|
||||
void pop_ceil_shake_draw(int col)
|
||||
void pop_ceil_shake_draw(int col) __banked
|
||||
{
|
||||
int x = COL_XH[col] * 8;
|
||||
heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */
|
||||
@@ -719,7 +830,7 @@ void pop_ceil_shake_draw(int col)
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
}
|
||||
|
||||
void pop_ceil_bake_empty(int col)
|
||||
void pop_ceil_bake_empty(int col) __banked
|
||||
{
|
||||
int x = COL_XH[col] * 8;
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
@@ -742,7 +853,7 @@ void pop_ceil_bake_empty(int col)
|
||||
* draw_main_y − 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька
|
||||
* potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y −
|
||||
* (pot_size<<2) − 14 (seg008:668). */
|
||||
void pop_torch_draw(int row, int col, uint8_t modif)
|
||||
void pop_torch_draw(int row, int col, uint8_t modif) __banked
|
||||
{
|
||||
/* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1
|
||||
* (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env
|
||||
@@ -759,7 +870,7 @@ void pop_torch_draw(int row, int col, uint8_t modif)
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
}
|
||||
|
||||
void pop_potion_draw(int row, int col, uint8_t modif)
|
||||
void pop_potion_draw(int row, int col, uint8_t modif) __banked
|
||||
{
|
||||
uint8_t type = potion_type(modif);
|
||||
int pot_size = (type >= 2 && type < 5) ? 1 : 0;
|
||||
@@ -777,7 +888,7 @@ void pop_potion_draw(int row, int col, uint8_t modif)
|
||||
* (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный +
|
||||
* redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе
|
||||
* в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */
|
||||
void pop_floor_bake(int row, int col)
|
||||
void pop_floor_bake(int row, int col) __banked
|
||||
{
|
||||
int x = COL_XH[col] * 8;
|
||||
int yb = POP_YOFF + 63 * row;
|
||||
@@ -795,13 +906,13 @@ void pop_floor_bake(int row, int col)
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
}
|
||||
|
||||
void pop_button_redraw(int row, int col) { pop_floor_bake(row, col); }
|
||||
void pop_button_redraw(int row, int col) __banked { pop_floor_bake(row, col); }
|
||||
|
||||
/* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка,
|
||||
* см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет,
|
||||
* а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое.
|
||||
* Куски двери непрозрачные — wipe не нужен. */
|
||||
void pop_leveldoor_redraw(int row, int col)
|
||||
void pop_leveldoor_redraw(int row, int col) __banked
|
||||
{
|
||||
gfx_set_bank(GFX_BANK_TRANSPARENT);
|
||||
draw_tile(row, col);
|
||||
@@ -814,7 +925,7 @@ void pop_leveldoor_redraw(int row, int col)
|
||||
* рисовал loose-правый-край env_b(42); теперь пусто → чистая грань).
|
||||
* g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые
|
||||
* пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */
|
||||
void pop_loose_bake_empty(int row, int col)
|
||||
void pop_loose_bake_empty(int row, int col) __banked
|
||||
{
|
||||
int x = COL_XH[col] * 8;
|
||||
int xr = COL_XH[col + 1] * 8;
|
||||
@@ -877,7 +988,8 @@ static mob_t mobs[MOB_MAX];
|
||||
|
||||
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
|
||||
* который он лёг → приложение/pop_map ставит debris + do_knock. */
|
||||
uint8_t pop_loose_landed;
|
||||
/* pop_loose_landed переехал в pop_state.c: его читает pop_map (W1/W2), а
|
||||
* писучие данные банкового модуля недоступны снаружи (см. pop_state.h). */
|
||||
|
||||
/* y_loose_land / y_something (seg007): [row+1], row = −1..3. Ряд −1 —
|
||||
* плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный
|
||||
@@ -886,7 +998,7 @@ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254};
|
||||
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
|
||||
#define MOB_Y_NONE 0x7FFF
|
||||
|
||||
void pop_loose_mob_spawn(int row, int col)
|
||||
void pop_loose_mob_spawn(int row, int col) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
mob_t *m = 0;
|
||||
@@ -923,7 +1035,7 @@ static int8_t obj_col, obj_row;
|
||||
|
||||
static void mob_render(mob_t *m, uint8_t pg);
|
||||
|
||||
void pop_loose_mob_reset(void)
|
||||
void pop_loose_mob_reset(void) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < MOB_MAX; i++) {
|
||||
@@ -988,7 +1100,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
mob_render(m, pg);
|
||||
}
|
||||
|
||||
void pop_loose_mob_tick(void)
|
||||
void pop_loose_mob_tick(void) __banked
|
||||
{
|
||||
uint8_t pg = gfx_get_draw_page() & 1, i;
|
||||
for (i = 0; i < MOB_MAX; i++)
|
||||
@@ -1038,7 +1150,7 @@ static void mob_render(mob_t *m, uint8_t pg)
|
||||
|
||||
/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
|
||||
* после kid_draw, до fore-слоя. */
|
||||
void pop_loose_mob_draw_over(void)
|
||||
void pop_loose_mob_draw_over(void) __banked
|
||||
{
|
||||
uint8_t pg = gfx_get_draw_page() & 1, i;
|
||||
for (i = 0; i < MOB_MAX; i++)
|
||||
@@ -1047,7 +1159,7 @@ void pop_loose_mob_draw_over(void)
|
||||
|
||||
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
|
||||
* y (низ куска). 0 — куска нет. */
|
||||
uint8_t pop_loose_mob_pos(int *col, int *y)
|
||||
uint8_t pop_loose_mob_pos(int *col, int *y) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < MOB_MAX; i++)
|
||||
@@ -1062,7 +1174,7 @@ uint8_t pop_loose_mob_pos(int *col, int *y)
|
||||
* кадрах падения — гейт по флагу). */
|
||||
static uint8_t border_dirty;
|
||||
|
||||
void pop_clip_sprite(int x, int w, int sy_top, int h)
|
||||
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
|
||||
{
|
||||
int sy_bot = sy_top + h;
|
||||
(void)x; (void)w;
|
||||
@@ -1074,7 +1186,7 @@ void pop_clip_sprite(int x, int w, int sy_top, int h)
|
||||
* вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает
|
||||
* чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу
|
||||
* + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */
|
||||
void pop_room_clip_borders(void)
|
||||
void pop_room_clip_borders(void) __banked
|
||||
{
|
||||
if (!border_dirty) return;
|
||||
border_dirty--;
|
||||
@@ -1084,10 +1196,11 @@ void pop_room_clip_borders(void)
|
||||
|
||||
void pop_room_draw(uint8_t room_num,
|
||||
const uint8_t *fg, const uint8_t *bg,
|
||||
const uint8_t *lcol_fg, const uint8_t *lcol_bg)
|
||||
const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked
|
||||
{
|
||||
int row, col;
|
||||
g_room = room_num;
|
||||
wall_pattern_reset(); /* кэш кладки привязан к номеру комнаты */
|
||||
g_fg = fg; g_bg = bg;
|
||||
g_lfg = lcol_fg; g_lbg = lcol_bg;
|
||||
|
||||
@@ -1177,8 +1290,25 @@ static void ceil_over_kid_tile(int col)
|
||||
fore_only_tile(-1, col); /* draw_tile_fore */
|
||||
}
|
||||
|
||||
/* Пересекается ли габарит тайла с окном fore-слоя. Шаг тайла — 32 px
|
||||
* (COL_XH[col]*8 = col*32), куски выходят вправо максимум на +8 (дивайдеры
|
||||
* и метки рисуются в ячейке правого соседа) и вверх на 42+высота кадра.
|
||||
* Даёт отсев ЦЕЛОГО тайла ДО обращения к атласу: спрайт 27..40 px шириной
|
||||
* задевает не все тайлы футпринта, а платили за каждый (atlas_image +
|
||||
* gfx_w0_map + разбор заголовка на КАЖДЫЙ кусок). */
|
||||
static uint8_t tile_in_fclip(int row, int col)
|
||||
{
|
||||
int x0, y0;
|
||||
if (!fclip_on) return 1;
|
||||
x0 = COL_XH[col] * 8;
|
||||
y0 = POP_YOFF + 63 * row;
|
||||
return (uint8_t)(x0 < fclip_x1 && x0 + 40 > fclip_x0 &&
|
||||
y0 - 8 < fclip_y1 && y0 + 70 > fclip_y0);
|
||||
}
|
||||
|
||||
static void fore_tile(int row, int col) /* передний слой одного тайла */
|
||||
{
|
||||
if (!tile_in_fclip(row, col)) return;
|
||||
/* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал
|
||||
* кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в
|
||||
* backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ
|
||||
@@ -1211,7 +1341,9 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx)
|
||||
{
|
||||
/* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую
|
||||
* Kid спускается/подтягивается, закрашивается целым тайлом */
|
||||
uint8_t code = tile_code_drawn(row, col);
|
||||
uint8_t code;
|
||||
if (!tile_in_fclip(row, col)) return;
|
||||
code = tile_code_drawn(row, col);
|
||||
int x, dmy, dby;
|
||||
const piece *t;
|
||||
if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */
|
||||
@@ -1301,8 +1433,9 @@ static void overlay_mid_tile(int row, int col)
|
||||
* сразу), поэтому порядок эмулируется явной проверкой. */
|
||||
static void other_overlay_tile(int row, int col)
|
||||
{
|
||||
uint8_t code = tile_code(row, col);
|
||||
uint8_t left;
|
||||
uint8_t code, left;
|
||||
if (!tile_in_fclip(row, col)) return;
|
||||
code = tile_code(row, col);
|
||||
/* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для
|
||||
* него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань
|
||||
* пустого соседа). В оригинале она уходит в y-сортируемый midtable и
|
||||
@@ -1325,13 +1458,20 @@ static void other_overlay_tile(int row, int col)
|
||||
}
|
||||
}
|
||||
|
||||
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
int8_t dir, uint8_t frame, uint8_t action)
|
||||
/* Футпринт персонажа в тайлах (порт set_char_collision, seg006:0723).
|
||||
* Выделено из pop_fore_over_kid, потому что тем же счётом пользуется страж
|
||||
* (pop_fore_over_char): в оригинале это общий код для любого Char.
|
||||
* Результат — в файловых статиках, а не через 6 указателей: на Z80 такой
|
||||
* возврат стоил бы дороже самой функции. */
|
||||
static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw;
|
||||
|
||||
static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
int8_t dir)
|
||||
{
|
||||
int wh = (int)((w + 1) >> 1); /* char_width_half */
|
||||
int xl = obj_x / 2 + 58; /* char_x_left (58-база) */
|
||||
int xr;
|
||||
int8_t cL, cR, cLraw, rT, rB, r, c, rTraw;
|
||||
int8_t cL, cR, cLraw, rT, rB, rTraw;
|
||||
if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
|
||||
xr = xl + wh;
|
||||
cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */
|
||||
@@ -1359,6 +1499,18 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
* (нижняя грань стены / кромка пола) не перерисуется поверх Kid. Форсим
|
||||
* включение ряда rB-1. */
|
||||
if (rB > 0 && rT >= rB) rT = rB - 1;
|
||||
fp_cL = cL; fp_cR = cR; fp_cLraw = cLraw;
|
||||
fp_rT = rT; fp_rB = rB; fp_rTraw = rTraw;
|
||||
}
|
||||
|
||||
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
int8_t dir, uint8_t frame, uint8_t action) __banked
|
||||
{
|
||||
int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
|
||||
fclip_on = 1; /* окно = прямоугольник спрайта */
|
||||
char_footprint(obj_x, obj_y, w, h, dir);
|
||||
cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
|
||||
rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
|
||||
/* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
|
||||
* Kid попадает в midtable — база порядка окклюзии в other_overlay_tile.
|
||||
* run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/
|
||||
@@ -1426,4 +1578,37 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
g_clip_top = 0;
|
||||
}
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
fclip_on = 0;
|
||||
}
|
||||
|
||||
/* Передние грани тайлов поверх ПРОИЗВОЛЬНОГО персонажа (страж, дальше —
|
||||
* тень/скелет). В оригинале любой Char — запись midtable, а foretable
|
||||
* рисуется после всех midtable (draw_tile_fore, seg008:690), поэтому
|
||||
* колонна и кромка пола перекрывают всех одинаково.
|
||||
*
|
||||
* Отличие от pop_fore_over_kid: только слой fore (+ полоса потолка).
|
||||
* Оверлеи тела пола (draw_other_overlay) завязаны на позы Kid — вис,
|
||||
* полёт, подъём, — которых у стража нет: он ходит и дерётся на полу
|
||||
* (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий
|
||||
* порт redraw_at_char2, а не догадки сейчас. */
|
||||
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
int8_t dir) __banked
|
||||
{
|
||||
int8_t r, c;
|
||||
fclip_on = 1; /* окно = прямоугольник спрайта */
|
||||
char_footprint(obj_x, obj_y, w, h, dir);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
for (r = fp_rT; r <= fp_rB; r++)
|
||||
for (c = fp_cL; c <= fp_cR; c++)
|
||||
fore_tile(r, c);
|
||||
if (fp_rTraw < 0) {
|
||||
g_clip_top = POP_YOFF;
|
||||
for (c = fp_cL; c <= fp_cR; c++) {
|
||||
ov_mark(COL_XH[c] * 8, 0, 64, CEIL_BAND_H);
|
||||
ceil_over_kid_tile(c);
|
||||
}
|
||||
g_clip_top = 0;
|
||||
}
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
fclip_on = 0;
|
||||
}
|
||||
|
||||
@@ -25,13 +25,26 @@
|
||||
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
|
||||
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
|
||||
|
||||
/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip-
|
||||
* примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий
|
||||
* вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от
|
||||
* того, вылезает край или нет.
|
||||
* static inline, а не чистый inline: SDCC оставляет мёртвое тело в каждом TU
|
||||
* (memory sdcc_inline_codegen_findings), но здесь это ~30 байт на два TU —
|
||||
* дешевле риска неразрешённой ссылки. */
|
||||
static inline uint8_t pop_onscreen_cols(int x, int y, uint16_t w, uint16_t h)
|
||||
{
|
||||
return (uint8_t)(x >= 0 && y >= 0 && w < 256 && h < 256 &&
|
||||
x + (int)w <= 320 && y + (int)h <= 256);
|
||||
}
|
||||
|
||||
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
|
||||
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
|
||||
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
|
||||
int pop_bg_load(void);
|
||||
int pop_bg_load(void) __banked;
|
||||
|
||||
/* Освободить EMM-страницы атласов. */
|
||||
void pop_bg_free(void);
|
||||
void pop_bg_free(void) __banked;
|
||||
|
||||
/* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы
|
||||
* (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца
|
||||
@@ -39,97 +52,114 @@ void pop_bg_free(void);
|
||||
* тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */
|
||||
void pop_room_draw(uint8_t room_num,
|
||||
const uint8_t *fg, const uint8_t *bg,
|
||||
const uint8_t *lcol_fg, const uint8_t *lcol_bg);
|
||||
const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked;
|
||||
|
||||
/* Задать ряд 0 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright,
|
||||
* когда тайл нижнего ряда стал empty (после падения loose): «стена вниз»
|
||||
* рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */
|
||||
void pop_room_set_below(const uint8_t *below_row0_fg);
|
||||
void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
|
||||
|
||||
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
|
||||
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
|
||||
* комнаты сверху (кромка уровня → пол). */
|
||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod);
|
||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
|
||||
|
||||
/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него —
|
||||
* звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ
|
||||
* спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики
|
||||
* берутся из pop_kid (kid_fp_*). См. redraw_at_char/set_char_collision. */
|
||||
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
int8_t dir, uint8_t frame, uint8_t action);
|
||||
int8_t dir, uint8_t frame, uint8_t action) __banked;
|
||||
|
||||
/* Окно fore-слоя: прямоугольник СПРАЙТА (экранные координаты, y уже с
|
||||
* POP_YOFF), поверх которого fore-проход возвращает куски тайлов. Ставит
|
||||
* тот, кто нарисовал спрайт (kid_draw / pop_guard_draw) — он один знает
|
||||
* итоговый габарит (у Kid — с учётом клинка и обрезки clip_char). Вне
|
||||
* fore-проходов окно игнорируется: фон рисуется целиком. Мотивация и
|
||||
* замер — в pop_bg.c у fclip_on. */
|
||||
void pop_fore_set_clip(int x, int y, int w, int h) __banked;
|
||||
|
||||
/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка,
|
||||
* без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата
|
||||
* (до ×8/7), как у kid_fp_obj_x(). */
|
||||
/* __banked: отрисовка стража уехала в СВОЙ банк (pop_gdraw.c), поэтому
|
||||
* вызов стал кроссбанковым и обязан идти через трамплин. Зовётся раз в
|
||||
* кадр, других вызывающих нет — цена одного трамплина здесь не видна. */
|
||||
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
int8_t dir) __banked;
|
||||
|
||||
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
|
||||
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
|
||||
* прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице
|
||||
* дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */
|
||||
void pop_fore_heal(void);
|
||||
void pop_fore_heal(void) __banked;
|
||||
|
||||
/* Loose floors — динамическая отрисовка (состояние pop_loose_modif в
|
||||
* pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick.
|
||||
* shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту
|
||||
* после падения (NORMAL, в ОЗУ-копию). */
|
||||
void pop_loose_shake_draw(int row, int col);
|
||||
void pop_loose_bake_empty(int row, int col);
|
||||
void pop_loose_shake_draw(int row, int col) __banked;
|
||||
void pop_loose_bake_empty(int row, int col) __banked;
|
||||
|
||||
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
|
||||
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
|
||||
* Состояние — pop_ceil_modif[10] в pop_map. */
|
||||
void pop_ceil_shake_draw(int col);
|
||||
void pop_ceil_bake_empty(int col);
|
||||
void pop_ceil_shake_draw(int col) __banked;
|
||||
void pop_ceil_bake_empty(int col) __banked;
|
||||
|
||||
/* Кусок приземлился (loose_land): 0 = нет, иначе tilepos+1 тайла, на который
|
||||
* он лёг. Ставит pop_loose_mob_tick, разбирает pop_loose_tick (debris+knock). */
|
||||
extern uint8_t pop_loose_landed;
|
||||
/* Кусок приземлился (loose_land) — переехал в pop_state.h: переменную
|
||||
* читают по обе стороны банковой границы. */
|
||||
#include "pop_state.h"
|
||||
|
||||
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
|
||||
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
|
||||
void pop_spike_redraw(int row, int col);
|
||||
void pop_spike_redraw(int row, int col) __banked;
|
||||
|
||||
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
|
||||
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
|
||||
* (пламя рисуется в ячейке правого соседа). */
|
||||
void pop_torch_draw(int row, int col, uint8_t modif);
|
||||
void pop_potion_draw(int row, int col, uint8_t modif);
|
||||
void pop_torch_draw(int row, int col, uint8_t modif) __banked;
|
||||
void pop_potion_draw(int row, int col, uint8_t modif) __banked;
|
||||
|
||||
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
|
||||
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
|
||||
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
|
||||
void pop_room_redraw_seam_left(void);
|
||||
void pop_room_redraw_seam_left(void) __banked;
|
||||
|
||||
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
|
||||
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
|
||||
* кнопка нажата/отжата, floor → debris от упавшей плиты. */
|
||||
void pop_floor_bake(int row, int col);
|
||||
void pop_button_redraw(int row, int col); /* = pop_floor_bake (кнопка) */
|
||||
void pop_floor_bake(int row, int col) __banked;
|
||||
void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кнопка) */
|
||||
|
||||
/* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с
|
||||
* запечкой — створка анимируется (pop_trob animate_leveldoor). */
|
||||
void pop_leveldoor_redraw(int row, int col);
|
||||
void pop_leveldoor_redraw(int row, int col) __banked;
|
||||
|
||||
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
|
||||
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick
|
||||
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
|
||||
void pop_loose_mob_spawn(int row, int col);
|
||||
void pop_loose_mob_tick(void);
|
||||
void pop_loose_mob_reset(void); /* остановить кусок (смена комнаты) */
|
||||
void pop_loose_mob_spawn(int row, int col) __banked;
|
||||
void pop_loose_mob_tick(void) __banked;
|
||||
void pop_loose_mob_reset(void) __banked; /* остановить кусок (смена комнаты) */
|
||||
|
||||
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
|
||||
* seg008:1561). Звать после kid_draw, перед fore-слоем. */
|
||||
void pop_loose_mob_draw_over(void);
|
||||
void pop_loose_mob_draw_over(void) __banked;
|
||||
|
||||
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
|
||||
uint8_t pop_loose_mob_pos(int *col, int *y);
|
||||
uint8_t pop_loose_mob_pos(int *col, int *y) __banked;
|
||||
|
||||
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
|
||||
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
|
||||
* верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра.
|
||||
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
|
||||
void pop_clip_sprite(int x, int w, int sy_top, int h);
|
||||
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked;
|
||||
|
||||
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
|
||||
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
|
||||
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
|
||||
* КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */
|
||||
void pop_room_clip_borders(void);
|
||||
void pop_room_clip_borders(void) __banked;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* pop_char.h — состояние ПЕРСОНАЖА, общее для Кида, стража и активной
|
||||
* копии Char. Порт char_type (types.h:302).
|
||||
*
|
||||
* Зачем отдельным заголовком: интерпретатор последовательностей в
|
||||
* оригинале один на всех (работает с Char, а вокруг стоят
|
||||
* loadkid/savekid и loadshad/saveshad, seg006:809). Пока у Кида был свой
|
||||
* усечённый тип, а у стража свой, общий интерпретатор был невозможен.
|
||||
* Поля и их порядок — как в char_type, чтобы порт seg005/seg006 ложился
|
||||
* один в один.
|
||||
*/
|
||||
#ifndef POP_CHAR_H
|
||||
#define POP_CHAR_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t frame;
|
||||
uint8_t x, y;
|
||||
int8_t direction; /* -1 влево, 0 вправо */
|
||||
int8_t curr_col, curr_row;
|
||||
uint8_t action;
|
||||
int8_t fall_x, fall_y;
|
||||
uint8_t room;
|
||||
uint8_t repeat;
|
||||
uint8_t charid; /* 0 = Kid, 2 = страж (см. CHARID_* в pop_guard.h) */
|
||||
uint8_t sword;
|
||||
int8_t alive; /* -1 = жив (как в оригинале), 0 = убит */
|
||||
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
|
||||
} pop_char_t;
|
||||
|
||||
#endif
|
||||
@@ -17,5 +17,7 @@
|
||||
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
|
||||
|
||||
#define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */
|
||||
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */
|
||||
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */
|
||||
|
||||
#endif
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <kbd_raw.h>
|
||||
#include "pop_kid.h"
|
||||
#include "pop_map.h"
|
||||
#include "pop_guard.h" /* charid, состояние меча, seq стража */
|
||||
|
||||
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
|
||||
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
|
||||
@@ -51,6 +52,7 @@
|
||||
#define SEQ_49_STAND_UP_FROM_CROUCH 49
|
||||
#define SEQ_50_CROUCH 50
|
||||
#define SEQ_79_CROUCH_HOP 79
|
||||
#define SEQ_78_DRINK 78 /* выпить зелье */
|
||||
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
|
||||
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
|
||||
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
|
||||
@@ -74,14 +76,21 @@
|
||||
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный
|
||||
* автомат held/released/ignore (авто-повтор); ctrl1_* — их память между
|
||||
* кадрами (rest/save). */
|
||||
static int8_t control_x, control_y, control_shift;
|
||||
static int8_t control_forward, control_backward, control_up, control_down, control_shift2;
|
||||
int8_t control_x, control_y, control_shift;
|
||||
/* НЕ static: в оригинале это глобалы, через которые ИИ стража подаёт
|
||||
* СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же
|
||||
* control(), что и у игрока. Поэтому шина общая. */
|
||||
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
|
||||
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
|
||||
|
||||
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
|
||||
* Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl
|
||||
* не дублируются в этот TU). */
|
||||
#define seqtbl_offset_char(seq) kid_set_seq(seq)
|
||||
/* В оригинале seqtbl_offset_char работает с АКТИВНЫМ персонажем, и
|
||||
* control() вызывается внутри окна Char. Раньше макрос вёл на
|
||||
* kid_set_seq (пишет прямо в Kid) — после введения окна это ломало ввод:
|
||||
* savekid затирал Kid копией Char со старой curr_seq. */
|
||||
#define seqtbl_offset_char(seq) pop_char_set_seq(seq)
|
||||
|
||||
static int8_t release_arrows(void)
|
||||
{
|
||||
@@ -118,6 +127,11 @@ static uint8_t get_item_action(void)
|
||||
seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
|
||||
return 1;
|
||||
}
|
||||
if (a == 3) { /* зелье — выпить (get_item) */
|
||||
control_shift2 = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(SEQ_78_DRINK);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -125,13 +139,14 @@ static void run_jump(void)
|
||||
{
|
||||
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
|
||||
* полировка K3; K2b просто запускает run-jump. */
|
||||
if (Kid.frame >= FRAME_7_RUN) {
|
||||
if (Char.frame >= FRAME_7_RUN) {
|
||||
control_up = release_arrows();
|
||||
seqtbl_offset_char(SEQ_4_RUN_JUMP);
|
||||
}
|
||||
}
|
||||
|
||||
static void safe_step(void); /* fwd */
|
||||
static void draw_sword(void); /* fwd: нужен в control_standing */
|
||||
|
||||
static void forward_pressed(void)
|
||||
{
|
||||
@@ -190,15 +205,15 @@ static void safe_step(void)
|
||||
control_forward = CONTROL_IGNORE;
|
||||
distance = pop_edge_distance();
|
||||
if (distance) {
|
||||
Kid.repeat = 1;
|
||||
Char.repeat = 1;
|
||||
seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE));
|
||||
} else if (pop_edge_type() == EDGE_WALL) {
|
||||
/* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад
|
||||
* (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */
|
||||
Kid.repeat = 1;
|
||||
Char.repeat = 1;
|
||||
seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE));
|
||||
} else if (Kid.repeat != 0) {
|
||||
Kid.repeat = 0;
|
||||
} else if (Char.repeat != 0) {
|
||||
Char.repeat = 0;
|
||||
seqtbl_offset_char(SEQ_44_STEP_ON_EDGE);
|
||||
} else {
|
||||
seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */
|
||||
@@ -225,7 +240,32 @@ static void control_standing(void)
|
||||
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
|
||||
get_item_action())
|
||||
return;
|
||||
/* Kid charid_0, без меча: остаётся нижняя часть control_standing. */
|
||||
/* Ветку «charid != kid && вниз+вперёд -> draw_sword» не портируем: у нас
|
||||
* соперник до control_standing не доходит, его перехватывает
|
||||
* control_guard_inactive в самом диспетчере (порядок как в seg005). */
|
||||
|
||||
/* seg005:352 — Кид с мечом ВИДИТ активного стража: достаёт клинок сам.
|
||||
* Без этого игрок стоит с мечом в ножнах, пока его рубят. */
|
||||
if (pop_have_sword) {
|
||||
if (offguard != 0 && control_shift >= CONTROL_RELEASED) {
|
||||
/* Меч убран намеренно (down в бою) — не доставать, пока не
|
||||
* отпустят Shift; в оригинале это переход на loc_6213. */
|
||||
} else if (can_guard_see_kid >= 2) {
|
||||
int16_t d = pop_char_opp_dist();
|
||||
if (d >= -10 && d < 90) {
|
||||
holding_sword = 1;
|
||||
/* (word)d < (word)-6 — беззнаковое: истинно для ВСЕГО, кроме
|
||||
* −6..−1. То есть «страж вплотную за спиной» -> отступить,
|
||||
* иначе достать меч. */
|
||||
if (d >= -6 && d < 0) { back_pressed(); return; }
|
||||
draw_sword();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
offguard = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (control_shift == CONTROL_HELD) {
|
||||
if (control_backward == CONTROL_HELD) back_pressed();
|
||||
else if (control_up == CONTROL_HELD) up_pressed();
|
||||
@@ -266,7 +306,7 @@ static void control_running(void)
|
||||
/* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок
|
||||
* seq_47 при беге в стену (как оригинал), с корректной позицией. */
|
||||
if (control_x == CONTROL_RELEASED &&
|
||||
(Kid.frame == FRAME_7_RUN || Kid.frame == FRAME_11_RUN)) {
|
||||
(Char.frame == FRAME_7_RUN || Char.frame == FRAME_11_RUN)) {
|
||||
control_forward = release_arrows();
|
||||
seqtbl_offset_char(SEQ_13_STOP_RUN);
|
||||
} else if (control_x == CONTROL_HELD_BACKWARD) {
|
||||
@@ -317,21 +357,216 @@ static void control_jumpup(void)
|
||||
standing_jump();
|
||||
}
|
||||
|
||||
/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */
|
||||
static void control(void)
|
||||
/* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
|
||||
* клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки
|
||||
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
|
||||
* уже как выпад. */
|
||||
static void draw_sword(void)
|
||||
{
|
||||
uint8_t f = Kid.frame;
|
||||
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) — звука пока нет */
|
||||
} else if (Char.charid != CHARID_1_SHADOW) {
|
||||
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
|
||||
}
|
||||
Char.sword = SWORD_2_DRAWN;
|
||||
seqtbl_offset_char(seq_id);
|
||||
}
|
||||
|
||||
/* control_guard_inactive (seg006:2123): неактивный страж реагирует ТОЛЬКО в
|
||||
* кадре 166 (стойка) и только на «вниз»: вниз+вперёд — достать меч,
|
||||
* вниз без «вперёд» — развернуться (stand flipped). */
|
||||
static void control_guard_inactive(void)
|
||||
{
|
||||
if (Char.frame == FRAME_166_STAND_INACTIVE && control_down == CONTROL_HELD) {
|
||||
if (control_forward == CONTROL_HELD) {
|
||||
draw_sword();
|
||||
} else {
|
||||
control_down = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(SEQ_80_STAND_FLIPPED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Боёвка (seg005:964..) ---------------------------------------- *
|
||||
* Всё это ветки ОБЩЕГО диспетчера: у стража тот же control(), просто
|
||||
* control_* ему выставляет ИИ (autocontrol_*, guards.c), а не клавиатура. */
|
||||
|
||||
/* back_with_sword (seg005): шаг назад — только из стоек. */
|
||||
static void back_with_sword(void)
|
||||
{
|
||||
uint8_t f = Char.frame;
|
||||
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
|
||||
f == FRAME_171_STAND_WITH_SWORD) {
|
||||
control_backward = CONTROL_IGNORE; /* без авто-повтора */
|
||||
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD);
|
||||
}
|
||||
}
|
||||
|
||||
/* forward_with_sword (seg005): шаг вперёд; у стража своя последовательность. */
|
||||
static void forward_with_sword(void)
|
||||
{
|
||||
uint8_t f = Char.frame;
|
||||
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
|
||||
f == FRAME_171_STAND_WITH_SWORD) {
|
||||
control_forward = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(Char.charid != CHARID_0_KID ? SEQ_56_GUARD_FORWARD
|
||||
: SEQ_86_FORWARD_WITH_SWORD);
|
||||
}
|
||||
}
|
||||
|
||||
/* sword_strike (seg005): выпад из стойки/шага либо добивание после парирования. */
|
||||
static void sword_strike(void)
|
||||
{
|
||||
uint8_t f = Char.frame;
|
||||
uint8_t seq_id;
|
||||
if (f == FRAME_157_WALK_WITH_SWORD || f == FRAME_158_STAND_WITH_SWORD ||
|
||||
f == FRAME_170_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD ||
|
||||
f == FRAME_165_WALK_WITH_SWORD) {
|
||||
seq_id = (Char.charid == CHARID_0_KID) ? SEQ_75_STRIKE : SEQ_58_GUARD_STRIKE;
|
||||
} else if (f == FRAME_150_PARRY || f == FRAME_161_PARRY) {
|
||||
seq_id = SEQ_66_STRIKE_AFTER_PARRY;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
control_shift2 = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(seq_id);
|
||||
}
|
||||
|
||||
/* parry (seg005): блок. Условие сильно разное у Кида и у соперника —
|
||||
* соперник блокирует ТОЛЬКО на кадре 152 (второй кадр удара), а Кид ещё и
|
||||
* по 153 (и тогда последовательность прокручивается сразу). */
|
||||
static void parry(void)
|
||||
{
|
||||
uint8_t cf = Char.frame, of = Opp.frame;
|
||||
uint8_t seq_id = SEQ_62_PARRY;
|
||||
uint8_t do_play_seq = 0;
|
||||
|
||||
if (cf == FRAME_158_STAND_WITH_SWORD || cf == FRAME_170_STAND_WITH_SWORD ||
|
||||
cf == FRAME_171_STAND_WITH_SWORD || cf == FRAME_168_BACK ||
|
||||
cf == FRAME_165_WALK_WITH_SWORD) {
|
||||
if (pop_char_opp_dist() >= 32 && Char.charid != CHARID_0_KID) {
|
||||
back_with_sword();
|
||||
return;
|
||||
} else if (Char.charid == CHARID_0_KID) {
|
||||
if (of == FRAME_168_BACK) return;
|
||||
if (of != FRAME_151_STRIKE_1 && of != FRAME_152_STRIKE_2 &&
|
||||
of != FRAME_162_BLOCK_TO_STRIKE) {
|
||||
if (of == FRAME_153_STRIKE_3) do_play_seq = 1;
|
||||
}
|
||||
} else {
|
||||
if (of != FRAME_152_STRIKE_2) return;
|
||||
}
|
||||
} else {
|
||||
if (cf != FRAME_167_BLOCKED) return;
|
||||
seq_id = SEQ_61_PARRY_AFTER_STRIKE;
|
||||
}
|
||||
control_up = CONTROL_IGNORE;
|
||||
seqtbl_offset_char(seq_id);
|
||||
if (do_play_seq) play_seq();
|
||||
}
|
||||
|
||||
/* swordfight (seg005:0CDB) — разбор ввода в бою. Порядок веток важен:
|
||||
* удар старше «убрать меч», тот старше блока, блок старше шагов. */
|
||||
static void swordfight(void)
|
||||
{
|
||||
uint8_t f = Char.frame;
|
||||
uint8_t seq_id;
|
||||
|
||||
if (f == FRAME_161_PARRY && control_shift2 >= CONTROL_RELEASED) {
|
||||
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD); /* отскок после парирования */
|
||||
return;
|
||||
} else if (control_shift2 == CONTROL_HELD) {
|
||||
if (Char.charid == CHARID_0_KID) kid_sword_strike = 15;
|
||||
sword_strike();
|
||||
if (control_shift2 == CONTROL_IGNORE) return;
|
||||
}
|
||||
|
||||
if (control_down == CONTROL_HELD) {
|
||||
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
|
||||
f == FRAME_171_STAND_WITH_SWORD) {
|
||||
control_down = CONTROL_IGNORE;
|
||||
Char.sword = SWORD_0_SHEATHED;
|
||||
if (Char.charid == CHARID_0_KID) {
|
||||
offguard = 1; /* убрал намеренно — сразу не доставать */
|
||||
guard_refrac = 9; /* и страж на это время придержан */
|
||||
holding_sword = 0;
|
||||
seq_id = SEQ_93_PUT_SWORD_AWAY_FAST;
|
||||
} else if (Char.charid == CHARID_1_SHADOW) {
|
||||
seq_id = SEQ_92_PUT_SWORD_AWAY;
|
||||
} else {
|
||||
seq_id = SEQ_87_GUARD_BECOME_INACT; /* Кид ушёл из виду */
|
||||
}
|
||||
seqtbl_offset_char(seq_id);
|
||||
}
|
||||
} else if (control_up == CONTROL_HELD) {
|
||||
parry();
|
||||
} else if (control_forward == CONTROL_HELD) {
|
||||
forward_with_sword();
|
||||
} else if (control_backward == CONTROL_HELD) {
|
||||
back_with_sword();
|
||||
}
|
||||
}
|
||||
|
||||
/* control_with_sword (seg005:964): пока соперник виден и близко — бой;
|
||||
* иначе страж убирает меч, а Кид просто стоит. */
|
||||
static void control_with_sword(void)
|
||||
{
|
||||
if (Char.action >= 2) return; /* actions_2_hang_climb и выше */
|
||||
if (pop_tile_at(Char.curr_col, Char.curr_row) == 11 /* loose */ ||
|
||||
can_guard_see_kid >= 2) {
|
||||
int16_t d = pop_char_opp_dist();
|
||||
if (d < 90) {
|
||||
if (d >= 0) { swordfight(); return; }
|
||||
/* Соперник за спиной: далеко — развернуться, вплотную — драться. */
|
||||
if (d < -4) { seqtbl_offset_char(SEQ_60_TURN_WITH_SWORD); return; }
|
||||
swordfight();
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Не видит / далеко. */
|
||||
if (Char.charid < CHARID_2_GUARD) {
|
||||
if (Char.frame == FRAME_171_STAND_WITH_SWORD) {
|
||||
Char.sword = SWORD_0_SHEATHED;
|
||||
seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); /* страж убит */
|
||||
}
|
||||
} else {
|
||||
swordfight();
|
||||
}
|
||||
}
|
||||
|
||||
/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */
|
||||
void pop_control(void)
|
||||
{
|
||||
uint8_t f = Char.frame;
|
||||
/* seg005 control(), самая первая ветка: персонаж УМИРАЕТ (alive >= 0).
|
||||
* Из любой стойки он валится в seq_71_dying; остальное управление в
|
||||
* этом состоянии не обрабатывается. Через это же место проходит чит K
|
||||
* (pop_guard_kill ставит Guard.alive = 0). */
|
||||
if (Char.alive >= 0) {
|
||||
if (f == FRAME_15_STAND || f == FRAME_166_STAND_INACTIVE ||
|
||||
f == FRAME_158_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD)
|
||||
seqtbl_offset_char(SEQ_71_DYING);
|
||||
return;
|
||||
}
|
||||
/* seg005 control(): в отскоке (bumped) и в свободном падении управление
|
||||
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
|
||||
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и
|
||||
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
|
||||
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
|
||||
* пару пикселей меньше, чем должен»). */
|
||||
if (Kid.action == 5 || Kid.action == 4) { /* bumped / in_freefall */
|
||||
if (Char.action == 5 || Char.action == 4) { /* bumped / in_freefall */
|
||||
release_arrows();
|
||||
return;
|
||||
}
|
||||
if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
|
||||
/* Порядок веток — как в seg005: меч старше charid, charid старше кадров.
|
||||
* Пока Kid безоружен, обе ветки — про стража. */
|
||||
if (Char.sword == SWORD_2_DRAWN)
|
||||
control_with_sword();
|
||||
else if (Char.charid >= CHARID_2_GUARD)
|
||||
control_guard_inactive();
|
||||
else if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
|
||||
control_standing();
|
||||
else if (f == FRAME_48_TURN)
|
||||
control_turning();
|
||||
@@ -406,12 +641,12 @@ static void flip_control_x(void)
|
||||
/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */
|
||||
static void user_control(void)
|
||||
{
|
||||
if (Kid.direction >= DIR_RIGHT) {
|
||||
if (Char.direction >= DIR_RIGHT) {
|
||||
flip_control_x();
|
||||
control();
|
||||
pop_control();
|
||||
flip_control_x();
|
||||
} else {
|
||||
control();
|
||||
pop_control();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,6 +686,12 @@ void pop_ctrl_tick(void)
|
||||
rest_ctrl_1();
|
||||
read_input();
|
||||
read_user_control();
|
||||
user_control(); /* -> control(): может сменить Kid.curr_seq */
|
||||
/* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
|
||||
* персонажем (как в оригинале), поэтому ввод игрока применяется к
|
||||
* Киду только внутри loadkid/savekid. Кадр не перезагружаем —
|
||||
* play_seq в kid_tick сделает это следующим шагом. */
|
||||
pop_loadkid();
|
||||
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
|
||||
pop_savekid_state();
|
||||
save_ctrl_1();
|
||||
}
|
||||
|
||||
@@ -32,4 +32,27 @@ void pop_ctrl_tick(void);
|
||||
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
|
||||
int8_t pop_ctrl_shift_held(void);
|
||||
|
||||
/* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ----
|
||||
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
|
||||
* диспетчера: он ВЫСТАВЛЯЕТ эти переменные, а дальше исполняется тот же
|
||||
* control(), что и для игрока. Значения — CONTROL_RELEASED/IGNORE/HELD. */
|
||||
#define POP_CONTROL_RELEASED 0
|
||||
#define POP_CONTROL_IGNORE 1
|
||||
#define POP_CONTROL_HELD (-1)
|
||||
/* Оси ЭТОГО кадра (абсолютные): X = -1 влево / +1 вправо, Y = -1 вверх /
|
||||
* +1 вниз. Для ИИ «вперёд/назад» уже в системе персонажа, поэтому оси он
|
||||
* заполняет как есть, без flip_control_x. */
|
||||
extern int8_t control_x, control_y, control_shift;
|
||||
#define POP_CONTROL_HELD_FORWARD (-1)
|
||||
#define POP_CONTROL_HELD_BACKWARD 1
|
||||
#define POP_CONTROL_HELD_UP (-1)
|
||||
#define POP_CONTROL_HELD_DOWN 1
|
||||
extern int8_t control_forward, control_backward, control_up, control_down,
|
||||
control_shift2;
|
||||
|
||||
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
|
||||
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж — из
|
||||
* своего тика напрямую. */
|
||||
void pop_control(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* pop_gdraw.c — ОТРИСОВКА стража. Резидент окна W3 (--w3 pop_gdraw.c).
|
||||
*
|
||||
* Почему отдельным файлом, а не внутри pop_guard.c: --w3 забирает МОДУЛЬ
|
||||
* целиком (код+rodata в 0xC000), а состояние стража и его логика обязаны
|
||||
* остаться в W1/W2 — из банка (guards.c) резидент W3 недостижим ни прямо,
|
||||
* ни транзитивно (docs/layout_plan_v2.md §2 R2). Значит граница проходит
|
||||
* ровно между «что рисует» (зовёт только главный цикл → резидент) и «что
|
||||
* знает» (зовёт и банк → W1/W2).
|
||||
*
|
||||
* Писучие статики резидента (--dataseg не трогаем) остаются в обычном
|
||||
* _DATA, то есть в W2 — сюда переезжает КОД, не данные.
|
||||
*/
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
#include "pop_guard.h"
|
||||
#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */
|
||||
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
|
||||
#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */
|
||||
#include "kid_data.h"
|
||||
|
||||
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
|
||||
#define GUARD_PAGES 5
|
||||
static atlas_t gp[GUARD_PAGES];
|
||||
static uint8_t g_ok;
|
||||
|
||||
/* Прямоугольник последней отрисовки ПО СТРАНИЦЕ дабл-буфера (как у Kid:
|
||||
* heal обязан стирать спрайт именно той страницы, в которую рисуем). */
|
||||
static int gx_l[2], gy_l[2];
|
||||
static uint16_t gw_l[2], gh_l[2];
|
||||
static uint8_t gvalid[2];
|
||||
|
||||
/* Клинок — ОТДЕЛЬНЫМ прямоугольником, не объединением со стражем: объединение
|
||||
* почти вдвое больше суммы двух (клинок уходит вперёд-вверх), а heal стоит
|
||||
* ровно по площади. Объединение считаем только для окна fore-клипа. */
|
||||
static int sx_l[2], sy_l[2];
|
||||
static uint16_t sw_l[2], sh_l[2];
|
||||
static uint8_t svalid[2];
|
||||
|
||||
int pop_guard_load(void) __banked
|
||||
{
|
||||
static const char *const names[GUARD_PAGES] = {
|
||||
"GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl",
|
||||
"GUARD\\g3.atl", "GUARD\\g4.atl"
|
||||
};
|
||||
uint8_t i;
|
||||
for (i = 0; i < GUARD_PAGES; i++)
|
||||
if (atlas_load(&gp[i], names[i]) != 0) return -1;
|
||||
g_ok = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_guard_free(void) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
if (!g_ok) return;
|
||||
for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]);
|
||||
g_ok = 0;
|
||||
}
|
||||
|
||||
void pop_guard_heal(void) __banked
|
||||
{
|
||||
uint8_t p = gfx_get_draw_page() & 1;
|
||||
if (gvalid[p]) gfx_heal(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
|
||||
if (svalid[p]) gfx_heal(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]);
|
||||
gvalid[p] = svalid[p] = 0;
|
||||
}
|
||||
|
||||
void pop_guard_draw(void) __banked
|
||||
{
|
||||
int obj_x, obj_y, top, fwd, fp_x;
|
||||
uint8_t page, idx, p, flip;
|
||||
const uint8_t *img;
|
||||
uint16_t w, h;
|
||||
/* Окно fore-клипа — по ОБЪЕДИНЁННОМУ габариту «страж + клинок»; сам
|
||||
* футпринт (pop_fore_over_char) считается по спрайту стража, поэтому
|
||||
* w/h ниже НЕ трогаем. */
|
||||
int clip_x = 0, clip_y = 0; uint16_t clip_w = 0, clip_h = 0;
|
||||
|
||||
if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return;
|
||||
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
|
||||
fwd = (int)Guard.x + ((Guard.direction < 0) ? -pop_gframe.dx : pop_gframe.dx);
|
||||
obj_x = (fwd << 1) - 116;
|
||||
obj_y = (int)Guard.y + pop_gframe.dy;
|
||||
if ((int8_t)(pop_gframe.flags ^ Guard.direction) >= 0) obj_x++;
|
||||
fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — футпринт */
|
||||
obj_x = obj_x * 8 / 7; /* calc_screen_x_coord */
|
||||
|
||||
page = (uint8_t)(pop_gframe.image >> 3);
|
||||
idx = (uint8_t)(pop_gframe.image & 7);
|
||||
if (page >= GUARD_PAGES) return;
|
||||
img = (const uint8_t *)atlas_image(&gp[page], idx);
|
||||
gfx_w0_map(gp[page].page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
if (w && h) {
|
||||
int bx = obj_x;
|
||||
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
|
||||
if (flip) bx -= (int)w;
|
||||
top = obj_y - (int)h + 1;
|
||||
p = gfx_get_draw_page() & 1;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
|
||||
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||
else
|
||||
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1;
|
||||
clip_x = bx; clip_y = top; clip_w = w; clip_h = h;
|
||||
}
|
||||
/* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable,
|
||||
* seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник
|
||||
* heal; окно fore-клипа — объединение (без него клинок полез бы поверх
|
||||
* переднего столба). */
|
||||
{
|
||||
int sx, sy; uint16_t sw, sh;
|
||||
if (pop_sword_draw(&Guard, &pop_gframe, obj_x, obj_y,
|
||||
&sx, &sy, &sw, &sh)) {
|
||||
p = gfx_get_draw_page() & 1;
|
||||
sx_l[p] = sx; sy_l[p] = sy; sw_l[p] = sw; sh_l[p] = sh;
|
||||
svalid[p] = 1;
|
||||
if (!clip_w) {
|
||||
clip_x = sx; clip_y = sy; clip_w = sw; clip_h = sh;
|
||||
} else {
|
||||
int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h;
|
||||
if (sx < clip_x) clip_x = sx;
|
||||
if (sy < clip_y) clip_y = sy;
|
||||
if (sx + (int)sw > x1) x1 = sx + (int)sw;
|
||||
if (sy + (int)sh > y1) y1 = sy + (int)sh;
|
||||
clip_w = (uint16_t)(x1 - clip_x);
|
||||
clip_h = (uint16_t)(y1 - clip_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
/* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c,
|
||||
* fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов. */
|
||||
pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h);
|
||||
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
|
||||
* midtable, а foretable рисуется после всех midtable-записей, поэтому
|
||||
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
|
||||
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
|
||||
* столб. */
|
||||
if (w && h)
|
||||
pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction);
|
||||
}
|
||||
|
||||
/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- *
|
||||
* Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри
|
||||
* тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража
|
||||
* справа налево от x = 314. Пустые деления у Кида — свой спрайт (chtab_2
|
||||
* id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы
|
||||
* просто не печатаем — полоса и так на чёрном фоне.
|
||||
*
|
||||
* Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через
|
||||
* pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */
|
||||
#define HP_Y 194
|
||||
#define HP_STEP 7
|
||||
#define HP_KID_FULL 216
|
||||
#define HP_KID_EMPTY 217
|
||||
#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */
|
||||
|
||||
/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ
|
||||
* страницах дабл-буфера (иначе на второй останется старое значение и его
|
||||
* будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг.
|
||||
*
|
||||
* Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80
|
||||
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
|
||||
* sdcc_z80_cmp_store_a_bug). */
|
||||
static uint8_t hp_todo; /* сколько страниц ещё обновить */
|
||||
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
|
||||
|
||||
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
|
||||
* комнату (фон перерисован целиком и стёр её) и при старте. */
|
||||
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
|
||||
|
||||
void pop_hp_draw(void) __banked
|
||||
{
|
||||
uint8_t i, n;
|
||||
uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0);
|
||||
uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0);
|
||||
|
||||
{ /* Изменилось хоть одно число — обновить обе страницы. */
|
||||
uint8_t k = hitp_curr, km = hitp_max;
|
||||
uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev ||
|
||||
gd != hp_gd_prev || gm != hp_gdmax_prev);
|
||||
hp_kid_prev = k; hp_kidmax_prev = km;
|
||||
hp_gd_prev = gd; hp_gdmax_prev = gm;
|
||||
if (changed) hp_todo = 2;
|
||||
}
|
||||
if (hp_todo == 0) return;
|
||||
hp_todo--;
|
||||
|
||||
/* Стереть прошлую полосу: фон под ней — чёрный борт. */
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
setfillstyle(SOLID_FILL, BLACK);
|
||||
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
||||
|
||||
/* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */
|
||||
n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max);
|
||||
for (i = hitp_curr; i < n; i++)
|
||||
pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF);
|
||||
for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++)
|
||||
pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF);
|
||||
|
||||
/* Страж: только полные деления, справа налево. Скелет полосы не имеет. */
|
||||
if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) {
|
||||
const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0);
|
||||
uint16_t w, h;
|
||||
gfx_w0_map(gp[0].page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
if (w && h) {
|
||||
/* Атлас стража тоже column-major — блит колоночный. */
|
||||
n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd);
|
||||
for (i = 0; i < n; i++)
|
||||
gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP,
|
||||
HP_Y + POP_YOFF, img, 0, 0, 0);
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,222 @@ int8_t pop_y_to_row(int16_t y)
|
||||
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
|
||||
}
|
||||
|
||||
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv)
|
||||
/* Уложить значение в диапазон 0..maxv. Вызовы оригинала — prandom(1),
|
||||
* prandom(255), prandom(0xFF): делитель степень двойки, то есть маска, а не
|
||||
* деление. __moduint на Z80 стоит заметно дороже проверки n & (n-1). */
|
||||
static uint16_t pop_rnd_fit(uint16_t v, uint16_t maxv)
|
||||
{
|
||||
unsigned long s = *seed * 214013UL + 2531011UL;
|
||||
*seed = s;
|
||||
return (uint16_t)((uint16_t)(s >> 16) % (uint16_t)(maxv + 1));
|
||||
uint16_t n = (uint16_t)(maxv + 1);
|
||||
if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
|
||||
return (uint16_t)(v & (uint16_t)(n - 1));
|
||||
return (uint16_t)(v % n);
|
||||
}
|
||||
|
||||
#if POP_PRANDOM_EXACT
|
||||
/* ---- LCG оригинала: s = s*214013 + 2531011 (seg009:321) ------------ *
|
||||
*
|
||||
* ЕДИНСТВЕННОЕ место в порте на ассемблере, и вот почему. На Z80 нет
|
||||
* умножения, а SDCC для 32-битного `s * 214013` зовёт __mullong — 8 430
|
||||
* тактов на вызов (замер в MAME). Написанное на C через 16-битные
|
||||
* половины это ~3 500. Здесь ~1 000, при полностью той же
|
||||
* последовательности, что в SDLPoP. Альтернатива «взять генератор
|
||||
* попроще» (8-битный RND Apple II, xorshift) экономит меньше и ломает
|
||||
* совместимость с эталоном, по которому сверяем картинку.
|
||||
*
|
||||
* Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
|
||||
* РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
|
||||
* 0x343FD) — 17 удвоений, ТРИ сложения и одно вычитание. Величина 3*s,
|
||||
* нужная в конце, попадается по дороге на втором шаге — её и сохраняем.
|
||||
*
|
||||
* ABI __sdcccall(1): arg1 (указатель на сид) -> HL, возврат -> DE. IX не
|
||||
* используется, сохранять нечего. Клоббер AF/BC/DE/HL. Аккумулятор —
|
||||
* DE:HL (DE старшее слово); s.lo живёт в BC, s.hi и 3*s — в статиках
|
||||
* (регистров на всё не хватает).
|
||||
*/
|
||||
uint16_t pop_pr_ptr; /* адрес сида */
|
||||
uint16_t pop_pr_sh; /* старшее слово исходного сида */
|
||||
uint16_t pop_pr_3s_lo, pop_pr_3s_hi; /* 3*s, нужное в конце */
|
||||
|
||||
static uint16_t pop_lcg_step(pop_rnd_t *seed) __naked
|
||||
{
|
||||
(void)seed;
|
||||
__asm
|
||||
ld (_pop_pr_ptr), hl
|
||||
ld c, (hl)
|
||||
inc hl
|
||||
ld b, (hl) ; BC = s.lo
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
ld (_pop_pr_sh), a
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
ld (_pop_pr_sh + 1), a ; (pop_pr_sh) = s.hi
|
||||
|
||||
ld h, b
|
||||
ld l, c
|
||||
ld de, (_pop_pr_sh) ; акк = s
|
||||
|
||||
;; акк = акк*2 + s => 3s
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, bc
|
||||
ex de, hl
|
||||
ld a, (_pop_pr_sh) ; LD A,(nn) флаги НЕ трогает
|
||||
adc a, l
|
||||
ld l, a
|
||||
ld a, (_pop_pr_sh + 1)
|
||||
adc a, h
|
||||
ld h, a
|
||||
ex de, hl
|
||||
ld (_pop_pr_3s_lo), hl
|
||||
ld (_pop_pr_3s_hi), de
|
||||
|
||||
;; акк = акк*4 + s => 13s
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, bc
|
||||
ex de, hl
|
||||
ld a, (_pop_pr_sh)
|
||||
adc a, l
|
||||
ld l, a
|
||||
ld a, (_pop_pr_sh + 1)
|
||||
adc a, h
|
||||
ld h, a
|
||||
ex de, hl
|
||||
|
||||
;; акк = акк*16 + s => 209s
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, bc
|
||||
ex de, hl
|
||||
ld a, (_pop_pr_sh)
|
||||
adc a, l
|
||||
ld l, a
|
||||
ld a, (_pop_pr_sh + 1)
|
||||
adc a, h
|
||||
ld h, a
|
||||
ex de, hl
|
||||
|
||||
;; акк *= 1024 => 214016s (10 удвоений)
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
|
||||
;; акк -= 3s => 214013s
|
||||
ld bc, (_pop_pr_3s_lo)
|
||||
or a ; CY = 0
|
||||
sbc hl, bc
|
||||
ld bc, (_pop_pr_3s_hi) ; LD BC,(nn) флаги НЕ трогает
|
||||
ex de, hl
|
||||
sbc hl, bc
|
||||
ex de, hl
|
||||
|
||||
;; акк += 2531011 = 0x0026_9EC3
|
||||
ld bc, #0x9EC3
|
||||
add hl, bc
|
||||
ld bc, #0x0026
|
||||
ex de, hl
|
||||
adc hl, bc
|
||||
ex de, hl
|
||||
|
||||
;; сохранить сид, вернуть СТАРШЕЕ слово в DE
|
||||
push de
|
||||
ex de, hl ; DE = младшее слово
|
||||
ld hl, (_pop_pr_ptr)
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
inc hl
|
||||
pop de ; DE = старшее слово
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
|
||||
{
|
||||
return pop_rnd_fit(pop_lcg_step(seed), maxv);
|
||||
}
|
||||
|
||||
#else /* !POP_PRANDOM_EXACT — генератор попроще, НЕ совместимый с эталоном */
|
||||
|
||||
/* xorshift16 + шаг Вейля. Зачем не LCG оригинала: тот 32-битный, и даже
|
||||
* без __mullong (см. ветку выше) стоит ~7 000 тактов на вызов — при двух
|
||||
* вызовах за кадр это 3 % бюджета на генератор случайных чисел. Здесь
|
||||
* умножений нет вовсе, только сдвиги и xor.
|
||||
*
|
||||
* Почему НЕ 8-битный RND Apple II (RNDseed := 5*RNDseed + 23 mod 256,
|
||||
* Prince-of-Persia-Apple-II): он ещё быстрее, но у LCG по модулю 256
|
||||
* младшие биты вырождены — бит 0 просто чередуется. Наши вызовы это
|
||||
* видят: раскладка кладки берёт prandom(1) (один бит) и prandom(4), и
|
||||
* вместо шума получилась бы правильная шахматка. У xorshift такой
|
||||
* структуры нет, а стоит он примерно столько же.
|
||||
*
|
||||
* Шаг Вейля (+0x9E37, нечётный) нужен ещё и потому, что у чистого
|
||||
* xorshift ноль — неподвижная точка, а сид кладки (номер комнаты + ряд +
|
||||
* колонка) вполне может оказаться нулём.
|
||||
*
|
||||
* ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP — другая (но
|
||||
* статистически такая же) раскладка кладки и другие броски в боёвке.
|
||||
* Поэтому по умолчанию собирается НЕ эта ветка: после переписывания LCG
|
||||
* на ассемблер выигрыш от смены генератора почти исчез, а сверять
|
||||
* картинку с эталоном стало важнее. Ветка оставлена как запасной ход. */
|
||||
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
|
||||
{
|
||||
uint16_t s = seed->lo;
|
||||
s ^= (uint16_t)(s << 7);
|
||||
s ^= (uint16_t)(s >> 9);
|
||||
s ^= (uint16_t)(s << 8);
|
||||
s = (uint16_t)(s + 0x9E37u);
|
||||
seed->lo = s;
|
||||
seed->hi = 0; /* старшая половина не используется */
|
||||
/* Отдаём СТАРШИЙ байт: у него нет короткопериодической структуры
|
||||
* младших бит, а вызывающие берут от результата именно младшие. */
|
||||
return pop_rnd_fit((uint16_t)(s >> 8), maxv);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -26,9 +26,36 @@ extern const int16_t pop_y_land[5];
|
||||
* над комнатой, 3 = под комнатой). */
|
||||
int8_t pop_y_to_row(int16_t y);
|
||||
|
||||
/* prandom (seg009:321) — 32-битный LCG оригинала. Сид ВНЕШНИЙ: у стен
|
||||
* (раскладка кладки) и у анимаций тайлов (фаза факелов) свои
|
||||
* последовательности, смешивать их нельзя. */
|
||||
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv);
|
||||
/* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у
|
||||
* анимаций тайлов (фаза факелов) свои последовательности, смешивать их
|
||||
* нельзя.
|
||||
*
|
||||
* ДВА генератора, выбор через POP_PRANDOM_EXACT (см. pop_geom.c):
|
||||
* 1 (по умолчанию) — LCG оригинала (s*214013+2531011) БИТ-В-БИТ: та же
|
||||
* раскладка кладки и те же броски, что в SDLPoP, поэтому по картинке
|
||||
* можно сверяться с эталоном;
|
||||
* 0 — xorshift16 + шаг Вейля, без единого умножения. Дешевле ровно на
|
||||
* 5 646 тактов за кадр, 1.3 % бюджета (замер A/B в MAME, комната 3,
|
||||
* 175 кадров: 403 632 против 397 986). Последовательность другая —
|
||||
* статистически эквивалентная, но не та же.
|
||||
*
|
||||
* Обе версии считают 16-битными половинами, а не unsigned long: на 32-бит
|
||||
* умножении SDCC зовёт __mullong, и это 8 430 тактов на вызов (замер в
|
||||
* MAME, breakpoint на __mullong).
|
||||
*
|
||||
* ЗАДЕЛ: разрыв между версиями определяется не алгоритмом, а кодогенерацией
|
||||
* SDCC — те же генераторы на Z80-ассемблере укладываются в 86..148 тактов.
|
||||
* Если процент понадобится, сначала переписать в asm ОРИГИНАЛЬНЫЙ LCG (там
|
||||
* умножение на константу), а менять генератор — только если и этого мало. */
|
||||
typedef struct { uint16_t lo, hi; } pop_rnd_t;
|
||||
|
||||
#ifndef POP_PRANDOM_EXACT
|
||||
#define POP_PRANDOM_EXACT 1
|
||||
#endif
|
||||
|
||||
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv);
|
||||
|
||||
/* Сид из 16-битного числа (старшая половина = 0). */
|
||||
#define pop_prandom_set(s, v) do { (s).lo = (uint16_t)(v); (s).hi = 0; } while (0)
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
/*
|
||||
* pop_guard.c — состояние стража (W1/W2). См. pop_guard.h.
|
||||
* Логика — в банке (guards.c), отрисовка — из главного цикла.
|
||||
* pop_guard.c — СОСТОЯНИЕ и ЛОГИКА стража (W1/W2). См. pop_guard.h.
|
||||
*
|
||||
* Здесь только то, что должно оставаться достижимым из банка (guards.c):
|
||||
* поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы.
|
||||
* ОТРИСОВКА — в pop_gdraw.c, резиденте W3 (её зовёт лишь главный цикл).
|
||||
*/
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
#include <stdint.h>
|
||||
#include "pop_guard.h"
|
||||
#include "pop_geom.h"
|
||||
#include "pop_level.h"
|
||||
#include "pop_kid.h" /* play_seq/kid_set_seq пока общие (seqtbl один) */
|
||||
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
|
||||
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
|
||||
#include "pop_map.h" /* hitp_* — HP Кида */
|
||||
#include "kid_data.h"
|
||||
|
||||
pop_char_t Guard;
|
||||
uint8_t guardhp_curr, guardhp_max;
|
||||
int8_t guardhp_delta;
|
||||
int8_t can_guard_see_kid;
|
||||
int8_t is_guard_notice;
|
||||
uint8_t guard_skill;
|
||||
int8_t guard_refrac, justblocked, kid_sword_strike;
|
||||
int8_t offguard, holding_sword;
|
||||
|
||||
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
|
||||
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
|
||||
* см. pop_geom.h — иначе перерисовка стены сбивала бы броски стража. */
|
||||
pop_rnd_t pop_fight_seed;
|
||||
|
||||
/* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его
|
||||
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
|
||||
kframe pop_gframe;
|
||||
|
||||
uint8_t pop_guard_present(void)
|
||||
{
|
||||
@@ -28,6 +43,10 @@ void pop_guard_reset(void)
|
||||
Guard.direction = 0;
|
||||
guardhp_curr = 0;
|
||||
guardhp_delta = 0;
|
||||
can_guard_see_kid = 0;
|
||||
is_guard_notice = 0;
|
||||
guard_refrac = justblocked = kid_sword_strike = 0;
|
||||
offguard = holding_sword = 0;
|
||||
}
|
||||
|
||||
/* kill guard cheat (seg000:786):
|
||||
@@ -40,56 +59,59 @@ void pop_guard_kill(void)
|
||||
{
|
||||
if (Guard.charid == CHARID_4_SKELETON) return;
|
||||
if (!pop_guard_present()) return;
|
||||
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит
|
||||
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше
|
||||
* общий control() ставит ему seq_71_dying. */
|
||||
guardhp_delta = (int8_t)-guardhp_curr;
|
||||
guardhp_curr = 0;
|
||||
Guard.alive = 0;
|
||||
}
|
||||
|
||||
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
|
||||
#define GUARD_PAGES 5
|
||||
static atlas_t gp[GUARD_PAGES];
|
||||
static uint8_t g_ok;
|
||||
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
|
||||
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
|
||||
void pop_do_delta_hp(void)
|
||||
{
|
||||
int16_t hp;
|
||||
if (hitp_delta) {
|
||||
hp = (int16_t)hitp_curr + hitp_delta;
|
||||
if (hp < 0) hp = 0;
|
||||
if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max;
|
||||
hitp_curr = (uint8_t)hp;
|
||||
pop_kid_hurt = 1; /* -> «брызги» поверх Кида */
|
||||
hitp_delta = 0;
|
||||
}
|
||||
if (guardhp_delta) {
|
||||
hp = (int16_t)guardhp_curr + guardhp_delta;
|
||||
if (hp < 0) hp = 0;
|
||||
if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max;
|
||||
guardhp_curr = (uint8_t)hp;
|
||||
guardhp_delta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Прямоугольник последней отрисовки ПО СТРАНИЦЕ дабл-буфера (как у Kid:
|
||||
* heal обязан стирать спрайт именно той страницы, в которую рисуем). */
|
||||
static int gx_l[2], gy_l[2];
|
||||
static uint16_t gw_l[2], gh_l[2];
|
||||
static uint8_t gvalid[2];
|
||||
|
||||
/* Кадр стража (порт cur_frame для Char): таблица у стражей СВОЯ
|
||||
* (frame_tbl_guard), и индексируется как frame + add_frame − 149, где
|
||||
/* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
|
||||
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame − 149, где
|
||||
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
||||
static kframe gframe;
|
||||
|
||||
int pop_guard_load(void)
|
||||
{
|
||||
static const char *const names[GUARD_PAGES] = {
|
||||
"GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl",
|
||||
"GUARD\\g3.atl", "GUARD\\g4.atl"
|
||||
};
|
||||
uint8_t i;
|
||||
for (i = 0; i < GUARD_PAGES; i++)
|
||||
if (atlas_load(&gp[i], names[i]) != 0) return -1;
|
||||
g_ok = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_guard_free(void)
|
||||
{
|
||||
uint8_t i;
|
||||
if (!g_ok) return;
|
||||
for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]);
|
||||
g_ok = 0;
|
||||
}
|
||||
|
||||
/* load_frame для стража (seg006:0293 ветка charid_2_guard). */
|
||||
static void guard_load_frame(void)
|
||||
void pop_guard_load_frame(void)
|
||||
{
|
||||
int16_t idx = (int16_t)Guard.frame;
|
||||
if (idx >= 102 && idx < 107) idx += 70; /* add_frame */
|
||||
idx -= 149;
|
||||
if (idx < 0 || idx >= KID_NGFRAMES) { gframe.image = 255; return; }
|
||||
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&gframe);
|
||||
if (idx < 0 || idx >= KID_NGFRAMES) { pop_gframe.image = 255; return; }
|
||||
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe);
|
||||
}
|
||||
|
||||
/* leave_guard (seg002:02F5): вызывать ПЕРЕД сменой комнаты, пока Guard ещё
|
||||
* держит стража уходящей комнаты. Мёртвого запоминаем вместе с
|
||||
* последовательностью — тогда при возврате он лежит там же, а не встаёт
|
||||
* живым. */
|
||||
void pop_guard_leave(void)
|
||||
{
|
||||
if (Guard.charid == 0) return; /* стража не было */
|
||||
pop_guard_state_save(Guard.room,
|
||||
(uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */
|
||||
Guard.direction, Guard.x, guard_skill,
|
||||
Guard.curr_seq,
|
||||
(uint8_t)(Guard.alive >= 0));
|
||||
}
|
||||
|
||||
void pop_guard_enter(uint8_t room)
|
||||
@@ -111,55 +133,32 @@ void pop_guard_enter(uint8_t room)
|
||||
Guard.alive = -1;
|
||||
Guard.action = 1; /* actions_1_run_jump */
|
||||
Guard.fall_x = Guard.fall_y = 0;
|
||||
/* seq_77_guard_stand_inactive даёт кадр 166 (frame_166_stand_inactive,
|
||||
* seqtbl.c:266). Интерпретатор последовательностей для стража придёт
|
||||
* вместе с ИИ (следующая фаза) — сейчас ставим кадр напрямую, чтобы
|
||||
* страж просто СТОЯЛ и было что рисовать/убивать читом. */
|
||||
Guard.frame = 166;
|
||||
guardhp_max = guardhp_curr = 3; /* get_guard_hp: skill 0, уровень 1 */
|
||||
/* enter_guard (seg002:214): мастерство из данных уровня, 12 градаций;
|
||||
* всё, что вне диапазона, оригинал сводит к 3. */
|
||||
guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3);
|
||||
/* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень].
|
||||
* Уровень 1 -> 3; extrastrength ненулевой только у skill 4. */
|
||||
guardhp_max = guardhp_curr = (uint8_t)(3 + (guard_skill == 4 ? 1 : 0));
|
||||
guardhp_delta = 0;
|
||||
guard_load_frame();
|
||||
}
|
||||
|
||||
void pop_guard_heal(void)
|
||||
{
|
||||
uint8_t p = gfx_get_draw_page() & 1;
|
||||
if (gvalid[p]) gfx_heal(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
|
||||
gvalid[p] = 0;
|
||||
}
|
||||
|
||||
void pop_guard_draw(void)
|
||||
{
|
||||
int obj_x, obj_y, top, fwd;
|
||||
uint8_t page, idx, p, flip;
|
||||
const uint8_t *img;
|
||||
uint16_t w, h;
|
||||
|
||||
if (!g_ok || Guard.charid == 0 || gframe.image == 255) return;
|
||||
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
|
||||
fwd = (int)Guard.x + ((Guard.direction < 0) ? -gframe.dx : gframe.dx);
|
||||
obj_x = (fwd << 1) - 116;
|
||||
obj_y = (int)Guard.y + gframe.dy;
|
||||
if ((int8_t)(gframe.flags ^ Guard.direction) >= 0) obj_x++;
|
||||
obj_x = obj_x * 8 / 7; /* calc_screen_x_coord */
|
||||
|
||||
page = (uint8_t)(gframe.image >> 3);
|
||||
idx = (uint8_t)(gframe.image & 7);
|
||||
if (page >= GUARD_PAGES) return;
|
||||
img = (const uint8_t *)atlas_image(&gp[page], idx);
|
||||
gfx_w0_map(gp[page].page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
if (w && h) {
|
||||
int bx = obj_x;
|
||||
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
|
||||
if (flip) bx -= (int)w;
|
||||
top = obj_y - (int)h + 1;
|
||||
p = gfx_get_draw_page() & 1;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1;
|
||||
/* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
|
||||
* ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит
|
||||
* ровно там, где лёг (порт ветки guards_seq_hi != 0). */
|
||||
{
|
||||
uint16_t saved_seq = pop_guard_state_seq(room);
|
||||
/* У ЗАПОМНЕННОГО трупа X берём сохранённую: pos_guards (seg003:620)
|
||||
* пересчитывает её из колонки только при загрузке уровня, а дальше
|
||||
* ею владеет leave_guard. Иначе тело при возврате прыгает в центр
|
||||
* тайла. */
|
||||
if (saved_seq) Guard.x = pop_guard_state_x(room);
|
||||
pop_loadshad();
|
||||
if (saved_seq) Char.curr_seq = saved_seq;
|
||||
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
|
||||
pop_saveshad();
|
||||
pop_guard_play(); /* до первого кадра */
|
||||
/* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */
|
||||
if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) {
|
||||
Guard.alive = 1;
|
||||
guardhp_curr = 0;
|
||||
}
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
* линкуются В СТРАНИЦУ БАНКА и снаружи не читаются, поэтому банк —
|
||||
* только код;
|
||||
* - ЛОГИКА (ИИ, боёвка) — в guards.c, собираемом как --bank 1=guards.c;
|
||||
* - ОТРИСОВКА стража — из W1/W2 (рядом с kid_draw): из банка примитивы
|
||||
* libbgi звать можно, но резидент W3 (pop_bg) — нельзя, поэтому слой
|
||||
* фона/окклюзии за стража делает главный цикл.
|
||||
* - ОТРИСОВКА стража — в pop_gdraw.c, РЕЗИДЕНТЕ W3 (--w3): её зовёт
|
||||
* только главный цикл, а из банка резидент недостижим. Так код
|
||||
* отрисовки не занимает дефицитные W1/W2 (куча).
|
||||
*
|
||||
* Поля char_type — как в оригинале (types.h:302), чтобы порт seg005/seg006
|
||||
* ложился один в один.
|
||||
@@ -18,6 +18,47 @@
|
||||
#define POP_GUARD_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "kid_data.h" /* kframe */
|
||||
#include "pop_char.h"
|
||||
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
|
||||
|
||||
/* seqids (types.h:1052+) — последовательности боя и стойки. */
|
||||
#define SEQ_55_DRAW_SWORD 55
|
||||
#define SEQ_56_GUARD_FORWARD 56 /* шаг вперёд с мечом (страж) */
|
||||
#define SEQ_57_BACK_WITH_SWORD 57
|
||||
#define SEQ_58_GUARD_STRIKE 58
|
||||
#define SEQ_60_TURN_WITH_SWORD 60
|
||||
#define SEQ_61_PARRY_AFTER_STRIKE 61
|
||||
#define SEQ_62_PARRY 62
|
||||
#define SEQ_66_STRIKE_AFTER_PARRY 66
|
||||
#define SEQ_71_DYING 71
|
||||
#define SEQ_75_STRIKE 75 /* удар (Кид) */
|
||||
#define SEQ_77_GUARD_STAND_INACTIVE 77
|
||||
#define SEQ_80_STAND_FLIPPED 80
|
||||
#define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */
|
||||
#define SEQ_87_GUARD_BECOME_INACT 87
|
||||
#define SEQ_90_EN_GARDE 90
|
||||
#define SEQ_92_PUT_SWORD_AWAY 92
|
||||
#define SEQ_93_PUT_SWORD_AWAY_FAST 93
|
||||
|
||||
/* frameids (types.h:911+) и состояния меча (types.h:332). */
|
||||
#define FRAME_150_PARRY 150
|
||||
#define FRAME_151_STRIKE_1 151
|
||||
#define FRAME_152_STRIKE_2 152
|
||||
#define FRAME_153_STRIKE_3 153
|
||||
#define FRAME_157_WALK_WITH_SWORD 157
|
||||
#define FRAME_158_STAND_WITH_SWORD 158
|
||||
#define FRAME_161_PARRY 161
|
||||
#define FRAME_162_BLOCK_TO_STRIKE 162
|
||||
#define FRAME_165_WALK_WITH_SWORD 165
|
||||
#define FRAME_166_STAND_INACTIVE 166
|
||||
#define FRAME_167_BLOCKED 167
|
||||
#define FRAME_168_BACK 168
|
||||
#define FRAME_169_BEGIN_BLOCK 169
|
||||
#define FRAME_170_STAND_WITH_SWORD 170
|
||||
#define FRAME_171_STAND_WITH_SWORD 171
|
||||
#define SWORD_0_SHEATHED 0
|
||||
#define SWORD_2_DRAWN 2
|
||||
|
||||
/* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */
|
||||
#define CHARID_0_KID 0
|
||||
@@ -26,43 +67,85 @@
|
||||
#define CHARID_4_SKELETON 4
|
||||
#define CHARID_5_PRINCESS 5
|
||||
|
||||
typedef struct {
|
||||
uint8_t frame;
|
||||
uint8_t x, y;
|
||||
int8_t direction;
|
||||
int8_t curr_col, curr_row;
|
||||
uint8_t action;
|
||||
int8_t fall_x, fall_y;
|
||||
uint8_t room;
|
||||
uint8_t repeat;
|
||||
uint8_t charid;
|
||||
uint8_t sword;
|
||||
int8_t alive; /* -1 = жив (как в оригинале), 0 = убит */
|
||||
uint16_t curr_seq;
|
||||
} pop_char_t;
|
||||
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
|
||||
#define NUM_GUARD_SKILLS 12
|
||||
|
||||
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
|
||||
|
||||
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
|
||||
extern uint8_t guardhp_curr, guardhp_max;
|
||||
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */
|
||||
|
||||
/* Видит ли страж Кида (порт can_guard_see_kid, seg003:688):
|
||||
* 0 — не видит, 1 — видит, но не пойдёт (между ними loose/чомпер/дыра),
|
||||
* 2 — видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в
|
||||
* кадр ДО логики персонажей; читают ИИ (autocontrol_*) и боёвка. */
|
||||
extern int8_t can_guard_see_kid;
|
||||
|
||||
/* Кид «нашумел» (порт is_guard_notice, seg000:364): заставляет неактивного
|
||||
* стража обернуться, даже если Кид у него за спиной. Ставит источник шума,
|
||||
* снимает сам ИИ. */
|
||||
extern int8_t is_guard_notice;
|
||||
|
||||
/* Боевые таймеры/уровень мастерства (seg000:295..311). Все убывают раз в
|
||||
* кадр в autocontrol_opponent — как в оригинале.
|
||||
* guard_skill — 0..11, из данных уровня; индекс в таблицы вероятностей;
|
||||
* guard_refrac — «отдышка» стража: пока >0, он не атакует и не наступает;
|
||||
* justblocked — Кид только что парировал (влияет на вероятность блока);
|
||||
* kid_sword_strike — Кид только что бил (стража это придерживает). */
|
||||
extern uint8_t guard_skill;
|
||||
extern int8_t guard_refrac, justblocked, kid_sword_strike;
|
||||
|
||||
/* Состояние меча КИДА (seg005/seg006):
|
||||
* offguard — меч убран НАМЕРЕННО (вниз в бою): пока стоит, Кид не
|
||||
* достаёт клинок автоматически, даже видя стража;
|
||||
* holding_sword — клинок в руке (для индикатора HP стража). */
|
||||
extern int8_t offguard, holding_sword;
|
||||
|
||||
/* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
|
||||
extern pop_rnd_t pop_fight_seed;
|
||||
|
||||
/* Луч видимости стража — раз в кадр, ДО тиков персонажей (в оригинале
|
||||
* check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */
|
||||
void pop_check_can_guard_see_kid(void) __banked;
|
||||
|
||||
/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в
|
||||
* кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
|
||||
void pop_check_sword_hurting(void) __banked;
|
||||
void pop_check_sword_hurt(void) __banked;
|
||||
|
||||
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
|
||||
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
|
||||
extern kframe pop_gframe;
|
||||
void pop_guard_load_frame(void);
|
||||
|
||||
/* Есть ли живой страж в отрисованной комнате (для главного цикла). */
|
||||
uint8_t pop_guard_present(void);
|
||||
|
||||
/* Атласы стража (GUARD\\g0..g4.atl) — грузить ДО initgraph, как Kid.
|
||||
* 0 — OK, -1 — ошибка (тогда страж просто не рисуется). */
|
||||
int pop_guard_load(void);
|
||||
void pop_guard_free(void);
|
||||
int pop_guard_load(void) __banked;
|
||||
void pop_guard_free(void) __banked;
|
||||
|
||||
/* Вход в комнату: поднять стража из данных уровня (порт enter_guard,
|
||||
* seg002:0112) либо убрать его, если в комнате стража нет. */
|
||||
void pop_guard_enter(uint8_t room);
|
||||
|
||||
/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале
|
||||
* enter_room, пока Guard ещё держит прежнего. */
|
||||
void pop_guard_leave(void);
|
||||
|
||||
/* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE,
|
||||
* как kid_draw) и стереть прошлый (heal). Звать из главного цикла рядом
|
||||
* с kid_draw: рисовать из БАНКА нельзя только резидентом W3, а тут мы в
|
||||
* W1/W2. */
|
||||
void pop_guard_heal(void);
|
||||
void pop_guard_draw(void);
|
||||
* как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c
|
||||
* (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */
|
||||
/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл
|
||||
* ПОСЛЕ персонажей — она ничего не перекрывает. */
|
||||
void pop_hp_draw(void) __banked;
|
||||
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
|
||||
void pop_hp_invalidate(void) __banked;
|
||||
|
||||
void pop_guard_heal(void) __banked;
|
||||
void pop_guard_draw(void) __banked;
|
||||
|
||||
/* Убрать стража со сцены (смена комнаты / инициализация). */
|
||||
void pop_guard_reset(void);
|
||||
@@ -71,6 +154,10 @@ void pop_guard_reset(void);
|
||||
* при включённых читах — проверку делает вызывающий. */
|
||||
void pop_guard_kill(void);
|
||||
|
||||
/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать
|
||||
* раз в кадр ПОСЛЕ тиков персонажей (в оригинале — в play_frame). */
|
||||
void pop_do_delta_hp(void);
|
||||
|
||||
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
|
||||
void pop_guard_tick(void) __banked;
|
||||
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
#include "pop_kid.h"
|
||||
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
|
||||
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
|
||||
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
|
||||
#include "pop_geom.h"
|
||||
#include "kid_data.h"
|
||||
|
||||
kid_t Kid;
|
||||
pop_char_t Kid;
|
||||
|
||||
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
|
||||
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
|
||||
@@ -27,15 +28,15 @@ int8_t knock;
|
||||
static atlas_t kidp[KID_MAXPAGES];
|
||||
static uint8_t kid_npages;
|
||||
|
||||
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): отдельный
|
||||
* спрайт клинка поверх Kid в кадрах «нашёл меч»/убирание в ножны (229..237).
|
||||
* Из sword_tbl взяты только строки 35..42 — те, что адресуют эти кадры (бой
|
||||
* не портирован); индекс в атласе = sword_frame − 35. Смещения таблицы —
|
||||
* в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ calc_screen_x_coord. */
|
||||
#define SWORD_SF_LO 35
|
||||
#define SWORD_SF_HI 42
|
||||
static const int8_t SWORD_DX[8] = { 7, 14, 14, 14, 28, 28, 21, 14 };
|
||||
static const int8_t SWORD_DY[8] = {-32,-32,-31,-29,-28,-28,-25,-22 };
|
||||
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
|
||||
* ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража.
|
||||
* Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется
|
||||
* sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет.
|
||||
* Смещения — в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ
|
||||
* calc_screen_x_coord, x — «вперёд» по направлению персонажа. */
|
||||
static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT;
|
||||
static const int8_t SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT;
|
||||
static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT;
|
||||
static atlas_t swordp;
|
||||
static uint8_t sword_ok;
|
||||
|
||||
@@ -46,6 +47,72 @@ int pop_sword_load(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* add_sword_to_objtable (seg006:1798). Клинок рисуется, если персонаж в
|
||||
* кадрах подъёма/ножен, ИЛИ держит меч, ИЛИ это ЖИВОЙ страж (у стража меч
|
||||
* в руке всегда, даже пока sword ещё sheathed). Страницу W0 оставляем
|
||||
* замапленной — вызывающий делает gfx_w0_unmap сам. */
|
||||
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
|
||||
int obj_x, int obj_y,
|
||||
int *rx, int *ry, uint16_t *rw, uint16_t *rh)
|
||||
{
|
||||
uint8_t sf, id, flip;
|
||||
const uint8_t *simg;
|
||||
uint16_t w, h;
|
||||
int bx, top;
|
||||
|
||||
if (!sword_ok) return 0;
|
||||
if (!((ch->frame >= 229 && ch->frame < 238) ||
|
||||
ch->sword != 0 ||
|
||||
(ch->charid == CHARID_2_GUARD && ch->alive < 0)))
|
||||
return 0;
|
||||
sf = (uint8_t)(fr->sword & 0x3F);
|
||||
if (sf == 0 || sf >= KID_NSWORD) return 0;
|
||||
id = SWORD_ID[sf];
|
||||
if (id == 0xFF) return 0;
|
||||
|
||||
simg = (const uint8_t *)atlas_image(&swordp, id);
|
||||
gfx_w0_map(swordp.page);
|
||||
w = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
|
||||
h = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
|
||||
if (!w || !h) return 0;
|
||||
|
||||
flip = (uint8_t)(ch->direction >= 0);
|
||||
bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]);
|
||||
top = obj_y + (int)SWORD_DY[sf] - (int)h + 1;
|
||||
if (flip) bx -= (int)w;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
/* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h:
|
||||
* подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */
|
||||
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
|
||||
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, 0, 0);
|
||||
else
|
||||
gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
*rx = bx; *ry = top; *rw = w; *rh = h;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y).
|
||||
* Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры
|
||||
* Кида, а держать вторую копию раскладки в pop_gdraw ни к чему. */
|
||||
void pop_kid_img_blit(uint8_t image, int x, int y)
|
||||
{
|
||||
uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */
|
||||
uint8_t idx = (uint8_t)(image & 7);
|
||||
const uint8_t *img;
|
||||
uint16_t w, h;
|
||||
if (pg >= kid_npages) return;
|
||||
img = (const uint8_t *)atlas_image(&kidp[pg], idx);
|
||||
gfx_w0_map(kidp[pg].page);
|
||||
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
|
||||
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
|
||||
/* Атласы Кида хранятся COLUMN-MAJOR (transpose_cols в pop_pack_kid —
|
||||
* ради бесплатного горизонтального флипа), поэтому блит обязан быть
|
||||
* колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной. */
|
||||
if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0);
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
int pop_kid_load(uint8_t npages)
|
||||
{
|
||||
static const char *const names[KID_MAXPAGES] = {
|
||||
@@ -98,6 +165,16 @@ static const uint8_t *kd_frame_ptr(uint8_t i)
|
||||
off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
|
||||
return (const uint8_t *)off;
|
||||
}
|
||||
|
||||
/* То же для таблицы кадров СТРАЖА (frame_tbl_guard дописана в ту же
|
||||
* страницу данных, см. pop_extract_kid_data.py). */
|
||||
static const uint8_t *kd_gframe_ptr(uint8_t i)
|
||||
{
|
||||
uint16_t off = (uint16_t)i;
|
||||
off += (uint16_t)(off << 2);
|
||||
off += (uint16_t)(KD_DATA_OFF + KID_BIN_GFRAMES_OFF);
|
||||
return (const uint8_t *)off;
|
||||
}
|
||||
#define KD_SEQ(cs) (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
|
||||
(uint16_t)((cs) - SEQTBL_BASE)))
|
||||
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
|
||||
@@ -106,8 +183,76 @@ static const uint8_t *kd_frame_ptr(uint8_t i)
|
||||
|
||||
static uint8_t kdat_blk, kdat_page, kdat_ok;
|
||||
|
||||
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы. */
|
||||
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
|
||||
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
|
||||
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
|
||||
* тикает ПОСЛЕ Кида, и без разделения kid_draw взял бы кадр стража. */
|
||||
static kframe cur_frame;
|
||||
static kframe kid_frame; /* кадр Kid для kid_draw */
|
||||
|
||||
/* Char (seg006): АКТИВНЫЙ персонаж. Интерпретатор последовательностей в
|
||||
* оригинале работает не с Kid и не с Guard, а с этой копией, а вокруг
|
||||
* стоят loadkid/savekid и loadshad/saveshad (seg006:809..825). Копия, а
|
||||
* не указатель, — и так же, как в оригинале, и быстрее на Z80: обращения
|
||||
* в горячем цикле идут абсолютной адресацией глобала, а 16-байтовое
|
||||
* копирование платится один раз на переключение персонажа. */
|
||||
pop_char_t Char;
|
||||
|
||||
/* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят
|
||||
* рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809). Через неё
|
||||
* ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед
|
||||
* ними: для стража Opp = Kid, для Кида Opp = Guard. */
|
||||
pop_char_t Opp;
|
||||
|
||||
/* Снимок последовательности на момент входа в окно Char: по нему
|
||||
* pop_savekid_state отличает «последовательность сменил ДИСПЕТЧЕР (в Char)»
|
||||
* от «её сменил pop_map (прямо в Kid)». */
|
||||
static uint16_t kid_seq_at_load;
|
||||
|
||||
void pop_loadkid(void) { Char = Kid; kid_seq_at_load = Kid.curr_seq; }
|
||||
void pop_savekid(void) { Kid = Char; }
|
||||
/* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально
|
||||
* меняет у активного персонажа — последовательность и состояние меча.
|
||||
*
|
||||
* Почему не `Kid = Char`, как делает savekid оригинала: у нас часть
|
||||
* действий control() исполняет pop_map (pop_down_action, pop_jump_up_seq,
|
||||
* safe_step...), и он пишет ПРЯМО в Kid — там ещё не портирован переход на
|
||||
* Char. Полное копирование затирало эти правки: Kid терял выравнивание x
|
||||
* и ряд, и спуск с уступа через вис не срабатывал — персонаж просто
|
||||
* приседал. Когда pop_map переведём на Char, здесь вернётся Kid = Char. */
|
||||
void pop_savekid_state(void)
|
||||
{
|
||||
/* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в Char
|
||||
* (pop_char_set_seq), pop_map — прямо в Kid (kid_set_seq: спуск с
|
||||
* уступа SEQ_68, зацеп, осторожный шаг). Решение pop_map старше:
|
||||
* диспетчер про эти переходы не знает и, копируя Char вслепую, затирал
|
||||
* их — спуск в проём не срабатывал, а персонаж даже не приседал, потому
|
||||
* что pop_down_action уже вернул «обработано». */
|
||||
uint8_t map_changed = (uint8_t)(Kid.curr_seq != kid_seq_at_load);
|
||||
if (!map_changed) Kid.curr_seq = Char.curr_seq;
|
||||
Kid.sword = Char.sword;
|
||||
}
|
||||
void pop_loadshad(void) { Char = Guard; }
|
||||
void pop_saveshad(void) { Guard = Char; }
|
||||
void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; }
|
||||
void pop_saveshad_and_opp(void) { Guard = Char; Kid = Opp; }
|
||||
void pop_loadkid_and_opp(void) { Char = Kid; Opp = Guard; }
|
||||
void pop_savekid_and_opp(void) { Kid = Char; Guard = Opp; }
|
||||
|
||||
/* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной.
|
||||
* +13 при встречных направлениях: у стоящих лицом друг к другу «ноль»
|
||||
* приходится на ширину тайла между ними. Живёт здесь, а не в guards.c,
|
||||
* потому что нужен по обе стороны банковой границы — и ИИ, и диспетчеру
|
||||
* боёвки (pop_ctrl). */
|
||||
int16_t pop_char_opp_dist(void)
|
||||
{
|
||||
int16_t distance;
|
||||
if (Char.room != Opp.room) return 999;
|
||||
distance = (int16_t)Opp.x - (int16_t)Char.x;
|
||||
if (Char.direction < 0) distance = -distance;
|
||||
if (distance >= 0 && Char.direction != Opp.direction) distance += 13;
|
||||
return distance;
|
||||
}
|
||||
|
||||
int pop_kid_data_load(const char *path)
|
||||
{
|
||||
@@ -166,18 +311,39 @@ void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5)
|
||||
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
|
||||
static void load_frame(void)
|
||||
{
|
||||
const uint8_t *f = kd_frame_ptr(Kid.frame);
|
||||
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
|
||||
* стража frame_tbl_guard с индексом frame + add_frame − 149, где
|
||||
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
||||
const uint8_t *f;
|
||||
if (Char.charid == CHARID_2_GUARD) {
|
||||
int16_t idx = (int16_t)Char.frame;
|
||||
if (idx >= 102 && idx < 107) idx += 70;
|
||||
idx -= 149;
|
||||
if (idx < 0 || idx >= KID_NGFRAMES) { cur_frame.image = 255; return; }
|
||||
f = kd_gframe_ptr((uint8_t)idx);
|
||||
} else {
|
||||
f = kd_frame_ptr(Char.frame);
|
||||
}
|
||||
cur_frame.image = f[0];
|
||||
cur_frame.dx = (int8_t)f[1];
|
||||
cur_frame.dy = (int8_t)f[2];
|
||||
cur_frame.flags = f[3];
|
||||
cur_frame.sword = f[4];
|
||||
/* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного
|
||||
* персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/
|
||||
* pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки
|
||||
* check_sword_hurting / check_sword_hurt) записывало Киду кадр, который
|
||||
* на самом деле принадлежал СТРАЖУ: kid_draw брал его image из атласа
|
||||
* Кида и рисовал произвольную позу. Теперь владелец один — тот, для
|
||||
* кого кадр реально загружен. */
|
||||
if (Char.charid == CHARID_0_KID) kid_frame = cur_frame;
|
||||
else pop_gframe = cur_frame;
|
||||
}
|
||||
|
||||
static uint8_t char_dx_forward(int8_t dx)
|
||||
{
|
||||
if (Kid.direction < 0) dx = (int8_t)-dx;
|
||||
return (uint8_t)(Kid.x + dx);
|
||||
if (Char.direction < 0) dx = (int8_t)-dx;
|
||||
return (uint8_t)(Char.x + dx);
|
||||
}
|
||||
|
||||
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
|
||||
@@ -189,6 +355,11 @@ uint8_t kid_cur_flags(void) { return cur_frame.flags; }
|
||||
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
|
||||
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
|
||||
|
||||
/* То же для АКТИВНОГО персонажа — внутри окна Char (порт
|
||||
* seqtbl_offset_char, seg006). Нужен стражу: его последовательность
|
||||
* ставится, когда Char уже загружен из Guard. */
|
||||
void pop_char_set_seq(uint8_t seq_id) { Char.curr_seq = kid_seq_off[seq_id]; }
|
||||
|
||||
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
|
||||
* Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов
|
||||
* (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце —
|
||||
@@ -198,42 +369,42 @@ void play_seq(void)
|
||||
if (!kdat_ok) return;
|
||||
gfx_w0_map(kdat_page);
|
||||
for (;;) {
|
||||
uint8_t cmd = SEQ(Kid.curr_seq);
|
||||
Kid.curr_seq++;
|
||||
uint8_t cmd = SEQ(Char.curr_seq);
|
||||
Char.curr_seq++;
|
||||
switch (cmd) {
|
||||
case 0xFB: /* DX */
|
||||
Kid.x = char_dx_forward((int8_t)SEQ(Kid.curr_seq));
|
||||
Kid.curr_seq++;
|
||||
Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xFA: /* DY */
|
||||
Kid.y = (uint8_t)(Kid.y + (int8_t)SEQ(Kid.curr_seq));
|
||||
Kid.curr_seq++;
|
||||
Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xFE: /* FLIP */
|
||||
Kid.direction = (int8_t)~Kid.direction;
|
||||
Char.direction = (int8_t)~Char.direction;
|
||||
break;
|
||||
case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
|
||||
Kid.curr_seq += 2;
|
||||
Char.curr_seq += 2;
|
||||
break;
|
||||
case 0xFF: { /* JMP */
|
||||
uint16_t a = (uint16_t)(SEQ(Kid.curr_seq) |
|
||||
((uint16_t)SEQ((uint16_t)(Kid.curr_seq + 1)) << 8));
|
||||
Kid.curr_seq = a;
|
||||
uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
|
||||
((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
|
||||
Char.curr_seq = a;
|
||||
break;
|
||||
}
|
||||
case 0xFD: /* UP */
|
||||
Kid.curr_row--;
|
||||
Char.curr_row--;
|
||||
break;
|
||||
case 0xFC: /* DOWN */
|
||||
Kid.curr_row++;
|
||||
Char.curr_row++;
|
||||
break;
|
||||
case 0xF9: /* ACTION */
|
||||
Kid.action = SEQ(Kid.curr_seq);
|
||||
Kid.curr_seq++;
|
||||
Char.action = SEQ(Char.curr_seq);
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xF8: /* SET_FALL */
|
||||
Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
|
||||
Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
|
||||
Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
|
||||
Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
|
||||
break;
|
||||
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
|
||||
knock = 1;
|
||||
@@ -242,20 +413,20 @@ void play_seq(void)
|
||||
knock = -1;
|
||||
break;
|
||||
case 0xF2: /* SOUND — потребить 1 байт */
|
||||
Kid.curr_seq++;
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xF1: /* END_LEVEL */
|
||||
break;
|
||||
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
|
||||
* (порт play_seq -> proc_get_object, seg006) */
|
||||
if (SEQ(Kid.curr_seq) == 1)
|
||||
if (SEQ(Char.curr_seq) == 1)
|
||||
pop_proc_get_object();
|
||||
Kid.curr_seq++;
|
||||
Char.curr_seq++;
|
||||
break;
|
||||
case 0xF6: /* DIE nop */
|
||||
break;
|
||||
default: /* КАДР */
|
||||
Kid.frame = cmd;
|
||||
Char.frame = cmd;
|
||||
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
|
||||
gfx_w0_unmap();
|
||||
return;
|
||||
@@ -263,6 +434,12 @@ void play_seq(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
|
||||
* loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот
|
||||
* работает с Char и сам по себе не знает, чей это персонаж. */
|
||||
void pop_kid_play(void) { pop_loadkid(); play_seq(); pop_savekid(); }
|
||||
void pop_guard_play(void) { pop_loadshad(); play_seq(); pop_saveshad(); }
|
||||
|
||||
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
|
||||
{
|
||||
Kid.curr_col = col;
|
||||
@@ -272,13 +449,15 @@ void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
|
||||
Kid.y = (uint8_t)pop_y_land[row + 1];
|
||||
Kid.fall_x = Kid.fall_y = 0;
|
||||
Kid.repeat = 0;
|
||||
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
|
||||
Kid.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */
|
||||
Kid.curr_seq = kid_seq_off[seq_id];
|
||||
play_seq(); /* до первого кадра */
|
||||
pop_kid_play(); /* до первого кадра */
|
||||
}
|
||||
|
||||
void kid_tick(void)
|
||||
{
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
}
|
||||
|
||||
/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
|
||||
@@ -308,6 +487,64 @@ static int kid_lx[2], kid_ly[2];
|
||||
static uint16_t kid_lw[2], kid_lh[2];
|
||||
static uint8_t kid_lvalid[2];
|
||||
|
||||
/* ОТДЕЛЬНЫЙ прямоугольник накладных спрайтов (клинок chtab_0, брызги урона).
|
||||
* Раньше он объединялся с прямоугольником персонажа — но объединение сильно
|
||||
* больше суммы двух: клинок уходит вперёд-вверх, и общий габарит выходил
|
||||
* почти вдвое шире и выше самого Kid, а heal стоит ровно по площади.
|
||||
* Поэтому heal делаем двумя вызовами по своим прямоугольникам, а объединение
|
||||
* оставляем ТОЛЬКО для окна fore-клипа (там это 4 сравнения, без рисования —
|
||||
* зато клинок/брызги честно уходят под передние грани тайлов). */
|
||||
static int kid_ox[2], kid_oy[2];
|
||||
static uint16_t kid_ow[2], kid_oh[2];
|
||||
static uint8_t kid_ovalid[2];
|
||||
|
||||
/* Добавить накладной спрайт в overlay-прямоугольник страницы dp. Клинок и
|
||||
* брызги в одном кадре встречаются редко и оба маленькие — их объединение
|
||||
* дешевле третьего слота. */
|
||||
static void kid_overlay_add(uint8_t dp, int x, int y, uint16_t w, uint16_t h)
|
||||
{
|
||||
if (!kid_ovalid[dp]) {
|
||||
kid_ox[dp] = x; kid_oy[dp] = y; kid_ow[dp] = w; kid_oh[dp] = h;
|
||||
kid_ovalid[dp] = 1;
|
||||
return;
|
||||
}
|
||||
{
|
||||
int x0 = kid_ox[dp], y0 = kid_oy[dp];
|
||||
int x1 = x0 + (int)kid_ow[dp], y1 = y0 + (int)kid_oh[dp];
|
||||
if (x < x0) x0 = x;
|
||||
if (y < y0) y0 = y;
|
||||
if (x + (int)w > x1) x1 = x + (int)w;
|
||||
if (y + (int)h > y1) y1 = y + (int)h;
|
||||
kid_ox[dp] = x0; kid_oy[dp] = y0;
|
||||
kid_ow[dp] = (uint16_t)(x1 - x0); kid_oh[dp] = (uint16_t)(y1 - y0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Окно fore-клипа = объединение «персонаж + накладные». Считается по
|
||||
* прямоугольникам, ничего не рисует. */
|
||||
static void kid_fore_clip(uint8_t dp)
|
||||
{
|
||||
int x0, y0, x1, y1;
|
||||
if (!kid_lvalid[dp] && !kid_ovalid[dp]) { pop_fore_set_clip(0, 0, 0, 0); return; }
|
||||
if (!kid_ovalid[dp]) {
|
||||
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
|
||||
(int)kid_lw[dp], (int)kid_lh[dp]);
|
||||
return;
|
||||
}
|
||||
if (!kid_lvalid[dp]) {
|
||||
pop_fore_set_clip(kid_ox[dp], kid_oy[dp] + POP_YOFF,
|
||||
(int)kid_ow[dp], (int)kid_oh[dp]);
|
||||
return;
|
||||
}
|
||||
x0 = kid_lx[dp] < kid_ox[dp] ? kid_lx[dp] : kid_ox[dp];
|
||||
y0 = kid_ly[dp] < kid_oy[dp] ? kid_ly[dp] : kid_oy[dp];
|
||||
x1 = kid_lx[dp] + (int)kid_lw[dp];
|
||||
y1 = kid_ly[dp] + (int)kid_lh[dp];
|
||||
if (kid_ox[dp] + (int)kid_ow[dp] > x1) x1 = kid_ox[dp] + (int)kid_ow[dp];
|
||||
if (kid_oy[dp] + (int)kid_oh[dp] > y1) y1 = kid_oy[dp] + (int)kid_oh[dp];
|
||||
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
|
||||
}
|
||||
|
||||
/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
|
||||
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
|
||||
static int kid_fp_x, kid_fp_y;
|
||||
@@ -351,23 +588,18 @@ void kid_draw_splash(void)
|
||||
int top = obj_y - (int)h + 1;
|
||||
if (flip) bx -= (int)w;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
|
||||
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
|
||||
else
|
||||
gfx_blit_cols(bx, top + POP_YOFF, img, flip);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
|
||||
/* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе
|
||||
* брызги останутся на экране после кадра. */
|
||||
/* Брызги — накладной спрайт: свой прямоугольник heal (см.
|
||||
* kid_overlay_add), окно fore пересчитываем объединением. */
|
||||
{
|
||||
uint8_t dp = gfx_get_draw_page();
|
||||
if (kid_lvalid[dp]) {
|
||||
int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx;
|
||||
int y0 = kid_ly[dp] < top ? kid_ly[dp] : top;
|
||||
int x1 = kid_lx[dp] + (int)kid_lw[dp];
|
||||
int y1 = kid_ly[dp] + (int)kid_lh[dp];
|
||||
if (bx + (int)w > x1) x1 = bx + (int)w;
|
||||
if (top + (int)h > y1) y1 = top + (int)h;
|
||||
kid_lx[dp] = x0; kid_ly[dp] = y0;
|
||||
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
|
||||
}
|
||||
kid_overlay_add(dp, bx, top, w, h);
|
||||
kid_fore_clip(dp);
|
||||
}
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
@@ -379,17 +611,23 @@ void kid_heal(void)
|
||||
uint8_t p = gfx_get_draw_page();
|
||||
if (kid_lvalid[p])
|
||||
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */
|
||||
kid_lvalid[p] = 0;
|
||||
if (kid_ovalid[p])
|
||||
gfx_heal(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]);
|
||||
kid_lvalid[p] = kid_ovalid[p] = 0;
|
||||
}
|
||||
|
||||
void kid_draw(void)
|
||||
{
|
||||
kframe fr = cur_frame; /* load_frame уже положил кадр (play_seq) */
|
||||
kframe fr = kid_frame; /* кадр Kid, сохранённый savekid */
|
||||
int fwd, obj_x, obj_y, top;
|
||||
uint8_t page, idx;
|
||||
const uint8_t *img;
|
||||
uint16_t w, h;
|
||||
|
||||
/* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
|
||||
* кадра / страница не загружена) перекрывать нечего, и fore-проход
|
||||
* не должен трогать экран. */
|
||||
pop_fore_set_clip(0, 0, 0, 0);
|
||||
if (fr.image == 255)
|
||||
return; /* пустой кадр */
|
||||
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
|
||||
@@ -434,8 +672,14 @@ void kid_draw(void)
|
||||
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
|
||||
}
|
||||
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
|
||||
if (skip < (int)h) /* +YOFF: центрирование */
|
||||
if (skip < (int)h) { /* +YOFF: центрирование */
|
||||
if (pop_onscreen_cols(bx, top + skip + POP_YOFF, w,
|
||||
(uint16_t)((int)h - skip)))
|
||||
gfx_blit_cols_part_noclip(bx, top + skip + POP_YOFF, img, flip,
|
||||
(uint8_t)skip, 0);
|
||||
else
|
||||
gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0);
|
||||
}
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
|
||||
* узко, только по прямоугольнику Кида и только при выходе. */
|
||||
@@ -448,44 +692,16 @@ void kid_draw(void)
|
||||
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
|
||||
}
|
||||
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
|
||||
* Kid. Прямоугольник heal расширяем объединением — иначе клинок, выйдя
|
||||
* за габарит Kid, оставит след. */
|
||||
if (sword_ok && Kid.frame >= 229 && Kid.frame < 238) {
|
||||
uint8_t sf = (uint8_t)(fr.sword & 0x3F);
|
||||
if (sf >= SWORD_SF_LO && sf <= SWORD_SF_HI) {
|
||||
uint8_t si = (uint8_t)(sf - SWORD_SF_LO);
|
||||
const uint8_t *simg = (const uint8_t *)atlas_image(&swordp, si);
|
||||
uint16_t sw, sh;
|
||||
gfx_w0_map(swordp.page);
|
||||
sw = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
|
||||
sh = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
|
||||
if (sw && sh) {
|
||||
uint8_t sflip = (Kid.direction >= 0);
|
||||
int sbx = obj_x + ((Kid.direction < 0) ? -(int)SWORD_DX[si]
|
||||
: (int)SWORD_DX[si]);
|
||||
int stop = obj_y + (int)SWORD_DY[si] - (int)sh + 1;
|
||||
uint8_t dp = gfx_get_draw_page() & 1;
|
||||
if (sflip) sbx -= (int)sw;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
gfx_blit_cols_part(sbx, stop + POP_YOFF, simg, sflip, 0, 0);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
if (!kid_lvalid[dp]) {
|
||||
kid_lx[dp] = sbx; kid_ly[dp] = stop;
|
||||
kid_lw[dp] = sw; kid_lh[dp] = sh;
|
||||
kid_lvalid[dp] = 1;
|
||||
} else {
|
||||
int x0 = kid_lx[dp], y0 = kid_ly[dp];
|
||||
int x1 = x0 + (int)kid_lw[dp], y1 = y0 + (int)kid_lh[dp];
|
||||
if (sbx < x0) x0 = sbx;
|
||||
if (stop < y0) y0 = stop;
|
||||
if (sbx + (int)sw > x1) x1 = sbx + (int)sw;
|
||||
if (stop + (int)sh > y1) y1 = stop + (int)sh;
|
||||
kid_lx[dp] = x0; kid_ly[dp] = y0;
|
||||
kid_lw[dp] = (uint16_t)(x1 - x0);
|
||||
kid_lh[dp] = (uint16_t)(y1 - y0);
|
||||
}
|
||||
}
|
||||
}
|
||||
* Kid — со СВОИМ прямоугольником heal (kid_overlay_add). */
|
||||
{
|
||||
int sx, sy; uint16_t sw, sh;
|
||||
if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh))
|
||||
kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh);
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
/* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char):
|
||||
* fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми
|
||||
* тайлами — см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего:
|
||||
* нулевое окно = fore-проход не трогает экран, что и правильно. */
|
||||
kid_fore_clip(gfx_get_draw_page() & 1);
|
||||
}
|
||||
|
||||
@@ -11,20 +11,11 @@
|
||||
#define POP_KID_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "pop_char.h"
|
||||
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
|
||||
|
||||
/* Состояние персонажа (подмножество char_type, types.h). */
|
||||
typedef struct {
|
||||
uint8_t frame;
|
||||
uint8_t x, y;
|
||||
int8_t direction; /* -1 влево, 0 вправо */
|
||||
int8_t curr_col, curr_row;
|
||||
uint8_t action;
|
||||
int8_t fall_x, fall_y;
|
||||
uint8_t repeat;
|
||||
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
|
||||
} kid_t;
|
||||
|
||||
extern kid_t Kid;
|
||||
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
|
||||
extern pop_char_t Kid;
|
||||
|
||||
/* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN
|
||||
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
|
||||
@@ -53,6 +44,15 @@ void pop_kid_data_free(void);
|
||||
int pop_sword_load(void);
|
||||
void pop_kid_free(void);
|
||||
|
||||
/* Нарисовать клинок chtab_0 поверх персонажа (порт add_sword_to_objtable,
|
||||
* seg006:1798) — общая точка для Кида и стража: меч в оригинале ОДИН на
|
||||
* всех. obj_x/obj_y — та же опорная точка, что у спрайта персонажа.
|
||||
* Возвращает 1 и прямоугольник клинка (для heal), 0 — клинка нет.
|
||||
* Страницу W0 оставляет замапленной: вызывающий делает gfx_w0_unmap. */
|
||||
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
|
||||
int obj_x, int obj_y,
|
||||
int *rx, int *ry, uint16_t *rw, uint16_t *rh);
|
||||
|
||||
/* Инициализировать Kid: последовательность seq_id, клетка col/row,
|
||||
* направление dir (-1/0). Ставит x/y по x_bump/y_land. */
|
||||
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir);
|
||||
@@ -63,7 +63,34 @@ void kid_tick(void);
|
||||
|
||||
/* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же
|
||||
* physics-шаг, как оригинал). */
|
||||
void play_seq(void);
|
||||
void play_seq(void); /* работает с Char — см. pop_kid_play ниже */
|
||||
|
||||
/* Активный персонаж (порт Char, seg006) и его загрузка/выгрузка.
|
||||
* Интерпретатор последовательностей общий для Kid и стража: у них общая
|
||||
* seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */
|
||||
extern pop_char_t Char;
|
||||
/* Соперник активного персонажа (порт Opp): ставится вместе с Char через
|
||||
* pop_loadshad_and_opp. Читают ИИ стража и боёвка (char_opp_dist). */
|
||||
extern pop_char_t Opp;
|
||||
void pop_loadkid(void);
|
||||
void pop_savekid(void);
|
||||
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
|
||||
void pop_loadshad(void);
|
||||
void pop_saveshad(void);
|
||||
void pop_loadshad_and_opp(void); /* Char = Guard, Opp = Kid (seg006:841) */
|
||||
void pop_saveshad_and_opp(void); /* Guard = Char, Kid = Opp */
|
||||
void pop_loadkid_and_opp(void); /* Char = Kid, Opp = Guard */
|
||||
void pop_savekid_and_opp(void); /* Kid = Char, Guard = Opp */
|
||||
|
||||
/* Дистанция до соперника в системе Char: >0 впереди, <0 за спиной,
|
||||
* 999 — разные комнаты (порт char_opp_dist, seg006:2135). */
|
||||
int16_t pop_char_opp_dist(void);
|
||||
|
||||
/* Прокрутить последовательность персонажа целиком (load + play_seq +
|
||||
* save). Звать вместо голого play_seq. */
|
||||
void pop_char_set_seq(uint8_t seq_id); /* seq активного персонажа */
|
||||
void pop_kid_play(void);
|
||||
void pop_guard_play(void);
|
||||
|
||||
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
|
||||
* таблицы во втором TU). */
|
||||
@@ -90,6 +117,10 @@ void kid_draw(void);
|
||||
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
|
||||
void kid_draw_splash(void);
|
||||
|
||||
/* Блит спрайта chtab_2 по id, левым верхним углом в (x,y) — для полосы HP
|
||||
* (id 216 полное деление / 217 пустое). */
|
||||
void pop_kid_img_blit(uint8_t image, int x, int y);
|
||||
|
||||
/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid —
|
||||
* вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */
|
||||
void kid_heal(void);
|
||||
|
||||
@@ -27,12 +27,18 @@ extern void _gfx_w0_isr(void);
|
||||
#define BP_GTILE 2119u
|
||||
#define BP_GDIR 2143u
|
||||
#define BP_GX 2167u
|
||||
#define BP_GSEQLO 2191u
|
||||
#define BP_GSKILL 2215u
|
||||
#define BP_GSEQHI 2239u
|
||||
#define BP_GCOLOR 2263u
|
||||
#define BP_START 2112u
|
||||
#define ROOM_TILES 30u
|
||||
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
|
||||
|
||||
/* fwd: инициализация живой копии стражей (тело ниже). */
|
||||
static void gstate_init(void);
|
||||
|
||||
|
||||
static uint8_t lvl_blk, lvl_page, lvl_ok;
|
||||
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
|
||||
static uint8_t dl1[256], dl2[256];
|
||||
@@ -74,6 +80,7 @@ int pop_level_load(const char *path)
|
||||
|
||||
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
||||
lvl_ok = 1;
|
||||
gstate_init(); /* живая копия состояния стражей — из уровня */
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -243,22 +250,95 @@ static uint8_t start_byte(uint16_t off)
|
||||
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
|
||||
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
|
||||
* колонки (pos_guards), поэтому guards_x из файла не читаем. */
|
||||
/* ---- ЖИВОЕ состояние стражей (порт level.guards_*) ------------------ *
|
||||
* В оригинале массивы стражей — часть структуры уровня в ОЗУ, и движок их
|
||||
* ПЕРЕПИСЫВАЕТ: leave_guard при выходе из комнаты кладёт туда текущие
|
||||
* позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq. Благодаря
|
||||
* этому, вернувшись, находишь труп там же, а не воскресшего стража.
|
||||
* У нас уровень лежит в EMM-странице только на чтение, поэтому держим
|
||||
* мутабельную копию в W2: 6 байт на комнату. */
|
||||
#define GS_TILE 0
|
||||
#define GS_DIR 1
|
||||
#define GS_X 2
|
||||
#define GS_SKILL 3
|
||||
#define GS_SEQL 4
|
||||
#define GS_SEQH 5
|
||||
static uint8_t gstate[24 * 6];
|
||||
|
||||
static void gstate_init(void)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)LVL_DATA_OFF;
|
||||
uint8_t r;
|
||||
gfx_w0_map(lvl_page);
|
||||
for (r = 0; r < 24; r++) {
|
||||
uint8_t *g = gstate + (uint16_t)r * 6;
|
||||
g[GS_TILE] = p[BP_GTILE + r];
|
||||
g[GS_DIR] = p[BP_GDIR + r];
|
||||
g[GS_X] = p[BP_GX + r];
|
||||
g[GS_SKILL] = p[BP_GSKILL + r];
|
||||
/* guards_seq_lo/hi в ФАЙЛЕ не используются — там 0xFF во всех
|
||||
* комнатах (оригинал чистит их в reset_level_unused_fields).
|
||||
* Осмысленное значение туда кладёт только leave_guard, поэтому
|
||||
* стартуем нулями: 0 = «поднимать стандартной стойкой».
|
||||
* Без этого в интерпретатор уходил curr_seq = 0xFFFF и он вис. */
|
||||
g[GS_SEQL] = 0;
|
||||
g[GS_SEQH] = 0;
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты. seq
|
||||
* сохраняем ТОЛЬКО у мёртвого — у живого seq_hi = 0 означает «поднять
|
||||
* стандартной стойкой», как в оригинале. */
|
||||
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
|
||||
uint8_t skill, uint16_t seq, uint8_t dead)
|
||||
{
|
||||
uint8_t *g;
|
||||
if (room < 1 || room > 24) return;
|
||||
g = gstate + (uint16_t)(room - 1) * 6;
|
||||
g[GS_TILE] = tile;
|
||||
g[GS_DIR] = (uint8_t)dir;
|
||||
g[GS_X] = x;
|
||||
g[GS_SKILL] = skill;
|
||||
if (dead) { g[GS_SEQL] = (uint8_t)seq; g[GS_SEQH] = (uint8_t)(seq >> 8); }
|
||||
else { g[GS_SEQH] = 0; }
|
||||
}
|
||||
|
||||
uint16_t pop_guard_state_seq(uint8_t room)
|
||||
{
|
||||
const uint8_t *g;
|
||||
if (room < 1 || room > 24) return 0;
|
||||
g = gstate + (uint16_t)(room - 1) * 6;
|
||||
if (g[GS_SEQH] == 0) return 0; /* живой — стандартная стойка */
|
||||
return (uint16_t)(g[GS_SEQL] | ((uint16_t)g[GS_SEQH] << 8));
|
||||
}
|
||||
|
||||
/* Запомненная X стража комнаты (для трупа: он лежит там, где лёг, а не в
|
||||
* центре тайла). */
|
||||
uint8_t pop_guard_state_x(uint8_t room)
|
||||
{
|
||||
if (room < 1 || room > 24) return 0;
|
||||
return gstate[(uint16_t)(room - 1) * 6 + GS_X];
|
||||
}
|
||||
|
||||
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
|
||||
uint8_t *color, uint8_t *skill)
|
||||
{
|
||||
const uint8_t *p;
|
||||
const uint8_t *g;
|
||||
uint8_t t;
|
||||
if (!lvl_ok || room < 1 || room > 24) return 0;
|
||||
gfx_w0_map(lvl_page);
|
||||
p = (const uint8_t *)LVL_DATA_OFF;
|
||||
t = p[BP_GTILE + room - 1];
|
||||
/* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
|
||||
* pop_guard_state_save); цвет статичен, его читаем со страницы. */
|
||||
g = gstate + (uint16_t)(room - 1) * 6;
|
||||
t = g[GS_TILE];
|
||||
if (t < 30) {
|
||||
*tile = t;
|
||||
*dir = (int8_t)p[BP_GDIR + room - 1];
|
||||
*color = (uint8_t)(p[BP_GCOLOR + room - 1] & 0x0F);
|
||||
*skill = p[BP_GSKILL + room - 1];
|
||||
}
|
||||
*dir = (int8_t)g[GS_DIR];
|
||||
*skill = g[GS_SKILL];
|
||||
gfx_w0_map(lvl_page);
|
||||
*color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
return (uint8_t)(t < 30);
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,18 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col);
|
||||
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
|
||||
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
|
||||
* Порт enter_guard (seg002:0112). */
|
||||
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты в ЖИВОЙ
|
||||
* копии массивов уровня. dead != 0 — сохранить и последовательность, тогда
|
||||
* при возврате в комнату страж поднимется трупом там же, где лёг.
|
||||
* tile == 30 (или больше) — «стража в комнате больше нет». */
|
||||
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
|
||||
uint8_t skill, uint16_t seq, uint8_t dead);
|
||||
|
||||
/* Последовательность запомненного стража (0 — поднимать стандартной
|
||||
* стойкой, как у живого). */
|
||||
uint16_t pop_guard_state_seq(uint8_t room);
|
||||
uint8_t pop_guard_state_x(uint8_t room);
|
||||
|
||||
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
|
||||
uint8_t *color, uint8_t *skill);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
* есть то, что понадобится стражам, чиста от графики. Разделение mob на
|
||||
* логику и отрисовку — задел следующей фазы. */
|
||||
#include "pop_bg.h"
|
||||
#include "pop_state.h"
|
||||
#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */
|
||||
|
||||
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
|
||||
@@ -113,6 +114,12 @@ uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавше
|
||||
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
|
||||
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
|
||||
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
|
||||
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
|
||||
/* Дельта HP Кида за кадр (порт hitp_delta). Её ставит СВОРД-путь боёвки
|
||||
* (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале.
|
||||
* Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и
|
||||
* проверен до боёвки, и переводить его на дельту без нужды рискованно. */
|
||||
int8_t hitp_delta;
|
||||
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
|
||||
static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */
|
||||
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
|
||||
@@ -149,13 +156,13 @@ uint8_t pop_ceil_fell; /* 0 = нет; иначе col+1 провал
|
||||
|
||||
uint8_t pop_debris_at; /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */
|
||||
|
||||
void pop_map_set_above(uint8_t *above_row2_fg) { g_above = above_row2_fg; }
|
||||
void pop_map_set_above(uint8_t *above_row2_fg) __banked { g_above = above_row2_fg; }
|
||||
|
||||
void pop_map_set(uint8_t *fg) { g_fg = fg; }
|
||||
void pop_map_set(uint8_t *fg) __banked { g_fg = fg; }
|
||||
|
||||
void pop_map_set_room(uint8_t room) { g_room = room; }
|
||||
void pop_map_set_room(uint8_t room) __banked { g_room = room; }
|
||||
|
||||
void pop_kid_hp_reset(void) { hitp_curr = 3; pop_kid_dead = 0; }
|
||||
void pop_kid_hp_reset(void) __banked { hitp_curr = hitp_max = 3; hitp_delta = 0; pop_kid_dead = 0; }
|
||||
|
||||
/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
|
||||
static uint8_t take_hp(uint8_t count)
|
||||
@@ -166,7 +173,7 @@ static uint8_t take_hp(uint8_t count)
|
||||
}
|
||||
|
||||
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
|
||||
const uint8_t *lcol_fg, const uint8_t *rcol_fg)
|
||||
const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked
|
||||
{
|
||||
g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
|
||||
if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; }
|
||||
@@ -201,6 +208,16 @@ static uint8_t get_tile(int8_t col, int8_t row)
|
||||
return g_curr_tile;
|
||||
}
|
||||
|
||||
/* Тайл ТЕКУЩЕЙ комнаты по клетке — наружу (порт get_tile(room,col,row) для
|
||||
* случая room == текущая). Нужен лучу видимости стража
|
||||
* (check_can_guard_see_kid, seg003:688): тот идёт по ряду между Кидом и
|
||||
* стражем, а оба по условию в одной (= отрисованной) комнате. Колонки за
|
||||
* пределами 0..9 get_tile разрешает через швы — здесь не используются. */
|
||||
uint8_t pop_tile_at(int8_t col, int8_t row) __banked
|
||||
{
|
||||
return get_tile(col, row);
|
||||
}
|
||||
|
||||
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
|
||||
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
|
||||
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
|
||||
@@ -441,7 +458,7 @@ static void land(void)
|
||||
if (deadly || take_hp(1)) { /* 3+ этажа или последнее HP */
|
||||
take_hp(100);
|
||||
kid_set_seq(SEQ_22_CRUSHED);
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
determine_col();
|
||||
pop_kid_dead = 1;
|
||||
return;
|
||||
@@ -450,7 +467,7 @@ static void land(void)
|
||||
}
|
||||
Kid.fall_x = Kid.fall_y = 0;
|
||||
kid_set_seq(seq);
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
determine_col();
|
||||
}
|
||||
|
||||
@@ -464,7 +481,7 @@ static void start_fall(void)
|
||||
else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP;
|
||||
else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */
|
||||
kid_set_seq(seq);
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
determine_col();
|
||||
/* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2).
|
||||
* Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose)
|
||||
@@ -530,7 +547,7 @@ static void check_grab(void)
|
||||
Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
|
||||
Kid.fall_y = 0;
|
||||
kid_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
determine_col();
|
||||
grab_timer = 12; /* (TODO: sound grab) */
|
||||
}
|
||||
@@ -587,7 +604,7 @@ static int edge_classify(int d)
|
||||
return d;
|
||||
}
|
||||
|
||||
int pop_edge_distance(void)
|
||||
int pop_edge_distance(void) __banked
|
||||
{
|
||||
uint8_t t; int d;
|
||||
determine_col();
|
||||
@@ -629,7 +646,7 @@ int pop_edge_distance(void)
|
||||
return distance_to_edge_weight();
|
||||
}
|
||||
|
||||
uint8_t pop_edge_type(void) { return g_edge_type; }
|
||||
uint8_t pop_edge_type(void) __banked { return g_edge_type; }
|
||||
|
||||
/* ---- Прыжок вверх с зацепом (seg005 check_jump_up и производные) --- *
|
||||
* pop_jump_up_seq выбирает seq для ↑ в стойке: чистый прыжок (seq_28 в
|
||||
@@ -682,7 +699,7 @@ static uint8_t jump_up_or_grab(void)
|
||||
return grab_up_with_floor_behind();
|
||||
}
|
||||
|
||||
uint8_t pop_jump_up_seq(void)
|
||||
uint8_t pop_jump_up_seq(void) __banked
|
||||
{
|
||||
uint8_t through = get_tile_above_char();
|
||||
get_tile_front_above_char(); /* g_curr_tile = спереди-сверху */
|
||||
@@ -697,7 +714,7 @@ uint8_t pop_jump_up_seq(void)
|
||||
|
||||
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
|
||||
* позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */
|
||||
int pop_wall_ahead(void)
|
||||
int pop_wall_ahead(void) __banked
|
||||
{
|
||||
int d = pop_edge_distance();
|
||||
return (g_edge_type == EDGE_WALL && d <= 2);
|
||||
@@ -708,7 +725,7 @@ int pop_wall_ahead(void)
|
||||
* тайловые предикаты + выбор seq (+ выравнивание Kid.x). */
|
||||
|
||||
/* grab_timer истёк — можно подтягиваться? */
|
||||
uint8_t pop_hang_grab_ready(void) { return grab_timer == 0; }
|
||||
uint8_t pop_hang_grab_ready(void) __banked { return grab_timer == 0; }
|
||||
|
||||
/* can_climb_up (seg005:09DF): тайл над персонажем — обычный подъём (seq_10)
|
||||
* либо подъём к ЗАКРЫТОЙ калитке/зеркалу/чомперу (seq_73 = «влез и съехал
|
||||
@@ -716,7 +733,7 @@ uint8_t pop_hang_grab_ready(void) { return grab_timer == 0; }
|
||||
* curr_tile2 == tiles_4_gate && direction != right && modif >> 2 < 6
|
||||
* — под поднятой решёткой (modif >> 2 >= 6) подъём обычный. Без проверки
|
||||
* модификатора Kid срывался и с открытых ворот. */
|
||||
uint8_t pop_hang_climb_up_seq(void)
|
||||
uint8_t pop_hang_climb_up_seq(void) __banked
|
||||
{
|
||||
uint8_t t = get_tile_above_char();
|
||||
if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Kid.direction == 0) ||
|
||||
@@ -728,7 +745,7 @@ uint8_t pop_hang_climb_up_seq(void)
|
||||
|
||||
/* Shift в висе: 1 = висит у стены/двери (caller играет seq_25); -1 =
|
||||
* сорваться (hang_fall); 0 = сверху пол, остаётся висеть (ничего). */
|
||||
int8_t pop_hang_wall(void)
|
||||
int8_t pop_hang_wall(void) __banked
|
||||
{
|
||||
uint8_t t = get_tile_at_char();
|
||||
if (Kid.action != ACT_HANGSTRAIGHT &&
|
||||
@@ -743,7 +760,7 @@ int8_t pop_hang_wall(void)
|
||||
/* hang_fall (seg005:0A46): сорваться. Нет пола ни сзади, ни под
|
||||
* персонажем — seq_23 (отпустить и падать); иначе прижать к стене (-7)
|
||||
* и seq_11 (встать, конец спуска). Меняет Kid.x, возвращает seq. */
|
||||
uint8_t pop_hang_fall_seq(void)
|
||||
uint8_t pop_hang_fall_seq(void) __banked
|
||||
{
|
||||
uint8_t behind = get_tile_behind_char();
|
||||
uint8_t at;
|
||||
@@ -760,7 +777,7 @@ uint8_t pop_hang_fall_seq(void)
|
||||
* либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 =
|
||||
* приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки
|
||||
* пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */
|
||||
uint8_t pop_down_action(void)
|
||||
uint8_t pop_down_action(void) __banked
|
||||
{
|
||||
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) {
|
||||
Kid.x = (uint8_t)char_dx_forward(5); /* у передней кромки — отступ */
|
||||
@@ -792,6 +809,9 @@ uint8_t pop_down_action(void)
|
||||
uint8_t pop_have_sword;
|
||||
uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */
|
||||
uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */
|
||||
/* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье). В оригинале это
|
||||
* flash_color; нам хватает двух значений, которые реально встречаются. */
|
||||
uint8_t pop_flash_red;
|
||||
static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */
|
||||
|
||||
/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
|
||||
@@ -805,7 +825,7 @@ static void do_pickup(int8_t obj_type, uint8_t tilepos)
|
||||
|
||||
/* proc_get_object (seg006:16CB): эффект поднятого предмета. Зовётся из
|
||||
* play_seq по опкоду SEQ_GET_ITEM с аргументом 1 (pop_kid). */
|
||||
void pop_proc_get_object(void)
|
||||
void pop_proc_get_object(void) __banked
|
||||
{
|
||||
if (pickup_obj_type == -1) {
|
||||
pop_have_sword = 1; /* в оригинале have_sword = -1 */
|
||||
@@ -815,13 +835,35 @@ void pop_proc_get_object(void)
|
||||
* чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */
|
||||
pop_flash_time = 8;
|
||||
}
|
||||
pickup_obj_type = 0; /* зелья (типы 1..6) не портированы */
|
||||
else {
|
||||
/* proc_get_object (seg006:1927), ветка зелий. Типы 3/4/6
|
||||
* (перо/переворот/открыть все ворота) — свойства поздних уровней,
|
||||
* их портируем вместе с этими уровнями. */
|
||||
switch (pickup_obj_type) {
|
||||
case 1: /* здоровье: +1 HP, красная вспышка */
|
||||
if (hitp_curr != hitp_max) {
|
||||
hitp_delta = 1;
|
||||
pop_flash_red = 1;
|
||||
pop_flash_time = 2;
|
||||
}
|
||||
break;
|
||||
case 5: /* «злое» зелье: −1 HP */
|
||||
hitp_delta = -1;
|
||||
pop_flash_red = 1;
|
||||
pop_flash_time = 2;
|
||||
break;
|
||||
default: /* 2 жизнь / 3 перо / 4 переворот /
|
||||
* 6 открыть — TODO с их уровнями */
|
||||
break;
|
||||
}
|
||||
}
|
||||
pickup_obj_type = 0;
|
||||
}
|
||||
|
||||
/* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет
|
||||
* (управление идёт обычной веткой); 1 — присесть (caller зовёт crouch());
|
||||
* 2 — поднять меч (caller ставит seq_91). */
|
||||
uint8_t pop_get_item_action(void)
|
||||
uint8_t pop_get_item_action(void) __banked
|
||||
{
|
||||
uint8_t t;
|
||||
int8_t icol;
|
||||
@@ -844,9 +886,17 @@ uint8_t pop_get_item_action(void)
|
||||
Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2));
|
||||
return 1;
|
||||
}
|
||||
if (t != TILE_SWORD) return 0; /* питьё зелий не портировано */
|
||||
do_pickup(-1, (uint8_t)(Kid.curr_row * 10 + icol));
|
||||
return 2;
|
||||
{
|
||||
uint8_t tp = (uint8_t)(Kid.curr_row * 10 + icol);
|
||||
if (t == TILE_SWORD) {
|
||||
do_pickup(-1, tp);
|
||||
return 2; /* поднять меч (seq_91) */
|
||||
}
|
||||
/* Зелье: тип лежит в СТАРШИХ битах модификатора (get_item,
|
||||
* seg005: do_pickup(curr_room_modif[curr_tilepos] >> 3)). */
|
||||
do_pickup((int8_t)(pop_trob_modif(g_room)[tp] >> 3), tp);
|
||||
return 3; /* пить (seq_78) */
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Диспетчер ----------------------------------------------------- */
|
||||
@@ -885,7 +935,7 @@ static void bumped_fall(void)
|
||||
Kid.fall_x = 0;
|
||||
} else {
|
||||
kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,7 +969,7 @@ static void bumped_floor(void)
|
||||
kid_set_seq(SEQ_46_HARDBUMP);
|
||||
else
|
||||
kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
}
|
||||
|
||||
/* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять
|
||||
@@ -1113,7 +1163,7 @@ static void do_knock(int tile_row)
|
||||
/* check_knock (seg003:0706): по флагу knock (ставит play_seq на seqtbl-
|
||||
* командах KNOCK_UP/DOWN) тряхнуть ряд. KNOCK_DOWN (knock=-1, приземление) →
|
||||
* ряд curr_row; KNOCK_UP (knock=+1) → ряд curr_row-1. */
|
||||
void pop_check_knock(void)
|
||||
void pop_check_knock(void) __banked
|
||||
{
|
||||
if (knock) {
|
||||
do_knock(Kid.curr_row - (knock > 0));
|
||||
@@ -1124,7 +1174,7 @@ void pop_check_knock(void)
|
||||
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
|
||||
* индексируются позицией тайла и не должны утечь из старой комнаты в новую.
|
||||
* Плюс останавливает падающий кусок (pop_bg). */
|
||||
void pop_loose_reset(void)
|
||||
void pop_loose_reset(void) __banked
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 30; i++) {
|
||||
@@ -1156,14 +1206,14 @@ static void fell_on_your_head(void)
|
||||
pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */
|
||||
if (take_hp(1)) {
|
||||
kid_set_seq(SEQ_22_CRUSHED);
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
determine_col();
|
||||
pop_kid_dead = 1;
|
||||
} else if (frame != FRAME_109_CROUCH) {
|
||||
if (get_tile_behind_char() == TILE_EMPTY)
|
||||
Kid.x = (uint8_t)char_dx_forward(-2);
|
||||
kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID);
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
determine_col();
|
||||
}
|
||||
}
|
||||
@@ -1183,7 +1233,7 @@ static void check_loose_fall_on_kid(void)
|
||||
|
||||
/* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ
|
||||
* (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */
|
||||
void pop_loose_tick(void)
|
||||
void pop_loose_tick(void) __banked
|
||||
{
|
||||
int pos;
|
||||
/* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80
|
||||
@@ -1280,7 +1330,7 @@ void pop_loose_tick(void)
|
||||
* check_spike_below колонки берутся get_tile_div_mod БЕЗ −7.
|
||||
*
|
||||
* Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
|
||||
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h)
|
||||
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
|
||||
{
|
||||
static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
|
||||
int wh = ((int)w + 1) >> 1; /* char_width_half */
|
||||
@@ -1429,7 +1479,7 @@ static void spiked(uint8_t tilepos)
|
||||
Kid.fall_y = 0;
|
||||
take_hp(100); /* (TODO: sound_48_spiked) */
|
||||
kid_set_seq(SEQ_51_SPIKED);
|
||||
play_seq();
|
||||
pop_kid_play();
|
||||
determine_col();
|
||||
pop_kid_dead = 1;
|
||||
}
|
||||
@@ -1481,7 +1531,7 @@ static void check_spike_below(void)
|
||||
}
|
||||
}
|
||||
|
||||
void pop_phys_tick(void)
|
||||
void pop_phys_tick(void) __banked
|
||||
{
|
||||
if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */
|
||||
if (grab_timer) grab_timer--;
|
||||
|
||||
@@ -17,25 +17,32 @@
|
||||
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
|
||||
* МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию,
|
||||
* что и в pop_room_draw, чтобы отрисовка видела изменения. */
|
||||
void pop_map_set(uint8_t *fg);
|
||||
void pop_map_set(uint8_t *fg) __banked;
|
||||
|
||||
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии:
|
||||
* lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2)
|
||||
* в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая
|
||||
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */
|
||||
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
|
||||
const uint8_t *lcol_fg, const uint8_t *rcol_fg);
|
||||
const uint8_t *lcol_fg, const uint8_t *rcol_fg) __banked;
|
||||
|
||||
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
|
||||
* Звать в enter_room. */
|
||||
void pop_map_set_room(uint8_t room);
|
||||
void pop_map_set_room(uint8_t room) __banked;
|
||||
|
||||
/* Тайл-код текущей комнаты по клетке (col 0..9, row 0..2) — порт
|
||||
* get_tile(room,col,row) для текущей комнаты. Нужен лучу видимости стража
|
||||
* (guards.c): он идёт по ряду между Кидом и стражем. */
|
||||
uint8_t pop_tile_at(int8_t col, int8_t row) __banked;
|
||||
|
||||
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
|
||||
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
|
||||
* респавн). */
|
||||
extern uint8_t pop_kid_dead;
|
||||
extern uint8_t hitp_curr;
|
||||
void pop_kid_hp_reset(void);
|
||||
extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
|
||||
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
|
||||
void pop_kid_hp_reset(void) __banked;
|
||||
|
||||
/* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
|
||||
* pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или
|
||||
@@ -46,8 +53,10 @@ extern uint8_t pop_item_taken;
|
||||
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
|
||||
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
|
||||
extern uint8_t pop_flash_time;
|
||||
uint8_t pop_get_item_action(void); /* 0 нет / 1 присесть / 2 поднять меч */
|
||||
void pop_proc_get_object(void); /* эффект — из play_seq (опкод GET_ITEM) */
|
||||
/* Цвет вспышки: 0 = ярко-жёлтая (подобран меч), 1 = красная (зелье). */
|
||||
extern uint8_t pop_flash_red;
|
||||
uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч / 3 пить */
|
||||
void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */
|
||||
|
||||
/* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
|
||||
* pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189).
|
||||
@@ -66,8 +75,8 @@ extern uint8_t pop_leave_timer;
|
||||
* на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back),
|
||||
* рядом с kid-отрисовкой. */
|
||||
extern uint8_t pop_loose_modif[30];
|
||||
void pop_loose_tick(void);
|
||||
void pop_loose_reset(void); /* сброс loose-состояния при смене комнаты */
|
||||
void pop_loose_tick(void) __banked;
|
||||
void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */
|
||||
|
||||
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
|
||||
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
|
||||
@@ -81,7 +90,7 @@ extern uint8_t pop_loose_fell;
|
||||
* pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности
|
||||
* между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты,
|
||||
* pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */
|
||||
void pop_map_set_above(uint8_t *above_row2_fg);
|
||||
void pop_map_set_above(uint8_t *above_row2_fg) __banked;
|
||||
extern uint8_t pop_ceil_modif[10];
|
||||
extern uint8_t pop_ceil_fell;
|
||||
extern uint8_t pop_debris_at;
|
||||
@@ -92,7 +101,7 @@ extern uint8_t pop_kid_hurt;
|
||||
|
||||
/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда
|
||||
* приземления/удара. Зовётся из pop_phys_tick. */
|
||||
void pop_check_knock(void);
|
||||
void pop_check_knock(void) __banked;
|
||||
|
||||
/* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру
|
||||
* или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр
|
||||
@@ -104,14 +113,14 @@ extern uint8_t pop_fell_out;
|
||||
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
|
||||
* determine_col + check_action (триггер падения / do_fall / приземление).
|
||||
* Порядок как в оригинале (seg000 play_kid_frame). */
|
||||
void pop_phys_tick(void);
|
||||
void pop_phys_tick(void) __banked;
|
||||
|
||||
/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
|
||||
* над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid,
|
||||
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
|
||||
* obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер.
|
||||
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
|
||||
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h);
|
||||
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked;
|
||||
|
||||
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
|
||||
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
|
||||
@@ -120,26 +129,26 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h);
|
||||
#define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */
|
||||
|
||||
/* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */
|
||||
int pop_edge_distance(void);
|
||||
uint8_t pop_edge_type(void);
|
||||
int pop_edge_distance(void) __banked;
|
||||
uint8_t pop_edge_type(void) __banked;
|
||||
|
||||
/* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной
|
||||
* позиции (передний край у стены, симметрично) и возвращает 1. Для
|
||||
* control_running (чистый стоп у стены). */
|
||||
int pop_wall_ahead(void);
|
||||
int pop_wall_ahead(void) __banked;
|
||||
|
||||
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
|
||||
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
|
||||
* check_jump_up/grab_up (K4.4). control_up гасит caller. */
|
||||
uint8_t pop_jump_up_seq(void);
|
||||
uint8_t pop_jump_up_seq(void) __banked;
|
||||
|
||||
/* ---- Вис/зацеп/спуск (K4.2/K4.3) — тайловые решения для pop_ctrl --- *
|
||||
* Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в
|
||||
* pop_ctrl; здесь — предикаты + выбор seq (+ выравнивание Kid.x). */
|
||||
uint8_t pop_hang_grab_ready(void); /* grab_timer истёк — можно climb-up */
|
||||
uint8_t pop_hang_climb_up_seq(void); /* seq_10 / seq_73 (подтягивание) */
|
||||
int8_t pop_hang_wall(void); /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
|
||||
uint8_t pop_hang_fall_seq(void); /* seq_23 / seq_11 (сорваться), +Kid.x */
|
||||
uint8_t pop_down_action(void); /* 1=обработано(спуск/отступ), 0=присед */
|
||||
uint8_t pop_hang_grab_ready(void) __banked; /* grab_timer истёк — можно climb-up */
|
||||
uint8_t pop_hang_climb_up_seq(void) __banked; /* seq_10 / seq_73 (подтягивание) */
|
||||
int8_t pop_hang_wall(void) __banked; /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
|
||||
uint8_t pop_hang_fall_seq(void) __banked; /* seq_23 / seq_11 (сорваться), +Kid.x */
|
||||
uint8_t pop_down_action(void) __banked; /* 1=обработано(спуск/отступ), 0=присед */
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* pop_state.c — данные, которые читают ОБЕ стороны банковой границы.
|
||||
*
|
||||
* Зачем отдельный модуль: sprinter-cc собирает банковый модуль с
|
||||
* --dataseg BANKn, поэтому ВСЯ его писучая память (глобалы, file-static,
|
||||
* статики внутри функций) линкуется В СТРАНИЦУ БАНКА и снаружи не читается
|
||||
* — проверено на .map (2026-07-30: глобал банкового модуля лёг по
|
||||
* 0x0001C000, а не в _DATA). Значит любую переменную, к которой нужен
|
||||
* доступ и из банка, и снаружи, надо держать в НЕбанковом модуле — здесь.
|
||||
*
|
||||
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
|
||||
*/
|
||||
#include "pop_state.h"
|
||||
|
||||
uint8_t pop_loose_landed;
|
||||
|
||||
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
|
||||
* но HP не теряет — иначе проверять боёвку приходится с трёх попаданий. */
|
||||
uint8_t pop_immortal;
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* pop_state.h — состояние, общее для банков и W1/W2. См. pop_state.c.
|
||||
*
|
||||
* Правило: если переменную читают/пишут по обе стороны банковой границы —
|
||||
* её место здесь, а НЕ в банковом модуле (там она уедет в страницу банка).
|
||||
*/
|
||||
#ifndef POP_STATE_H
|
||||
#define POP_STATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе
|
||||
* tilepos+1 тайла, на который он лёг. Ставит отрисовка mob (банк),
|
||||
* разбирает логика loose-полов (pop_map, W1/W2). */
|
||||
extern uint8_t pop_loose_landed;
|
||||
|
||||
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
|
||||
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
|
||||
extern uint8_t pop_immortal;
|
||||
|
||||
#endif
|
||||
@@ -47,7 +47,7 @@ static uint8_t trob_drawn[POP_ROOMTILES];
|
||||
|
||||
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
|
||||
* выбора следующего кадра пламени. */
|
||||
static unsigned long trob_seed;
|
||||
static pop_rnd_t trob_seed;
|
||||
|
||||
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
|
||||
|
||||
@@ -345,12 +345,29 @@ uint8_t pop_trob_room_active(uint8_t room)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint8_t torch_tick; /* делитель скорости анимации пламени */
|
||||
|
||||
/* Темп анимации пламени в ЛОГИЧЕСКИХ кадрах (один логический кадр = 3
|
||||
* vsync и соответствует игровому тику оригинала):
|
||||
* 1 — кадр пламени каждый тик, как в оригинале; кадры при этом ровные по
|
||||
* тактам, что нужно для счёта бюджета;
|
||||
* 2 — вдвое реже (так было до 2026-07-30; пламя шло на половинной
|
||||
* скорости, а бюджет кадра «прыгал» на 6.2 % от того, попал кадр на
|
||||
* анимацию или нет). */
|
||||
#define TORCH_ANIM_DIV 1
|
||||
|
||||
#if TORCH_ANIM_DIV > 1
|
||||
static uint8_t torch_tick;
|
||||
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
|
||||
#else
|
||||
#define TORCH_ANIM_NOW 1
|
||||
#endif
|
||||
|
||||
void pop_process_trobs(uint8_t cur_room)
|
||||
{
|
||||
uint8_t i, w = 0;
|
||||
#if TORCH_ANIM_DIV > 1
|
||||
torch_tick++;
|
||||
#endif
|
||||
|
||||
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
|
||||
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
|
||||
@@ -394,11 +411,9 @@ void pop_process_trobs(uint8_t cur_room)
|
||||
break;
|
||||
case TILE_TORCH:
|
||||
case TILE_TORCH_DEBRIS:
|
||||
/* animate_torch (seg007:03C1): кадр пламени; анимация вечная —
|
||||
* type остаётся 1, пока комната отрисована. Смена кадра — через
|
||||
* ЛОГИЧЕСКИЙ кадр (torch_tick): у нас кадр короче игрового тика
|
||||
* оригинала, и пламя «кипело» вдвое быстрее нужного. */
|
||||
if (room == cur_room && (torch_tick & 1) == 0)
|
||||
/* animate_torch (seg007:03C1): анимация вечная — type остаётся
|
||||
* 1, пока комната отрисована. Темп — TORCH_ANIM_DIV. */
|
||||
if (room == cur_room && TORCH_ANIM_NOW)
|
||||
mod[tp] = get_torch_frame(mod[tp]);
|
||||
break;
|
||||
case TILE_POTION:
|
||||
|
||||
@@ -21,8 +21,38 @@
|
||||
|
||||
__sfr __at (0xFE) io_border;
|
||||
|
||||
/* Сколько банков грузит crt0_banked (читает его стартовый код). */
|
||||
const uint8_t n_banks = 1;
|
||||
/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (включено по умолчанию; make PROF=0 —
|
||||
* выключить). Цвет бордюра держится, пока фаза считает, поэтому высота
|
||||
* полосы на скриншоте = время фазы в растровых строках. Это единственный
|
||||
* способ смотреть профиль без дебаггера: выборка PC вырождается (мост MAME
|
||||
* отвечает из фреймового колбэка, все сэмплы падают в обработчик
|
||||
* прерывания); точные такты — breakpoint'ы с {printf totalcycles; g}.
|
||||
*
|
||||
* ЦВЕТА: у бордюра Sprinter бит 0 (красный) ИГНОРИРУЕТСЯ, работают только
|
||||
* бит 1 (синий) и бит 2 (зелёный) — значит различимых состояний ровно
|
||||
* ЧЕТЫРЕ, и значения обязаны быть чётными. Отсюда фазы сгруппированы по
|
||||
* четыре, а не по семь (нечётные номера раньше сливались с соседями и
|
||||
* полосы не читались):
|
||||
* 0 чёрный — ожидание vsync (запас кадра)
|
||||
* 2 синий — ввод, heal и ЛОГИКА (kid_tick/физика/страж)
|
||||
* 4 зелёный — слой ФОНА (loose/trobs/redraw_needed/шов)
|
||||
* 6 циан — СПРАЙТЫ (страж, Kid, fore поверх них) */
|
||||
#ifndef PROF_BORDER
|
||||
#define PROF_BORDER 1
|
||||
#endif
|
||||
#if PROF_BORDER
|
||||
#define PROF(n) io_border = (n)
|
||||
#else
|
||||
#define PROF(n) ((void)0)
|
||||
#endif
|
||||
|
||||
/* Сколько банков грузит crt0_banked (читает его стартовый код).
|
||||
* 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map
|
||||
* (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP).
|
||||
* Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ
|
||||
* вплотную (16 021 из 16 384), а расти ему ещё есть куда — тайлы поздних
|
||||
* уровней, чомперы, зеркало. */
|
||||
const uint8_t n_banks = 4;
|
||||
|
||||
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
|
||||
uint8_t pop_cheats;
|
||||
@@ -48,6 +78,7 @@ uint8_t pop_cheats;
|
||||
* Убирается закомментированием ОДНОЙ строки ниже. */
|
||||
#define ROOMNAV 1
|
||||
|
||||
|
||||
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
|
||||
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
|
||||
* чтобы enter_room переключал комнату без длинного списка параметров. */
|
||||
@@ -139,6 +170,7 @@ static void apply_overrides(uint8_t room, uint8_t *tiles, uint8_t base, uint8_t
|
||||
static void enter_room(uint8_t room)
|
||||
{
|
||||
uint8_t pg;
|
||||
pop_guard_leave(); /* запомнить стража уходящей комнаты */
|
||||
uint8_t *room_mod;
|
||||
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
|
||||
apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */
|
||||
@@ -166,10 +198,16 @@ static void enter_room(uint8_t room)
|
||||
}
|
||||
}
|
||||
pop_map_set_room(room);
|
||||
/* Kid.room — как в оригинале: «комната персонажа». Полная модель
|
||||
* (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение
|
||||
* Kid.room == Guard.room нужно уже сейчас: без него луч видимости
|
||||
* стража никогда не срабатывает. */
|
||||
Kid.room = room;
|
||||
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
|
||||
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
|
||||
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
|
||||
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
|
||||
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
|
||||
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
|
||||
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
|
||||
* а не по свежему backtable. Фаза P0. */
|
||||
@@ -207,7 +245,7 @@ int main(void)
|
||||
#ifdef ROOMNAV
|
||||
uint8_t nav_prev = 0;
|
||||
#endif
|
||||
uint8_t cheat_prev = 0;
|
||||
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0;
|
||||
|
||||
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
|
||||
puts("pop_bg_load failed");
|
||||
@@ -267,7 +305,7 @@ int main(void)
|
||||
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
||||
* переключ). Выключение возвращает показ на страницу 0. */
|
||||
|
||||
io_border = 2; /* синий - начало */
|
||||
PROF(2); /* ввод + heal */
|
||||
|
||||
uint8_t sp = kbd_raw_down(KBD_SPACE);
|
||||
if (sp && !sp_prev) {
|
||||
@@ -284,14 +322,25 @@ int main(void)
|
||||
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
|
||||
if (kbd_raw_down(0x16)) frozen = 1;
|
||||
if (kbd_raw_down(0x1E)) frozen = 0;
|
||||
{ /* Читы (seg000: switch под if (cheats_enabled)). K — убить
|
||||
* стража; по фронту нажатия, чтобы не срабатывало каждый кадр. */
|
||||
{ /* Читы (seg000: switch под if (cheats_enabled)). Все — по
|
||||
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
|
||||
* K — убить стража;
|
||||
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
|
||||
* иначе боёвку проверять получается ровно три раза);
|
||||
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
|
||||
* берётся с пола (pop_have_sword ставит подбор). */
|
||||
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);
|
||||
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
|
||||
if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal;
|
||||
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
|
||||
cheat_prev = k;
|
||||
immo_prev = i;
|
||||
sword_prev = s;
|
||||
}
|
||||
if (frozen) {
|
||||
io_border = 0; /* черный: ждём vsync */
|
||||
PROF(0); /* чёрный: ждём vsync */
|
||||
gfx_wait_vsync();
|
||||
continue;
|
||||
}
|
||||
@@ -331,13 +380,20 @@ int main(void)
|
||||
back = dbuf ? (uint8_t)(front ^ 1) : 0;
|
||||
gfx_set_draw_page(back);
|
||||
|
||||
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
|
||||
pop_ctrl_tick(); /* ввод -> control(): смена seq */
|
||||
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
|
||||
pop_guard_heal(); /* и прошлый кадр стража */
|
||||
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
|
||||
PROF(2); /* логика (тот же синий) */
|
||||
kid_tick(); /* play_seq: следующий кадр */
|
||||
pop_phys_tick(); /* K3: падение/приземление/стена */
|
||||
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
|
||||
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
|
||||
* попадания с обеих сторон, потом применить, потом свести HP. */
|
||||
pop_check_sword_hurting();
|
||||
pop_check_sword_hurt();
|
||||
pop_do_delta_hp();
|
||||
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
|
||||
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
|
||||
pop_fell_out = 0;
|
||||
@@ -360,16 +416,14 @@ int main(void)
|
||||
}
|
||||
}
|
||||
|
||||
io_border = 4; /* зеленый */
|
||||
PROF(4); /* слой фона */
|
||||
|
||||
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
|
||||
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
|
||||
io_border = 6; /* голубой */
|
||||
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
|
||||
/* process_trobs — рисования внутри нет) */
|
||||
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
|
||||
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
|
||||
io_border = 4; /* синий */
|
||||
{ /* ворота соседа слева (room8) видны в col0 — */
|
||||
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
|
||||
if (lft) {
|
||||
@@ -386,7 +440,6 @@ int main(void)
|
||||
}
|
||||
}
|
||||
}
|
||||
io_border = 2; /* зеленый */
|
||||
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
|
||||
dead_frames++;
|
||||
if (dead_frames > RESPAWN_SETTLE &&
|
||||
@@ -399,13 +452,11 @@ int main(void)
|
||||
pop_kid_hp_reset();
|
||||
}
|
||||
}
|
||||
io_border = 6; /* голубой */
|
||||
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
|
||||
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
|
||||
uint8_t fcol = (uint8_t)(fpos % 10);
|
||||
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
|
||||
pop_loose_fell = 0;
|
||||
io_border = 2; /* синий */
|
||||
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
|
||||
if (down) {
|
||||
int8_t lrow = pop_room_col_landing(down, fcol);
|
||||
@@ -442,23 +493,29 @@ int main(void)
|
||||
* Приоритет как в оригинале: активный flash_time > кадр урона
|
||||
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
|
||||
if (pop_flash_time) {
|
||||
flash_bg(255, 255, 85); /* color_14_brightyellow */
|
||||
/* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (flash_color
|
||||
* оригинала; нам хватает этих двух). */
|
||||
if (pop_flash_red) flash_bg(255, 0, 0); /* color_4_red */
|
||||
else flash_bg(255, 255, 85); /* color_14_brightyellow */
|
||||
pop_flash_time--;
|
||||
if (!pop_flash_time) pop_flash_red = 0;
|
||||
flash_on = 1;
|
||||
} else if (pop_kid_hurt) {
|
||||
flash_bg(255, 85, 85); /* color_12_brightred */
|
||||
flash_on = 1;
|
||||
}
|
||||
io_border = 4; /* зеленый */
|
||||
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
|
||||
PROF(6); /* спрайты: страж, Kid, fore поверх */
|
||||
pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */
|
||||
PROF(6); /* kid_draw (тот же циан) */
|
||||
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
|
||||
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
|
||||
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
|
||||
kid_draw_splash();
|
||||
pop_kid_hurt = 0;
|
||||
}
|
||||
io_border = 0; /* черный */
|
||||
pop_hp_draw(); /* полоса HP — только при изменении */
|
||||
PROF(6); /* fore поверх Kid + борта */
|
||||
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
|
||||
kid_fp_width(), kid_fp_height(),
|
||||
Kid.direction, Kid.frame, Kid.action);
|
||||
@@ -468,7 +525,7 @@ int main(void)
|
||||
* сделан (скрытая страница), wait_vsync ловит начало кадра,
|
||||
* set_visible_page переключает во время бланка (gfx.h паттерн). */
|
||||
|
||||
io_border = 0; /* черный: ждём vsync */
|
||||
PROF(0); /* чёрный: ждём vsync */
|
||||
|
||||
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
|
||||
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
|
||||
|
||||
@@ -14,7 +14,7 @@ seqtbl адресуется базой SEQTBL_BASE=0x196E: элемент = kid_
|
||||
import os
|
||||
import re
|
||||
|
||||
SRC = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/src"
|
||||
SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "SDLPoP", "src")
|
||||
OUT_BIN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "poc", "res", "kid")
|
||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
@@ -44,6 +44,18 @@ def parse_frame_table(txt, name="frame_table_kid"):
|
||||
return frames
|
||||
|
||||
|
||||
def parse_sword_tbl(txt):
|
||||
"""sword_tbl[] (seg006:2047) -> список (id, x, y). id 255 = клинка нет.
|
||||
Индекс — sword_frame (cur_frame.sword & 0x3F); один и тот же chtab_0 и у
|
||||
Кида, и у стража."""
|
||||
m = re.search(r'const sword_table_type sword_tbl\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
|
||||
rows = []
|
||||
for row in re.findall(r'\{([^}]*)\}', m.group(1)):
|
||||
a, b, c = [int(p.strip(), 0) for p in row.split(',')]
|
||||
rows.append((a & 0xFF, b & 0xFF, c & 0xFF))
|
||||
return rows
|
||||
|
||||
|
||||
def parse_byte_array(txt, name):
|
||||
m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S)
|
||||
return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))]
|
||||
@@ -69,6 +81,7 @@ def main():
|
||||
gframes = parse_frame_table(seg006, "frame_tbl_guard")
|
||||
seqtbl = parse_byte_array(seqc, "original_seqtbl")
|
||||
offs = parse_word_array(seqc, "original_seqtbl_offsets")
|
||||
swtbl = parse_sword_tbl(seg006)
|
||||
|
||||
with open(OUT, "w") as f:
|
||||
f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n"
|
||||
@@ -102,7 +115,22 @@ def main():
|
||||
f.write("#define KID_NSEQ %d\n" % len(offs))
|
||||
f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n")
|
||||
f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n")
|
||||
f.write("#endif\n")
|
||||
|
||||
# sword_tbl — макро-инициализаторами, а не готовыми массивами: так
|
||||
# 159 байт таблицы лягут в ОДИН TU (pop_kid.c), а не в каждый, кто
|
||||
# включил заголовок.
|
||||
f.write("/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.\n"
|
||||
" * Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.\n"
|
||||
" * Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),\n"
|
||||
" * x — «вперёд» по направлению персонажа. */\n")
|
||||
f.write("#define KID_NSWORD %d\n" % len(swtbl))
|
||||
for i, (nm, fmt) in enumerate((("ID", "0x%02X"), ("DX", "%d"), ("DY", "%d"))):
|
||||
# dx/dy — знаковые: печатаем со знаком, иначе int8_t = overflow
|
||||
vals = [r[i] if i == 0 else (r[i] - 256 if r[i] > 127 else r[i])
|
||||
for r in swtbl]
|
||||
f.write("#define KID_SWORD_%s_INIT { \\\n" % nm)
|
||||
f.write(emit_c(vals, 12, fmt).replace("\n", " \\\n") + " \\\n}\n")
|
||||
f.write("\n#endif\n")
|
||||
|
||||
# kid_data.bin: кадры (по 5 байт) + seqtbl подряд
|
||||
os.makedirs(OUT_BIN_DIR, exist_ok=True)
|
||||
|
||||
@@ -28,12 +28,20 @@ PAL_BASE = 0x90 # слоты палитры Sprinter под страж
|
||||
COLOR = 2 # level.guards_color обоих стражей уровня 1
|
||||
SHIFT = 3 # g[id>>3], idx id&7 (спрайты крупные)
|
||||
MASK = (1 << SHIFT) - 1
|
||||
MAX_ID = 34 # res751..res784
|
||||
MAX_ID = 33 # атлас-индексы 0..33 = res751..res784
|
||||
|
||||
|
||||
def load_guard(id):
|
||||
"""id -> res(750+id).png -> (w, h, 8bpp COLUMN-MAJOR); 0 -> 0xFF (прозр.)."""
|
||||
path = os.path.join(GUARD_DIR, f"res{750 + id}.png")
|
||||
"""Атлас-индекс id (== frame.image) -> res(751+id).png -> (w, h, 8bpp
|
||||
COLUMN-MAJOR); 0 -> 0xFF (прозрачность).
|
||||
|
||||
Off-by-one как у Kid (см. pop_pack_kid.load_kid): load_chtab_from_file
|
||||
зовётся с ресурсом 750, а images[i-1] = res(750+i), то есть images[0] =
|
||||
res751. Рисование берёт get_image(chtab, obj_id+1 − 1) = images[image],
|
||||
значит frame.image=N -> res(751+N). С res(750+N) страж стоял в позе
|
||||
предыдущего кадра (замечено на frame_166_stand_inactive: image 17
|
||||
рисовался как res767 — выпад — вместо res768). """
|
||||
path = os.path.join(GUARD_DIR, f"res{751 + id}.png")
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
im = Image.open(path)
|
||||
@@ -55,7 +63,7 @@ def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
pages = {}
|
||||
n = 0
|
||||
for id in range(1, MAX_ID + 1):
|
||||
for id in range(0, MAX_ID + 1):
|
||||
g = load_guard(id)
|
||||
if g is None:
|
||||
continue
|
||||
|
||||
@@ -26,7 +26,10 @@ OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid")
|
||||
PAL_BASE = 0x70 # слоты палитры Sprinter под Kid
|
||||
SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F
|
||||
GUARD_PAL_BASE = 0x90 # chtab_5 (страж) -> слоты 0x90..0x9F
|
||||
SWORD_IDS = tuple(range(20, 28)) # sword_tbl для кадров 229..236 (найти-убрать)
|
||||
# chtab_0 целиком: sword_tbl (seg006:2047) адресует id 0..33 — подъём меча,
|
||||
# ножны И вся боёвка. Меч в оригинале ОДИН на всех: и Кид, и страж рисуют
|
||||
# клинок из этого же chtab_0 (add_sword_to_objtable, seg006:1798).
|
||||
SWORD_IDS = tuple(range(0, 34))
|
||||
SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ)
|
||||
MASK = (1 << SHIFT) - 1
|
||||
|
||||
@@ -150,8 +153,7 @@ def main():
|
||||
names.append(None)
|
||||
print(f" (kid стр {pg}: пусто)")
|
||||
|
||||
# chtab_0: меч в руке. Пакуем ТОЛЬКО кадры подъёма/ножен (sword_tbl
|
||||
# 35..42 -> id 20..27); индекс в атласе = id - 20 (бой не портирован).
|
||||
# chtab_0: меч в руке — весь chtab (id 0..33), индекс в атласе = id.
|
||||
sw = {}
|
||||
for i, id in enumerate(SWORD_IDS):
|
||||
w, h, pix = load_sword(id)
|
||||
|
||||
+21
-2
@@ -23,6 +23,7 @@
|
||||
# CODE=W1|W2 DATA=W1|W2|SAME BANKED=W1|W3
|
||||
# Example: --memory-manual CODE=W2,DATA=W2,BANKED=W3
|
||||
# -I PATH additional include path (repeatable)
|
||||
# -DNAME[=VAL] preprocessor define, passed to sdcc as is (repeatable)
|
||||
# -L 0xADDR code load address (default: derived from --memory mode)
|
||||
# -E 0xADDR entry address (default: same as -L)
|
||||
# -S 0xADDR stack address (default: 0xBFFE)
|
||||
@@ -32,6 +33,9 @@
|
||||
# --data-loc 0xN override SDCC --data-loc (default: derived from --memory)
|
||||
# -Wl FLAG extra linker flag (repeatable)
|
||||
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
|
||||
# --bank-data put bank modules' writable data INTO the bank page
|
||||
# (default: it goes to the shared _DATA in W1/W2, which
|
||||
# stays mapped and is visible from everywhere)
|
||||
# automatically and adds -Wl-b_BANKN=0x{N}C000
|
||||
# --w3 FILE.c place FILE.c resident in window 3 (0xC000), called
|
||||
# DIRECTLY (no trampoline). Repeatable. Defaults to
|
||||
@@ -106,6 +110,8 @@ LD_EXTRA=()
|
||||
MKEXE_EXTRA=()
|
||||
BANK_SPECS=() # entries like "1=engine.c"
|
||||
W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3)
|
||||
USER_DEFS=() # -DFOO / -DFOO=bar — пробрасываются в sdcc как есть
|
||||
BANK_LOCAL_DATA=0 # 1 (--bank-data): писучие данные банков — В СТРАНИЦУ банка
|
||||
W3_RELS=() # заполняется при компиляции W3-модулей
|
||||
W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3
|
||||
USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small)
|
||||
@@ -139,7 +145,9 @@ while [[ $# -gt 0 ]]; do
|
||||
--w3) W3_SPECS+=("$2"); shift 2;;
|
||||
--stack-size) STACK_SIZE="$2"; shift 2;;
|
||||
-Wl) LD_EXTRA+=("$2"); shift 2;;
|
||||
-D*) USER_DEFS+=("$1"); shift;;
|
||||
--bank) BANK_SPECS+=("$2"); shift 2;;
|
||||
--bank-data) BANK_LOCAL_DATA=1; shift;;
|
||||
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
|
||||
--max-allocs) MAX_ALLOCS="$2"; shift 2;;
|
||||
--gfx) GFX_MODE="$2"; shift 2;;
|
||||
@@ -366,7 +374,7 @@ run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC"
|
||||
|
||||
# 2. user sources → .rel (HOME)
|
||||
USER_RELS=()
|
||||
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}")
|
||||
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}" "${USER_DEFS[@]}")
|
||||
[[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT)
|
||||
[[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS")
|
||||
for src in "${SOURCES[@]}"; do
|
||||
@@ -420,8 +428,19 @@ if [[ "$CRT0_TYPE" == "banked" ]]; then
|
||||
bank_n="${spec%%=*}"
|
||||
bank_src="${spec#*=}"
|
||||
rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel"
|
||||
# По умолчанию --dataseg НЕ задаём: писучие данные банкового
|
||||
# модуля (глобалы, file-static, статики функций) остаются в
|
||||
# общем _DATA, то есть в W1/W2 — они замаплены всегда и видны
|
||||
# и банку, и остальному коду. С --dataseg BANKn они уезжают в
|
||||
# СТРАНИЦУ БАНКА и снаружи не читаются (проверено на .map:
|
||||
# глобал банкового модуля лёг по 0x0001C000): это экономит
|
||||
# W1/W2, но требует ручного контроля видимости, поэтому
|
||||
# включается явно через --bank-data.
|
||||
bank_data_flags=()
|
||||
[[ $BANK_LOCAL_DATA -eq 1 ]] && bank_data_flags=(--dataseg "BANK${bank_n}")
|
||||
run "$SDCC" "${CC_FLAGS[@]}" \
|
||||
--codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" --dataseg "BANK${bank_n}" \
|
||||
--codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" \
|
||||
"${bank_data_flags[@]}" \
|
||||
-c -o "$rel" "$bank_src"
|
||||
BANK_RELS+=("$rel")
|
||||
# Virtual address: bank_n in upper byte, BANK_LOW16 in low half.
|
||||
|
||||
+17
-10
@@ -1,5 +1,5 @@
|
||||
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
|
||||
accfill 3772
|
||||
accfill 3786
|
||||
argv 3431
|
||||
assrtest 3847
|
||||
atlas 9193
|
||||
@@ -9,15 +9,15 @@ banked 1056
|
||||
bankedbg 1067
|
||||
banklocl 4683
|
||||
banktest 3767
|
||||
bgi_img 7057
|
||||
bgitest 3737
|
||||
bgi_img 7200
|
||||
bgitest 3751
|
||||
bios_text 4461
|
||||
blitperf 5860
|
||||
blitw 4939
|
||||
cat 913
|
||||
cblstream 6217
|
||||
cbltest 6366
|
||||
cblwav 6434
|
||||
cblstream 6431
|
||||
cbltest 6580
|
||||
cblwav 6648
|
||||
conio 4605
|
||||
conio2 3929
|
||||
dec_test 860
|
||||
@@ -27,12 +27,12 @@ fdmax 6023
|
||||
filetest 10547
|
||||
fpsdiv 4870
|
||||
gets 509
|
||||
gfx_dbuf 5032
|
||||
gfx_demo 4103
|
||||
gfx_dbuf 5112
|
||||
gfx_demo 4117
|
||||
gfxbanks 6044
|
||||
hello 4167
|
||||
hello2 4244
|
||||
irqtest 6527
|
||||
irqtest 6647
|
||||
kbdraw 4911
|
||||
ls 4835
|
||||
malloc 4461
|
||||
@@ -45,7 +45,8 @@ palfile 5406
|
||||
ptime 5744
|
||||
rpgprof 12082
|
||||
rpgwalk 10309
|
||||
rt_test 4892
|
||||
rt_test 5154
|
||||
scroll 2974
|
||||
seek 4188
|
||||
simple 955
|
||||
solidt 11531
|
||||
@@ -60,4 +61,10 @@ strtest 1340
|
||||
text_palette 5024
|
||||
timedir 5454
|
||||
w0page 8831
|
||||
w3bankgfx 5240
|
||||
w3bgfx 5246
|
||||
w3big 3673
|
||||
w3huge 3700
|
||||
w3probe 3556
|
||||
w3tiny 3471
|
||||
winrest 4459
|
||||
|
||||
|
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* gfx_blit_cols_part_noclip — блит спрайта column-major БЕЗ клипа по экрану,
|
||||
* с обрезкой сверху (как gfx_blit_cols_part). Пара к gfx_blit_cols_part
|
||||
* ровно так же, как gfx_blit_part_noclip — к gfx_blit_part.
|
||||
*
|
||||
* Зачем: у персонажей PoP спрайт почти всегда целиком на экране, а общий
|
||||
* gfx_blit_cols_part платит за проверку четырёх границ, пересчёт w/h/cx/sy и
|
||||
* 16-битную арифметику независимо от того, вылезает край или нет — замер в
|
||||
* MAME (roomtest, страж + клинок): 5 622 такта подготовки против 13 596 на
|
||||
* сам accel-проход. Здесь та же колонка уходит в _bgi_blit_cols_raw без
|
||||
* подготовки.
|
||||
*
|
||||
* Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет):
|
||||
* - прямоугольник назначения целиком на экране;
|
||||
* - ширина и высота кадра <= 255, y <= 255;
|
||||
* - skip < высоты кадра.
|
||||
* Не выполняется — зовите gfx_blit_cols_part.
|
||||
*
|
||||
* Флип, как и в gfx_blit_cols_part, бесплатный: подаём src с последней
|
||||
* колонки и отрицательный страйд — accel сам кладёт её в левый x.
|
||||
*
|
||||
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения,
|
||||
* собранного с --w3, её вызывать нельзя — после _bgi_begin окно W3 занято
|
||||
* видеобанком и код вызывающего исчезает из адресного пространства.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include "../_bgi.h"
|
||||
|
||||
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
|
||||
uint8_t skip, uint8_t rows)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img;
|
||||
uint8_t w = p[0]; /* ширина кадра (< 256) */
|
||||
uint8_t fh = p[2]; /* высота кадра (< 256) */
|
||||
const uint8_t *src = p + 4;
|
||||
uint8_t h;
|
||||
|
||||
if (w == 0 || fh == 0 || skip >= fh) return;
|
||||
h = (uint8_t)(fh - skip);
|
||||
if (rows && rows < h) h = rows;
|
||||
|
||||
if (flip) src += (uint16_t)(w - 1) * fh; /* последняя колонка -> левый x */
|
||||
src += skip;
|
||||
|
||||
_bgi_begin();
|
||||
_bgi_blit_cols_raw(src, (uint8_t *)(_gfx_addr_base + (uint16_t)x),
|
||||
w, h, flip ? -(int)fh : (int)fh, (uint8_t)y);
|
||||
_bgi_end();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* gfx_blit_part_noclip — блит ПОД-ПРЯМОУГОЛЬНИКА картинки getimage-формата
|
||||
* БЕЗ клипа по экрану, в ТЕКУЩЕМ банке (gfx_set_bank). Пара к
|
||||
* gfx_blit_noclip ровно так же, как gfx_blit_part — к gfx_blit.
|
||||
*
|
||||
* Зачем: тайловому движку часто нужно положить поверх спрайта не весь
|
||||
* тайл, а только ту его часть, что перекрывает спрайт. Через
|
||||
* gfx_blit_part это идёт общим ядром _gfx_blit_full (16-битная
|
||||
* арифметика, клип, нарезка полос) — ~13 К тактов накладных на вызов
|
||||
* независимо от размера; здесь тот же под-прямоугольник рисуется линейным
|
||||
* спрайтовым ядром (~4.6 К на 32×3). Замер PoP roomtest 2026-07-29:
|
||||
* fore-слой поверх персонажа рисовал 9 целых тайлов за 432 К тактов
|
||||
* (стена = блок 64×63); с обрезкой по прямоугольнику спрайта тех же
|
||||
* пикселей остаётся втрое меньше.
|
||||
*
|
||||
* Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет):
|
||||
* - под-прямоугольник (sx,sy,w,h) лежит внутри картинки;
|
||||
* - прямоугольник назначения целиком на экране;
|
||||
* - w,h <= 255, y <= 255.
|
||||
* Не выполняется — зовите gfx_blit_part.
|
||||
*
|
||||
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения,
|
||||
* собранного с --w3, её вызывать нельзя — после _bgi_begin окно W3 занято
|
||||
* видеобанком и код вызывающего исчезает из адресного пространства.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
void gfx_blit_part_noclip(int x, int y, const void *img,
|
||||
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)img;
|
||||
uint16_t stride = (uint16_t)(p[0] | (p[1] << 8));
|
||||
const uint8_t *src = p + 4 + (uint16_t)sy * stride + sx;
|
||||
|
||||
_bgi_begin();
|
||||
_gfx_blit_sprite_noclip(x, y, src, stride, w, h);
|
||||
_bgi_end();
|
||||
}
|
||||
@@ -105,6 +105,14 @@ void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h);
|
||||
void gfx_blit_part(int x, int y, const void *img,
|
||||
int sx, int sy, int w, int h);
|
||||
|
||||
/* Тот же под-прямоугольник БЕЗ клипа по экрану — линейным спрайтовым
|
||||
* ядром (пара к gfx_blit_noclip). Вызывающий гарантирует: под-
|
||||
* прямоугольник внутри img, назначение целиком на экране, w,h,y <= 255.
|
||||
* Для тайловых движков, кладущих поверх спрайта только перекрывающую его
|
||||
* часть тайла (PoP: fore-слой). */
|
||||
void gfx_blit_part_noclip(int x, int y, const void *img,
|
||||
uint8_t sx, uint8_t sy, uint8_t w, uint8_t h);
|
||||
|
||||
/* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel-
|
||||
* проходом. flip!=0 = горизонтальное зеркало (направление персонажа) без
|
||||
* CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot-
|
||||
@@ -120,6 +128,16 @@ void gfx_blit_cols(int x, int y, const void *img, uint8_t flip);
|
||||
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
|
||||
int skip, int rows);
|
||||
|
||||
/* То же БЕЗ клипа по экрану — пара к gfx_blit_cols_part так же, как
|
||||
* gfx_blit_part_noclip к gfx_blit_part. Вызывающий гарантирует: спрайт
|
||||
* целиком на экране, ширина/высота кадра и y <= 255, skip < высоты.
|
||||
* rows = 0 — «до низа кадра». Экономит ~5.6 К тактов подготовки на вызов
|
||||
* (замер PoP roomtest: подготовка 5 622 против 13 596 на сам accel-проход),
|
||||
* что для персонажа со спрайтом-компаньоном (клинок) даёт заметную долю
|
||||
* кадра. */
|
||||
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
|
||||
uint8_t skip, uint8_t rows);
|
||||
|
||||
/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
|
||||
* оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда
|
||||
* работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;
|
||||
|
||||
+4
-4
@@ -30,10 +30,10 @@
|
||||
#define _IRQ_VEC_BUF_SIZE (256 + 257)
|
||||
|
||||
/* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в
|
||||
* W1). С запасом над реальным размером трамплина (244 Б после
|
||||
* FIFO-drain + overrun-recovery в raw-клавиатурной ветке, 2026-07-18 —
|
||||
* проверять __irq_tramp_end при правках, запас держать ≥ 60 Б). */
|
||||
#define _IRQ_TRAMP_BUF_SIZE 320
|
||||
* W1). С запасом над реальным размером трамплина (267 Б после
|
||||
* упаковки held-карты клавиатуры в биты, 2026-07-30 — проверять
|
||||
* __irq_tramp_end при правках, запас держать >= 60 Б). */
|
||||
#define _IRQ_TRAMP_BUF_SIZE 336
|
||||
|
||||
extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
|
||||
|
||||
|
||||
+31
-5
@@ -10,7 +10,8 @@
|
||||
* (порт данных 0x18 читается один раз, деструктивно — либо мы,
|
||||
* либо DSS, не оба), декодируется make/break (PS/2 Scan Code
|
||||
* Set 2: 0xF0 — префикс отпускания, 0xE0 — префикс расширенной
|
||||
* клавиши) в битовую карту `_kbdraw_down[]`, и к DSS НЕ идём
|
||||
* клавиши) в битовую карту `_kbdraw_down[]` (бит на код, 64 Б),
|
||||
* и к DSS НЕ идём
|
||||
* (private RETI — тот же паттерн, что и у CBL ниже). Пока raw
|
||||
* открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>).
|
||||
* - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке
|
||||
@@ -61,6 +62,7 @@ void _irq_tramp(void) __naked
|
||||
;; break-кода = клавиша «залипает» навсегда (найдено 2026-07-16).
|
||||
push hl
|
||||
push de
|
||||
push bc ; маска бита строится в BC (см. ниже)
|
||||
tr_kbd_drain:
|
||||
in a, (#_IRQ_SIO_DATA) ; читаем — тем самым дренируем байт
|
||||
cp #0xF0
|
||||
@@ -68,7 +70,27 @@ void _irq_tramp(void) __naked
|
||||
cp #0xE0
|
||||
jr Z, tr_kbd_setext
|
||||
|
||||
;; --- обычный байт кода: индекс в _kbdraw_down[код(+256 EXT)] ---
|
||||
;; --- обычный байт кода: БИТ code&7 в байте _kbdraw_down[code>>3]
|
||||
;; (карта упакована по битам: 64 Б вместо 512, см. _kbdraw.h).
|
||||
;; Маску строим сдвигом, а НЕ таблицей: таблица потребовала бы
|
||||
;; `ld hl,#метка` внутри трамплина, а он копируется в W2 побайтно
|
||||
;; и обязан быть без абсолютных само-ссылок (см. шапку). ---
|
||||
ld e, a ; E = код
|
||||
and #7
|
||||
ld b, a
|
||||
inc b ; B = (код&7)+1 для djnz
|
||||
ld a, #1
|
||||
jr tr_kbd_mskdec
|
||||
tr_kbd_mskloop:
|
||||
add a, a
|
||||
tr_kbd_mskdec:
|
||||
djnz tr_kbd_mskloop ; A = 1 << (код&7)
|
||||
ld c, a ; C = маска бита
|
||||
ld a, e
|
||||
rrca
|
||||
rrca
|
||||
rrca
|
||||
and #0x1F ; A = код>>3 (код <= 255)
|
||||
ld hl, #__kbdraw_down
|
||||
ld d, #0
|
||||
ld e, a
|
||||
@@ -76,16 +98,19 @@ void _irq_tramp(void) __naked
|
||||
ld a, (__kbdraw_pending)
|
||||
bit 1, a ; _KBDRAW_PEND_EXT?
|
||||
jr Z, tr_kbd_noext
|
||||
ld de, #256
|
||||
ld de, #32 ; +256 кодов = +32 байта карты
|
||||
add hl, de
|
||||
tr_kbd_noext:
|
||||
ld a, (__kbdraw_pending)
|
||||
bit 0, a ; _KBDRAW_PEND_BREAK?
|
||||
jr Z, tr_kbd_mkdown
|
||||
xor a, a ; break → снять (0)
|
||||
ld a, c
|
||||
cpl ; break → сбросить бит
|
||||
and a, (hl)
|
||||
jr tr_kbd_wr
|
||||
tr_kbd_mkdown:
|
||||
ld a, #1 ; make → взвести (1)
|
||||
ld a, c ; make → взвести бит
|
||||
or a, (hl)
|
||||
tr_kbd_wr:
|
||||
ld (hl), a
|
||||
xor a, a
|
||||
@@ -120,6 +145,7 @@ void _irq_tramp(void) __naked
|
||||
ld a, #1
|
||||
ld (__kbdraw_overrun), a
|
||||
tr_kbd_noovr:
|
||||
pop bc
|
||||
pop de
|
||||
pop hl
|
||||
pop af
|
||||
|
||||
+7
-2
@@ -23,7 +23,12 @@ extern uint8_t _kbdraw_pending; /* FSM декодера make/break, см. в
|
||||
extern uint8_t _kbdraw_overrun; /* 1 = ISR поймал Rx-overrun SIO (потерян
|
||||
* байт → возможно потерян break); снимает
|
||||
* kbd_raw_sync (чистит _kbdraw_down) */
|
||||
extern uint8_t _kbdraw_down[512]; /* 0/1 по коду; +256 для расширенных
|
||||
* (префикс 0xE0) — см. kbd_raw_down */
|
||||
/* Held-состояние: БИТ на скан-код, код>>3 = байт, код&7 = номер бита;
|
||||
* коды 0..255 обычные, 256..511 — расширенные (префикс 0xE0), т.е.
|
||||
* смещение +32 байта. Упаковка вместо байта на код: 64 Б вместо 512 —
|
||||
* заметная доля _DATA на 32-килобайтной раскладке (замер PoP roomtest
|
||||
* 2026-07-30). Ведёт трамплин прерывания (libc/irq/_irq_tramp.c),
|
||||
* читает kbd_raw_down(). */
|
||||
extern uint8_t _kbdraw_down[64];
|
||||
|
||||
#endif
|
||||
|
||||
@@ -9,4 +9,4 @@ uint8_t _kbdraw_active;
|
||||
uint8_t _kbdraw_pending;
|
||||
uint8_t _kbdraw_overrun; /* ISR взвёл при Rx-overrun SIO (потерян
|
||||
* break) — kbd_raw_sync снимет залипшее */
|
||||
uint8_t _kbdraw_down[512];
|
||||
uint8_t _kbdraw_down[64]; /* БИТ на скан-код (см. _kbdraw.h) */
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
uint8_t kbd_raw_down(uint16_t code)
|
||||
{
|
||||
if (code >= 512) return 0;
|
||||
return _kbdraw_down[code];
|
||||
return (uint8_t)((_kbdraw_down[code >> 3] >> (code & 7)) & 1);
|
||||
}
|
||||
|
||||
+10
-2
@@ -36,7 +36,15 @@ void kbd_raw_sync(void)
|
||||
if (!_kbdraw_overrun) return;
|
||||
_kbdraw_overrun = 0;
|
||||
|
||||
for (i = 0; i < 6; i++) save[i] = _kbdraw_down[_kbd_mod_codes[i]];
|
||||
/* Карта упакована по битам (см. _kbdraw.h): сохраняем/возвращаем
|
||||
* именно БИТ модификатора, а не байт. */
|
||||
for (i = 0; i < 6; i++) {
|
||||
uint16_t c = _kbd_mod_codes[i];
|
||||
save[i] = (uint8_t)((_kbdraw_down[c >> 3] >> (c & 7)) & 1);
|
||||
}
|
||||
memset(_kbdraw_down, 0, sizeof(_kbdraw_down));
|
||||
for (i = 0; i < 6; i++) _kbdraw_down[_kbd_mod_codes[i]] = save[i];
|
||||
for (i = 0; i < 6; i++) {
|
||||
uint16_t c = _kbd_mod_codes[i];
|
||||
if (save[i]) _kbdraw_down[c >> 3] |= (uint8_t)(1 << (c & 7));
|
||||
}
|
||||
}
|
||||
|
||||
+31
-6
@@ -38,9 +38,31 @@ dd if=/dev/zero of="$RAW" bs=512 count=$SECTORS status=none
|
||||
printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC"
|
||||
export MTOOLSRC="$MTC" MTOOLS_SKIP_CHECK=1
|
||||
|
||||
# mtools собран с enable-raw-term и, когда ему нужно что-то спросить,
|
||||
# открывает УПРАВЛЯЮЩИЙ ТЕРМИНАЛ напрямую (/dev/tty) — в обход stdin.
|
||||
# Поэтому ни `< /dev/null`, ни перенаправление stdio не спасают: из
|
||||
# терминала сборка молча вставала навсегда на `mmd z:/BG` (подтверждено
|
||||
# lsof: fd 0 = /dev/null, fd 4 = /dev/tty, процесс спит). Лечится только
|
||||
# отсутствием управляющего терминала — запускаем mtools в новой сессии,
|
||||
# тогда открыть /dev/tty нечего и mtools идёт неинтерактивным путём.
|
||||
#
|
||||
# setsid(1) в macOS нет, поэтому делаем то же самое питоном.
|
||||
# Каналов, откуда mtools может ждать ответ, ДВА, и закрывать надо оба:
|
||||
# /dev/tty — убирается новой сессией (os.setsid);
|
||||
# stdin — убирается перенаправлением из /dev/null.
|
||||
# Закрыть только один недостаточно: с одним лишь setsid mmd переключался на
|
||||
# stdin и вис уже на нём (lsof: fd 0 = /dev/ttys002).
|
||||
mt() {
|
||||
python3 -c 'import os,sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \
|
||||
"$@" < /dev/null
|
||||
}
|
||||
|
||||
# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16.
|
||||
mpartition -I -c -t $CYLS -h $HEADS -s $SECS z:
|
||||
mformat z:
|
||||
echo "--- разметка и формат образа ---"
|
||||
mt mpartition -I -c -t $CYLS -h $HEADS -s $SECS z:
|
||||
mt mformat z:
|
||||
|
||||
echo "--- копирование файлов ---"
|
||||
|
||||
# Аргумент может быть либо файлом (кладём в корень), либо парой
|
||||
# КАТАЛОГ:файл — тогда каталог создаётся на образе и файл кладётся туда.
|
||||
@@ -48,16 +70,19 @@ mformat z:
|
||||
for f in "$@"; do
|
||||
case "$f" in
|
||||
*:*) dir="${f%%:*}"; src="${f#*:}"
|
||||
mmd "z:/$dir" 2>/dev/null || true
|
||||
mcopy -o "$src" "z:/$dir/" ;;
|
||||
*) mcopy -o "$f" z: ;;
|
||||
# Каталог может уже быть — это не ошибка; stderr НЕ прячем, иначе
|
||||
# непрошедший вопрос mtools снова окажется невидимым.
|
||||
mt mmd "z:/$dir" >/dev/null 2>&1 || true
|
||||
mt mcopy -o "$src" "z:/$dir/" ;;
|
||||
*) mt mcopy -o "$f" z: ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "--- содержимое образа ---"
|
||||
mdir -/ z:
|
||||
mt mdir -/ z:
|
||||
|
||||
# RAW -> CHD (без сжатия = быстро; та же геометрия).
|
||||
echo "--- конвертация RAW -> CHD ---"
|
||||
chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \
|
||||
-chs $CYLS,$HEADS,$SECS -ss 512 -f
|
||||
echo "готово: $OUT_CHD"
|
||||
|
||||
Reference in New Issue
Block a user