// 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 #include #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(); }