cd8d566d82
Порт 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>
265 lines
13 KiB
C
265 lines
13 KiB
C
/*
|
||
* 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;
|
||
}
|