summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonAki <konaki@anokonaki.org>2026-10-16 01:30:11 +0800
committerKonAki <konaki@anokonaki.org>2026-10-16 01:30:11 +0800
commit7f76b57eb13bebb81b0e13875cf51114a2b9888c (patch)
tree0af8bb892ec430a81eb786942dc33ca60ad3b027
parent3dbb48bb7ec9f52c01ac172f8d75e6d0126d244f (diff)
add rmfield: player and events walking an rpg maker map
-rw-r--r--kon/konrmfield.c698
-rw-r--r--kon/konrmfield.h133
2 files changed, 831 insertions, 0 deletions
diff --git a/kon/konrmfield.c b/kon/konrmfield.c
new file mode 100644
index 0000000..f165df7
--- /dev/null
+++ b/kon/konrmfield.c
@@ -0,0 +1,698 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * konrmfield.c
+ *
+ * Copyright (c) 2026 KonAki
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "konrmfield.h"
+
+#include <stddef.h>
+#include <string.h>
+
+/*** helpers ***/
+
+static int validDir(int dir)
+{
+ return dir == 2 || dir == 4 || dir == 6 || dir == 8;
+}
+
+static int clampInt(int v, int lo, int hi)
+{
+ return v < lo ? lo : v > hi ? hi : v;
+}
+
+static int roll(kon_rmField_t *field, int n)
+{
+ uint32_t s = field->seed;
+
+ s ^= s << 13;
+ s ^= s >> 17;
+ s ^= s << 5;
+ field->seed = s ? s : 1;
+ return n > 0 ? (int)(field->seed % (uint32_t)n) : 0;
+}
+
+static int dirX(int dir)
+{
+ return dir == 6 ? 1 : dir == 4 ? -1 : 0;
+}
+
+static int dirY(int dir)
+{
+ return dir == 2 ? 1 : dir == 8 ? -1 : 0;
+}
+
+/* how far a is right of b, the short way round on a map that loops */
+static int deltaX(const kon_rmMap_t *map, int a, int b)
+{
+ int r = a - b;
+
+ if ((map->scrollType == 2 || map->scrollType == 3) && (r < 0 ? -r : r) > map->width / 2) r += r < 0 ? map->width : -map->width;
+ return r;
+}
+
+static int deltaY(const kon_rmMap_t *map, int a, int b)
+{
+ int r = a - b;
+
+ if ((map->scrollType == 1 || map->scrollType == 3) && (r < 0 ? -r : r) > map->height / 2) r += r < 0 ? map->height : -map->height;
+ return r;
+}
+
+static int moving(const kon_rmChar_t *c)
+{
+ return c->remain > 0 || c->jumpCount > 0;
+}
+
+static void setDir(kon_rmChar_t *c, int dir)
+{
+ if (!c->dirFix && validDir(dir)) c->dir = dir;
+ c->stopCount = 0;
+}
+
+/*** what blocks what ***/
+
+static int collidedWithEvents(const kon_rmField_t *field, int self, int x, int y, int normalOnly)
+{
+ int i;
+
+ for (i = 1; i < field->count; i++) {
+ const kon_rmChar_t *e = &field->chars[i];
+
+ if (i == self || !e->active || e->through || e->x != x || e->y != y) continue;
+ if (!normalOnly || e->priority == 1) return 1;
+ }
+ return 0;
+}
+
+static int canPass(const kon_rmField_t *field, int index, int x, int y, int dir)
+{
+ const kon_rmChar_t *c = &field->chars[index];
+ const kon_rmChar_t *player = &field->chars[KON_RM_PLAYER];
+ int x2 = kon_rmRoundX(field->map, x + dirX(dir));
+ int y2 = kon_rmRoundY(field->map, y + dirY(dir));
+
+ if (!kon_rmValid(field->map, x2, y2)) return 0;
+ if (c->through) return 1;
+ if (!kon_rmPassable(field->map, field->tileset, x, y, dir)) return 0;
+ if (!kon_rmPassable(field->map, field->tileset, x2, y2, 10 - dir)) return 0;
+ if (index == KON_RM_PLAYER) return !collidedWithEvents(field, -1, x2, y2, 1);
+ if (collidedWithEvents(field, index, x2, y2, 0)) return 0;
+ return !(c->priority == 1 && player->active && !player->through && player->x == x2 && player->y == y2);
+}
+
+static int canPassDiagonally(const kon_rmField_t *field, int index, int x, int y, int horz, int vert)
+{
+ int x2 = kon_rmRoundX(field->map, x + dirX(horz));
+ int y2 = kon_rmRoundY(field->map, y + dirY(vert));
+
+ if (canPass(field, index, x, y, vert) && canPass(field, index, x, y2, horz)) return 1;
+ return canPass(field, index, x, y, horz) && canPass(field, index, x2, y, vert);
+}
+
+/*** starting events ***/
+
+static void start(kon_rmField_t *field, int index)
+{
+ kon_rmChar_t *e = &field->chars[index];
+
+ if (!e->active || !e->page) return;
+ if (field->hasScript && !field->hasScript(e->page->script, field->user)) return;
+ e->starting = 1;
+ if (e->trigger <= 2 && !e->locked) {
+ const kon_rmChar_t *p = &field->chars[KON_RM_PLAYER];
+ int sx = deltaX(field->map, e->x, p->x), sy = deltaY(field->map, e->y, p->y);
+
+ e->preLockDir = e->dir;
+ if ((sx < 0 ? -sx : sx) > (sy < 0 ? -sy : sy)) setDir(e, sx > 0 ? 4 : 6);
+ else if (sy != 0) setDir(e, sy > 0 ? 8 : 2);
+ e->locked = 1;
+ }
+}
+
+/* events on x, y that start with one of the triggers (bits of mask), the ones at the same height as the characters or the others */
+static int startAt(kon_rmField_t *field, int x, int y, int mask, int normal)
+{
+ int i, n = 0;
+
+ if (field->busy || !kon_rmValid(field->map, x, y)) return 0;
+ for (i = 1; i < field->count; i++) {
+ kon_rmChar_t *e = &field->chars[i];
+
+ if (!e->active || e->x != x || e->y != y || !((mask >> e->trigger) & 1) || (e->priority == 1) != normal) continue;
+ start(field, i);
+ if (e->starting) n++;
+ }
+ return n;
+}
+
+static void touchFront(kon_rmField_t *field, int index, int dir)
+{
+ const kon_rmChar_t *c = &field->chars[index];
+ int x2 = kon_rmRoundX(field->map, c->x + dirX(dir));
+ int y2 = kon_rmRoundY(field->map, c->y + dirY(dir));
+
+ if (field->busy) return;
+ if (index == KON_RM_PLAYER) {
+ startAt(field, x2, y2, 0x06, 1);
+ } else if (c->trigger == 2 && c->priority == 1 && c->jumpCount == 0) {
+ const kon_rmChar_t *p = &field->chars[KON_RM_PLAYER];
+
+ if (p->active && p->x == x2 && p->y == y2) start(field, index);
+ }
+}
+
+/*** steps ***/
+
+static int step(kon_rmField_t *field, int index, int dir)
+{
+ kon_rmChar_t *c = &field->chars[index];
+ int ok = canPass(field, index, c->x, c->y, dir);
+
+ c->success = ok;
+ setDir(c, dir);
+ if (ok) {
+ c->moveDx = dirX(dir);
+ c->moveDy = dirY(dir);
+ c->x = kon_rmRoundX(field->map, c->x + c->moveDx);
+ c->y = kon_rmRoundY(field->map, c->y + c->moveDy);
+ c->remain = 256;
+ c->stopCount = 0;
+ c->steps++;
+ if (index == KON_RM_PLAYER) field->steps++;
+ } else {
+ touchFront(field, index, dir);
+ }
+ return ok;
+}
+
+static void stepDiagonal(kon_rmField_t *field, int index, int horz, int vert)
+{
+ kon_rmChar_t *c = &field->chars[index];
+ int ok = canPassDiagonally(field, index, c->x, c->y, horz, vert);
+
+ c->success = ok;
+ if (ok) {
+ c->moveDx = dirX(horz);
+ c->moveDy = dirY(vert);
+ c->x = kon_rmRoundX(field->map, c->x + c->moveDx);
+ c->y = kon_rmRoundY(field->map, c->y + c->moveDy);
+ c->remain = 256;
+ c->stopCount = 0;
+ c->steps++;
+ if (index == KON_RM_PLAYER) field->steps++;
+ }
+ if (c->dir == 10 - horz) setDir(c, horz);
+ if (c->dir == 10 - vert) setDir(c, vert);
+}
+
+static void stepRandom(kon_rmField_t *field, int index)
+{
+ kon_rmChar_t *c = &field->chars[index];
+ int dir = 2 + roll(field, 4) * 2;
+
+ if (canPass(field, index, c->x, c->y, dir)) step(field, index, dir);
+}
+
+static void stepToward(kon_rmField_t *field, int index, int away)
+{
+ kon_rmChar_t *c = &field->chars[index];
+ const kon_rmChar_t *p = &field->chars[KON_RM_PLAYER];
+ int sx = deltaX(field->map, c->x, p->x), sy = deltaY(field->map, c->y, p->y);
+ int h = (sx > 0) != away ? 4 : 6, v = (sy > 0) != away ? 8 : 2;
+
+ if ((sx < 0 ? -sx : sx) > (sy < 0 ? -sy : sy)) {
+ step(field, index, h);
+ if (!c->success && sy != 0) step(field, index, v);
+ } else if (sy != 0) {
+ step(field, index, v);
+ if (!c->success && sx != 0) step(field, index, h);
+ }
+}
+
+static void turnToward(kon_rmField_t *field, int index, int away)
+{
+ kon_rmChar_t *c = &field->chars[index];
+ const kon_rmChar_t *p = &field->chars[KON_RM_PLAYER];
+ int sx = deltaX(field->map, c->x, p->x), sy = deltaY(field->map, c->y, p->y);
+ int h = (sx > 0) != away ? 4 : 6, v = (sy > 0) != away ? 8 : 2;
+
+ if ((sx < 0 ? -sx : sx) > (sy < 0 ? -sy : sy)) setDir(c, h);
+ else if (sy != 0) setDir(c, v);
+}
+
+static void jump(kon_rmField_t *field, int index, int dx, int dy)
+{
+ kon_rmChar_t *c = &field->chars[index];
+ int x2 = kon_rmRoundX(field->map, c->x + dx), y2 = kon_rmRoundY(field->map, c->y + dy);
+ int d2 = dx * dx + dy * dy, dist = 0;
+
+ if (dx > 1000 || dx < -1000 || dy > 1000 || dy < -1000 || !kon_rmValid(field->map, x2, y2)) return;
+ if (dx != 0 || dy != 0) {
+ if ((dx < 0 ? -dx : dx) > (dy < 0 ? -dy : dy)) setDir(c, dx < 0 ? 4 : 6);
+ else if (dy != 0) setDir(c, dy < 0 ? 8 : 2);
+ }
+ while ((dist + 1) * (dist + 1) <= d2 + d2 / 4 + 1) dist++; /* about the length, rounded */
+ c->x = x2;
+ c->y = y2;
+ c->jumpDx = dx;
+ c->jumpDy = dy;
+ c->jumpPeak = clampInt(10 + dist - c->moveSpeed, 1, 2000);
+ c->jumpCount = c->jumpPeak * 2;
+ c->stopCount = 0;
+ c->steps++;
+}
+
+/*** routes ***/
+
+static void runCommand(kon_rmField_t *field, int index, int code, int count, const int32_t *p)
+{
+ kon_rmChar_t *c = &field->chars[index];
+ int a = count > 0 ? p[0] : 0;
+
+ switch (code) {
+ case 1: case 2: case 3: case 4: step(field, index, code == 1 ? 2 : code == 2 ? 4 : code == 3 ? 6 : 8); break;
+ case 5: stepDiagonal(field, index, 4, 2); break;
+ case 6: stepDiagonal(field, index, 6, 2); break;
+ case 7: stepDiagonal(field, index, 4, 8); break;
+ case 8: stepDiagonal(field, index, 6, 8); break;
+ case 9: stepRandom(field, index); break;
+ case 10: stepToward(field, index, 0); break;
+ case 11: stepToward(field, index, 1); break;
+ case 12: step(field, index, c->dir); break;
+ case 13: {
+ int fix = c->dirFix;
+
+ c->dirFix = 1;
+ step(field, index, 10 - c->dir);
+ c->dirFix = fix;
+ break;
+ }
+ case 14: jump(field, index, a, count > 1 ? p[1] : 0); break;
+ case 15: c->routeWait = a > 1 ? (a > 1000000 ? 1000000 : a) - 1 : 0; break;
+ case 16: setDir(c, 2); break;
+ case 17: setDir(c, 4); break;
+ case 18: setDir(c, 6); break;
+ case 19: setDir(c, 8); break;
+ case 20: setDir(c, c->dir == 2 ? 4 : c->dir == 4 ? 8 : c->dir == 8 ? 6 : 2); break;
+ case 21: setDir(c, c->dir == 2 ? 6 : c->dir == 6 ? 8 : c->dir == 8 ? 4 : 2); break;
+ case 22: setDir(c, 10 - c->dir); break;
+ case 23: setDir(c, roll(field, 2) ? (c->dir == 2 ? 6 : c->dir == 6 ? 8 : c->dir == 8 ? 4 : 2) : (c->dir == 2 ? 4 : c->dir == 4 ? 8 : c->dir == 8 ? 6 : 2)); break;
+ case 24: setDir(c, 2 + roll(field, 4) * 2); break;
+ case 25: turnToward(field, index, 0); break;
+ case 26: turnToward(field, index, 1); break;
+ case 27: case 28: if (field->switchSet && count > 0) field->switchSet(a, code == 27, field->user); break;
+ case 29: c->moveSpeed = clampInt(a, 1, 6); break;
+ case 30: c->moveFrequency = clampInt(a, 1, 5); break;
+ case 31: c->walkAnime = 1; break;
+ case 32: c->walkAnime = 0; break;
+ case 33: c->stepAnime = 1; break;
+ case 34: c->stepAnime = 0; break;
+ case 35: c->dirFix = 1; break;
+ case 36: c->dirFix = 0; break;
+ case 37: c->through = 1; break;
+ case 38: c->through = 0; break;
+ case 39: c->transparent = 1; break;
+ case 40: c->transparent = 0; break;
+ case 42: c->opacity = clampInt(a, 0, 255); break;
+ default: break; /* the image, blend, sound and script commands are not here */
+ }
+}
+
+static void routeStep(kon_rmField_t *field, int index)
+{
+ kon_rmChar_t *c = &field->chars[index];
+ int code, count;
+
+ if (c->routeWait > 0) {
+ c->routeWait--;
+ return;
+ }
+ c->success = 1;
+ if (!c->route || c->routeIndex < 0 || c->routeIndex + 2 > c->routeSize) {
+ /* the end of the route */
+ if (c->routeRepeat) c->routeIndex = 0;
+ else if (c->routeForced) c->routeForced = 0;
+ return;
+ }
+ code = c->route[c->routeIndex];
+ count = c->route[c->routeIndex + 1];
+ if (count < 0 || count > 8 || c->routeIndex + 2 + count > c->routeSize) {
+ c->routeIndex = c->routeSize;
+ return;
+ }
+ runCommand(field, index, code, count, c->route + c->routeIndex + 2);
+ if (c->success || c->routeSkippable) c->routeIndex += 2 + count;
+}
+
+/*** pages ***/
+
+static void setupPage(kon_rmField_t *field, kon_rmChar_t *c, int pageIndex, const kon_rmPage_t *page)
+{
+ int dir = validDir(page->direction) ? page->direction : 2;
+
+ c->pageIndex = pageIndex;
+ c->page = page;
+ c->active = 1;
+ if (c->originalDir != dir) {
+ c->originalDir = dir;
+ c->preLockDir = 0;
+ c->dirFix = 0;
+ c->dir = dir;
+ }
+ c->moveSpeed = clampInt(page->moveSpeed, 1, 6);
+ c->moveFrequency = clampInt(page->moveFrequency, 1, 5);
+ c->moveType = clampInt(page->moveType, 0, 3);
+ c->priority = clampInt(page->priority, 0, 2);
+ c->trigger = clampInt(page->trigger, 0, 4);
+ c->walkAnime = page->walkAnime;
+ c->stepAnime = page->stepAnime;
+ c->dirFix = page->directionFix;
+ c->through = page->through;
+ c->transparent = 0;
+ c->opacity = 255;
+ c->route = page->route;
+ c->routeSize = page->route ? page->routeSize : 0;
+ c->routeIndex = 0;
+ c->routeWait = 0;
+ c->routeRepeat = page->routeRepeat;
+ c->routeSkippable = page->routeSkippable;
+ c->routeForced = 0;
+ c->locked = 0;
+ c->starting = 0;
+ c->stopCount = 0;
+ (void)field;
+}
+
+static void clearPage(kon_rmChar_t *c)
+{
+ c->pageIndex = -1;
+ c->page = NULL;
+ c->active = 0;
+ c->locked = 0;
+ c->starting = 0;
+ c->route = NULL;
+ c->routeSize = 0;
+ c->routeForced = 0;
+}
+
+void kon_rmFieldRefresh(kon_rmField_t *field)
+{
+ int i;
+
+ if (!field || !field->map) return;
+ for (i = 0; i < field->map->eventCount && i + 1 < field->count; i++) {
+ const kon_rmEvent_t *event = &field->map->events[i];
+ kon_rmChar_t *c = &field->chars[i + 1];
+ int page = kon_rmEventPage(event, field->map->id, field->state);
+
+ if (page == c->pageIndex) continue;
+ if (page < 0) clearPage(c);
+ else setupPage(field, c, page, &event->pages[page]);
+ }
+}
+
+/*** the field ***/
+
+int kon_rmFieldInit(kon_rmField_t *field, const kon_rmMap_t *map, const kon_rmTileset_t *tileset, const kon_rmState_t *state, kon_rmChar_t *chars, int capacity)
+{
+ int i;
+
+ if (!field || !map || !chars || capacity < map->eventCount + 1 || map->eventCount < 0) return -1;
+ memset(field, 0, sizeof *field);
+ field->map = map;
+ field->tileset = tileset;
+ field->state = state;
+ field->chars = chars;
+ field->count = map->eventCount + 1;
+ field->seed = 2463534242u;
+ memset(chars, 0, sizeof *chars * (size_t)field->count);
+ for (i = 0; i < field->count; i++) {
+ kon_rmChar_t *c = &chars[i];
+
+ c->dir = 2;
+ c->originalDir = 2;
+ c->moveSpeed = 4;
+ c->moveFrequency = 3;
+ c->priority = 1;
+ c->opacity = 255;
+ c->pageIndex = -1;
+ if (i > 0) {
+ c->x = map->events[i - 1].x;
+ c->y = map->events[i - 1].y;
+ c->eventId = map->events[i - 1].id;
+ }
+ }
+ chars[KON_RM_PLAYER].active = 1;
+ chars[KON_RM_PLAYER].trigger = 0;
+ kon_rmFieldRefresh(field);
+ return 0;
+}
+
+void kon_rmFieldSeed(kon_rmField_t *field, uint32_t seed)
+{
+ if (field) field->seed = seed ? seed : 1;
+}
+
+void kon_rmFieldPlace(kon_rmField_t *field, int x, int y, int dir)
+{
+ kon_rmChar_t *p;
+
+ if (!field || !field->chars) return;
+ p = &field->chars[KON_RM_PLAYER];
+ if (kon_rmValid(field->map, x, y)) {
+ p->x = kon_rmRoundX(field->map, x);
+ p->y = kon_rmRoundY(field->map, y);
+ }
+ p->remain = 0;
+ p->jumpCount = 0;
+ if (validDir(dir)) p->dir = dir;
+}
+
+static int stopThreshold(const kon_rmChar_t *c)
+{
+ return 30 * (5 - c->moveFrequency);
+}
+
+static void selfMove(kon_rmField_t *field, int index)
+{
+ kon_rmChar_t *c = &field->chars[index];
+ const kon_rmChar_t *p = &field->chars[KON_RM_PLAYER];
+
+ if (c->stopCount <= stopThreshold(c)) return;
+ switch (c->moveType) {
+ case 1:
+ switch (roll(field, 6)) {
+ case 0: case 1: stepRandom(field, index); break;
+ case 2: case 3: case 4: step(field, index, c->dir); break;
+ default: c->stopCount = 0; break;
+ }
+ break;
+ case 2: {
+ int sx = deltaX(field->map, c->x, p->x), sy = deltaY(field->map, c->y, p->y);
+
+ if ((sx < 0 ? -sx : sx) + (sy < 0 ? -sy : sy) < 20) {
+ switch (roll(field, 6)) {
+ case 0: case 1: case 2: case 3: stepToward(field, index, 0); break;
+ case 4: stepRandom(field, index); break;
+ default: step(field, index, c->dir); break;
+ }
+ } else {
+ stepRandom(field, index);
+ }
+ break;
+ }
+ case 3: routeStep(field, index); break;
+ default: break;
+ }
+}
+
+static void updateChar(kon_rmField_t *field, int index)
+{
+ kon_rmChar_t *c = &field->chars[index];
+
+ if (c->jumpCount > 0) {
+ c->jumpCount--;
+ return;
+ }
+ if (c->remain > 0) {
+ c->remain -= 1 << clampInt(c->moveSpeed + c->dashing, 1, 8);
+ if (c->remain <= 0) {
+ c->remain = 0;
+ c->dashing = 0;
+ }
+ return;
+ }
+ c->dashing = 0;
+ c->stopCount++;
+ if (c->routeForced) routeStep(field, index);
+ else if (index > 0 && !c->locked && c->moveType) selfMove(field, index);
+}
+
+void kon_rmFieldUpdate(kon_rmField_t *field)
+{
+ kon_rmChar_t *p;
+ int i, wasMoving;
+
+ if (!field || !field->chars) return;
+ p = &field->chars[KON_RM_PLAYER];
+ wasMoving = moving(p);
+ for (i = 0; i < field->count; i++) {
+ if (field->chars[i].active) updateChar(field, i);
+ }
+ for (i = 1; i < field->count; i++) {
+ kon_rmChar_t *e = &field->chars[i];
+
+ if (e->active && e->trigger == 3 && !field->busy) start(field, i);
+ }
+ if (wasMoving && !moving(p) && !field->busy) startAt(field, p->x, p->y, 0x06, 0);
+}
+
+/*** the player ***/
+
+int kon_rmFieldPlayerStep(kon_rmField_t *field, int dir, int dash)
+{
+ kon_rmChar_t *p;
+
+ if (!field || !field->chars || !validDir(dir)) return 0;
+ p = &field->chars[KON_RM_PLAYER];
+ if (field->busy || moving(p) || p->routeForced) return 0;
+ step(field, KON_RM_PLAYER, dir);
+ if (p->success && dash && !field->map->disableDashing) p->dashing = 1;
+ return p->success;
+}
+
+int kon_rmFieldAction(kon_rmField_t *field)
+{
+ const kon_rmChar_t *p;
+ int x, y, x2, y2, n;
+
+ if (!field || !field->chars) return 0;
+ p = &field->chars[KON_RM_PLAYER];
+ if (field->busy || moving(p)) return 0;
+ if (startAt(field, p->x, p->y, 0x01, 0)) return 1;
+ x = kon_rmRoundX(field->map, p->x + dirX(p->dir));
+ y = kon_rmRoundY(field->map, p->y + dirY(p->dir));
+ n = startAt(field, x, y, 0x07, 1);
+ if (n == 0 && kon_rmHasFlag(field->map, field->tileset, x, y, KON_RM_FLAG_COUNTER)) {
+ x2 = kon_rmRoundX(field->map, x + dirX(p->dir));
+ y2 = kon_rmRoundY(field->map, y + dirY(p->dir));
+ n = startAt(field, x2, y2, 0x07, 1);
+ }
+ return n > 0;
+}
+
+/*** events that start ***/
+
+int kon_rmFieldNextStart(kon_rmField_t *field, int *eventId, int *pageIndex)
+{
+ int i;
+
+ if (!field || !field->chars) return 0;
+ for (i = 1; i < field->count; i++) {
+ kon_rmChar_t *e = &field->chars[i];
+
+ if (!e->active || !e->starting) continue;
+ e->starting = 0;
+ if (eventId) *eventId = e->eventId;
+ if (pageIndex) *pageIndex = e->pageIndex;
+ return 1;
+ }
+ return 0;
+}
+
+int kon_rmFieldFind(const kon_rmField_t *field, int eventId)
+{
+ int i;
+
+ if (!field || !field->chars) return -1;
+ for (i = 1; i < field->count; i++) {
+ if (field->chars[i].eventId == eventId) return i;
+ }
+ return -1;
+}
+
+void kon_rmFieldUnlock(kon_rmField_t *field, int index)
+{
+ kon_rmChar_t *c;
+
+ if (!field || !field->chars || index < 0 || index >= field->count) return;
+ c = &field->chars[index];
+ if (!c->locked) return;
+ c->locked = 0;
+ if (validDir(c->preLockDir)) setDir(c, c->preLockDir);
+}
+
+/*** moving by command ***/
+
+void kon_rmFieldRoute(kon_rmField_t *field, int index, const int32_t *route, int size, int repeat, int skippable)
+{
+ kon_rmChar_t *c;
+
+ if (!field || !field->chars || index < 0 || index >= field->count) return;
+ c = &field->chars[index];
+ c->route = route;
+ c->routeSize = route && size > 0 ? size : 0;
+ c->routeIndex = 0;
+ c->routeWait = 0;
+ c->routeRepeat = repeat;
+ c->routeSkippable = skippable;
+ c->routeForced = 1;
+}
+
+int kon_rmFieldRouteBusy(const kon_rmField_t *field, int index)
+{
+ if (!field || !field->chars || index < 0 || index >= field->count) return 0;
+ return field->chars[index].routeForced;
+}
+
+/*** drawing ***/
+
+int kon_rmCharX(const kon_rmChar_t *c)
+{
+ int jump = 0;
+
+ if (c->jumpCount > 0) jump = c->jumpDx * 256 * c->jumpCount / (c->jumpPeak * 2);
+ return c->x * 256 - c->moveDx * c->remain - jump;
+}
+
+int kon_rmCharY(const kon_rmChar_t *c)
+{
+ int jump = 0;
+
+ if (c->jumpCount > 0) jump = c->jumpDy * 256 * c->jumpCount / (c->jumpPeak * 2);
+ return c->y * 256 - c->moveDy * c->remain - jump;
+}
+
+int kon_rmCharJump(const kon_rmChar_t *c)
+{
+ int d;
+
+ if (c->jumpCount <= 0) return 0;
+ d = c->jumpCount - c->jumpPeak;
+ return (c->jumpPeak * c->jumpPeak - d * d) / 2;
+}
+
+int kon_rmCharMoving(const kon_rmChar_t *c)
+{
+ return moving(c);
+}
diff --git a/kon/konrmfield.h b/kon/konrmfield.h
new file mode 100644
index 0000000..50a5607
--- /dev/null
+++ b/kon/konrmfield.h
@@ -0,0 +1,133 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * konrmfield.h
+ *
+ * Copyright (c) 2026 KonAki
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef KONRMFIELD_H
+#define KONRMFIELD_H
+
+#include "konrmmap.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*** definition ***/
+
+/* the player and the events of one RPG Maker map, walking around on it by RPG Maker's rules: a step at a time at the speed of
+ the character, events that wander or follow a route, what blocks what, and what starts an event. nothing is drawn and no
+ script runs here, the game asks who wants to start and runs the page's script itself.
+ a character is in chars[0] for the player, then one for each event of the map in the order of the map's events, so
+ chars[i + 1] is events[i]. you give kon_rmFieldInit the array, it has to have room for eventCount + 1 */
+
+#define KON_RM_PLAYER 0
+
+typedef struct kon_rmChar {
+ int active; /* the player always, an event when one of its pages is on */
+ int x, y, dir; /* the tile it is on, or is walking to, and where it looks (2, 4, 6, 8) */
+ int moveDx, moveDy, remain; /* a step still to do: its direction, and what is left of it in 256ths of a tile */
+ int jumpCount, jumpPeak, jumpDx, jumpDy;
+ int moveSpeed, moveFrequency, dashing;
+ int priority, trigger, moveType;
+ int through, dirFix, walkAnime, stepAnime, transparent, opacity;
+ int pageIndex, eventId, originalDir;
+ const kon_rmPage_t *page;
+ int stopCount, locked, preLockDir, starting;
+ int steps; /* how many it has taken, for the walking animation */
+ int success; /* did the last step work */
+ const int32_t *route; /* what it follows: a page's route, or one it was told to */
+ int routeSize, routeIndex, routeWait, routeRepeat, routeSkippable, routeForced;
+} kon_rmChar_t;
+
+typedef struct kon_rmField {
+ const kon_rmMap_t *map;
+ const kon_rmTileset_t *tileset;
+ const kon_rmState_t *state; /* what the pages need to know, see konrmmap.h */
+ kon_rmChar_t *chars;
+ int count;
+ uint32_t seed;
+ int busy; /* set it while a script that stops the player is running */
+ int steps; /* the steps of the player, count them for encounters and take them back to 0 */
+
+ /* both can stay NULL. the first is how a route that turns a switch on or off reaches your game, the second says whether
+ the script a page names has anything in it, an event with an empty one does not start */
+ void (*switchSet)(int id, int on, void *user);
+ int (*hasScript)(const char *label, void *user);
+ void *user;
+} kon_rmField_t;
+
+/* returns 0, or -1 when there is no room in chars or something is NULL. the events start on their first page that is on */
+int kon_rmFieldInit(kon_rmField_t *field, const kon_rmMap_t *map, const kon_rmTileset_t *tileset, const kon_rmState_t *state, kon_rmChar_t *chars, int capacity);
+
+void kon_rmFieldSeed(kon_rmField_t *field, uint32_t seed);
+void kon_rmFieldPlace(kon_rmField_t *field, int x, int y, int dir);
+
+/* looks at the pages again, call it when a switch, variable, item or self switch has changed */
+void kon_rmFieldRefresh(kon_rmField_t *field);
+
+/* one frame (a 60th of a second) for everybody */
+void kon_rmFieldUpdate(kon_rmField_t *field);
+
+/*** the player ***/
+
+/* tries a step. nothing happens while the player is still walking, while busy or in a forced route. it turns and returns 0 when
+ something is in the way, and then an event in front that starts by touch does. dash only counts where the map allows it */
+int kon_rmFieldPlayerStep(kon_rmField_t *field, int dir, int dash);
+
+/* the action button: an event on the player's tile or in front of it, or behind a counter. returns 1 when one starts */
+int kon_rmFieldAction(kon_rmField_t *field);
+
+/*** events that start ***/
+
+/* the next event that wants its script to run, lowest first: the touched or talked to, and the autorun ones. call it when no
+ script is running. returns 1 and fills eventId and pageIndex (the script is the page's script label), or 0 for none.
+ events that started by touch or action look at the player until kon_rmFieldUnlock. parallel pages (trigger 4) never
+ come out of here, they are for the game to run on the side */
+int kon_rmFieldNextStart(kon_rmField_t *field, int *eventId, int *pageIndex);
+
+/* index into chars of the event with that id, or -1 */
+int kon_rmFieldFind(const kon_rmField_t *field, int eventId);
+
+/* lets an event go on walking and turn back the way it was looking. for the ones that started by touch or action */
+void kon_rmFieldUnlock(kon_rmField_t *field, int index);
+
+/*** moving by command ***/
+
+/* makes a character follow a route (the same shape as a page's: code, count, count numbers). the page's own route waits
+ until it is done. kon_rmFieldRouteBusy says 1 until the route has run to its end, a repeating one never ends */
+void kon_rmFieldRoute(kon_rmField_t *field, int index, const int32_t *route, int size, int repeat, int skippable);
+int kon_rmFieldRouteBusy(const kon_rmField_t *field, int index);
+
+/*** drawing ***/
+
+/* where it is in 256ths of a tile, in the middle of a step too. a map that loops can give a bit past its edge, the picture
+ wraps it. the jump height is in pixels, for what is drawn over the ground */
+int kon_rmCharX(const kon_rmChar_t *c);
+int kon_rmCharY(const kon_rmChar_t *c);
+int kon_rmCharJump(const kon_rmChar_t *c);
+int kon_rmCharMoving(const kon_rmChar_t *c);
+
+#ifdef __cplusplus
+}
+#endif
+#endif