Files
Sprinter-SDCC/applications/PoP/roomtest/pop_ctrl.c
T
snark13 79ea473910 PoP roomtest: страж замечает Кида и достаёт меч (ИИ, шаг 1)
Первый самостоятельный кусок ИИ стража (план, пункт 2).  В оригинале у
соперника нет своего диспетчера: autocontrol_* (seg002) выставляет те же
глобалы control_*, что и ввод игрока, а дальше исполняется общий control().
Инфраструктура под это встала прошлым коммитом, здесь — сама логика.

Порт:
- check_can_guard_see_kid (seg003:688) — луч видимости по ряду: стены и
  верхи дверей рвут его совсем, loose/чомпер/дыра/неподнятые ворота дают
  «вижу, но не пойду».  В guards.c (банк 1);
- Opp + loadshad_and_opp (seg006:841) и char_opp_dist (seg006:2135);
- autocontrol_guard_inactive (seg002:710) + move_* (seg002:0706..);
- ветки control(): control_guard_inactive (seg006:2123) и draw_sword
  (seg005:945) — соперник уходит сразу в seq_90 en garde;
- pop_guard_tick перестроен по play_guard_frame (seg000:1246): окно
  Char/Opp вокруг ИИ, диспетчера и play_seq.

По дороге:
- Kid.alive не выставлялся (= 0 = «мёртв» в семантике оригинала), из-за
  чего луч видимости не мог сработать в принципе — ставим -1 в kid_init;
- Kid.room не выставлялась вовсе; условие Kid.room == Guard.room всегда
  было ложным.  Ставим в enter_room (полная модель Kid.room != drawn_room
  у шва по-прежнему впереди);
- pop_tile_at — тайл текущей комнаты наружу из pop_map (луч видимости);
- control_x/y/shift открыты в шину: ИИ заполняет оси как есть, без
  flip_control_x (его «вперёд» уже в системе персонажа).

Проверено в MAME (комната 3, страж на tile 17): страж переходит из
стойки 166 в 171 stand_with_sword, sword=2, can_guard_see_kid=2; ввод
игрока не пострадал.  Клинок отдельным спрайтом пока не рисуется —
sword.atl содержит только кадры 229..237 (подъём меча Кидом), остальные
строки sword_tbl приедут с боёвкой.

Бюджет (225 кадров, комната 3): 384 168 – 384 636 тактов, 0.893–0.895
кадра, запас 45 364.  Прошлый замер 382 584 – 383 064 — шаг стоил ~1 570.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:09:12 +03:00

513 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_ctrl.c — управление Kid (Фаза K2b). Порт SDLPoP:
* вход — read_user_control/user_control/rest_ctrl_1/save_ctrl_1
* (seg006.c) поверх held-state <kbd_raw.h>;
* диспетчер — control() + control_standing/turning/startrun/running/
* crouched (seg005.c), Kid-only ветки (без меча/стражи);
* действия — forward/back/up/down_pressed, crouch, standing_jump,
* run_jump, safe_step (seg005.c).
*
* Map-запросы (стена/край/захват) ЗАСТАБЛЕНЫ «открытый пол» (map_* ниже) —
* реальная коллизия в K3/K4. Поэтому: forward -> всегда бег, down -> присед,
* back -> разворот, up+forward -> прыжок с места; osторожный шаг и climb
* помечены TODO(K3/K4).
*
* Значения CONTROL_* (types.h): RELEASED=0, IGNORE=1, HELD=-1;
* X: LEFT/FORWARD=-1, RIGHT/BACKWARD=+1; Y: UP=-1, DOWN=+1.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h" /* charid, состояние меча, seq стража */
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
* scancodes. TODO: убрать после отладки. */
#define KBD_DBG_STEPL 0x3B /* J */
#define KBD_DBG_STEPR 0x4B /* L */
/* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */
#define CONTROL_RELEASED 0
#define CONTROL_IGNORE 1
#define CONTROL_HELD (-1)
#define CONTROL_HELD_LEFT (-1)
#define CONTROL_HELD_RIGHT 1
#define CONTROL_HELD_FORWARD (-1)
#define CONTROL_HELD_BACKWARD 1
#define CONTROL_HELD_UP (-1)
#define CONTROL_HELD_DOWN 1
#define DIR_RIGHT 0 /* dir_0_right */
#define SEQ_1_START_RUN 1
#define SEQ_2_STAND 2
#define SEQ_3_STANDING_JUMP 3
#define SEQ_4_RUN_JUMP 4
#define SEQ_5_TURN 5
#define SEQ_6_RUN_TURN 6
#define SEQ_13_STOP_RUN 13
#define SEQ_26_CROUCH_WHILE_RUNNING 26
#define SEQ_43_START_RUN_AFTER_TURN 43
#define SEQ_49_STAND_UP_FROM_CROUCH 49
#define SEQ_50_CROUCH 50
#define SEQ_79_CROUCH_HOP 79
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
#define SEQ_44_STEP_ON_EDGE 44
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
#define SEQ_28_JUMP_NOTHING_ABOVE 28 /* прыжок вверх: сверху пусто */
#define SEQ_25_HANG_AGAINST_WALL 25 /* вис у стены (Shift в висе) */
#define FRAME_7_RUN 7
#define FRAME_11_RUN 11
#define FRAME_15_STAND 15
#define FRAME_48_TURN 48
#define FRAME_50_TURN 50
#define FRAME_67_JUMPUP1 67
#define FRAME_70_JUMPHANG 70
#define FRAME_87_HANG1 87
#define FRAME_109_CROUCH 109
/* ---- Состояние управления (seg006 globals) ------------------------ *
* control_x/y/shift — «сырое» состояние осей ЭТОГО кадра (абсолютно:
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный
* автомат held/released/ignore (авто-повтор); ctrl1_* — их память между
* кадрами (rest/save). */
int8_t control_x, control_y, control_shift;
/* НЕ static: в оригинале это глобалы, через которые ИИ стража подаёт
* СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же
* control(), что и у игрока. Поэтому шина общая. */
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
* Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl
* не дублируются в этот TU). */
/* В оригинале seqtbl_offset_char работает с АКТИВНЫМ персонажем, и
* control() вызывается внутри окна Char. Раньше макрос вёл на
* kid_set_seq (пишет прямо в Kid) — после введения окна это ломало ввод:
* savekid затирал Kid копией Char со старой curr_seq. */
#define seqtbl_offset_char(seq) pop_char_set_seq(seq)
static int8_t release_arrows(void)
{
control_backward = control_forward = control_up = control_down = CONTROL_RELEASED;
return CONTROL_IGNORE; /* seg006: return 1 */
}
/* Край/стена впереди — из pop_map (get_edge_distance): pop_edge_distance()
* возвращает дистанцию и ставит pop_edge_type() = EDGE_WALL/FLOOR/CLOSER. */
/* ---- Действия (seg005) -------------------------------------------- */
static void crouch(void)
{
seqtbl_offset_char(SEQ_50_CROUCH);
control_down = release_arrows();
}
static void standing_jump(void)
{
control_up = control_forward = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_3_STANDING_JUMP);
}
/* check_get_item (seg005:061F): решение по тайлам принимает pop_map, здесь
* только исполнение. Триггер в оригинале — Shift (control_shift2) в стойке
* или в приседе: первый раз Kid приседает к предмету, второй — поднимает.
* Возврат 1 = управление обработано (caller делает return). */
static uint8_t get_item_action(void)
{
uint8_t a = pop_get_item_action();
if (a == 1) { crouch(); return 1; }
if (a == 2) {
control_shift2 = CONTROL_IGNORE; /* do_pickup: без авто-повтора */
seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
return 1;
}
return 0;
}
static void run_jump(void)
{
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
* полировка K3; K2b просто запускает run-jump. */
if (Char.frame >= FRAME_7_RUN) {
control_up = release_arrows();
seqtbl_offset_char(SEQ_4_RUN_JUMP);
}
}
static void safe_step(void); /* fwd */
static void forward_pressed(void)
{
int distance = pop_edge_distance();
if (pop_edge_type() == EDGE_WALL && distance < 8) {
/* рядом стена — вместо бега осторожный шаг (в стену не разбежится) */
if (control_forward == CONTROL_HELD)
safe_step();
} else {
seqtbl_offset_char(SEQ_1_START_RUN);
}
}
static void back_pressed(void)
{
control_backward = release_arrows();
/* Kid без меча -> просто разворот. */
seqtbl_offset_char(SEQ_5_TURN);
}
static void down_pressed(void)
{
control_down = CONTROL_IGNORE; /* без авто-повтора */
/* У края пола (край сзади) со зацепом снизу — спуск на этаж (seq_68);
* у передней кромки — отступ; иначе присед. Решает pop_down_action. */
if (!pop_down_action())
crouch();
}
/* Прыжок вверх в стойке (seg005 check_jump_up): чистый вертикальный прыжок
* ЛИБО прыжок-с-зацепом за уступ выше (запрыгнуть на этаж вверх). Выбор seq
* + выравнивание Kid.x — в pop_jump_up_seq (pop_map). */
static void jump_up(void)
{
control_up = release_arrows();
seqtbl_offset_char(pop_jump_up_seq());
}
static void up_pressed(void)
{
/* (дверь уровня/телепорт — нет в тесте.) ↑+forward = прыжок с места;
* иначе вертикальный прыжок вверх. */
if (control_x == CONTROL_HELD_FORWARD)
standing_jump();
else
jump_up();
}
/* Осторожный шаг (Shift+forward): шаг РОВНО до кромки/стены. distance>0 —
* шаг длины distance (seq 29..42 = step1..step14); ==0 у края — шаг на
* кромку (seq_44) либо неосторожный шаг за край (seq_39). Порт seg005. */
static void safe_step(void)
{
int distance;
control_shift2 = CONTROL_IGNORE; /* без авто-повтора */
control_forward = CONTROL_IGNORE;
distance = pop_edge_distance();
if (distance) {
Char.repeat = 1;
seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE));
} else if (pop_edge_type() == EDGE_WALL) {
/* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад
* (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */
Char.repeat = 1;
seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE));
} else if (Char.repeat != 0) {
Char.repeat = 0;
seqtbl_offset_char(SEQ_44_STEP_ON_EDGE);
} else {
seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */
}
}
/* ---- control_* обработчики состояний ------------------------------ */
static void control_crouched(void)
{
/* Shift в приседе — поднять предмет (seg005 control_crouched). */
if (control_shift2 == CONTROL_HELD && get_item_action()) return;
if (control_y != CONTROL_HELD_DOWN) {
seqtbl_offset_char(SEQ_49_STAND_UP_FROM_CROUCH);
} else if (control_forward == CONTROL_HELD) {
control_forward = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_79_CROUCH_HOP);
}
}
static void control_standing(void)
{
/* Shift в стойке над предметом — присесть к нему (seg005:0358, первая
* строка control_standing). Идёт ДО остальных shift-веток. */
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
get_item_action())
return;
/* Kid charid_0, без меча: остаётся нижняя часть control_standing. */
if (control_shift == CONTROL_HELD) {
if (control_backward == CONTROL_HELD) back_pressed();
else if (control_up == CONTROL_HELD) up_pressed();
else if (control_down == CONTROL_HELD) down_pressed();
else if (control_x == CONTROL_HELD_FORWARD && control_forward == CONTROL_HELD)
safe_step();
} else if (control_forward == CONTROL_HELD) {
if (control_up == CONTROL_HELD) standing_jump(); /* keyboard mode */
else forward_pressed();
} else if (control_backward == CONTROL_HELD) {
back_pressed();
} else if (control_up == CONTROL_HELD) {
if (control_forward == CONTROL_HELD) standing_jump();
else up_pressed();
} else if (control_down == CONTROL_HELD) {
down_pressed();
} else if (control_x == CONTROL_HELD_FORWARD) {
forward_pressed();
}
}
static void control_turning(void)
{
if (control_shift >= CONTROL_RELEASED &&
control_x == CONTROL_HELD_FORWARD && control_y >= CONTROL_RELEASED) {
seqtbl_offset_char(SEQ_43_START_RUN_AFTER_TURN);
}
}
static void control_startrun(void)
{
if (control_y == CONTROL_HELD_UP && control_x == CONTROL_HELD_FORWARD)
standing_jump();
}
static void control_running(void)
{
/* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок
* seq_47 при беге в стену (как оригинал), с корректной позицией. */
if (control_x == CONTROL_RELEASED &&
(Char.frame == FRAME_7_RUN || Char.frame == FRAME_11_RUN)) {
control_forward = release_arrows();
seqtbl_offset_char(SEQ_13_STOP_RUN);
} else if (control_x == CONTROL_HELD_BACKWARD) {
control_backward = release_arrows();
seqtbl_offset_char(SEQ_6_RUN_TURN);
} else if (control_y == CONTROL_HELD_UP && control_up == CONTROL_HELD) {
run_jump();
} else if (control_down == CONTROL_HELD) {
control_down = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_26_CROUCH_WHILE_RUNNING);
}
}
/* ---- Вис (seg005 control_hanging/can_climb_up/hang_fall) ---------- *
* Тайловые решения (можно ли подтянуться, куда сорваться) — в pop_map;
* здесь управление осями control_*. */
static void hang_fall(void)
{
control_down = release_arrows();
seqtbl_offset_char(pop_hang_fall_seq()); /* seq_23/seq_11 (+ выравнивание x) */
}
static void can_climb_up(void)
{
control_up = control_shift2 = release_arrows();
seqtbl_offset_char(pop_hang_climb_up_seq()); /* seq_10 / seq_73 */
}
static void control_hanging(void)
{
if (control_y == CONTROL_HELD && pop_hang_grab_ready()) {
can_climb_up(); /* ↑ — подтянуться */
} else if (control_shift == CONTROL_HELD) {
int8_t w = pop_hang_wall();
if (w > 0) seqtbl_offset_char(SEQ_25_HANG_AGAINST_WALL); /* у стены */
else if (w < 0) hang_fall(); /* сорваться */
/* w==0: сверху пол — держимся (ничего) */
} else {
hang_fall(); /* отпустил Shift — сорваться */
}
}
/* control_jumpup (seg005): в старте прыжка вверх ←/→ или forward → перевести
* в прыжок с места. */
static void control_jumpup(void)
{
if (control_x == CONTROL_HELD_FORWARD || control_forward == CONTROL_HELD)
standing_jump();
}
/* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
* клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
* уже как выпад. */
static void draw_sword(void)
{
uint8_t seq_id = SEQ_55_DRAW_SWORD;
control_forward = control_shift2 = release_arrows();
if (Char.charid != CHARID_0_KID && Char.charid != CHARID_1_SHADOW)
seq_id = SEQ_90_EN_GARDE;
/* TODO: у Кида здесь play_sound(19) + offguard = 0 — звука пока нет. */
Char.sword = SWORD_2_DRAWN;
seqtbl_offset_char(seq_id);
}
/* control_guard_inactive (seg006:2123): неактивный страж реагирует ТОЛЬКО в
* кадре 166 (стойка) и только на «вниз»: вниз+вперёд — достать меч,
* вниз без «вперёд» — развернуться (stand flipped). */
static void control_guard_inactive(void)
{
if (Char.frame == FRAME_166_STAND_INACTIVE && control_down == CONTROL_HELD) {
if (control_forward == CONTROL_HELD) {
draw_sword();
} else {
control_down = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_80_STAND_FLIPPED);
}
}
}
/* control_with_sword (seg005:964) — боёвка. Порт следующим шагом; пока
* персонаж с обнажённым мечом просто держит стойку. */
static void control_with_sword(void)
{
}
/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */
void pop_control(void)
{
uint8_t f = Char.frame;
/* seg005 control(): в отскоке (bumped) и в свободном падении управление
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
* пару пикселей меньше, чем должен»). */
if (Char.action == 5 || Char.action == 4) { /* bumped / in_freefall */
release_arrows();
return;
}
/* Порядок веток — как в seg005: меч старше charid, charid старше кадров.
* Пока Kid безоружен, обе ветки — про стража. */
if (Char.sword == SWORD_2_DRAWN)
control_with_sword();
else if (Char.charid >= CHARID_2_GUARD)
control_guard_inactive();
else if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
control_standing();
else if (f == FRAME_48_TURN)
control_turning();
else if (f < 4)
control_startrun();
else if (f >= FRAME_67_JUMPUP1 && f < FRAME_70_JUMPHANG)
control_jumpup();
else if (f < FRAME_15_STAND)
control_running();
else if (f >= FRAME_87_HANG1 && f < 100)
control_hanging();
else if (f == FRAME_109_CROUCH)
control_crouched();
}
/* ---- Модель ввода (seg006) ---------------------------------------- */
static void read_input(void)
{
uint8_t l = kbd_raw_down(KBD_LEFT);
uint8_t r = kbd_raw_down(KBD_RIGHT);
uint8_t u = kbd_raw_down(KBD_UP);
uint8_t d = kbd_raw_down(KBD_DOWN);
/* Отладочный осторожный шаг: J/L = Shift+влево/вправо (см. KBD_DBG_STEP*). */
uint8_t sl = kbd_raw_down(KBD_DBG_STEPL);
uint8_t sr = kbd_raw_down(KBD_DBG_STEPR);
if (sl) l = 1;
if (sr) r = 1;
control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED);
control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED);
control_shift = (sl || sr || kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
? CONTROL_HELD : CONTROL_RELEASED;
}
/* read_user_control (seg006:1557): «свежее нажатие» (RELEASED->HELD) с
* подавлением авто-повтора (IGNORE держится, пока клавишу не отпустят). */
static void read_user_control(void)
{
if (control_forward >= CONTROL_RELEASED) {
if (control_x == CONTROL_HELD_FORWARD) {
if (control_forward == CONTROL_RELEASED) control_forward = CONTROL_HELD;
} else control_forward = CONTROL_RELEASED;
}
if (control_backward >= CONTROL_RELEASED) {
if (control_x == CONTROL_HELD_BACKWARD) {
if (control_backward == CONTROL_RELEASED) control_backward = CONTROL_HELD;
} else control_backward = CONTROL_RELEASED;
}
if (control_up >= CONTROL_RELEASED) {
if (control_y == CONTROL_HELD_UP) {
if (control_up == CONTROL_RELEASED) control_up = CONTROL_HELD;
} else control_up = CONTROL_RELEASED;
}
if (control_down >= CONTROL_RELEASED) {
if (control_y == CONTROL_HELD_DOWN) {
if (control_down == CONTROL_RELEASED) control_down = CONTROL_HELD;
} else control_down = CONTROL_RELEASED;
}
if (control_shift2 >= CONTROL_RELEASED) {
if (control_shift == CONTROL_HELD) {
if (control_shift2 == CONTROL_RELEASED) control_shift2 = CONTROL_HELD;
} else control_shift2 = CONTROL_RELEASED;
}
}
static void flip_control_x(void)
{
int8_t t;
control_x = (int8_t)-control_x;
t = control_forward; control_forward = control_backward; control_backward = t;
}
/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */
static void user_control(void)
{
if (Char.direction >= DIR_RIGHT) {
flip_control_x();
pop_control();
flip_control_x();
} else {
pop_control();
}
}
static void rest_ctrl_1(void)
{
control_forward = ctrl1_forward; control_backward = ctrl1_backward;
control_up = ctrl1_up; control_down = ctrl1_down; control_shift2 = ctrl1_shift2;
}
static void save_ctrl_1(void)
{
ctrl1_forward = control_forward; ctrl1_backward = control_backward;
ctrl1_up = control_up; ctrl1_down = control_down; ctrl1_shift2 = control_shift2;
}
/* ---- Публичный API ------------------------------------------------ */
int pop_ctrl_init(void)
{
control_forward = control_backward = control_up = control_down = CONTROL_RELEASED;
control_shift2 = CONTROL_RELEASED;
ctrl1_forward = ctrl1_backward = ctrl1_up = ctrl1_down = ctrl1_shift2 = CONTROL_RELEASED;
return kbd_raw_open();
}
void pop_ctrl_close(void)
{
kbd_raw_close();
}
int8_t pop_ctrl_shift_held(void)
{
return (int8_t)(control_shift == CONTROL_HELD);
}
void pop_ctrl_tick(void)
{
kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */
rest_ctrl_1();
read_input();
read_user_control();
/* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
* персонажем (как в оригинале), поэтому ввод игрока применяется к
* Киду только внутри loadkid/savekid. Кадр не перезагружаем —
* play_seq в kid_tick сделает это следующим шагом. */
pop_loadkid();
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
pop_savekid_state();
save_ctrl_1();
}