applications/PoP: порт Prince of Persia — PoC (roomtest) + пайплайн

Порт PoP на Sprinter.  Текущий PoC — applications/PoP/roomtest/:
комната 1 (фон-композиция тайлов) + Kid с управлением на raw-клавиатуре
и коллизией с картой.

- roomtest — pop_bg (фон), pop_kid (спрайты Kid, column-major флип,
  seqtbl-анимация), pop_ctrl (порт control() PoP на held-state
  kbd_raw), pop_map (коллизия seg004/005: бег/стоп у стены,
  падение/приземление, отскок seq_47, вертикальный прыжок K4.1).
  MEMORY=small (DATA сразу за CODE, ~23КБ кода не лезет в huge).
- toolchain (PoP) — pop_pack_kid/pop_pack_bg/render_room/extract —
  распаковка res-графики MSDOS в атласы + композиция комнат.
- toolchain/ (корень) — make_hdd.sh (быстрый HDD-тест вместо FDD),
  png_strip.py / room_compose.py (ассет-пайплайн).
- docs — PORT_PLAN, KID_PLAN, форматы ресурсов (Apple II / MSDOS / DAT).
- bgtest/coltest/poc — ранние PoC (фон, коллизия, первый прототип).

.gitignore: build-артефакты applications/*/*/*; исключены внешние
референс-репозитории (SDLPoP/mininim/PR/Apple-II — свои git-клоны) и
оригинальные game-данные MSDOS/ (копирайт, только для реверса форматов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:51:08 +03:00
parent 484b18d10c
commit cd8d566d82
196 changed files with 6630 additions and 0 deletions
+382
View File
@@ -0,0 +1,382 @@
/*
* pop_map.c — коллизия Kid с картой (Фаза K3.2: падение). Порт SDLPoP
* seg005/seg006. Пер-кадровый physics-диспетч check_action() по Char.action:
* grounded (stand/run/step/crouch) -> check_on_floor(): кадр с флагом
* FRAME_NEEDS_FLOOR (0x40), под персонажем не floor -> start_fall();
* стена -> in_wall();
* freefall (action 4) -> do_fall(): fall_y-ускорение (в fall_accel/speed),
* достиг y_land[curr_row+1] -> land()/inc_curr_row.
*
* Координатная система PoP: экран.left = 58, тайл = 14 ед. (TILE_SIZEX);
* col = (xpos-58)/14 (округл. вниз), xl = остаток; get_tile_div_mod_m7 =
* div_mod(xpos-7). Карта: fg[row*10+col] & 0x1F; col<0/>9,row<0/>2 = стена.
*
* MVP: без spike/HP/grab/gate — падение + мягкое/среднее приземление.
* Бег В стену (check_bumped) и осторожный шаг у края (get_edge_distance) —
* следующим шагом.
*/
#include <stdint.h>
#include "pop_kid.h"
#include "pop_map.h"
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
#define TILE_EMPTY 0
#define TILE_WALL 20
#define ACT_STAND 0
#define ACT_RUNJUMP 1
#define ACT_HANGCLIMB 2
#define ACT_MIDAIR 3
#define ACT_FREEFALL 4
#define ACT_BUMPED 5
#define ACT_HANGSTRAIGHT 6
#define SEQ_7_FALL 7 /* stepfall (stand/run/step/crouch) */
#define SEQ_17_SOFT_LAND 17
#define SEQ_18_FALL_STANDJUMP 18
#define SEQ_19_FALL 19 /* stepfall2 (run frames 9/13) */
#define SEQ_20_MEDIUM_LAND 20
#define SEQ_21_FALL_RUNJUMP 21
#define SEQ_47_BUMP 47 /* удар в стену: dx(-4) отскок + 50-52 + stand */
#define FRAME_NEEDS_FLOOR 0x40
#define FRAME_WEIGHT_X 0x1F
#define TILE_SIZEX 14
#define TILE_RIGHTX 13
#define SCREENSPACE_X 58
#define FALL_ACCEL 3
#define FALL_MAX 33
#define TILE_MIDX 7
#define FIRST_ONSCREEN_COLUMN 5
#define FRAME_THIN 0x20
static const int16_t y_land[5] = {-8, 55, 118, 181, 244};
static const int8_t dir_front[2] = {-1, 1}; /* [dir+1]: dir=-1->-1, 0->+1 */
static const int16_t x_bump[20] = {
-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
};
/* wall_type(tile): gate/doortop=1, mirror=2, chomper=3, wall=4, иначе 0. */
static const int8_t wall_dl[6] = {0, 10, 0, -1, 0, 0}; /* wall_dist_from_left */
static const int8_t wall_dr[6] = {0, 0, 10, 13, 0, 0}; /* wall_dist_from_right */
static int8_t g_infrontx; /* колонка «перед персонажем» (set в get_tile_infrontof) */
/* ---- Карта комнаты ------------------------------------------------- */
uint8_t pop_fell_out; /* 1 = Kid упал за низ комнаты (сброс приложением) */
static const uint8_t *g_fg; /* fg[30], row*10+col */
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
void pop_map_set(const uint8_t *fg) { g_fg = fg; }
static uint8_t get_tile(int8_t col, int8_t row)
{
if (col < 0 || col > 9 || row < 0 || row > 2)
g_curr_tile = TILE_WALL; /* край уровня = стена */
else
g_curr_tile = g_fg[row * 10 + col] & 0x1F;
return g_curr_tile;
}
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;
}
}
static uint8_t get_tile_at_char(void)
{
return get_tile(Kid.curr_col, Kid.curr_row);
}
static uint8_t get_tile_infrontof_char(void)
{
g_infrontx = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col);
return get_tile(g_infrontx, Kid.curr_row);
}
static uint8_t wall_type(uint8_t t)
{
switch (t) {
case 4: case 7: case 12: return 1; /* стена справа (gate/doortop) */
case 13: return 2; /* стена слева (mirror) */
case 18: return 3; /* chomper */
case 20: return 4; /* стена с обеих сторон */
default: return 0;
}
}
/* ---- Координаты ---------------------------------------------------- */
static int char_dx_forward(int8_t dx)
{
int d = dx;
if (Kid.direction < 0) d = -d;
return (int)Kid.x + d;
}
/* col = (xpos-58)/14 округл. вниз; g_obj_xl = позиция в тайле (0..13). */
static int8_t get_tile_div_mod(int xpos)
{
int x = xpos - SCREENSPACE_X;
int xh = x / TILE_SIZEX;
int xl = x % TILE_SIZEX;
if (xl < 0) { --xh; xl += TILE_SIZEX; } /* округление вниз */
g_obj_xl = (uint8_t)xl;
return (int8_t)xh;
}
static int8_t get_tile_div_mod_m7(int xpos) { return get_tile_div_mod(xpos - 7); }
/* «весовая» точка кадра (dx с вычетом weight-битов флагов), в направлении. */
static int dx_weight(void)
{
int8_t offset = (int8_t)(kid_cur_dx() - (kid_cur_flags() & FRAME_WEIGHT_X));
return char_dx_forward(offset);
}
static void determine_col(void)
{
Kid.curr_col = get_tile_div_mod_m7(dx_weight());
}
/* расстояние до края тайла (для in_wall). */
static int distance_to_edge(int xpos)
{
int d;
get_tile_div_mod_m7(xpos); /* -> g_obj_xl */
d = g_obj_xl;
if (Kid.direction == 0) d = TILE_RIGHTX - d; /* dir_0_right */
return d;
}
static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); }
/* ---- ТОЧНАЯ коллизия со стеной (seg004) --------------------------- *
* Передний край персонажа char_x_*_coll = char_dx_forward(cur_frame.dx)
* (obj_x/2+58 сокращается ровно к этому; ширина спрайта НЕ нужна), ∓4 для
* THIN-кадров. dist_from_wall_forward — расстояние переднего края до грани
* стены tiletype в колонке tcol: >0 зазор, <=0 достал/зашёл (насколько). */
static int char_front_coll(void)
{
int cx = char_dx_forward(kid_cur_dx());
if (kid_cur_flags() & FRAME_THIN)
cx += (Kid.direction < 0) ? 4 : -4; /* THIN: край внутрь */
return cx;
}
static int dist_from_wall_forward(uint8_t tiletype, int8_t tcol)
{
uint8_t type = wall_type(tiletype);
int coll_left, cx;
if (type == 0) return 127;
coll_left = x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
cx = char_front_coll();
if (Kid.direction < 0) /* лицом влево */
return cx - (coll_left + TILE_RIGHTX - wall_dr[type]);
else /* лицом вправо */
return wall_dl[type] + coll_left - cx;
}
/* Выровнять передний край РОВНО к грани стены, если он зашёл за неё (bumped:
* Char.x += dist; char_dx_forward отражает знак для обоих направлений).
* Стена ищется под персонажем, иначе перед ним. */
static void in_wall(void)
{
uint8_t t; int8_t tcol; int d;
t = get_tile_at_char(); tcol = Kid.curr_col;
if (!wall_type(t)) { t = get_tile_infrontof_char(); tcol = g_infrontx; }
if (!wall_type(t)) return;
d = dist_from_wall_forward(t, tcol);
if (d < 0) {
Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */
determine_col();
get_tile_at_char();
}
}
/* ---- Падение ------------------------------------------------------- */
static void inc_curr_row(void) { Kid.curr_row++; }
static void land(void)
{
uint8_t seq;
Kid.y = (uint8_t)y_land[Kid.curr_row + 1];
/* к краю пола — чуть назад (как оригинал) */
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3)
Kid.x = (uint8_t)char_dx_forward(-3);
if (Kid.fall_y < 22)
seq = SEQ_17_SOFT_LAND; /* 1 этаж — мягко */
else
seq = SEQ_20_MEDIUM_LAND; /* 2+ этажа — присесть (HP/смерть — позже) */
Kid.fall_x = Kid.fall_y = 0;
kid_set_seq(seq);
play_seq();
determine_col();
}
static void start_fall(void)
{
uint8_t frame = Kid.frame, seq;
inc_curr_row();
if (frame == 9 || frame == 13) seq = SEQ_19_FALL; /* падение из бега */
else if (frame == 26) seq = SEQ_18_FALL_STANDJUMP;
else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP;
else seq = SEQ_7_FALL; /* stand/step/crouch */
kid_set_seq(seq);
play_seq();
determine_col();
if (get_tile_at_char() == TILE_WALL)
in_wall();
}
static void do_fall(void)
{
uint8_t nrow = (uint8_t)(Kid.curr_row + 1);
if (nrow > 4) nrow = 4; /* защита y_land[] от выхода */
if ((uint16_t)y_land[nrow] > (uint16_t)Kid.y) {
/* ещё летит (check_grab — K4) */
} else {
if (get_tile_at_char() == TILE_WALL)
in_wall();
if (tile_is_floor(g_curr_tile))
land();
else if (Kid.curr_row < 2)
inc_curr_row(); /* следующий ряд ВНУТРИ комнаты */
else
pop_fell_out = 1; /* упал за низ комнаты (нет пола,
комнаты снизу нет) — приложение
сбросит Kid (K-later: переход) */
}
}
static void fall_accel(void)
{
if (Kid.action == ACT_FREEFALL) {
int fy = (uint8_t)Kid.fall_y + FALL_ACCEL;
if (fy > FALL_MAX) fy = FALL_MAX;
Kid.fall_y = (int8_t)fy;
}
}
static void fall_speed(void)
{
Kid.y = (uint8_t)(Kid.y + (uint8_t)Kid.fall_y);
if (Kid.action == ACT_FREEFALL) {
Kid.x = (uint8_t)char_dx_forward(Kid.fall_x);
determine_col();
}
}
/* ---- get_edge_distance (seg004:067C) — ТОЧНО ---------------------- *
* Расстояние переднего края до стены/края впереди + тип g_edge_type: WALL
* (стена в пределах тайла), FLOOR (пол — далеко, 11), CLOSER (край уступа
* над ямой — distance_to_edge_weight). Стена сперва под персонажем, потом
* перед ним; dist<=TILE_RIGHTX -> WALL, иначе FLOOR. */
static uint8_t g_edge_type;
static int edge_classify(int d)
{
if (d <= TILE_RIGHTX) g_edge_type = EDGE_WALL;
else { g_edge_type = EDGE_FLOOR; d = 11; }
return d;
}
int pop_edge_distance(void)
{
uint8_t t; int d;
determine_col();
t = get_tile_at_char();
if (wall_type(t)) {
d = dist_from_wall_forward(t, Kid.curr_col);
if (d >= 0) return edge_classify(d);
}
t = get_tile_infrontof_char();
if (wall_type(t)) {
d = dist_from_wall_forward(t, g_infrontx);
if (d >= 0) return edge_classify(d);
}
if (tile_is_floor(t)) { g_edge_type = EDGE_FLOOR; return 11; }
g_edge_type = EDGE_CLOSER; /* яма впереди — до кромки уступа */
return distance_to_edge_weight();
}
uint8_t pop_edge_type(void) { return g_edge_type; }
/* Тайл НАД персонажем (curr_row-1, curr_col) сплошной (стена/пол)? — выбор
* seq прыжка вверх: solid -> seq_14 (в потолок), иначе seq_28 (пусто сверху).
* K4.1 упрощение: без grab-логики (уступ сверху-впереди) — та в K4.2. */
uint8_t pop_above_solid(void)
{
uint8_t t = get_tile(Kid.curr_col, (int8_t)(Kid.curr_row - 1));
return (t == TILE_WALL || tile_is_floor(t));
}
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
* позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */
int pop_wall_ahead(void)
{
int d = pop_edge_distance();
return (g_edge_type == EDGE_WALL && d <= 2);
}
/* ---- Диспетчер ----------------------------------------------------- */
static void check_on_floor(void)
{
if (kid_cur_flags() & FRAME_NEEDS_FLOOR) {
if (get_tile_at_char() == TILE_WALL)
in_wall(); /* забежал в стену -> выровнять */
if (!tile_is_floor(g_curr_tile))
start_fall();
}
}
static void check_action(void)
{
uint8_t action = Kid.action, frame = Kid.frame;
if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) {
if (frame == 109 || (frame >= 110 && frame <= 119))
check_on_floor();
} else if (action == ACT_FREEFALL) {
do_fall();
} else if (action == ACT_MIDAIR) {
/* frames 102..105: check_grab — K4 */
} else if (action != ACT_HANGCLIMB) {
check_on_floor();
}
}
/* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять
* к грани И проиграть отскок seq_47_bump (dx4 назад + кадры 50-52 + stand),
* как в оригинале (bumped_floor). Это даёт «шаг назад со звуком» при беге/
* шаге в стену вместо падения. Уже в отскоке (action bumped) — только
* выравниваем, без рестарта. В падении/висе не трогаем. */
static void check_bumped(void)
{
uint8_t t; int8_t tcol; int d;
if (Kid.action == ACT_FREEFALL || Kid.action == ACT_HANGCLIMB) return;
t = get_tile_at_char(); tcol = Kid.curr_col;
if (!wall_type(t)) { t = get_tile_infrontof_char(); tcol = g_infrontx; }
if (!wall_type(t)) return;
d = dist_from_wall_forward(t, tcol);
if (d >= 0) return; /* ещё не зашёл за грань */
Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */
determine_col();
if (Kid.action == ACT_BUMPED) return; /* уже в отскоке — не рестартить */
Kid.fall_y = 0;
kid_set_seq(SEQ_47_BUMP); /* отскок (TODO: bumped_sound) */
play_seq();
determine_col();
}
void pop_phys_tick(void)
{
fall_accel();
fall_speed();
determine_col();
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
check_action();
}