libbgi: авто-анимация спрайтов — кадровая + tween (tests/spranim все PASS)

Реализация §9г по требованиям пользователя:
- sprite_anim(first,last,speed,mode): ANIM_LOOP / ANIM_PINGPONG /
  ANIM_ONCE (one-shot замирает на last), | ANIM_HORIZ для
  горизонтальных лент.  Смена кадра в тике = ±an_step к оси ленты —
  без умножений (осевое смещение first считается в setup циклом).
- sprite_moveto(tx,ty,max_step,interval): Брезенхэм порциями
  ≤max_step вдоль большей оси (меньшая — err-аккумулятором,
  нелинейные шаги Y сами собой), деления нет.
- Одновременность кадровой и tween — независимые поля/биты.
- Статус: sprite_anim_status() — битовое поле SPR_ANIM_ON/DONE +
  SPR_MOVE_ON/DONE; sprite_anim_frame() — текущий индекс кадра;
  sprite_moving().
- Стопы: sprite_anim_stop(frame | -1 = текущий);
  sprite_move_stop(0 = замереть / 1 = прыжок в цель + DONE).
- Тикер _sprite_tick — проход 0 sprite_update через funcptr
  _spr_tick_fn (DCE: без sprite_anim/moveto код не линкуется,
  цена — один if на кадр; +40 Б программе с движком, sprite_t +18 Б).

tests/spranim (MAME dev, все PASS): LOOP/PINGPONG (разворот на границе
без удвоения краёв)/ONCE+DONE; пример (0,0)→(100,50), шаг 5,
интервал 4 → ровно 80 кадров, Y идёт 3/2/3/2; стопы обоих видов.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 16:14:31 +03:00
parent 0e935343d9
commit eb9ca0179d
15 changed files with 506 additions and 4 deletions
+3 -2
View File
@@ -2,9 +2,9 @@
accfill 3772 accfill 3772
argv 3431 argv 3431
assrtest 3847 assrtest 3847
atlas 9154 atlas 9193
attrprob 4087 attrprob 4087
balls 6208 balls 6249
banked 1056 banked 1056
bankedbg 1067 bankedbg 1067
banklocl 4683 banklocl 4683
@@ -44,6 +44,7 @@ rt_test 4892
seek 4188 seek 4188
simple 955 simple 955
solidt 11531 solidt 11531
spranim 8752
spriteclip 4119 spriteclip 4119
sprites 6791 sprites 6791
stattest 7549 stattest 7549
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
2 accfill
3 argv
4 assrtest
5 atlas
6 attrprob
7 balls
8 banked
9 bankedbg
10 banklocl
44 seek
45 simple
46 solidt
47 spranim
48 spriteclip
49 sprites
50 stattest
+14 -2
View File
@@ -700,10 +700,22 @@ Probe-тест tests/w0page (MAME dev, 2026-07-13, ВСЕ PASS):
W1, но только tiny (в small/big/huge в W1 код) — отвергнута в пользу W1, но только tiny (в small/big/huge в W1 код) — отвергнута в пользу
универсального W0. универсального W0.
## 9г. Авто-анимация спрайтов (ПРЕДЛОЖЕНИЕ 2026-07-13, обсуждается) ## 9г. Авто-анимация спрайтов (РЕАЛИЗОВАНО 2026-07-13; tests/spranim PASS)
Идея пользователя: sprite_update (вызывается раз на кадр) сам тикает Идея пользователя: sprite_update (вызывается раз на кадр) сам тикает
анимации — приложение только описывает их декларативно. анимации — приложение только описывает их декларативно. Реализация:
sprite_anim(first,last,speed,mode: LOOP/PINGPONG/ONCE|HORIZ) /
sprite_anim_stop(frame|-1) / sprite_moveto(tx,ty,max_step,interval) /
sprite_move_stop(at_target) / статус sprite_anim_status() (биты
SPR_ANIM_ON/DONE, SPR_MOVE_ON/DONE) / sprite_anim_frame() /
sprite_moving(). Кадровая и tween независимы (одновременно — да);
тикер _sprite_tick подключается указателем _spr_tick_fn (DCE),
sprite_update зовёт его проходом 0. Кадровая — ±an_step к оси без
умножений; tween — Брезенхэм порциями ≤max_step по большей оси.
Проверено tests/spranim (T1 LOOP / T2 PINGPONG / T3 ONCE+DONE /
T4 пример (0,0)→(100,50)/5/4 = ровно 80 кадров, Y 3/2/3/2 /
T5 стопы) — все PASS. Цена: ~+40 Б программе с движком, sprite_t
+18 Б. Ниже — исходный дизайн-разбор.
### Кадровая анимация (смена изображений) ### Кадровая анимация (смена изображений)
+9
View File
@@ -155,6 +155,15 @@ void _gfx_w0_isr(void);
#define _ATL_DIR_OFF 0x68u /* каталог: count × 8 Б (макс 19) */ #define _ATL_DIR_OFF 0x68u /* каталог: count × 8 Б (макс 19) */
#define _ATL_DATA_OFF 0x100u /* ленты; = резерв ISR-стаба */ #define _ATL_DATA_OFF 0x100u /* ленты; = резерв ISR-стаба */
/* ---- Авто-анимация спрайтов (§9г) --------------------------------- *
* Тикер подключается указателем (DCE: программа без sprite_anim/
* sprite_moveto не тянет его код): дефолт NULL (_BSS), ставят
* sprite_anim()/sprite_moveto(). sprite_update зовёт в начале кадра.
* Типы из <sprite.h> здесь недоступны (подключается после) — void*. */
typedef void (*_spr_tick_fp)(void *arr, uint8_t count);
extern _spr_tick_fp _spr_tick_fn; /* common/_spr_tick_fn.c */
void _sprite_tick(void *arr, uint8_t count); /* common/_sprite_tick.c */
/* ---- Fill-burst'ы акселератора 256 (register-ABI, asm-only) ------ * /* ---- Fill-burst'ы акселератора 256 (register-ABI, asm-only) ------ *
* Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без * Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без
* глобального скретча в _DATA: * глобального скретча в _DATA:
+10
View File
@@ -0,0 +1,10 @@
/*
* _spr_tick_fn — указатель на тикер авто-анимаций (data-модуль).
* Дефолт NULL (_BSS, crt0 зануляет): sprite_update платит один if на
* кадр; sprite_anim()/sprite_moveto() ставят _sprite_tick — только
* тогда код тикера попадает в программу (DCE-паттерн funcptr, как
* clip/noclip).
*/
#include "../_bgi.h"
_spr_tick_fp _spr_tick_fn;
+94
View File
@@ -0,0 +1,94 @@
/*
* _sprite_tick — тикер авто-анимаций (§9г): вызывается из sprite_update
* (раз на КАДР, до heal/блит-проходов — dirty должен взводиться раньше).
* Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ (идут одновременно);
* тикаются и у скрытых спрайтов («время идёт», ветка дороже).
*
* Кадровая: смена кадра = ±an_step к оси ленты (sy; при _SPR_ANIM_HORZ
* — sx) — БЕЗ умножения; wrap цикла — прыжок оси в an_lo (посчитан
* sprite_anim'ом один раз). Маятник разворачивается НА границе (…2,3,
* 2,1,0,1…, без удвоения крайних). ANIM_ONCE замирает на last:
* SPR_ANIM_ON снимается, SPR_ANIM_DONE взводится.
*
* Перемещение: раз в mv_speed кадров — порция ≤mv_step итераций
* классического Брезенхэма (err-аккумулятор: большая ось шагает каждую
* итерацию, меньшая подтягивается — нелинейные 2/3/2/3 сами собой,
* деления нет). Цель достигнута — SPR_MOVE_ON→off, SPR_MOVE_DONE.
*/
#include "../_bgi.h"
#include <sprite.h>
void _sprite_tick(void *arr, uint8_t count)
{
sprite_t *s = (sprite_t *)arr;
uint8_t i;
for (i = 0; i < count; i++, s++) {
/* ---- кадровая анимация ---- */
if ((s->an_flags & SPR_ANIM_ON) && --s->an_timer == 0) {
int *axis = (s->an_flags & _SPR_ANIM_HORZ) ? &s->sx : &s->sy;
uint8_t moved = 1;
s->an_timer = s->an_speed;
if (!(s->an_flags & _SPR_ANIM_BACK)) {
if (s->an_frame < s->an_last) {
s->an_frame++;
*axis += s->an_step;
} else if (s->an_flags & _SPR_ANIM_ONCE) {
s->an_flags &= (uint8_t)~SPR_ANIM_ON;
s->an_flags |= SPR_ANIM_DONE;
moved = 0;
} else if (s->an_flags & _SPR_ANIM_PP) {
s->an_flags |= _SPR_ANIM_BACK; /* разворот на границе */
if (s->an_frame > s->an_first) {
s->an_frame--;
*axis -= s->an_step;
} else
moved = 0; /* лента из 1 кадра */
} else { /* ANIM_LOOP: wrap */
s->an_frame = s->an_first;
*axis = s->an_lo;
}
} else { /* маятник, назад */
if (s->an_frame > s->an_first) {
s->an_frame--;
*axis -= s->an_step;
} else {
s->an_flags &= (uint8_t)~_SPR_ANIM_BACK;
if (s->an_frame < s->an_last) {
s->an_frame++;
*axis += s->an_step;
} else
moved = 0;
}
}
if (moved)
s->flags |= _SPR_DIRTY;
}
/* ---- tween-перемещение ---- */
if ((s->an_flags & SPR_MOVE_ON) && --s->mv_timer == 0) {
uint8_t n = s->mv_step;
int x = s->x, y = s->y;
s->mv_timer = s->mv_speed;
while (n--) {
int e2;
if (x == s->mv_tx && y == s->mv_ty)
break;
e2 = s->mv_err * 2;
if (e2 >= s->mv_dy) { s->mv_err += s->mv_dy; x += s->mv_sgnx; }
if (e2 <= s->mv_dx) { s->mv_err += s->mv_dx; y += s->mv_sgny; }
}
if (x != s->x || y != s->y) {
s->x = x;
s->y = y;
s->flags |= _SPR_DIRTY;
}
if (x == s->mv_tx && y == s->mv_ty) {
s->an_flags &= (uint8_t)~SPR_MOVE_ON;
s->an_flags |= SPR_MOVE_DONE;
}
}
}
}
+2
View File
@@ -25,6 +25,8 @@ void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx)
s->sx = 0; s->sy = 0; s->sx = 0; s->sy = 0;
s->flags = 0; s->flags = 0;
s->page = a->page; s->page = a->page;
s->an_flags = 0; /* авто-анимации выключены */
s->an_frame = 0;
s->drawn[0].on = 0; s->drawn[0].on = 0;
s->drawn[1].on = 0; s->drawn[1].on = 0;
} }
+54
View File
@@ -0,0 +1,54 @@
/*
* sprite_anim — запустить кадровую авто-анимацию: кадры first..last
* вдоль оси ленты (вертикальная; ANIM_HORIZ — вдоль X), смена раз в
* speed кадров, mode = ANIM_LOOP / ANIM_PINGPONG / ANIM_ONCE.
*
* Всё дорогое считается ЗДЕСЬ один раз (не в тике): шаг оси = размер
* кадра, осевое смещение first — циклом сложений (без __mulint).
* Ставит текущий кадр на first, сбрасывает SPR_ANIM_DONE, подключает
* тикер (_spr_tick_fn) — до первого вызова sprite_anim/sprite_moveto
* код тикера в программу не линкуется. Требование: индексы ≤ 127,
* first ≤ last. Move-статусы (SPR_MOVE_*) не трогаются.
*/
#include "../_bgi.h"
#include <sprite.h>
void sprite_anim(sprite_t *s, uint8_t first, uint8_t last,
uint8_t speed, uint8_t mode)
{
uint8_t f = s->an_flags & (SPR_MOVE_ON | SPR_MOVE_DONE);
uint8_t step = (mode & ANIM_HORIZ) ? s->w : s->h;
int lo = 0;
uint8_t i;
for (i = 0; i < first; i++)
lo += step; /* first*step без умножения */
if (!speed)
speed = 1;
s->an_step = step;
s->an_lo = lo;
s->an_first = first;
s->an_last = last;
s->an_frame = first;
s->an_speed = speed;
s->an_timer = speed;
f |= SPR_ANIM_ON;
if (mode & ANIM_HORIZ)
f |= _SPR_ANIM_HORZ;
if ((mode & 0x03u) == ANIM_PINGPONG)
f |= _SPR_ANIM_PP;
if ((mode & 0x03u) == ANIM_ONCE)
f |= _SPR_ANIM_ONCE;
s->an_flags = f;
/* Ось — на кадр first (обычный путь sprite_frame: dirty при смене). */
if (mode & ANIM_HORIZ)
sprite_frame(s, lo, s->sy);
else
sprite_frame(s, s->sx, lo);
_spr_tick_fn = _sprite_tick;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* sprite_anim_stop — остановить кадровую анимацию. frame ≥ 0 — какой
* кадр останется на экране (индекс вдоль оси ленты, ≤127); -1 —
* замереть на текущем. Ось кадра считается циклом сложений (стоп —
* редкий вызов, __mulint не тянем). Валидна после sprite_anim
* (an_step/ось настроены им); SPR_ANIM_DONE не трогает.
*/
#include "../_bgi.h"
#include <sprite.h>
void sprite_anim_stop(sprite_t *s, int8_t frame)
{
s->an_flags &= (uint8_t)~(SPR_ANIM_ON | _SPR_ANIM_BACK);
if (frame >= 0 && (uint8_t)frame != s->an_frame) {
int off = 0;
uint8_t i;
for (i = 0; i < (uint8_t)frame; i++)
off += s->an_step;
s->an_frame = (uint8_t)frame;
if (s->an_flags & _SPR_ANIM_HORZ)
sprite_frame(s, off, s->sy);
else
sprite_frame(s, s->sx, off);
}
}
+2
View File
@@ -23,6 +23,8 @@ void sprite_init(sprite_t *s, const void *img)
s->h = p[2]; s->h = p[2];
s->flags = 0; s->flags = 0;
s->page = 0; /* img в обычной памяти (не W0-атлас) */ s->page = 0; /* img в обычной памяти (не W0-атлас) */
s->an_flags = 0; /* авто-анимации выключены */
s->an_frame = 0;
s->drawn[0].on = 0; s->drawn[0].on = 0;
s->drawn[1].on = 0; s->drawn[1].on = 0;
} }
+19
View File
@@ -0,0 +1,19 @@
/*
* sprite_move_stop — остановить tween-перемещение: at_target = 0 —
* замереть в текущих координатах (SPR_MOVE_DONE НЕ взводится — спрайт
* цели не достиг); 1 — прыгнуть в цель и взвести SPR_MOVE_DONE.
* Ничего не делает, если перемещение не шло.
*/
#include "../_bgi.h"
#include <sprite.h>
void sprite_move_stop(sprite_t *s, uint8_t at_target)
{
if (!(s->an_flags & SPR_MOVE_ON))
return;
s->an_flags &= (uint8_t)~SPR_MOVE_ON;
if (at_target) {
sprite_move(s, s->mv_tx, s->mv_ty);
s->an_flags |= SPR_MOVE_DONE;
}
}
+46
View File
@@ -0,0 +1,46 @@
/*
* sprite_moveto — запустить tween-перемещение к (tx,ty): раз в interval
* кадров тикер делает до max_step итераций Брезенхэма (большая ось —
* итерация = пиксель; меньшая подтягивается err-аккумулятором).
* Пример: (0,0)→(100,50), max_step 5, interval 4 → 20 шагов × 4 кадра
* = 80 кадров (1.6 с), Y идёт 2/3/2/3.
*
* Дельты/знаки/аккумулятор считаются здесь один раз. Если цель ==
* текущая позиция — сразу SPR_MOVE_DONE (тикер не включается).
* Кадровые статусы (SPR_ANIM_*) не трогаются.
*/
#include "../_bgi.h"
#include <sprite.h>
void sprite_moveto(sprite_t *s, int tx, int ty,
uint8_t max_step, uint8_t interval)
{
int dx = tx - s->x;
int dy = ty - s->y;
s->mv_sgnx = 1;
if (dx < 0) { dx = -dx; s->mv_sgnx = -1; }
s->mv_sgny = 1;
if (dy < 0) { dy = -dy; s->mv_sgny = -1; }
s->an_flags &= (uint8_t)~SPR_MOVE_DONE;
if (dx == 0 && dy == 0) {
s->an_flags = (s->an_flags & (uint8_t)~SPR_MOVE_ON) | SPR_MOVE_DONE;
return;
}
s->mv_tx = tx;
s->mv_ty = ty;
s->mv_dx = dx;
s->mv_dy = -dy;
s->mv_err = dx - dy;
if (!max_step) max_step = 1;
if (!interval) interval = 1;
s->mv_step = max_step;
s->mv_speed = interval;
s->mv_timer = interval;
s->an_flags |= SPR_MOVE_ON;
_spr_tick_fn = _sprite_tick;
}
+6
View File
@@ -35,6 +35,12 @@ void sprite_update(sprite_t *arr, uint8_t count)
uint8_t i; uint8_t i;
sprite_t *s; sprite_t *s;
/* Проход 0: тик авто-анимаций (§9г) — ДО heal/блита (двигает x/y и
* ось кадра, взводя dirty). Указатель ставят sprite_anim/
* sprite_moveto; без них — один if на кадр, код тикера не линкуется. */
if (_spr_tick_fn)
(*_spr_tick_fn)(arr, count);
/* Проход 1: heal ВСЕХ старых позиций, требующих обновления. */ /* Проход 1: heal ВСЕХ старых позиций, требующих обновления. */
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin(); _bgi_begin();
+74
View File
@@ -46,6 +46,26 @@
#define _SPR_DIRTY1 0x08u #define _SPR_DIRTY1 0x08u
#define _SPR_DIRTY (_SPR_DIRTY0 | _SPR_DIRTY1) #define _SPR_DIRTY (_SPR_DIRTY0 | _SPR_DIRTY1)
/* Статус-биты sprite_t.an_flags (чтение — sprite_anim_status()):
* *_ON — анимация активна (тикается в sprite_update); *_DONE — дошла
* до конца сама (one-shot до last / перемещение до цели) или была
* добита sprite_move_stop(at_target=1). DONE сбрасывается новым
* запуском соответствующей анимации. */
#define SPR_ANIM_ON 0x01u /* кадровая анимация идёт */
#define SPR_ANIM_DONE 0x02u /* one-shot дошёл до last */
#define SPR_MOVE_ON 0x04u /* tween-перемещение идёт */
#define SPR_MOVE_DONE 0x08u /* перемещение достигло цели */
#define _SPR_ANIM_BACK 0x10u /* внутр.: маятник идёт назад */
#define _SPR_ANIM_HORZ 0x20u /* внутр.: лента горизонтальная (ось sx) */
#define _SPR_ANIM_PP 0x40u /* внутр.: режим маятника */
#define _SPR_ANIM_ONCE 0x80u /* внутр.: режим one-shot */
/* Режимы sprite_anim(). */
#define ANIM_LOOP 0u /* 0,1,..,last,first,.. (цикл) */
#define ANIM_PINGPONG 1u /* 0,1,..,last,last-1,..,first,1,.. */
#define ANIM_ONCE 2u /* first..last, замереть на last + DONE */
#define ANIM_HORIZ 0x80u /* ИЛИ-флаг: кадры вдоль X (сетка nx>1) */
typedef struct { typedef struct {
const void *img; /* getimage-формат / атлас-лента (вне W3) */ const void *img; /* getimage-формат / атлас-лента (вне W3) */
int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */ int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */
@@ -55,6 +75,19 @@ typedef struct {
uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница * uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница *
* W0-атласа (img — адрес 0x0100-0x3FFF, движок * * W0-атласа (img — адрес 0x0100-0x3FFF, движок *
* сам мапит страницу в W0 на время блита) */ * сам мапит страницу в W0 на время блита) */
/* --- авто-анимация (владеет тикер; настраивать ТОЛЬКО через
* sprite_anim../sprite_moveto.., читать — inline-аксессорами) --- */
uint8_t an_flags; /* статус: SPR_ANIM_.. | SPR_MOVE_.. */
uint8_t an_frame; /* ТЕКУЩИЙ индекс кадра (0-based по ленте) */
uint8_t an_first, an_last; /* границы анимации (индексы) */
uint8_t an_speed, an_timer; /* кадров между сменами / счётчик */
uint8_t an_step; /* шаг оси = fh (верт. лента) или fw (гориз.) */
int an_lo; /* осевое смещение кадра first (для wrap) */
uint8_t mv_speed, mv_timer; /* кадров между шагами / счётчик */
uint8_t mv_step; /* макс. пикселей вдоль большей оси за шаг */
int mv_tx, mv_ty; /* цель */
int mv_dx, mv_dy, mv_err;/* Брезенхэм: dx, -dy, аккумулятор */
int8_t mv_sgnx, mv_sgny; /* знаки шага осей */
/* --- внутреннее (владеет движок) --- */ /* --- внутреннее (владеет движок) --- */
struct { struct {
int x, y; /* где нарисован на странице p */ int x, y; /* где нарисован на странице p */
@@ -107,6 +140,47 @@ inline void sprite_hide(sprite_t *s) {
* активный спрайт прошёл над статиком — приложение помечает статик). */ * активный спрайт прошёл над статиком — приложение помечает статик). */
inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; } inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; }
/* ==== Авто-анимация (§9г дизайна) ====================================
* Тикается САМА в начале каждого sprite_update (раз на кадр), код
* тикера подключается через funcptr — программа без анимаций его не
* линкует. Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ и могут
* идти одновременно. Пока кадровая активна — не трогать sprite_frame
* вручную (тикер ведёт ось сам); границы кадров должны лежать вдоль
* ОДНОЙ оси ленты (вертикальная по умолчанию, ANIM_HORIZ — вдоль X). */
/* Запустить кадровую анимацию: кадры first..last (индексы вдоль оси
* ленты), смена раз в speed кадров (1 = каждый кадр), mode = ANIM_LOOP/
* ANIM_PINGPONG/ANIM_ONCE (| ANIM_HORIZ для горизонтальной ленты).
* Старт с first; сбрасывает SPR_ANIM_DONE. */
void sprite_anim(sprite_t *s, uint8_t first, uint8_t last,
uint8_t speed, uint8_t mode);
/* Остановить кадровую анимацию: frame = индекс кадра, который останется
* на экране, или -1 — замереть на текущем. */
void sprite_anim_stop(sprite_t *s, int8_t frame);
/* Запустить перемещение к (tx,ty): раз в interval кадров — шаг до
* max_step пикселей вдоль БОЛЬШЕЙ оси (меньшая тянется Брезенхэмом,
* без деления). Сбрасывает SPR_MOVE_DONE. Пример: (0,0)→(100,50),
* max_step 5, interval 4 → 20 шагов × 4 кадра = 80 кадров (1.6 с). */
void sprite_moveto(sprite_t *s, int tx, int ty,
uint8_t max_step, uint8_t interval);
/* Остановить перемещение: at_target = 0 — замереть где есть;
* 1 — прыгнуть в цель (и взвести SPR_MOVE_DONE). */
void sprite_move_stop(sprite_t *s, uint8_t at_target);
/* Статус анимаций: битовое поле SPR_ANIM_ON/DONE | SPR_MOVE_ON/DONE. */
inline uint8_t sprite_anim_status(const sprite_t *s) { return s->an_flags; }
/* Текущий индекс кадра ленты (ведёт тикер; валиден и после stop). */
inline uint8_t sprite_anim_frame(const sprite_t *s) { return s->an_frame; }
/* Перемещение ещё идёт? */
inline uint8_t sprite_moving(const sprite_t *s) {
return s->an_flags & SPR_MOVE_ON;
}
/* ==== Атласы в EMM-страницах (W0) ==================================== /* ==== Атласы в EMM-страницах (W0) ====================================
* Файл .atl (docs/sprite-api-design.md §3.1, вариант II): 0x100-байтовый * Файл .atl (docs/sprite-api-design.md §3.1, вариант II): 0x100-байтовый
* заголовок (магия 'SPA1', count; каталог с 0x68) + getimage-ленты; * заголовок (магия 'SPA1', count; каталог с 0x68) + getimage-ленты;
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := spranim
EXTRA_FLAGS ?= --gfx 256 --memory tiny
include $(PROJ_ROOT)/app.mk
+142
View File
@@ -0,0 +1,142 @@
/*
* spranim — тест авто-анимаций спрайтового движка (§9г):
* sprite_anim (LOOP/PINGPONG/ONCE) + sprite_moveto + статусы + стопы.
*
* Логика проверяется ПРОГРАММНО по полям sprite_t: спрайты невидимы,
* тикер работает независимо от видимости, блитов нет — sprite_update
* гоняется в текстовом режиме (скобка W3 безвредна). Ожидания:
*
* T1 LOOP 0..3 speed=2: смена раз в 2 тика: после 8 тиков frame
* снова 0 (0→1→2→3→0), sy вернулся в 0.
* T2 PINGPONG 0..3 speed=1: 1,2,3,2,1,0,1 — на тике 7 frame==1,
* разворот на границе без удвоения краёв.
* T3 ONCE 0..2 speed=1: после 2 тиков frame==2 и ANIM_ON ещё стоит,
* после 3-го — ON снят, DONE взведён, frame==2 (замер на last).
* T4 MOVETO (0,0)→(100,50) step=5 interval=4 (пример из §9г):
* тик 4: (5,3); тик 8: (10,5); тик 79: НЕ DONE; тик 80: (100,50),
* MOVE_ON снят, MOVE_DONE взведён. Параллельно у спрайта идёт
* LOOP-анимация (одновременность) — frame успел смениться.
* T5 sprite_anim_stop(1): ON снят, frame==1, ось = 16.
* sprite_move_stop(at_target=1) на полпути: прыжок в цель + DONE.
*
* Кадры: RAM-лента 16×16×4 (getimage), кадр k залит k — содержимое
* не важно (блитов нет), важен размер h=16 → an_step.
*/
#include <stdio.h>
#include <string.h>
#include <sprite.h>
static uint8_t strip[4 + 16 * 16 * 4];
static sprite_t sp[4];
static void build_strip(void)
{
memset(strip, 0, sizeof strip);
strip[0] = 16; strip[2] = 64; /* w=16, h=64 (4 кадра) */
}
static void tick(sprite_t *arr, uint8_t n, uint16_t times)
{
while (times--)
sprite_update(arr, n);
}
int main(void)
{
uint8_t ok;
puts("SPRANIM: frame/tween auto-animation");
build_strip();
/* ---- T1: LOOP ---------------------------------------------------- */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 3, 2, ANIM_LOOP);
tick(sp, 1, 2);
ok = (sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16);
tick(sp, 1, 4); /* всего 6: 0→1→2→3 */
ok = ok && (sprite_anim_frame(&sp[0]) == 3 && sp[0].sy == 48);
tick(sp, 1, 2); /* всего 8: wrap → 0 */
ok = ok && (sprite_anim_frame(&sp[0]) == 0 && sp[0].sy == 0);
printf("T1 LOOP: %s\n", ok ? "PASS" : "FAIL");
/* ---- T2: PINGPONG ------------------------------------------------ */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 3, 1, ANIM_PINGPONG);
tick(sp, 1, 3); /* 1,2,3 */
ok = (sprite_anim_frame(&sp[0]) == 3);
tick(sp, 1, 1); /* разворот: 2 */
ok = ok && (sprite_anim_frame(&sp[0]) == 2 && sp[0].sy == 32);
tick(sp, 1, 2); /* 1,0 */
ok = ok && (sprite_anim_frame(&sp[0]) == 0);
tick(sp, 1, 1); /* разворот: 1 */
ok = ok && (sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16);
printf("T2 PINGPONG: %s\n", ok ? "PASS" : "FAIL");
/* ---- T3: ONCE ---------------------------------------------------- */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 2, 1, ANIM_ONCE);
tick(sp, 1, 2); /* 1,2 — дошли до last */
ok = (sprite_anim_frame(&sp[0]) == 2 &&
(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
!(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE));
tick(sp, 1, 1); /* фиксация: ON→off,DONE */
ok = ok && (sprite_anim_frame(&sp[0]) == 2 && sp[0].sy == 32 &&
!(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE));
tick(sp, 1, 5); /* стоит на месте */
ok = ok && (sprite_anim_frame(&sp[0]) == 2);
printf("T3 ONCE: %s\n", ok ? "PASS" : "FAIL");
/* ---- T4: MOVETO + одновременная кадровая ------------------------- */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16; /* x=y=0 после init */
sprite_anim(&sp[0], 0, 3, 3, ANIM_LOOP);
sprite_moveto(&sp[0], 100, 50, 5, 4);
ok = (sprite_moving(&sp[0]) != 0);
tick(sp, 1, 4);
ok = ok && (sp[0].x == 5 && sp[0].y == 3); /* Y: 3/2/3/2 */
tick(sp, 1, 4);
ok = ok && (sp[0].x == 10 && sp[0].y == 5);
tick(sp, 1, 71); /* всего 79 — ещё едет */
ok = ok && sprite_moving(&sp[0]) &&
!(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE);
tick(sp, 1, 1); /* 80: приехали */
ok = ok && (sp[0].x == 100 && sp[0].y == 50 &&
!sprite_moving(&sp[0]) &&
(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
ok = ok && (sprite_anim_frame(&sp[0]) != 0 ||
sp[0].an_timer != 3); /* кадровая тоже тикала */
printf("T4 MOVETO: %s (x=%d y=%d)\n",
ok ? "PASS" : "FAIL", sp[0].x, sp[0].y);
/* ---- T5: стопы ---------------------------------------------------- */
sprite_init(&sp[0], strip);
sp[0].w = 16; sp[0].h = 16;
sprite_anim(&sp[0], 0, 3, 1, ANIM_LOOP);
tick(sp, 1, 2); /* frame 2 */
sprite_anim_stop(&sp[0], 1); /* остановить на кадре 1 */
ok = (!(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16);
tick(sp, 1, 4); /* не двигается */
ok = ok && (sprite_anim_frame(&sp[0]) == 1);
sprite_moveto(&sp[0], 60, 60, 3, 1);
tick(sp, 1, 5); /* на полпути */
sprite_move_stop(&sp[0], 1); /* добить в цель */
ok = ok && (sp[0].x == 60 && sp[0].y == 60 &&
!sprite_moving(&sp[0]) &&
(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
sprite_moveto(&sp[0], 0, 0, 3, 1);
tick(sp, 1, 5);
sprite_move_stop(&sp[0], 0); /* замереть где есть */
ok = ok && (sp[0].x != 0 && !sprite_moving(&sp[0]) &&
!(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
printf("T5 STOPS: %s\n", ok ? "PASS" : "FAIL");
puts("done");
return 0;
}