/*
 * _sprite_tick — тикер авто-анимаций (§9г): вызывается из sprite_update
 * (раз на КАДР, до heal/блит-проходов — dirty должен взводиться раньше).
 * Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ (идут одновременно);
 * тикаются и у скрытых спрайтов («время идёт»).
 *
 * ГОРЯЧИЙ ПУТЬ (профиль rpgwalk 2026-07-13: тик ел 7.3К тактов/спрайт —
 * №2 после блита; переписан 2026-07-14):
 *   - один ранний выход на спрайт без активных анимаций;
 *   - декремент таймеров — в цикле, до call'а: недозревший таймер не
 *     платит за вызов хелпера;
 *   - сама смена кадра/порция перемещения — в static-хелперах
 *     tick_anim/tick_move: читаемость + SDCC работает с ЛОКАЛЬНЫМИ
 *     копиями полей в регистрах вместо IX-обращений (~19Т каждое)
 *     на каждый чих;
 *   - кадровая анимация двигает КЭШ АДРЕСА кадра (s->src ± an_step
 *     байт; wrap — прыжок на готовый an_src0) — ни выбора оси, ни
 *     пиксельных смещений: всё посчитано sprite_anim'ом один раз.
 *
 * Кадровая: маятник разворачивается НА границе (…2,3,2,1,0,1… — без
 * удвоения крайних); ANIM_ONCE замирает на last: SPR_ANIM_ON снимается,
 * SPR_ANIM_DONE взводится.  sx/sy НЕ обновляются (см. sprite.h: их
 * абсолютно переустановит следующий sprite_frame/sprite_anim).
 *
 * Перемещение: раз в mv_speed кадров — порция ≤mv_step шагов главной
 * оси беззнакового DDA (минорная подтягивается аккумулятором —
 * нелинейные 2/3/2/3 сами собой, деления нет; знаковых 16-бит
 * сравнений Брезенхэма нет — на Z80 они были главной статьёй расхода).
 * Цель достигнута (mv_rem == 0) — SPR_MOVE_ON→off, SPR_MOVE_DONE.
 */
#include "../_bgi.h"
#include <sprite.h>

/* Одна смена кадра кадровой анимации (таймер уже дозрел).
 *
 * РУЧНОЙ ASM (2026-07-14): та же болезнь SDCC, что у tick_move —
 * C-версия спиллилась в IX-фрейм (~120 обращений, ~4-5К тактов на
 * вызов); asm ~1К.  При медленных анимациях (speed ≥ 5) вклад мал,
 * но при speed 1-2 tick_anim зовётся на каждый кадр для каждого
 * спрайта и становился главной статьёй тика.
 *
 * C-эквивалент (семантика 1:1, тест tests/spranim T1/T2/T3/T6):
 *
 *   s->an_timer = s->an_speed;
 *   if (!(fl & PP_BACK)) {                     // вперёд
 *       if (fr < last)            { fr++; src += step; }
 *       else if (fl & ONCE)       { ON→off, DONE→on; return; } // не грязнить
 *       else if (fl & PP)         { BACK→on;   // разворот на границе:
 *           if (fr > first) { fr--; src -= step; } else нет-шага; }
 *       else ..LOOP..             { fr = first; src = an_src0; }
 *   } else {                                   // маятник, обратно
 *       if (fr > first)           { fr--; src -= step; }
 *       else { BACK→off;                       // разворот на first
 *           if (fr < last) { fr++; src += step; } else нет-шага; }
 *   }
 *   если шаг был: an_frame = fr; s->src = src; flags |= DIRTY;
 *
 * Офсеты полей sprite_t (см. пробник в шапке tick_move):
 *   src=2 flags=16 an_flags=18 an_frame=19 an_first=20 an_last=21
 *   an_speed=22 an_timer=23 an_step=24 an_src0=26
 * Биты an_flags: ON=0x01 DONE=0x02 PP_BACK=0x10 PP=0x40 ONCE=0x80.
 * Вход __sdcccall(1): s → HL.  IX callee-saved.
 */
static void tick_anim(sprite_t *s) __naked
{
    (void)s;
    __asm
        push  ix
        push  hl
        pop   ix                  ; IX = s

        ;; an_timer = an_speed — перезарядить темп
        ld    a, 22 (ix)
        ld    23 (ix), a

        ld    b, 19 (ix)          ; B = fr (текущий кадр)
        ld    c, 18 (ix)          ; C = an_flags
        bit   4, c                ; _SPR_PP_BACK?
        jr    NZ, ta_backward

        ;; ---- ход вперёд (LOOP/ONCE всегда; маятник — фаза «туда») ----
        ld    a, b
        cp    a, 21 (ix)          ; CF = (fr < an_last)
        jr    NC, ta_at_last
        inc   b                   ; шаг вперёд: fr++
        call  ta_src_fwd              ; src += an_step
        jr    ta_commit              ; commit
ta_at_last:
        ;; fr == last: конец отрезка — по режиму
        bit   7, c                ; _SPR_ANIM_ONCE?
        jr    Z, ta_not_once
        ;; ONCE: замереть на last, отчитаться, НЕ грязнить
        ld    a, c
        and   a, #0xFE            ; ~SPR_ANIM_ON
        or    a, #0x02            ; SPR_ANIM_DONE
        ld    18 (ix), a
        jr    ta_done              ; выход без dirty
ta_not_once:
        bit   6, c                ; _SPR_ANIM_PP?
        jr    Z, ta_loop_wrap
        ;; маятник: разворот на границе — сразу шаг назад
        ;; (…2,3,2,… — крайний кадр не удваивается)
        set   4, c                ; _SPR_PP_BACK
        ld    18 (ix), c
        ld    a, 20 (ix)          ; an_first
        cp    a, b                ; CF = (first < fr)
        jr    NC, ta_done          ; лента из 1 кадра — шага нет
        dec   b
        call  ta_src_back              ; src -= an_step
        jr    ta_commit
ta_loop_wrap:
        ;; LOOP: wrap — прыжок на заранее посчитанный first
        ld    b, 20 (ix)          ; fr = an_first
        ld    l, 26 (ix)
        ld    h, 27 (ix)          ; HL = an_src0
        ld    2 (ix), l
        ld    3 (ix), h           ; src = an_src0
        jr    ta_commit_fr              ; commit (src уже записан)

        ;; ---- маятник, фаза «обратно» ----
ta_backward:
        ld    a, 20 (ix)          ; an_first
        cp    a, b                ; CF = (first < fr)
        jr    NC, ta_turn_fwd
        dec   b                   ; шаг назад
        call  ta_src_back              ; src -= an_step
        jr    ta_commit
ta_turn_fwd:
        ;; на first: разворот — сразу шаг вперёд
        res   4, c                ; снять _SPR_PP_BACK
        ld    18 (ix), c
        ld    a, b
        cp    a, 21 (ix)          ; CF = (fr < an_last)
        jr    NC, ta_done          ; лента из 1 кадра — шага нет
        inc   b
        call  ta_src_fwd              ; src += an_step

        ;; ---- commit: кадр сменился ----
ta_commit:                           ; (src записан хелпером)
ta_commit_fr:
        ld    19 (ix), b          ; an_frame = fr
        ld    a, 16 (ix)
        or    a, #0x0C            ; flags |= _SPR_DIRTY
        ld    16 (ix), a
ta_done:
        pop   ix
        ret

        ;; ---- хелперы: s->src ± an_step; клоббер A,DE,HL ----
ta_src_fwd:                           ; src += an_step
        ld    l, 2 (ix)
        ld    h, 3 (ix)
        ld    e, 24 (ix)
        ld    d, 25 (ix)
        add   hl, de
        ld    2 (ix), l
        ld    3 (ix), h
        ret
ta_src_back:                           ; src -= an_step
        ld    l, 2 (ix)
        ld    h, 3 (ix)
        ld    e, 24 (ix)
        ld    d, 25 (ix)
        or    a, a                ; CF = 0
        sbc   hl, de
        ld    2 (ix), l
        ld    3 (ix), h
        ret
    __endasm;
}

/* Одна порция tween-перемещения (таймер уже дозрел): до mv_step шагов
 * главной оси DDA (см. sprite_moveto: вся арифметика беззнаковая,
 * «приехали» = mv_rem == 0 — сравнений координат нет вообще).
 *
 * РУЧНОЙ ASM (2026-07-14): SDCC на этой функции спиллил ВСЁ в IX-фрейм
 * (~100 IX-обращений по ~19Т, ~6.5К тактов на вызов при порции в один
 * шаг) — и никакие реструктуризации C этого не лечили.  asm держит
 * горячее в регистрах: ~2К тактов на вызов.
 *
 * C-эквивалент (семантика 1:1, тест tests/spranim):
 *
 *   uint8_t n = s->mv_step;                    // порция...
 *   if (n > s->mv_rem) n = s->mv_rem;          // ...не длиннее остатка
 *   s->mv_rem -= n;
 *   s->mv_timer = s->mv_speed;
 *   mins = 0;                                  // шагов минорной оси
 *   for (k = n; k--; ) {                       // DDA-аккумулятор
 *       acc += dmin;
 *       if (acc >= dmaj) { acc -= dmaj; mins++; }
 *   }
 *   major_coord += sgnmaj*n;  minor_coord += sgnmin*mins;
 *   s->flags |= _SPR_DIRTY;
 *   if (!s->mv_rem) { MOVE_ON→off; MOVE_DONE→on; }
 *
 * Офсеты полей sprite_t — сверены offsetof-пробником по фактическому
 * кодогену SDCC 4.5 (поля пакуются подряд, выравнивания нет):
 *   x=6 y=8 flags=16 an_flags=18 mv_speed=28 mv_timer=29 mv_step=30
 *   mv_rem=35 mv_acc=37 mv_dmaj=39 mv_dmin=41 mv_ymaj=43
 *   mv_sgnmaj=44 mv_sgnmin=45
 * ПРИ ИЗМЕНЕНИИ ПОРЯДКА ПОЛЕЙ В sprite.h — ПЕРЕСЧИТАТЬ (см. заметку
 * у sprite_t).  Вход __sdcccall(1): s → HL.  IX callee-saved.
 */
static void tick_move(sprite_t *s) __naked
{
    (void)s;
    __asm
        push  ix
        push  hl
        pop   ix                  ; IX = s

        ;; mv_timer = mv_speed — перезарядить темп
        ld    a, 28 (ix)
        ld    29 (ix), a

        ;; B = n = min(mv_step, mv_rem);  HL = rem
        ld    b, 30 (ix)
        ld    l, 35 (ix)
        ld    h, 36 (ix)
        ld    a, h
        or    a, a
        jr    NZ, tm_n_ok          ; rem >= 256 — заведомо >= n
        ld    a, l
        cp    a, b
        jr    NC, tm_n_ok          ; rem >= n — порция целиком
        ld    b, l                ; хвост маршрута короче: n = rem
tm_n_ok:
        ;; rem -= n (заём в старший байт вручную)
        ld    a, l
        sub   a, b
        ld    l, a
        jr    NC, tm_rem_stored
        dec   h
tm_rem_stored:
        ld    35 (ix), l
        ld    36 (ix), h

        push  bc                  ; сохранить n (в B) на время цикла

        ;; ---- DDA минорной оси: n прогонов аккумулятора ----
        ;; HL = acc, DE = dmin, C = mins (счётчик шагов минорной), B = k
        ld    l, 37 (ix)
        ld    h, 38 (ix)
        ld    e, 41 (ix)
        ld    d, 42 (ix)
        ld    c, #0
tm_dda_loop:
        add   hl, de              ; acc += dmin
        ;; беззнаковое acc >= dmaj?
        ld    a, l
        sub   a, 39 (ix)
        ld    a, h
        sbc   a, 40 (ix)
        jr    C, tm_dda_next           ; acc < dmaj — минорная стоит
        ;; acc -= dmaj; mins++
        ld    a, l
        sub   a, 39 (ix)
        ld    l, a
        ld    a, h
        sbc   a, 40 (ix)
        ld    h, a
        inc   c
tm_dda_next:
        djnz  tm_dda_loop
        ld    37 (ix), l
        ld    38 (ix), h

        ld    e, c                ; E = mins
        pop   bc                  ; B = n

        ;; ---- применить порцию к координатам ----
        ;; sgn* = ±1: rlca выносит знак в CF (0xFF → CF=1)
        ld    a, 43 (ix)          ; ymaj?
        or    a, a
        jr    NZ, tm_ymaj

        ;; --- главная ось X (поле +6), минорная Y (+8) ---
        ld    a, 44 (ix)          ; sgnmaj
        rlca
        ld    a, b                ; A = n
        jr    NC, tm_xmaj_n_set
        neg                       ; A = -n
tm_xmaj_n_set:
        push  ix
        pop   hl
        ld    bc, #6
        add   hl, bc              ; HL = &x
        call  tm_add16              ; x += ±n
        ld    a, e
        or    a, a
        jp    Z, tm_mark_dirty           ; минорная не шагала (далеко: jp)
        ld    a, 45 (ix)          ; sgnmin
        rlca
        ld    a, e                ; A = mins
        jr    NC, tm_xmaj_min_set
        neg
tm_xmaj_min_set:
        push  ix
        pop   hl
        ld    bc, #8
        add   hl, bc              ; HL = &y
        call  tm_add16              ; y += ±mins
        jp    tm_mark_dirty

        ;; --- главная ось Y (+8), минорная X (+6) ---
tm_ymaj:
        ld    a, 44 (ix)          ; sgnmaj
        rlca
        ld    a, b
        jr    NC, tm_ymaj_n_set
        neg
tm_ymaj_n_set:
        push  ix
        pop   hl
        ld    bc, #8
        add   hl, bc              ; HL = &y
        call  tm_add16              ; y += ±n
        ld    a, e
        or    a, a
        jr    Z, tm_mark_dirty
        ld    a, 45 (ix)          ; sgnmin
        rlca
        ld    a, e
        jr    NC, tm_ymaj_min_set
        neg
tm_ymaj_min_set:
        push  ix
        pop   hl
        ld    bc, #6
        add   hl, bc              ; HL = &x
        call  tm_add16              ; x += ±mins

tm_mark_dirty:
        ;; flags |= _SPR_DIRTY (0x0C) — порция всегда двигает major
        ld    a, 16 (ix)
        or    a, #0x0C
        ld    16 (ix), a
        ;; rem == 0 → приехали: MOVE_ON снять, MOVE_DONE взвести
        ld    a, 35 (ix)
        or    a, 36 (ix)
        jr    NZ, tm_done
        ld    a, 18 (ix)
        and   a, #0xFB            ; ~SPR_MOVE_ON
        or    a, #0x08            ; SPR_MOVE_DONE
        ld    18 (ix), a
tm_done:
        pop   ix
        ret

        ;; ---- хелпер: *(int16 @HL) += sign_extend(A); клоббер A,BC ----
tm_add16:
        ld    c, a
        add   a, a                ; CF = знак A
        sbc   a, a                ; A = 0x00 / 0xFF
        ld    b, a                ; BC = знак-расширенная дельта
        ld    a, (hl)
        add   a, c
        ld    (hl), a
        inc   hl
        ld    a, (hl)
        adc   a, b
        ld    (hl), a
        ret
    __endasm;
}

void _sprite_tick(void *arr, uint8_t count)
{
    sprite_t *s = (sprite_t *)arr;
    uint8_t i;

    for (i = 0; i < count; i++, s++) {
        uint8_t fl = s->an_flags;

        /* Типичный спрайт в кадре НЕ дозрел ни до смены кадра, ни до
         * шага — держим этот путь коротким: одна проверка флагов +
         * декремент(ы) таймера. */
        if (!(fl & (SPR_ANIM_ON | SPR_MOVE_ON)))
            continue;
        if ((fl & SPR_ANIM_ON) && --s->an_timer == 0)
            tick_anim(s);
        if ((fl & SPR_MOVE_ON) && --s->mv_timer == 0)
            tick_move(s);
    }
}
