From b010d24792916577eaa3affe703021938c1175c3 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 13 Jul 2026 17:27:00 +0300 Subject: [PATCH] =?UTF-8?q?examples/rpgwalk:=208=20RPG-=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D0=BE=D0=BD=D0=B0=D0=B6=D0=B5=D0=B9=20=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=D1=8F=D1=82=20=D0=BF=D0=BE=20=D1=82=D1=80=D0=B0=D0=B2=D1=8F?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D1=83=20=D0=BF=D0=BE=D0=BB=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Демо на реальном арте (third_party/16x16-RPG-characters, bard): conv_sprites.py режет PNG 192×128 (8 персонажей = блоки 3×4 кадров: ряды вниз/влево/вправо/вверх × кадры маятника 0/1/2) в 8 вертикальных лент по 12 кадров и строит bard.pal (слоты 0-15 EGA + цвета PNG с 16, прозрачность → 0xFF). 8×12 кадров не лезут в одну EMM-страницу — ДВА атласа по 4 персонажа (движок сам переключает страницы W0). Палитра из файла: gfx_pal_fload + gfx_pal_sync. Смена направления = sprite_anim(dir*3, dir*3+2, PINGPONG). Правила хождения (двухфазная машина на sprite_moveto): до края → разворот 180° → случайная точка (не меньше четверти экрана) → поворот ±90° → снова до края. Co-Authored-By: Claude Fable 5 --- docs/size_baseline.tsv | 1 + examples/rpgwalk/.gitignore | 4 + examples/rpgwalk/Makefile | 19 ++++ examples/rpgwalk/conv_sprites.py | 66 +++++++++++ examples/rpgwalk/rpgwalk.c | 188 +++++++++++++++++++++++++++++++ 5 files changed, 278 insertions(+) create mode 100644 examples/rpgwalk/.gitignore create mode 100644 examples/rpgwalk/Makefile create mode 100644 examples/rpgwalk/conv_sprites.py create mode 100644 examples/rpgwalk/rpgwalk.c diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 1f8aece..a2d3a04 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -41,6 +41,7 @@ mouse 4382 openenv 6124 palfile 5406 ptime 5744 +rpgwalk 9484 rt_test 4892 seek 4188 simple 955 diff --git a/examples/rpgwalk/.gitignore b/examples/rpgwalk/.gitignore new file mode 100644 index 0000000..7db6d1a --- /dev/null +++ b/examples/rpgwalk/.gitignore @@ -0,0 +1,4 @@ +*.raw +*.atl +bard.pal +sprites.stamp diff --git a/examples/rpgwalk/Makefile b/examples/rpgwalk/Makefile new file mode 100644 index 0000000..8ae0aac --- /dev/null +++ b/examples/rpgwalk/Makefile @@ -0,0 +1,19 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := rpgwalk +EXTRA_FLAGS ?= --gfx 256 --memory tiny +EXTRA_DATA := bard1.atl bard2.atl bard.pal +include $(PROJ_ROOT)/app.mk + +# 8 персонажей по 12 кадров = 24 КБ пикселей — НЕ лезут в одну +# EMM-страницу (лимит 16К-0x100): два атласа по 4 персонажа. +sprites.stamp: conv_sprites.py $(PROJ_ROOT)/toolchain/mkatlas.py + python3 conv_sprites.py + python3 $(PROJ_ROOT)/toolchain/mkatlas.py bard1.atl \ + char0.raw:16x16:1x12 char1.raw:16x16:1x12 \ + char2.raw:16x16:1x12 char3.raw:16x16:1x12 + python3 $(PROJ_ROOT)/toolchain/mkatlas.py bard2.atl \ + char4.raw:16x16:1x12 char5.raw:16x16:1x12 \ + char6.raw:16x16:1x12 char7.raw:16x16:1x12 + touch $@ + +$(EXAMPLE).exe: sprites.stamp diff --git a/examples/rpgwalk/conv_sprites.py b/examples/rpgwalk/conv_sprites.py new file mode 100644 index 0000000..680c181 --- /dev/null +++ b/examples/rpgwalk/conv_sprites.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Конвертер third_party/16x16-RPG-characters (02-bard.png) → ленты +атласа + палитра для examples/rpgwalk. + +Источник: RGBA 192×128 = 8 рядов × 12 картинок 16×16; персонаж = +блок 3×4 (4 блока в ряду, 2 ряда блоков = 8 персонажей): + строка блока 0 — идёт ВНИЗ (на нас), кадры 0/1/2 (маятник); + строка 1 — ВЛЕВО; строка 2 — ВПРАВО; строка 3 — ВВЕРХ. + +Выход: + char0.raw..char7.raw — вертикальные ленты 16×192 (12 кадров: + dir*3 + frame — sprite_anim(dir*3, dir*3+2, PINGPONG)); + bard.pal — палитра gfx_pal_fload: слоты 0..15 = EGA (как ставит + initgraph, таблица из _bgi_mode_set), слоты 16.. = цвета PNG. +Пиксели: прозрачное (alpha<128) → 0xFF, цвет → 16 + индекс. +""" +from PIL import Image + +SRC = ("../../third_party/16x16-RPG-characters/sprites/" + "old-style/02-bard.png") +BASE = 16 # цвета PNG кладём после EGA-шестнадцати + +# B, G, R, pad — как в libbgi/bgi256/_bgi_mode_set.c +EGA = [ + (0, 0, 0), (168, 0, 0), (0, 168, 0), (168, 168, 0), + (0, 0, 168), (168, 0, 168), (0, 84, 168), (168, 168, 168), + (84, 84, 84), (255, 84, 84), (84, 255, 84), (255, 255, 84), + (84, 84, 255), (255, 84, 255), (84, 255, 255), (255, 255, 255), +] + +im = Image.open(SRC).convert("RGBA") +assert im.size == (192, 128), im.size + +colors = {} # (r,g,b) -> палитровый индекс + + +def px(x, y): + r, g, b, a = im.getpixel((x, y)) + if a < 128: + return 0xFF + key = (r, g, b) + if key not in colors: + assert BASE + len(colors) < 0xFF, "палитра переполнена" + colors[key] = BASE + len(colors) + return colors[key] + + +for c in range(8): + bx, by = (c % 4) * 48, (c // 4) * 64 + data = bytearray() + for d in range(4): # вниз/влево/вправо/вверх + for f in range(3): # кадры маятника + for y in range(16): + for x in range(16): + data.append(px(bx + f * 16 + x, by + d * 16 + y)) + open(f"char{c}.raw", "wb").write(data) + +pal = bytearray() +for b, g, r in EGA: + pal += bytes((b, g, r, 0)) +for (r, g, b) in colors: # dict сохраняет порядок вставки + pal += bytes((b, g, r, 0)) +open("bard.pal", "wb").write(pal) + +print(f"conv_sprites: 8 лент 16x192, палитра EGA+{len(colors)} цветов " + f"({len(pal)} Б)") diff --git a/examples/rpgwalk/rpgwalk.c b/examples/rpgwalk/rpgwalk.c new file mode 100644 index 0000000..dc65549 --- /dev/null +++ b/examples/rpgwalk/rpgwalk.c @@ -0,0 +1,188 @@ +/* + * rpgwalk — демо: 8 уникальных RPG-персонажей ходят по травяному полю. + * + * Спрайты — third_party/16x16-RPG-characters (bard, CC), два W0-атласа + * по 4 персонажа (12 кадров каждому не лезут в одну страницу на + * восьмерых); палитра — из PNG через gfx_pal_fload (слоты 0..15 EGA, + * 16+ цвета художника) + gfx_pal_sync для дабл-буфера. + * + * Лента персонажа: 12 кадров вертикально, dir*3 + кадр: + * dir 0 = вниз (на нас), 1 = влево, 2 = вправо, 3 = вверх; + * шаг ноги — sprite_anim(dir*3, dir*3+2, ANIM_PINGPONG) (маятник 0/1/2). + * + * Правила хождения (машина из двух фаз на sprite_moveto): + * фаза 0: идём ДО КРАЯ экрана; у края — разворот на 180° и + * фаза 1: идём до случайной точки (но не меньше ЧЕТВЕРТИ экрана); + * там — поворот на ±90° (случайно) и снова фаза 0. + * + * ESC — выход. + */ +#include +#include +#include +#include +#include +#include + +#define NCHR 8 +#define MINX 0 +#define MAXX (320 - 16) +#define MINY 14 /* под заголовком */ +#define MAXY (256 - 16) +#define QX 80 /* четверть экрана */ +#define QY 64 + +static atlas_t at1, at2; +static sprite_t sp[NCHR]; +static uint8_t dir[NCHR]; /* 0 вниз / 1 влево / 2 вправо / 3 вверх */ +static uint8_t phase[NCHR]; /* 0 = до края, 1 = до случайной точки */ + +/* Направление → шаг ноги той же скоростью (у всех чуть разной). */ +static void set_dir(uint8_t i, uint8_t d) +{ + dir[i] = d; + sprite_anim(&sp[i], (uint8_t)(d * 3), (uint8_t)(d * 3 + 2), + (uint8_t)(5 + (i & 3)), ANIM_PINGPONG); +} + +/* Идти до края экрана вдоль dir[i] (скорость: 1px, интервал 1-2). */ +static void go_edge(uint8_t i) +{ + sprite_t *s = &sp[i]; + int tx = s->x, ty = s->y; + + switch (dir[i]) { + case 0: ty = MAXY; break; + case 1: tx = MINX; break; + case 2: tx = MAXX; break; + default: ty = MINY; break; + } + sprite_moveto(s, tx, ty, 1, (uint8_t)(1 + (i & 1))); + phase[i] = 0; +} + +/* Развернуться на 180° и идти до случайной точки (≥ четверти экрана). */ +static void go_back_random(uint8_t i) +{ + sprite_t *s = &sp[i]; + int tx = s->x, ty = s->y, avail, dist; + uint8_t d = dir[i]; + + d = (uint8_t)(d ^ ((d < 2) ? 3 : 1)); /* 0↔3, 1↔2 — разворот */ + set_dir(i, d); + switch (d) { + case 0: avail = MAXY - s->y; dist = QY; break; + case 1: avail = s->x - MINX; dist = QX; break; + case 2: avail = MAXX - s->x; dist = QX; break; + default: avail = s->y - MINY; dist = QY; break; + } + if (avail > dist) + dist += rand() % (avail - dist + 1); + else + dist = avail; /* некуда — до края */ + switch (d) { + case 0: ty += dist; break; + case 1: tx -= dist; break; + case 2: tx += dist; break; + default: ty -= dist; break; + } + sprite_moveto(s, tx, ty, 1, (uint8_t)(1 + (i & 1))); + phase[i] = 1; +} + +/* Повернуть на ±90° (случайно) и идти до края. */ +static void turn_90(uint8_t i) +{ + uint8_t d = dir[i]; + uint8_t vert = (d == 0 || d == 3); + + set_dir(i, vert ? (uint8_t)(1 + (rand() & 1)) /* → влево/вправо */ + : (uint8_t)((rand() & 1) ? 0 : 3)); /* → вниз/вверх */ + go_edge(i); +} + +static void draw_field(void) +{ + int i; + + srand(4321); /* фон идентичен на обеих страницах */ + setfillstyle(SOLID_FILL, GREEN); + bar(0, 0, 319, 255); + for (i = 0; i < 400; i++) /* травинки */ + putpixel(rand() % 320, rand() % 244 + 12, + (i & 3) ? LIGHTGREEN : BROWN); + for (i = 0; i < 24; i++) { /* цветочки */ + int x = rand() % 312 + 4, y = rand() % 232 + 16; + putpixel(x, y, (i & 1) ? WHITE : YELLOW); + putpixel(x - 1, y, LIGHTGREEN); + putpixel(x + 1, y, LIGHTGREEN); + } + setcolor(WHITE); + outtextxy(52, 3, "RPGWALK: 8 bards on patrol, ESC"); +} + +int main(void) +{ + uint8_t i, page, hidden; + datetime_t dt; + + if (atlas_load(&at1, "bard1.atl") != 0 && + atlas_load(&at1, "a:\\bard1.atl") != 0) + return 1; + if (atlas_load(&at2, "bard2.atl") != 0 && + atlas_load(&at2, "a:\\bard2.atl") != 0) + return 1; + + initgraph(); + gfx_sprite_clip(0); /* маршруты в пределах */ + if (gfx_pal_fload(0, "bard.pal") < 0) + gfx_pal_fload(0, "a:\\bard.pal"); /* EGA + цвета PNG */ + gfx_pal_sync(); + + getdatetime(&dt); + srand((unsigned)(dt.second * 77u + dt.minute)); + + for (i = 0; i < NCHR; i++) { + /* Персонажи 0-3 из первого атласа, 4-7 из второго: спрайты + * лежат подряд по атласам — один OUT смены страницы на кадр. */ + atlas_sprite_init(&sp[i], (i < 4) ? &at1 : &at2, (uint8_t)(i & 3)); + sp[i].x = 24 + (rand() % (MAXX - 48)); + sp[i].y = MINY + 8 + (rand() % (MAXY - MINY - 16)); + set_dir(i, (uint8_t)(rand() & 3)); + go_edge(i); + sprite_show(&sp[i]); + } + + for (page = 0; page < 2; page++) { /* фон на обе страницы */ + gfx_set_draw_page(page); + draw_field(); + sprite_update(sp, NCHR); + } + gfx_set_visible_page(0); + getdatetime(&dt); + srand((unsigned)(dt.second * 77u + dt.minute)); + + for (;;) { + if (kbhit() && getch() == 27) + break; + + for (i = 0; i < NCHR; i++) + if (!sprite_moving(&sp[i])) { + if (phase[i] == 0) + go_back_random(i); /* у края: разворот */ + else + turn_90(i); /* дошли: поворот ±90° */ + } + + hidden = gfx_get_visible_page() ^ 1; + gfx_set_draw_page(hidden); + sprite_update(sp, NCHR); + gfx_wait_vsync(); + gfx_set_visible_page(hidden); + } + + closegraph(); + atlas_free(&at2); + atlas_free(&at1); + return 0; +}