/** * @file world.c * @brief The content pipeline: sprites, characters, the town map, and what the * map's actors do once they exist. * * Almost nothing here is game logic. It is the four steps between a directory * of JSON and a world with people standing in it, in the one order that works, * plus the two hooks -- a `movementlogicfunc` and a `renderfunc` -- that a game * has to supply because the library does not, and the collision world the * library resolves through. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "jrpg.h" /* * The three character templates and the twenty-four sprites that dress them. * The file names are a product of these three tables rather than a list of * twenty-four strings, because that is what the naming convention *is*: one * sprite per character, per motion, per facing. A missing combination shows up * as a load failure naming the file, not as art that silently never appears. */ /* Character template Sprite name prefix */ static char *CAST[] = { "player", /* jrpg_player jrpg_player_* */ "elder", /* jrpg_elder jrpg_elder_* */ "shopkeeper" /* jrpg_shopkeeper jrpg_shopkeeper_* */ }; static char *MOTIONS[] = { "idle", "walk" }; static char *FACINGS[] = { "up", "down", "left", "right" }; #define CAST_COUNT (sizeof(CAST) / sizeof(CAST[0])) #define MOTION_COUNT (sizeof(MOTIONS) / sizeof(MOTIONS[0])) #define FACING_COUNT (sizeof(FACINGS) / sizeof(FACINGS[0])) /** @brief One person in the town who has something to say. */ typedef struct { char *actor; /**< Registry key, which is the `name` of the map object that spawned them. */ char *line; /**< What they say. */ } jrpg_Townsfolk; /* * Actor (the map object's name) What they say */ static jrpg_Townsfolk TOWNSFOLK[] = { { "elder", "ELDER: The old road north is closed. Nothing to be done about it,\nand nothing beyond it worth the walk." }, { "shopkeeper", "SHOPKEEPER: Nothing in stock but tiles and good intentions.\nCome back when I have inventory." } }; #define TOWNSFOLK_COUNT (sizeof(TOWNSFOLK) / sizeof(TOWNSFOLK[0])) /** @brief How close the player has to stand, in pixels between sprite centres. */ #define TALK_RANGE 64.0f /* * The player's footprint, in pixels from the top-left of a 32x32 frame. A * character stands on the bottom third of their sprite, so that is the part * that has to fit through a doorway -- testing the whole frame would make every * corridor two tiles wide. * * 0 32 * +------------------------------+ 0 * | | * | (art) | * | | * | +----------------+ | 20 FEET_TOP * | | footprint | | * +------+----------------+------+ 30 FEET_TOP + FEET_H * 6 26 * FEET_X FEET_X + FEET_W */ #define FEET_X 6.0f #define FEET_W 20.0f #define FEET_TOP 20.0f #define FEET_H 10.0f /** * @brief How far past the edge of the map the boundary walls extend, in pixels. * * The walls cover the map's outer ring of cells *and* this much beyond it. The * ring is what the old hand-rolled rule made solid and what the art is drawn to * expect; the overhang is what stops an actor arriving fast enough from being * resolved out the far side of a wall exactly one tile thick. */ #define EDGE_OVERHANG 16.0f /** @brief Where the party member walks, in pixels relative to the player. */ #define FOLLOWER_OFFSET_X (-14.0f) #define FOLLOWER_OFFSET_Y (10.0f) akgl_CollisionWorld jrpg_collision; /** @brief The four map-edge walls, and the shapes they were built from. */ static akgl_CollisionProxy *jrpg_edges[4]; static akgl_CollisionShape jrpg_edge_shapes[4]; /** * @brief Load one sprite definition by its three naming components. * * @param cast Character stem, e.g. `"player"`. * @param motion `"idle"` or `"walk"`. * @param facing `"up"`, `"down"`, `"left"` or `"right"`. * @return `NULL` on success, otherwise an error context owned by the caller. */ static akerr_ErrorContext *sprite_load(char *cast, char *motion, char *facing) { PREPARE_ERROR(errctx); char path[PATH_MAX]; int written = 0; FAIL_ZERO_RETURN(errctx, cast, AKERR_NULLPOINTER, "cast"); FAIL_ZERO_RETURN(errctx, motion, AKERR_NULLPOINTER, "motion"); FAIL_ZERO_RETURN(errctx, facing, AKERR_NULLPOINTER, "facing"); PASS(errctx, aksl_snprintf( &written, path, sizeof(path), "%s/sprite_jrpg_%s_%s_%s.json", JRPG_ASSET_DIR, cast, motion, facing )); PASS(errctx, akgl_sprite_load_json(path)); SUCCEED_RETURN(errctx); } /** * @brief Load one character definition by its stem. * @param cast Character stem, e.g. `"player"`. * @return `NULL` on success, otherwise an error context owned by the caller. */ static akerr_ErrorContext *character_load(char *cast) { PREPARE_ERROR(errctx); char path[PATH_MAX]; int written = 0; FAIL_ZERO_RETURN(errctx, cast, AKERR_NULLPOINTER, "cast"); PASS(errctx, aksl_snprintf( &written, path, sizeof(path), "%s/character_jrpg_%s.json", JRPG_ASSET_DIR, cast )); PASS(errctx, akgl_character_load_json(path)); SUCCEED_RETURN(errctx); } /** * @brief Wall off the edges of the map with four static proxies. * * The town's decoration layer says what is solid -- a building, a tree, a lamp * post -- and the map's edge is not on it. Nothing else stops an actor walking * off the edge of the world. * * Four proxies rather than a hundred solid tiles, because static geometry that * is not tile-shaped is exactly what a proxy is for: a long thin box costs one * slot whatever its length. `layermask` is set by hand because * akgl_collision_shape_box defaults a shape to LAYER_ACTOR, which is right for * an actor and wrong for scenery -- an actor responds to LAYER_STATIC and would * walk straight through these. * * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKGL_ERR_HEAP If the proxy pool is exhausted. */ static akerr_ErrorContext *wall_off_the_edges(void) { SDL_FRect sides[4]; float32_t ow = 0.0f; /* Map width, plus the overhang at each end. */ float32_t oh = 0.0f; /* Map height, the same. */ float32_t tw = 0.0f; /* One tile of ring, plus one overhang. */ float32_t th = 0.0f; int i = 0; PREPARE_ERROR(errctx); ow = (float32_t)(akgl_gamemap->width * akgl_gamemap->tilewidth) + (2.0f * EDGE_OVERHANG); oh = (float32_t)(akgl_gamemap->height * akgl_gamemap->tileheight) + (2.0f * EDGE_OVERHANG); tw = (float32_t)akgl_gamemap->tilewidth + EDGE_OVERHANG; th = (float32_t)akgl_gamemap->tileheight + EDGE_OVERHANG; /* x y w h */ sides[0] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, tw, oh }; /* west */ sides[1] = (SDL_FRect){ (ow - tw - EDGE_OVERHANG), -EDGE_OVERHANG, tw, oh }; /* east */ sides[2] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, ow, th }; /* north */ sides[3] = (SDL_FRect){ -EDGE_OVERHANG, (oh - th - EDGE_OVERHANG), ow, th }; /* south */ for ( i = 0; i < 4; i++ ) { /* * Acquire and initialize adjacent, with nothing between them: until * akgl_collision_proxy_initialize takes the reference the slot is still * free, and the next acquire hands out the same pointer. */ PASS(errctx, akgl_heap_next_collision_proxy(&jrpg_edges[i])); PASS(errctx, akgl_collision_shape_box(&jrpg_edge_shapes[i], &sides[i], 0.0f)); jrpg_edge_shapes[i].layermask = AKGL_COLLISION_LAYER_STATIC; jrpg_edge_shapes[i].collidemask = AKGL_COLLISION_LAYER_NONE; jrpg_edge_shapes[i].flags |= AKGL_COLLISION_FLAG_STATIC; PASS(errctx, akgl_collision_proxy_initialize(jrpg_edges[i], NULL, &jrpg_edge_shapes[i], 0.0f, 0.0f, 0.0f)); PASS(errctx, jrpg_collision.partitioner.insert(&jrpg_collision.partitioner, jrpg_edges[i])); } SUCCEED_RETURN(errctx); } akerr_ErrorContext *jrpg_world_load(void) { PREPARE_ERROR(errctx); char path[PATH_MAX]; int written = 0; size_t c = 0; size_t m = 0; size_t f = 0; // Sprites first. A character JSON names its sprites by registry key and // akgl_character_load_json refuses one it cannot find, so loading the two // the other way round fails on every mapping in the file. for ( c = 0; c < CAST_COUNT; c++ ) { for ( m = 0; m < MOTION_COUNT; m++ ) { for ( f = 0; f < FACING_COUNT; f++ ) { PASS(errctx, sprite_load(CAST[c], MOTIONS[m], FACINGS[f])); } } } for ( c = 0; c < CAST_COUNT; c++ ) { PASS(errctx, character_load(CAST[c])); } // And the map last: loading it creates an actor for every `actor` object in // its object layer, and each of those resolves a character by name. PASS(errctx, aksl_snprintf(&written, path, sizeof(path), "%s/town.tmj", JRPG_ASSET_DIR)); PASS(errctx, akgl_tilemap_load(path, akgl_gamemap)); // The map declares its own physics -- `physics.model` and zero gravity on // both axes -- and akgl_tilemap_load builds a backend for it and sets // use_own_physics. It does not *switch* to it: akgl_game_update simulates // through the global akgl_physics, whatever that points at. Honouring the // map is the caller's job, and this is it. if ( akgl_gamemap->use_own_physics ) { akgl_physics = &akgl_gamemap->physics; } // Collision is opt-in, and this is the whole of turning it on. The cell // arguments are placeholders: akgl_collision_bind_tilemap overwrites them // from the map's own tile size, and reads which layers are solid off each // layer's `collidable` property rather than an index the game and the map // had to agree on out of band. PASS(errctx, akgl_collision_world_init(&jrpg_collision, NULL, 16.0f, 16.0f)); PASS(errctx, akgl_collision_bind_tilemap(&jrpg_collision, akgl_gamemap)); PASS(errctx, wall_off_the_edges()); akgl_physics->collision = &jrpg_collision; SUCCEED_RETURN(errctx); } akerr_ErrorContext *jrpg_world_populate(void) { PREPARE_ERROR(errctx); akgl_Actor *player = NULL; akgl_Actor *follower = NULL; SDL_FRect body; int i = 0; // Every actor the map spawned, including the player. // // akgl_actor_initialize sets movement_controls_face, and the default // facefunc it installs clears every facing bit and then sets the one // matching a *movement* bit. An NPC has no movement bits, so on the first // frame its state falls from ALIVE|FACE_DOWN (17) to ALIVE (16), which is // a state combination no character JSON maps a sprite to -- and an actor // with no sprite for its state is skipped rather than reported. Every NPC // in the town disappears on frame one, silently, and so does the player as // soon as they stop walking. for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) { if ( akgl_heap_actors[i].refcount == 0 ) { continue; } akgl_heap_actors[i].movement_controls_face = false; } player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL); FAIL_ZERO_RETURN( errctx, player, AKGL_ERR_REGISTRY, "town.tmj spawned no actor named \"player\"" ); player->movementlogicfunc = &jrpg_actor_logic_walk; // The footprint, and only the player gets one. The NPCs stand still and are // spoken to, not walked into: giving them shapes would make the town a // pinball table and would change what the proximity test in jrpg.c means. // The follower is a child, snapped to the player's position every step, so // a shape on it would fight the snap rather than stop anything. body = (SDL_FRect){ FEET_X, FEET_TOP, FEET_W, FEET_H }; PASS(errctx, akgl_collision_shape_box(&player->shape, &body, 0.0f)); player->shape_override = true; // The party member. Nothing in the map creates it, and nothing has to: an // actor is a pool object with a name, and the character it instantiates is // already registered. This one borrows the elder's template, which is the // whole point of splitting instance from template. PASS(errctx, akgl_heap_next_actor(&follower)); PASS(errctx, akgl_actor_initialize(follower, JRPG_FOLLOWER_NAME)); PASS(errctx, akgl_actor_set_character(follower, "jrpg_elder")); follower->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN); follower->movement_controls_face = false; follower->visible = true; follower->layer = player->layer; follower->renderfunc = &jrpg_follower_render; PASS(errctx, player->addchild(player, follower)); // A child's velocity fields are read as a fixed offset from the parent // rather than as a velocity: akgl_physics_simulate snaps a child to // `parent->x + vx` and never simulates it. Set after addchild, because // addchild is what makes them mean an offset. follower->vx = FOLLOWER_OFFSET_X; follower->vy = FOLLOWER_OFFSET_Y; SUCCEED_RETURN(errctx); } akerr_ErrorContext *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "obj->basechar"); // A conversation freezes the world. AKGL_ERR_LOGICINTERRUPT raised from a // movementlogicfunc means "skip the rest of this tick for this actor", and // akgl_physics_simulate swallows it in a HANDLE block -- so gravity, drag, // the velocity recompute and the move are all skipped and the frame carries // on. Thrust has already been integrated by the time this runs, so it is // zeroed here; leaving it would let it accumulate while frozen and lurch on // the first step after the box closes. The movement bits go too, so the // walk animation stops rather than marching on the spot -- which does mean // a direction held across the close has to be pressed again. if ( jrpg_textbox_showing() ) { obj->tx = 0.0f; obj->ty = 0.0f; AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_ALL); FAIL_RETURN( errctx, AKGL_ERR_LOGICINTERRUPT, "%s does not move while a conversation is open", (char *)obj->name ); } // Everything the default hook does -- re-copy the character's speed limits, // turn the movement bits into signed acceleration -- is still wanted. This // hook adds to it rather than replacing it. // // Walls used to be here too: a prediction of where this step would land, // tested axis at a time. It was only ever correct because the town has zero // gravity and zero drag, so `v` is `t` and the prediction is exact. The // library resolves after the move instead, on a position that already // happened, and that qualifier goes away with it. PASS(errctx, akgl_actor_logic_movement(obj, dt)); SUCCEED_RETURN(errctx); } akerr_ErrorContext *jrpg_follower_render(akgl_Actor *obj) { PREPARE_ERROR(errctx); akgl_Actor *parent = NULL; FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); // The guard. akgl_physics_simulate has already written this actor's x and y // as `parent->x + vx` and `parent->y + vy`, which is an absolute world // position; akgl_actor_render adds the parent's position to it a second // time. Detaching the parent for the duration of the draw takes the branch // that does not. CLEANUP puts it back on every path, including the failing // one -- an actor left with a NULL parent would be simulated as a free // agent on the very next step. parent = obj->parent; obj->parent = NULL; ATTEMPT { CATCH(errctx, akgl_actor_render(obj)); } CLEANUP { obj->parent = parent; } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *jrpg_camera_follow(void) { PREPARE_ERROR(errctx); akgl_Actor *player = NULL; float32_t mapw = 0.0f; float32_t maph = 0.0f; player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL); FAIL_ZERO_RETURN(errctx, player, AKGL_ERR_REGISTRY, "No actor named \"player\""); mapw = (float32_t)(akgl_gamemap->width * akgl_gamemap->tilewidth); maph = (float32_t)(akgl_gamemap->height * akgl_gamemap->tileheight); akgl_camera->x = (player->x + 16.0f) - (akgl_camera->w / 2.0f); akgl_camera->y = (player->y + 16.0f) - (akgl_camera->h / 2.0f); if ( akgl_camera->x < 0.0f ) { akgl_camera->x = 0.0f; } if ( akgl_camera->y < 0.0f ) { akgl_camera->y = 0.0f; } if ( akgl_camera->x > (mapw - akgl_camera->w) ) { akgl_camera->x = mapw - akgl_camera->w; } if ( akgl_camera->y > (maph - akgl_camera->h) ) { akgl_camera->y = maph - akgl_camera->h; } SUCCEED_RETURN(errctx); } akerr_ErrorContext *jrpg_cmhf_talk(akgl_Actor *obj, SDL_Event *event) { PREPARE_ERROR(errctx); akgl_Actor *npc = NULL; size_t i = 0; float32_t dx = 0.0f; float32_t dy = 0.0f; FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event"); if ( jrpg_textbox_showing() ) { jrpg_textbox_close(); SUCCEED_RETURN(errctx); } for ( i = 0; i < TOWNSFOLK_COUNT; i++ ) { npc = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, TOWNSFOLK[i].actor, NULL); if ( npc == NULL ) { continue; } dx = npc->x - obj->x; dy = npc->y - obj->y; if ( sqrtf((dx * dx) + (dy * dy)) > TALK_RANGE ) { continue; } PASS(errctx, jrpg_textbox_open(TOWNSFOLK[i].line)); SUCCEED_RETURN(errctx); } SUCCEED_RETURN(errctx); } akerr_ErrorContext *jrpg_cmhf_ignore(akgl_Actor *obj, SDL_Event *event) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event"); SUCCEED_RETURN(errctx); }