Files
akbasic/include/akbasic/sprite.h
Andrew Kesterson a9600c3fcc Say what was hit, which way is out, and how far
The narrowphase has been producing a contact since it went in, and the interpreter
was throwing it away. `RCOLLISION(n, f)` reports it: what was hit (a sprite or a
`SOLID` rectangle), which one, the contact normal, the penetration depth, the
contact point, and which axis to reverse.

**The normal points out of the other thing and toward this one**, so a program
moves along it by the depth and is exactly clear. That sign is the one assertion
in the new test that could not be caught any other way -- both parties of a
sprite-against-sprite hit get their own record, each pointing the way *that*
sprite has to move, and sharing one would tell both to go the same direction,
which is how two things end up stuck inside each other.

**Field 7 is the one that deletes the most BASIC.** It is the minimum translation
axis, computed from the normal in C, and it is there because doing it in BASIC
means comparing two floats -- which is exactly where this dialect's left-operand
rule catches people. `BALLBRICKS`/`TESTCELL` in the artwork breakout spend six
lines computing an overlap rectangle and comparing its width to its height to get
this number.

The record is **sticky and deepest-wins**: replaced whenever that sprite is in a
contact and otherwise left alone, so `BUMP` stays the event and this stays the
detail of it. Making it clear itself when nothing touches would break the pairing,
because `BUMP` accumulates across steps and a once-a-frame poll would find the
detail already gone. Reading `BUMP` clears both, so they cannot disagree.

Deliberately narrower than `akgl_Contact`: no actor pointers, because BASIC has no
actor; no tile fields, because there is no tilemap; no z, because every test is
planar; and **no `dt` and no `sensor`**, which libakgl documents as filled in by
the resolver. This interpreter never resolves anything, so those two come back
zero and mean nothing, and an always-zero field in a reference table is a lie.

Documented with the two caveats that matter: fields 2, 3 and 4 are floats and want
a `%` variable, and the contact *point* is exact only for two boxes -- libakgl's
solver returns a point on the portal it converged to, while the normal and depth
are exact for every pair.

Chapter 8's collision section stops claiming only type 1 exists, which has been
false since the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
2026-08-02 10:53:26 -04:00

400 lines
18 KiB
C

/**
* @file sprite.h
* @brief Declares the sprite backend: where SPRITE, MOVSPR and SPRSAV land.
*
* The same shape as akbasic/graphics.h and for the same reason -- the core
* library builds with no SDL and no libakgl present, so a sprite verb calls
* through a record of function pointers rather than reaching for akgl_actor_*.
* akbasic_sprite_init_akgl() lives in the separate akbasic_akgl target.
*
* **A sprite is a Commodore sprite, not a libakgl sprite.** libakgl's has a
* sheet, a frame list, an animation speed and a state-to-sprite map, all of
* which arrive from a JSON document; BASIC 7.0 has a word for none of it. What
* BASIC has is eight numbered slots, each holding one still image with a
* position, a colour, a visibility flag and two expansion bits. So the JSON
* layer is bypassed rather than reached through: the adaptor builds the same
* structures directly and hands the interpreter this much smaller surface.
*
* The one place the two overlap is loading an image from a file, which is why
* `define_file` is here: a program that has art on disk should be able to say so,
* and libakgl already resolves a relative asset path and decodes the image. That
* path is not bypassed -- it is exactly akgl_spritesheet_initialize().
*
* A runtime with no sprite backend is the normal case, so every verb that needs
* one refuses with AKBASIC_ERR_DEVICE. The verbs that only touch interpreter
* state -- SPRCOLOR, and the RSP* readbacks -- work regardless, which is what
* makes them testable without a device.
*/
#ifndef _AKBASIC_SPRITE_H_
#define _AKBASIC_SPRITE_H_
#include <akerror.h>
#include <akbasic/graphics.h>
#include <akbasic/types.h>
/** @brief Sprites a program has. Eight, as on a C128; the verbs number them 1-8. */
#define AKBASIC_MAX_SPRITES 8
/**
* @brief The collision shapes SPRHIT understands.
*
* **Deliberately libakgl's own numbers**, so the two documents agree and nobody
* has to maintain a mapping between them. A kind this interpreter has no use for
* is still refused by name rather than silently accepted.
*/
#define AKBASIC_SHAPE_NONE 0
#define AKBASIC_SHAPE_BOX 1
#define AKBASIC_SHAPE_CIRCLE 2
#define AKBASIC_SHAPE_CAPSULE_X 3
#define AKBASIC_SHAPE_CAPSULE_Y 4
/** @brief One past the last kind, for the range check. */
#define AKBASIC_SHAPE_LAST 5
/**
* @brief Static collision rectangles a program can register with SOLID.
*
* Sixty-four, which is the smallest power of two that holds a sixty-brick wall
* with headroom. The ceiling matters because these come out of libakgl's
* collision proxy pool, which is shared with whatever host this interpreter is
* embedded in: eight sprites plus sixty-four solids is seventy-two of
* #AKGL_MAX_HEAP_COLLISION_PROXY, and the rest is the host's.
*/
#ifndef AKBASIC_MAX_SOLIDS
#define AKBASIC_MAX_SOLIDS 64
#endif
/** @brief Width of a Commodore sprite in pixels. */
#define AKBASIC_SPRITE_WIDTH 24
/** @brief Height of a Commodore sprite in pixels. */
#define AKBASIC_SPRITE_HEIGHT 21
/**
* @brief Bytes in a sprite pattern: three per row, twenty-one rows.
*
* The C128 documents SPRSAV's string as the SSHAPE data format at a fixed 24x21,
* which is 63 bytes of bitmap plus a four-byte header. Only the bitmap is
* carried here -- the header records a width and height that are constants for a
* sprite -- so a DIMmed integer array of this many elements is a whole pattern.
*/
#define AKBASIC_SPRITE_PATTERN_BYTES 63
/**
* @brief Fastest MOVSPR speed, and how far one unit of it travels.
*
* `MOVSPR n, angle # speed` takes 0-15 on a C128 and the reference manual does
* not say what a unit is in pixels; it says 15 is fastest and 0 stops. So the
* mapping is ours: one unit is #AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND device
* pixels per second, which puts speed 15 at four seconds to cross a 320-pixel
* screen and proportionally longer across a wider one. Recorded as a deviation
* in TODO.md section 5 rather than presented as fidelity.
*/
#define AKBASIC_SPRITE_MAX_SPEED 15
/** @brief Pixels per second one unit of MOVSPR speed is worth. See #AKBASIC_SPRITE_MAX_SPEED. */
#define AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND 5.0
/**
* @brief One sprite, as BASIC sees it.
*
* Every field here is something a BASIC verb sets and an RSP* function reads
* back, which is why the whole thing lives on the runtime rather than on the
* device: a host that swaps one renderer for another does not expect the
* program's sprite positions to go with it. The device is told about changes; it
* is never asked what they were.
*/
typedef struct
{
bool defined; /* SPRSAV has given this slot an image */
bool enabled; /* SPRITE n, 1 */
bool behind; /* priority: drawn behind the drawing surface */
bool xexpand; /* SPRITE's x-expand bit */
bool yexpand;
bool multicolour;
int colorindex; /* 1-16, the sprite's own colour */
double x; /* position in device pixels; see graphics.h */
double y;
double angle; /* MOVSPR's continuous motion: degrees */
int speed; /* clockwise from vertical, and 0-15 */
/**
* SPRHIT's collision shape: one of the #AKBASIC_SHAPE_* kinds, and a
* rectangle measured from the sprite's top-left corner.
*
* `shapeexplicit` is what separates "the program asked for the whole frame"
* from "the program has not said". They are the same shape today and will
* not stay that way: a frame-fitting shape has to be rebuilt when the
* picture or an expansion bit changes, and one the program gave in pixels
* must not be.
*/
int shapekind;
double shapex1;
double shapey1;
double shapex2;
double shapey2;
bool shapeexplicit;
} akbasic_Sprite;
/**
* @brief One rectangle registered by SOLID, as BASIC sees it.
*
* The id is the array index plus one and is the program's own number, not a
* minted handle. A brick in a sixty-brick wall has a natural index; making the
* program carry one back out of the interpreter would be inventing a second
* numbering for something that already has one.
*/
typedef struct
{
bool active;
double x1;
double y1;
double x2;
double y2;
} akbasic_Solid;
/**
* @brief What sprite n last collided with, and how.
*
* Narrower than libakgl's `akgl_Contact` on purpose. No actor pointers, because
* BASIC has no actor; no tile fields, because there is no tilemap; no z, because
* every test is planar. And **no `dt` and no `sensor`**, which libakgl documents
* as filled in by the resolver -- this interpreter never resolves anything, so
* they would come back zero and mean nothing. An always-zero field in a reference
* table is a lie.
*
* `axis` is the one field that is not the library's. It is the minimum
* translation axis -- which way to reverse -- and it is here because working it
* out from the normal is exactly where this dialect's left-operand arithmetic
* rule catches people, and because it is the single field that deletes the most
* BASIC.
*/
typedef struct
{
int what; /* 0 nothing yet, 1 a sprite, 2 static geometry */
int other; /* the other sprite's number, or the SOLID id */
double nx; /* unit normal, out of the other and toward this */
double ny;
double depth; /* how far in, in pixels */
double px; /* where they touched */
double py;
int axis; /* 1 reverse x, 2 reverse y */
} akbasic_Contact;
/**
* @brief The sprite verbs' own state, which lives on the runtime.
*
* `bumped` accumulates rather than reporting an instant. A collision that
* happens between two BUMP() calls is still news when the second one asks, and a
* program polling once a frame would otherwise miss anything that started and
* ended inside a frame. BUMP() clears what it reports, which is what a C128 does
* and what makes "has this sprite hit anything since I last looked" answerable.
*/
typedef struct
{
akbasic_Sprite sprites[AKBASIC_MAX_SPRITES];
akbasic_Solid solids[AKBASIC_MAX_SOLIDS];
akbasic_Contact contacts[AKBASIC_MAX_SPRITES];
int sharedcolor1; /* SPRCOLOR's two multicolour registers, 1-16 */
int sharedcolor2;
uint16_t bumped; /* bit n-1 set when sprite n has collided */
uint16_t bumpedsolid; /* and when it has met static geometry */
int64_t lastservicems; /* when MOVSPR's continuous motion last moved */
} akbasic_SpriteState;
/**
* @brief Where the sprite verbs land.
*
* Sprites are numbered 1 through #AKBASIC_MAX_SPRITES at this boundary, the same
* way a BASIC program numbers them. Translating to a zero-based slot is the
* backend's business, and doing it here would mean two numbering schemes in one
* file for no gain.
*
* There is deliberately no entry point for the RSP* readbacks. Everything they
* report is in #akbasic_SpriteState, which the interpreter owns, so asking the
* device would be asking it to repeat what it was told.
*/
typedef struct akbasic_SpriteBackend
{
void *self;
/**
* Install a pattern into sprite @p n from packed bitmap bytes.
*
* Three bytes per row, most significant bit leftmost, twenty-one rows --
* the layout a C128 keeps in its sprite block, and what a type-in listing's
* DATA statements hold. @p fg is what a set bit draws and @p bg what a clear
* one does; a clear bit is normally transparent, which is @p bg with a zero
* alpha.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*define)(struct akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg);
/**
* Install a pattern into sprite @p n from a region SSHAPE saved.
*
* The handle is the graphics backend's, not this one's -- SSHAPE puts it in
* a BASIC string and SPRSAV hands it straight back. An adaptor that serves
* both devices resolves it; one that does not may refuse with
* AKBASIC_ERR_DEVICE.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*define_shape)(struct akbasic_SpriteBackend *self, int n, int handle);
/**
* Install a pattern into sprite @p n by loading an image file.
*
* @p path is what the program wrote, resolved by the backend rather than
* here: the interpreter has no idea what a path means to the host, and the
* akgl adaptor resolves it exactly as libakgl resolves a spritesheet named
* by a sprite document. @p root is the directory to fall back to when the
* path does not resolve against the working directory, normally the
* directory the running program was loaded from; it may be NULL.
*
* Unlike the other two, this one does not force 24x21: the image's own size
* is the sprite's size. A modern PC has no reason to throw away art that
* does not happen to be sprite-shaped.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*define_file)(struct akbasic_SpriteBackend *self, int n, const char *path, const char *root);
/** Show or hide sprite @p n. */
akerr_ErrorContext AKERR_NOIGNORE *(*show)(struct akbasic_SpriteBackend *self, int n, bool visible);
/** Put sprite @p n at (@p x, @p y), in device pixels. */
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akbasic_SpriteBackend *self, int n, double x, double y);
/**
* Set sprite @p n's own colour, its expansion bits and its priority.
*
* One call rather than four because `SPRITE` sets them together and a
* backend that has to rebuild a texture to change a colour would otherwise
* do it four times for one statement.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*configure)(struct akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind);
/** Set the two multicolour registers every multicolour sprite shares. */
akerr_ErrorContext AKERR_NOIGNORE *(*shared_colors)(struct akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2);
/**
* Report which sprites are currently overlapping another sprite.
*
* Bit n-1 of @p mask is set when sprite n overlaps at least one other. The
* interpreter calls this once per step, ORs the result into
* akbasic_SpriteState::bumped, and raises the collision interrupt when
* anything is set -- so a backend answers about *now* and does not have to
* remember anything.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*collisions)(struct akbasic_SpriteBackend *self, uint16_t *mask);
/**
* Give sprite @p n a collision shape.
*
* @p kind is an #AKBASIC_SHAPE_NONE .. #AKBASIC_SHAPE_CAPSULE_Y, and the
* rectangle is two corners measured from the sprite's top-left, in device
* pixels -- the same form `BOX` and `SSHAPE` take, because a dialect with two
* spellings for a rectangle is a dialect nobody can write from memory.
*
* Optional. A backend that withholds it makes `SPRHIT` refuse by name, which
* is what a device that cannot do what was asked is supposed to do.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*shape)(struct akbasic_SpriteBackend *self, int n, int kind,
double x1, double y1, double x2, double y2);
/**
* Register, replace or retire static collision geometry @p id.
*
* @p define false retires it and the rectangle is ignored. Rectangles are in
* window pixels -- the coordinates `BOX` draws in and `MOVSPR` positions in,
* with `SCALE` not applying, exactly as for a sprite.
*
* Optional, like `shape`. A backend that withholds it makes `SOLID` refuse
* by name.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*solid)(struct akbasic_SpriteBackend *self, int id, bool define,
double x1, double y1, double x2, double y2);
/**
* Report which sprites are overlapping static geometry.
*
* Bit n-1 is set when sprite n overlaps at least one rectangle `solid`
* registered. Answered from the same pass as `collisions`, so the two are
* always about the same instant whichever order they are called in.
*
* Optional. A runtime whose backend withholds it reports no static
* collisions rather than refusing, exactly as one with no backend at all
* reports none -- a step is not a statement and has no line to blame.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*solids)(struct akbasic_SpriteBackend *self, uint16_t *mask);
/**
* Describe sprite @p n's deepest collision from the scan just run.
*
* @p found comes back false when that sprite hit nothing, and @p dest is
* then untouched. Deepest rather than first, because the deepest is the one
* a program has to undo.
*
* A pure accessor: it reads what the same pass that answered `collisions`
* and `solids` already worked out. That is why the contract on `collisions`
* says the two are about the same instant -- this depends on it.
*
* Optional. Withholding it makes `RCOLLISION` report nothing rather than
* refuse, the way a runtime with no backend at all does.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*contact)(struct akbasic_SpriteBackend *self, int n,
akbasic_Contact *dest, bool *found);
} akbasic_SpriteBackend;
/**
* @brief Reset the sprite state to its power-on values.
*
* Eight undefined, hidden sprites at the origin in white, the two shared
* multicolour registers at their C128 defaults, and nothing bumped. NEW comes
* back through here.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_state_init(akbasic_SpriteState *obj);
/**
* @brief Unpack 63 bytes of sprite bitmap into one pixel per byte.
*
* Three bytes per row, most significant bit leftmost. @p dest receives
* #AKBASIC_SPRITE_WIDTH * #AKBASIC_SPRITE_HEIGHT entries, each 0 or 1. Shared by
* every backend that has to turn a pattern into pixels, and separate from them
* so the bit order is asserted once rather than trusted eight times.
*
* @param pattern Packed bitmap; at least @p bytes readable.
* @param bytes How many bytes @p pattern holds; must be #AKBASIC_SPRITE_PATTERN_BYTES.
* @param dest Output destination populated by the function; 504 entries.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When either pointer is NULL.
* @throws AKBASIC_ERR_BOUNDS When `bytes` is not #AKBASIC_SPRITE_PATTERN_BYTES.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_unpack(const uint8_t *pattern, int bytes, uint8_t *dest);
struct akbasic_Runtime;
/**
* @brief Move every sprite MOVSPR set in continuous motion.
*
* Called once per akbasic_runtime_step(), before the line runs, and paced off
* akbasic_runtime_settime() -- exactly the way the PLAY queue is paced, and for
* the same reason: this library owns no loop and must not block, so the caller
* that owns the frame owns the clock.
*
* A host that never sets the time leaves it at zero and nothing moves, which is
* a still picture rather than a hang.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_service(struct akbasic_Runtime *obj);
/**
* @brief Ask the device what is overlapping, and raise the collision interrupt.
*
* Also called once per step. What it finds is ORed into
* akbasic_SpriteState::bumped for BUMP() to report, and raised as
* #AKBASIC_INTERRUPT_SPRITE -- unconditionally, because an unarmed interrupt
* records nothing and there is therefore nothing to ask first.
*
* A runtime with no sprite device, or one whose backend withholds `collisions`,
* does nothing here rather than refusing: a step is not a statement and has no
* program line to blame.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_collision_service(struct akbasic_Runtime *obj);
#endif // _AKBASIC_SPRITE_H_