joeyagi/agiObj.c
2026-10-06 16:59:41 -05:00

609 lines
19 KiB
C

// Screen objects: cel cycling, motion and what stops it (AGI
// specification 4.2's object commands and 2.7's control lines).
//
// Each interpreter cycle, before logic 0 runs, objPlanMotion turns the
// special motions (wander, follow.ego, move.obj) into a direction; after
// it, objUpdateAll erases the updated objects, steps their cels and moves
// them a step in their direction, then blits them again. A step is
// refused by an unconditional barrier (control 0), a conditional one
// (control 1) unless the object ignores blocks, water rules, the block
// rectangle and other objects' baselines; objects at fixed priority 15
// ignore control lines. Leaving the screen or crossing the horizon stops
// the object at the edge and reports it in v2 (ego) or v4/v5.
#include "agi.h"
#include <stddef.h>
#include <string.h>
#include "joey/core.h"
// Wandering objects walk a random direction for a random number of steps.
#define WANDER_STEPS_MIN 6u
#define WANDER_STEPS_SPAN 45u
#define FOLLOW_DETOUR_MIN 2u
#define FOLLOW_DETOUR_SPAN 8u
#define STEP_DEFAULT 1u
#define SPIRAL_LEGS 4u
#define SPIRAL_MAX (2u * AGI_WIDTH)
#define SIGN_STATES 3u
// Automatic loops by direction (specification 4.2): views with two or
// three loops face right or left; four or more also face up and down.
// LOOP_KEEP leaves the loop alone.
#define LOOP_KEEP 0xFFu
#define LOOP_RIGHT 0u
#define LOOP_LEFT 1u
#define LOOP_DOWN 2u
#define LOOP_UP 3u
#define LOOPS_FOUR 4u
#define LOOPS_TWO 2u
// ----- Prototypes -----
static void advanceCel(uint8_t n);
static bool blockedByObject(uint8_t n, int16_t x, int16_t y);
static uint8_t clampToScreen(AgiObjectT *o, int16_t *x, int16_t *y);
static bool legalPosition(uint8_t n, int16_t x, int16_t y);
static void moveObject(uint8_t n);
static bool onScreen(const AgiObjectT *o, int16_t x, int16_t y);
static void reportEdge(uint8_t n, uint8_t edge);
static void selectLoop(AgiObjectT *o);
static void stopSpecialMotion(uint8_t n);
// ----- Tables -----
// A step in each direction.
static const int8_t kDirDx[AGI_DIR_COUNT] = { 0, 0, 1, 1, 1, 0, -1, -1, -1 };
static const int8_t kDirDy[AGI_DIR_COUNT] = { 0, -1, -1, 0, 1, 1, 1, 0, -1 };
static const uint8_t kLoopFew[AGI_DIR_COUNT] = { LOOP_KEEP, LOOP_KEEP, LOOP_RIGHT, LOOP_RIGHT, LOOP_RIGHT, LOOP_KEEP, LOOP_LEFT, LOOP_LEFT, LOOP_LEFT };
static const uint8_t kLoopFour[AGI_DIR_COUNT] = { LOOP_KEEP, LOOP_UP, LOOP_RIGHT, LOOP_RIGHT, LOOP_RIGHT, LOOP_DOWN, LOOP_LEFT, LOOP_LEFT, LOOP_LEFT };
// Direction for each (sign of dx, sign of dy), indexed [dy + 1][dx + 1].
static const uint8_t kDirFromSigns[SIGN_STATES][SIGN_STATES] = {
{ AGI_DIR_NW, AGI_DIR_N, AGI_DIR_NE },
{ AGI_DIR_W, AGI_DIR_NONE, AGI_DIR_E },
{ AGI_DIR_SW, AGI_DIR_S, AGI_DIR_SE }
};
// objFixPosition's spiral: left, down, right, up.
static const int8_t kSpiralDx[SPIRAL_LEGS] = { -1, 0, 1, 0 };
static const int8_t kSpiralDy[SPIRAL_LEGS] = { 0, 1, 0, -1 };
// ----- Internal helpers (alphabetical) -----
static void advanceCel(uint8_t n) {
AgiObjectT *o;
uint8_t last;
o = &gAgi->objects[n];
if (o->view == NULL || o->loop >= o->view->loopCount) {
return;
}
last = o->view->loops[o->loop].celCount;
if (last == 0u) {
return;
}
last--;
switch (o->cycleMode) {
case AGI_CYCLE_NORMAL:
o->cel = (o->cel >= last) ? 0u : (uint8_t)(o->cel + 1u);
break;
case AGI_CYCLE_REVERSE:
o->cel = (o->cel == 0u || o->cel > last) ? last : (uint8_t)(o->cel - 1u);
break;
case AGI_CYCLE_END_LOOP:
case AGI_CYCLE_REVERSE_LOOP:
// The flag is set as the end cel is reached.
if (o->cycleMode == AGI_CYCLE_END_LOOP && o->cel < last) {
o->cel++;
} else if (o->cycleMode == AGI_CYCLE_REVERSE_LOOP && o->cel > 0u) {
o->cel--;
}
if ((o->cycleMode == AGI_CYCLE_END_LOOP) ? (o->cel >= last) : (o->cel == 0u)) {
gAgi->flags[o->loopFlag] = 1u;
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_CYCLING);
o->cycleMode = AGI_CYCLE_NORMAL;
}
break;
default:
break;
}
objSetCel(n, o->cel);
}
// Another object's baseline in the way: the two baselines overlap (or
// touch) and are on the same line, or this step crosses the other's.
static bool blockedByObject(uint8_t n, int16_t x, int16_t y) {
const AgiObjectT *o;
const AgiObjectT *other;
uint8_t i;
o = &gAgi->objects[n];
if ((o->flags & OBJ_IGNORE_OBJS) != 0u) {
return false;
}
for (i = 0u; i < gAgi->objectCount; i++) {
other = &gAgi->objects[i];
if (i == n || (other->flags & (OBJ_ANIMATED | OBJ_DRAWN)) != (OBJ_ANIMATED | OBJ_DRAWN) ||
(other->flags & OBJ_IGNORE_OBJS) != 0u) {
continue;
}
if (x + (int16_t)o->width < other->x || other->x + (int16_t)other->width < x) {
continue;
}
if (y == other->y || (o->y < other->y && y > other->y) || (o->y > other->y && y < other->y)) {
return true;
}
}
return false;
}
// Keep a position on the screen and below the horizon; returns the edge
// it ran into, if any.
static uint8_t clampToScreen(AgiObjectT *o, int16_t *x, int16_t *y) {
uint8_t edge;
edge = AGI_EDGE_NONE;
if (*x < 0) {
*x = 0;
edge = AGI_EDGE_LEFT;
} else if (*x + (int16_t)o->width > AGI_WIDTH) {
*x = (int16_t)(AGI_WIDTH - (int16_t)o->width);
edge = AGI_EDGE_RIGHT;
}
if (*y > AGI_MAX_Y) {
*y = AGI_MAX_Y;
edge = AGI_EDGE_BOTTOM;
} else if ((o->flags & OBJ_IGNORE_HORIZON) == 0u && *y <= (int16_t)gAgi->horizon) {
*y = (int16_t)(gAgi->horizon + 1u);
edge = AGI_EDGE_TOP;
} else if (*y - (int16_t)o->height + 1 < 0) {
*y = (int16_t)(o->height - 1u);
edge = AGI_EDGE_TOP;
}
return edge;
}
// Whether the object may stand at (x, y): what its baseline covers in the
// priority screen, the block rectangle and other objects. Sets f0/f3 for
// ego as a side effect.
static bool legalPosition(uint8_t n, int16_t x, int16_t y) {
const AgiObjectT *o;
int16_t i;
uint8_t c;
bool anyWater;
bool allWater;
bool signal;
bool insideNow;
bool insideNew;
o = &gAgi->objects[n];
anyWater = false;
allWater = true;
signal = false;
if (!((o->flags & OBJ_FIX_PRIORITY) != 0u && o->priority == AGI_PRIORITY_MAX)) {
for (i = 0; i < (int16_t)o->width; i++) {
c = gfxControlAt((int16_t)(x + i), y);
if (c == AGI_CTL_BARRIER) {
return false;
}
if (c == AGI_CTL_BLOCK && (o->flags & OBJ_IGNORE_BLOCKS) == 0u) {
return false;
}
if (c == AGI_CTL_WATER) {
anyWater = true;
} else {
allWater = false;
}
if (c == AGI_CTL_SIGNAL) {
signal = true;
}
}
if ((o->flags & OBJ_ON_WATER) != 0u && !allWater) {
return false;
}
if ((o->flags & OBJ_ON_LAND) != 0u && anyWater) {
return false;
}
} else {
allWater = false;
}
if (gAgi->blockActive && (o->flags & OBJ_IGNORE_BLOCKS) == 0u) {
insideNow = o->x >= gAgi->blockX1 && o->x <= gAgi->blockX2 && o->y >= gAgi->blockY1 && o->y <= gAgi->blockY2;
insideNew = x >= gAgi->blockX1 && x <= gAgi->blockX2 && y >= gAgi->blockY1 && y <= gAgi->blockY2;
if (insideNow != insideNew) {
return false;
}
}
if (blockedByObject(n, x, y)) {
return false;
}
if (n == AGI_EGO) {
gAgi->flags[FLAG_EGO_WATER] = allWater ? 1u : 0u;
gAgi->flags[FLAG_EGO_SIGNAL] = signal ? 1u : 0u;
}
return true;
}
// One step in the object's direction, if the way is clear.
static void moveObject(uint8_t n) {
AgiObjectT *o;
int16_t x;
int16_t y;
uint8_t edge;
o = &gAgi->objects[n];
if (o->direction == AGI_DIR_NONE || o->direction >= AGI_DIR_COUNT) {
return;
}
x = (int16_t)(o->x + kDirDx[o->direction] * (int16_t)o->stepSize);
y = (int16_t)(o->y + kDirDy[o->direction] * (int16_t)o->stepSize);
edge = clampToScreen(o, &x, &y);
if ((x != o->x || y != o->y) && legalPosition(n, x, y)) {
o->x = x;
o->y = y;
} else if (o->motion == AGI_MOTION_WANDER) {
o->randomSteps = 0u;
} else if (o->motion == AGI_MOTION_FOLLOW) {
// A follower that runs into something wanders off a few steps
// before it heads for ego again (measured: it never settles
// against ego's side).
o->direction = (uint8_t)(AGI_DIR_N + jlRandomRange(AGI_DIR_COUNT - 1u));
o->randomSteps = (uint8_t)(FOLLOW_DETOUR_MIN + jlRandomRange(FOLLOW_DETOUR_SPAN));
}
// Ego's edge is current: a step that touches none clears it.
if (edge != AGI_EDGE_NONE) {
reportEdge(n, edge);
} else if (n == AGI_EGO) {
gAgi->vars[VAR_EGO_EDGE] = AGI_EDGE_NONE;
}
}
// The object's cel is wholly on the screen, below the horizon unless it
// ignores it.
static bool onScreen(const AgiObjectT *o, int16_t x, int16_t y) {
return x >= 0 && x + (int16_t)o->width <= AGI_WIDTH && y <= AGI_MAX_Y && y - (int16_t)o->height + 1 >= 0 &&
((o->flags & OBJ_IGNORE_HORIZON) != 0u || y > (int16_t)gAgi->horizon);
}
static void reportEdge(uint8_t n, uint8_t edge) {
if (n == AGI_EGO) {
gAgi->vars[VAR_EGO_EDGE] = edge;
} else {
gAgi->vars[VAR_OBJ_EDGE_OBJ] = n;
gAgi->vars[VAR_OBJ_EDGE] = edge;
}
}
static void selectLoop(AgiObjectT *o) {
uint8_t loop;
if ((o->flags & OBJ_FIX_LOOP) != 0u || o->view == NULL || o->view->loopCount < LOOPS_TWO) {
return;
}
loop = (o->view->loopCount >= LOOPS_FOUR) ? kLoopFour[o->direction] : kLoopFew[o->direction];
if (loop != LOOP_KEEP && loop != o->loop) {
objSetLoop((uint8_t)(o - gAgi->objects), loop);
}
}
// A special motion is over: set its flag, stop, and give back what it
// took (the step size, and for ego the keyboard).
static void stopSpecialMotion(uint8_t n) {
AgiObjectT *o;
o = &gAgi->objects[n];
if (o->motion == AGI_MOTION_MOVE) {
o->stepSize = o->savedStepSize;
if (n == AGI_EGO) {
gAgi->playerControl = true;
}
}
gAgi->flags[o->motionFlag] = 1u;
o->motion = AGI_MOTION_NORMAL;
o->direction = AGI_DIR_NONE;
if (n == AGI_EGO) {
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
}
}
// ----- Public API (alphabetical) -----
// animate.obj: bring an object under the interpreter's control. Its
// step and cycle settings are left as they are: new.room (unanimate.all)
// is what sets them back to the defaults, so before the first new.room
// they are all 0 (measured: such objects neither cycle nor move).
void objAnimate(uint8_t n) {
AgiObjectT *o;
o = &gAgi->objects[n];
if ((o->flags & OBJ_ANIMATED) == 0u) {
o->flags = (uint16_t)((o->flags & OBJ_SKIP_STEP) | OBJ_ANIMATED | OBJ_UPDATE | OBJ_CYCLING);
}
}
// The objects as the interpreter starts: all zero, each yet to make its
// first step.
void objClearAll(void) {
uint8_t i;
memset(gAgi->objects, 0, sizeof(gAgi->objects));
for (i = 0u; i < AGI_MAX_OBJECTS; i++) {
gAgi->objects[i].flags = OBJ_SKIP_STEP;
}
}
// The direction from one point toward another, treating offsets smaller
// than `slack` as none (measured: move.obj with step 2 goes the last 2
// pixels, with step 3 stops 2 short).
uint8_t objDirectionTo(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY, uint8_t slack) {
int16_t dx;
int16_t dy;
int16_t s;
uint8_t sx;
uint8_t sy;
s = (int16_t)((slack == 0u) ? 1u : slack);
dx = (int16_t)(toX - fromX);
dy = (int16_t)(toY - fromY);
sx = (dx <= -s) ? 0u : (dx >= s) ? 2u : 1u;
sy = (dy <= -s) ? 0u : (dy >= s) ? 2u : 1u;
return kDirFromSigns[sy][sx];
}
// Put an object somewhere it may stand (measured from Sierra's
// interpreter): below the horizon, then, if it may not stand there,
// spiralling out a pixel at a time -- left 1, down 1, right 2, up 2,
// left 3, ... -- to the first place on the screen clear of barriers and
// other objects.
void objFixPosition(uint8_t n) {
AgiObjectT *o;
int16_t x;
int16_t y;
uint16_t len;
uint16_t step;
uint8_t leg;
o = &gAgi->objects[n];
x = o->x;
y = o->y;
if ((o->flags & OBJ_IGNORE_HORIZON) == 0u && y <= (int16_t)gAgi->horizon) {
y = (int16_t)(gAgi->horizon + 1u);
}
leg = 0u;
len = 1u;
while (!(onScreen(o, x, y) && legalPosition(n, x, y)) && len <= SPIRAL_MAX) {
for (step = 0u; step < len; step++) {
x = (int16_t)(x + kSpiralDx[leg]);
y = (int16_t)(y + kSpiralDy[leg]);
if (onScreen(o, x, y) && legalPosition(n, x, y)) {
break;
}
}
if (step < len) {
break;
}
// The legs lengthen every second turn.
leg = (uint8_t)((leg + 1u) % SPIRAL_LEGS);
if ((leg & 1u) == 0u) {
len++;
}
}
o->x = x;
o->y = y;
}
// move.obj: head for (x, y) at `step` pixels a step (0 keeps the step
// size); flag is set on arrival.
void objMoveTo(uint8_t n, uint8_t x, uint8_t y, uint8_t step, uint8_t flag) {
AgiObjectT *o;
o = &gAgi->objects[n];
o->motion = AGI_MOTION_MOVE;
o->targetX = x;
o->targetY = y;
o->savedStepSize = o->stepSize;
if (step != 0u) {
o->stepSize = step;
}
o->motionFlag = flag;
o->direction = objDirectionTo(o->x, o->y, x, y, o->stepSize);
gAgi->flags[flag] = 0u;
if (n == AGI_EGO) {
gAgi->playerControl = false;
gAgi->vars[VAR_EGO_DIR] = o->direction;
}
if (o->direction == AGI_DIR_NONE) {
stopSpecialMotion(n);
}
}
// Before logic 0: work out where wandering, following and moving objects
// head this cycle.
void objPlanMotion(void) {
uint8_t i;
AgiObjectT *o;
AgiObjectT *ego;
ego = &gAgi->objects[AGI_EGO];
for (i = 0u; i < gAgi->objectCount; i++) {
o = &gAgi->objects[i];
if ((o->flags & (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) != (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) {
continue;
}
switch (o->motion) {
case AGI_MOTION_WANDER:
if (o->randomSteps == 0u) {
o->direction = (uint8_t)jlRandomRange(AGI_DIR_COUNT);
o->randomSteps = (uint8_t)(WANDER_STEPS_MIN + jlRandomRange(WANDER_STEPS_SPAN));
} else {
o->randomSteps--;
}
break;
case AGI_MOTION_FOLLOW:
if (o->randomSteps > 0u) {
o->randomSteps--;
break;
}
o->direction = objDirectionTo((int16_t)(o->x + o->width / 2u), o->y,
(int16_t)(ego->x + ego->width / 2u), ego->y, o->followDistance);
if (o->direction == AGI_DIR_NONE) {
stopSpecialMotion(i);
}
break;
case AGI_MOTION_MOVE:
o->direction = objDirectionTo(o->x, o->y, o->targetX, o->targetY, o->stepSize);
if (o->direction == AGI_DIR_NONE) {
stopSpecialMotion(i);
}
break;
default:
break;
}
if (i == AGI_EGO) {
gAgi->vars[VAR_EGO_DIR] = o->direction;
}
}
}
void objSetCel(uint8_t n, uint8_t cel) {
AgiObjectT *o;
const AgiCelT *c;
bool mirror;
o = &gAgi->objects[n];
o->cel = cel;
if (o->view == NULL || o->loop >= o->view->loopCount) {
return;
}
// A cel (or loop) the new view or loop lacks becomes 0 (measured).
if (cel >= o->view->loops[o->loop].celCount) {
o->cel = 0u;
}
c = agiViewCel(o->view, o->loop, o->cel, &mirror);
if (c != NULL) {
o->width = c->width;
o->height = c->height;
}
}
void objSetLoop(uint8_t n, uint8_t loop) {
AgiObjectT *o;
o = &gAgi->objects[n];
if (o->view == NULL) {
o->loop = loop;
return;
}
o->loop = (loop >= o->view->loopCount) ? 0u : loop;
objSetCel(n, o->cel);
}
bool objSetView(uint8_t n, uint8_t viewId) {
AgiObjectT *o;
const AgiViewT *view;
view = agiViewGet(viewId);
if (view == NULL) {
return false;
}
o = &gAgi->objects[n];
o->view = view;
o->viewId = viewId;
if (n == AGI_EGO) {
gAgi->vars[VAR_EGO_VIEW] = viewId;
}
objSetLoop(n, o->loop);
return true;
}
// unanimate.all (and new.room): every object off the screen and out of
// the interpreter's hands, with the default step and cycle settings. Its
// view, loop, cel and position stay (new.room places ego from them).
void objUnanimateAll(void) {
uint8_t i;
AgiObjectT *o;
gfxEraseAll();
for (i = 0u; i < AGI_MAX_OBJECTS; i++) {
o = &gAgi->objects[i];
o->flags = OBJ_SKIP_STEP;
o->direction = AGI_DIR_NONE;
o->motion = AGI_MOTION_NORMAL;
o->cycleMode = AGI_CYCLE_NORMAL;
o->stepSize = STEP_DEFAULT;
o->stepTime = STEP_DEFAULT;
o->stepCount = STEP_DEFAULT;
o->cycleTime = STEP_DEFAULT;
o->cycleCount = STEP_DEFAULT;
}
gfxPresentDirty();
}
// After logic 0: step every updated object's cel and move it, then draw
// them all again.
void objUpdateAll(void) {
uint8_t i;
AgiObjectT *o;
gfxEraseUpdating();
for (i = 0u; i < gAgi->objectCount; i++) {
o = &gAgi->objects[i];
if ((o->flags & (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) != (OBJ_ANIMATED | OBJ_UPDATE | OBJ_DRAWN)) {
continue;
}
selectLoop(o);
// A cycle count of 0 (an object never given a cycle time) never
// comes due; a step count of 0 is due every cycle.
if ((o->flags & OBJ_CYCLING) != 0u && o->cycleCount != 0u) {
o->cycleCount--;
if (o->cycleCount == 0u) {
o->cycleCount = o->cycleTime;
if ((o->flags & OBJ_SKIP_CYCLE) != 0u) {
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_SKIP_CYCLE);
} else {
advanceCel(i);
}
}
}
if (o->stepCount > 0u) {
o->stepCount--;
}
if (o->stepCount == 0u) {
o->stepCount = o->stepTime;
o->prevX = o->x;
o->prevY = o->y;
if ((o->flags & OBJ_SKIP_STEP) != 0u) {
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_SKIP_STEP);
} else {
moveObject(i);
}
}
}
gfxRedrawUpdating();
}