Files
snark13 cd8d566d82 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>
2026-07-17 17:51:08 +03:00

265 lines
13 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* poc.c — PoC порта Prince of Persia (applications/PoP/docs/PORT_PLAN.md
* §5): проверяем управление (raw-клавиатура, held-state) + коллизию
* по краям экрана + анимацию ходьбы + прыжок/присед поверх готового
* спрайтового движка (sprite.h).
*
* ВАЖНО: персонаж — ВРЕМЕННАЯ ЗАГЛУШКА (лицензированный спрайт-пак
* third_party/16x16-RPG-characters через tools/gen_kid_placeholder.py,
* тот же источник, что уже использует examples/rpgwalk), НЕ графика
* оригинальной Prince of Persia — см. §5 и §8.5 плана. У заглушки нет
* отдельных поз прыжка/приседа — механика (тайминг дуги, состояние,
* коллизия с полом) проверяется на том же спрайте без смены позы;
* визуально это упрощение, не финальный вид.
*
* Дуга прыжка (jump_height[]) — СВОЯ, приблизительная (не таблица
* смещений оригинала — см. §6 плана: авторские таблицы кадров решено
* не переносить, только код/структуры).
*
* Нет ещё (следующие итерации): реальный уровень/фон по BLUETYPE,
* рывок вбок при прыжке с разбега, зацепление за уступ.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <kbd_raw.h>
#include <conio.h>
#include <stdio.h>
#include "level.h"
#include "room.h"
/* Комната 1 уровня 1 — РЕАЛЬНАЯ геометрия (foretable/backtable),
* вырезана tools/extract_room.py из applications/PoP/SDLPoP/data/
* LEVELS/res2001.bin (level_load_room(), см. level.h/room.c) — не
* плейсхолдер. Верхний ряд (row0) — floor-уступ на cols 3-7 (там
* реально стоит персонаж в оригинале), стены по cols 8-9; ряды 1-2 —
* ниже уступа (торч/колонны/пол — Фаза 1 просто их отрисовывает по
* тем же типам, без многоуровневой физики падения). */
static level_t test_level;
#define CHAR_ROW 0 /* ряд, где стоит персонаж (floor-уступ room1) */
#define ROW_Y(r) ((r) * 64) /* room_row_h все по 64 */
#define GROUND_Y ROW_Y(CHAR_ROW + 1) /* низ ряда CHAR_ROW = верх пола */
#define KIDY (GROUND_Y - 16) /* y спрайта (16 px высотой) */
#define SPEED 2 /* px/кадр — заглушка, не авторский темп */
/* Настоящая преграда (Wall/Pillar) слева/справа от кандидата x в
* CHAR_ROW — блокирует движение (грубая проверка по краям хитбокса
* 16px, без под-тайловой подгонки — для PoC достаточно, см.
* PORT_PLAN.md §6). tile_blocks_side(), НЕ tile_is_solid(): Floor —
* тайл, на котором Кид СТОИТ (тот же CHAR_ROW), tile_is_solid() его
* тоже считает "твёрдым" (можно стоять сверху) — если проверять им же
* боковую преграду, Кид не мог сдвинуться с собственного пола (баг,
* найден 2026-07-16). */
static uint8_t col_blocked(int x)
{
uint8_t c0, c1;
if (x < 0 || x + 15 >= ROOM_COLS * ROOM_TILE_W)
return 1;
c0 = (uint8_t)(x / ROOM_TILE_W);
c1 = (uint8_t)((x + 15) / ROOM_TILE_W);
if (tile_blocks_side(level_tile(&test_level, 1, c0, CHAR_ROW)->type))
return 1;
if (tile_blocks_side(level_tile(&test_level, 1, c1, CHAR_ROW)->type))
return 1;
return 0;
}
/* Ленты атласа: dir*3+frame, dir 0=вниз/1=влево/2=вправо/3=вверх,
* 3 кадра маятника на направление (см. tools/gen_kid_placeholder.py). */
#define DIR_DOWN 0
#define DIR_LEFT 1
#define DIR_RIGHT 2
/* Дуга прыжка: своя, приблизительная (не авторская таблица, см. шапку
* файла) — высота над полом (px) по кадрам 0..19, УЖЕ ПОЛНЫЙ горб
* (подъём 2→22 к элементу 10, спуск обратно к 2 к элементу 19) — БЕЗ
* зеркалирования в коде, массив читается один раз целиком. БАГ,
* найденный пользователем 2026-07-15: раньше код ЕЩЁ РАЗ зеркалил
* этот уже полный горб на 40 кадров — получалось два полных прыжка
* подряд от одного триггера (не проблема клавиатуры/декодера, чистая
* рассинхронизация данных и комментария). 20 кадров @ 50 Гц ~= 0.4 с. */
static const uint8_t jump_height[20] = {
2, 4, 7, 10, 13, 16, 18, 20, 21, 22,
22, 21, 20, 18, 16, 13, 10, 7, 4, 2
};
#define JUMP_FRAMES 20
static atlas_t at;
static sprite_t kid;
static uint8_t facing = DIR_DOWN; /* текущее направление анимации */
static uint8_t jumping = 0; /* 0 = на земле */
static uint8_t jump_t = 0; /* кадр дуги, 0..JUMP_FRAMES-1 */
static uint8_t crouching = 0;
/* Прыжок — level-triggered НАМЕРЕННО (не edge-detect): если UP всё ещё
* зажат к моменту приземления — следующий прыжок стартует СРАЗУ (цепочка
* прыжков, пока держишь); отпустил раньше — второй прыжок не начнётся
* сам, только по следующему нажатию. Раньше здесь были up_prev/
* jump_cooldown — попытка "починить" ровно ЭТО поведение, приняв его
* за баг; убрано 2026-07-15 после уточнения желаемого поведения. */
static void draw_room(void)
{
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
room_draw(&test_level, 1);
setcolor(LIGHTGRAY);
outtextxy(60, 4, "PoP PoC: hold LEFT/RIGHT to walk, ESC to quit");
outtextxy(4, 14, "(placeholder tiles -- not original PoP art)");
}
/* HUD-плашка состояния (нет отдельной позы прыжка/приседа — статус
* текстом, рисуется банком 0x50, heal спрайтового движка её не
* трогает — как fps-плашка в examples/rpgwalk). */
static void show_state(uint8_t jump, uint8_t crouch)
{
setfillstyle(SOLID_FILL, BLACK);
bar(0, 24, 60, 32);
setcolor(YELLOW);
if (jump)
outtextxy(0, 24, "JUMP");
else if (crouch)
outtextxy(0, 24, "CROUCH");
}
static void set_facing(uint8_t dir)
{
if (facing == dir)
return;
facing = dir;
sprite_anim(&kid, (uint8_t)(dir * 3), (uint8_t)(dir * 3 + 2),
6, ANIM_PINGPONG);
}
int main(void)
{
uint8_t page, hidden;
int x;
if (level_load_room(&test_level, 1, "room1.dat") != 0 &&
level_load_room(&test_level, 1, "a:\\room1.dat") != 0) {
puts("room1.dat not found");
return 1;
}
/* Стартовая позиция. В данных room1 start_location = tileOffset 0
* (row0/col0) — а там EMPTY (провал, без пола); в Фазе 1 нет физики
* падения, поэтому для PoC ставим Кида на floor-уступ (col 3, где он
* реально стоит в оригинале). Когда появится многоуровневая физика —
* брать col из start_location. start_direction: -1 = влево, 0 =
* вправо (level.h). */
x = 3 * ROOM_TILE_W;
facing = (test_level.start_direction < 0) ? DIR_LEFT : DIR_RIGHT;
if (atlas_load(&at, "kid.atl") != 0 &&
atlas_load(&at, "a:\\kid.atl") != 0) {
puts("kid.atl not found");
return 1;
}
if (tile_atlas_load("") == 0 && tile_atlas_load("a:\\") == 0) {
puts("tile atlases not found");
atlas_free(&at);
return 1;
}
initgraph();
/* room.pal = EGA16 + Kid + Floor + Wall — ОДНА палитра на всё,
* собрана tools/build_room_palette.py (см. --seed-pal в
* toolchain/png_strip.py) — Kid и тайлы на экране одновременно,
* их "свои" цвета обязаны жить в одной таблице. */
if (gfx_pal_fload(0, "room.pal") < 0)
gfx_pal_fload(0, "a:\\room.pal");
gfx_pal_sync();
gfx_sprite_clip(0); /* коллизия по краям гарантирует границы */
/* kid.atl — ОДНА лента (12 кадров вертикально: dir*3+кадр, см. шапку
* файла и examples/rpgwalk); индекс atlas_sprite_init — это НОМЕР
* ЛЕНТЫ (персонажа), а не кадра. Лента одна → всегда 0. Стартовый
* кадр направления выставляем sprite_frame (вертикальная лента, fh=16:
* кадр N на sy=N*16). Баг Соннета (найден 2026-07-16): здесь стоял
* facing*3 как индекс ЛЕНТЫ — при старте лицом влево (idx 3) читался
* мусор за каталогом атласа → мусорные w/h/src → блит спрайта заливал
* пол-экрана «снегом». */
atlas_sprite_init(&kid, &at, 0);
sprite_frame(&kid, 0, (int)(facing * 3) * 16);
kid.x = x;
kid.y = KIDY;
sprite_show(&kid);
for (page = 0; page < 2; page++) { /* фон + спрайт на обе страницы */
gfx_set_draw_page(page);
draw_room();
sprite_update(&kid, 1);
}
gfx_set_visible_page(0);
/* kbd_raw требует BSS в W2 (IM2-таблица) — недоступно в --memory small
* (там BSS может уехать в W1 → EINVAL); poc собирается --memory huge
* (CODE в W1, DATA/BSS в W2). closegraph ДО puts — иначе сообщение
* ушло бы в графический режим (невидимо), а программа молча вышла бы
* в prompt (баг Соннета, найден 2026-07-16). */
if (kbd_raw_open() != 0) {
closegraph();
puts("kbd_raw_open failed (need memory mode with BSS in W2)");
atlas_free(&at);
return 1;
}
for (;;) {
uint8_t moving = 0;
int y = KIDY;
if (jumping) {
/* дуга идёт сама; направлением можно скользить вбок,
* поза не меняется (нет отдельного кадра прыжка) */
if (kbd_raw_down(KBD_LEFT)) {
if (!col_blocked(x - SPEED)) x -= SPEED;
} else if (kbd_raw_down(KBD_RIGHT)) {
if (!col_blocked(x + SPEED)) x += SPEED;
}
y = KIDY - jump_height[jump_t];
jump_t++;
if (jump_t >= JUMP_FRAMES) {
jumping = 0;
y = KIDY;
}
} else if (crouching) {
if (!kbd_raw_down(KBD_DOWN))
crouching = 0;
} else if (kbd_raw_down(KBD_UP)) {
jumping = 1;
jump_t = 0;
} else if (kbd_raw_down(KBD_DOWN)) {
crouching = 1;
} else if (kbd_raw_down(KBD_LEFT)) {
if (!col_blocked(x - SPEED)) x -= SPEED;
set_facing(DIR_LEFT);
moving = 1;
} else if (kbd_raw_down(KBD_RIGHT)) {
if (!col_blocked(x + SPEED)) x += SPEED;
set_facing(DIR_RIGHT);
moving = 1;
}
if (!moving && !jumping && (sprite_anim_status(&kid) & SPR_ANIM_ON))
sprite_anim_stop(&kid, (int8_t)(facing * 3));
sprite_move(&kid, x, y);
if (kbd_raw_down(KBD_ESC))
break;
hidden = gfx_get_visible_page() ^ 1;
gfx_set_draw_page(hidden);
show_state(jumping, crouching);
sprite_update(&kid, 1);
gfx_wait_vsync();
gfx_set_visible_page(hidden);
}
kbd_raw_close();
closegraph();
tile_atlas_free();
atlas_free(&at);
return 0;
}