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:
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user