/* * 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 #include #include #include #include #include #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; }