Skip to content

jengine-realm

An Ultima-style realm: a world of linked maps, a clock the people live by, conversation you type at, and tactical grid combat.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-realm
Version0.1.0
Entry pointjengine_realm::RealmPackage
Script namespaceRealm
Listed byprojects/arcade-classics, projects/rpg-classics

List it in the top-level "packages" array of project.jengine:

{
"packages": [
{ "name": "jengine-realm", "source": { "type": "path", "path": "../../packages/jengine-realm" } }
]
}

Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the editor’s Rebuild viewport banner). Native and web builds link it on their own.

A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field for a package component and keeps it as inert custom data.

A script can check that the package is linked with if Realm == nil then return end.

Scene keyRust type
realm_gameRealmGamepackage only: needs a v2 scene

Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.

CallFlat name
Realm.new_game(e)realm_new_game
Realm.move(e, dx, dy)realm_move
Realm.talk(e)realm_talk
Realm.say(e, word)realm_say
Realm.end_talk(e)realm_end_talk
Realm.rest(e, hours)realm_rest
Realm.travel(e, map, x, y)realm_travel
Realm.battle_move(e, dx, dy)realm_battle_move
Realm.battle_attack(e, target)realm_battle_attack
Realm.battle_approach(e, target)realm_battle_approach
Realm.battle_guard(e)realm_battle_guard
Realm.battle_flee(e)realm_battle_flee
Realm.battle_end_turn(e)realm_battle_end_turn
Realm.battle_ai(e)realm_battle_ai
Realm.battle_auto(e)realm_battle_auto
Realm.pause(e, on)realm_pause
Realm.effect(e, effects)realm_effect
Realm.load(e, json)realm_load
  • projects/arcade-classics/assets/scenes/crown.scene
  • projects/rpg-classics/assets/scenes/realm.scene

From packages/jengine-realm/README.md.

An Ultima-style realm: a world of linked maps, a clock the people live by, conversation you type at, and tactical combat on a grid.

Used by projects/arcade-classics (Hollow Crown) and projects/rpg-classics (ASHEN VIRTUE, an Ultima IV: eight virtues, a shrine, companions, a dungeon - built entirely from the data-driven parts below, with no game-specific code in the package).

ModuleWhat it is
clockThe hour, and what it does to the light and to who is awake.
peopleDaily rounds and keyword conversation, both data-table rows.
realmThe maps (ASCII drawings in a table), travel, wandering monsters, the save.
battleA fight on a grid, one turn at a time.

Three things, and none of them is the combat

Section titled “Three things, and none of them is the combat”

The clock. Everything else asks it questions. Shops keep hours, the light changes across dawn and dusk (daylight eases 0..1 rather than stepping, because a world that snaps from day to night at the top of the hour looks broken even when it is correct), and a person is in the tavern at eight and asleep at midnight. A step on the map is a minute, so a long walk costs a morning and a town errand costs nothing.

Typed conversation. You ask a person about a word. A row’s keywords map words to answers and its grants map a word to a flag — which is the entire quest system:

"keywords": { "crown": "It was taken. The CAPTAIN saw it go." },
"grants": { "crown": "heard_of_crown:1" }

The answers name the next word in capitals, so the trail is followed rather than guessed at. Matching is deliberately loose (trimmed, lowercased), because a conversation that only answers exact lowercase words is a conversation with a parser. Nobody answers questions in their sleep, and asking at three in the morning gets a mumble rather than a quest.

Tactics on a map. A fight is eleven by nine tiles, one turn at a time in initiative order. A character walks some tiles and strikes what it can reach, so where everyone is standing is the game: an archer with a reach of four behind a knight with a reach of one is a different situation from the two of them side by side. Flight is a position, not a dice roll — reach the edge of the field and the party leaves.

Monsters path with a breadth-first sweep outward from their target rather than stepping whichever way looks closer. The greedy version oscillates the moment there is a wall: it sidesteps round the obstacle, and then the sidestep is the thing it wants to undo, so the monster jitters on the spot for the whole fight.

The same trick the endless runner uses for its chunks, and it matters more here, because a game like this is mostly maps.

HHHHHHHHHHHHHHHHHHHH
H__________________H
H_HHHHH____HHHHHH__H
H________!_________H
HHHHHHHHH+HHHHHHHHHH + is a door out

A door is "x,y>map,x,y". Both ends are checked when the tables load, along with every scheduled position and every monster a map can summon, so a door into a wall fails loudly at startup instead of stranding the party later.

The package writes whichever map the party is on straight into the scene’s tilemap layer — walking through a door is one component write rather than a thousand script commands.

Everything Hollow Crown hard-coded is a table row now, and every new field has a default, so Hollow Crown’s tables load and play exactly as before.

Table / fieldWhat it adds
tiles table (optional)the map characters: glyph, gid, solid, battlefield clutter, talk_over (a shop counter a person is talked across). Without it the built-in nine apply; with it every character of every map must be a row
config table (optional, row realm)virtues (names, in display order), virtue_start, virtue_max, start_gold, start_map, party_max, reveal_topics, xp_per_level
person answersanswers that need things and do things: { text, needs, else, does, done, again }. A need may carry its own refusal after a `
person absent_ifconditions that take a person off the map ("member:wren" - a companion who joined)
map items"x,y:flag:Name" - stepping on the tile picks it up (sets the flag, once)
map fights"x,y:foe:flag" - a guarded tile: a fight on stepping onto it until it is won
door ?cond"x,y>map,x,y?lantern" - a door that opens only while the condition holds; the party is kept off it (locked event)
foe xp, on_win, on_fleeexperience for whoever is standing, and effects of winning or running - a fair fight fled is how valor is lost

Conditions (Realm::check): a flag name, !cond, member:id, heard:word, or <quantity><op><n> over gold, party, hour, day, level, steps, virtue.<name>, flag.<name> (ops >= <= > < == !=).

Effects (Realm::apply): gold+N / gold-N, virtue.<name>+N, xp+N, <flag>+N, flag:name[=N], clear:name, join:id, leave:id, heal, hurt:N, teach:word, wait:N (minutes), battle:foe, travel:map,x,y, event:name (a { kind: "event", name } for the script).

Topics are learned when reveal_topics is off: a conversation offers name, job and every word the party has heard someone say in CAPITALS (kept in the save as heard). Typing any word still works - that is the point of a text field.

Levels: with xp_per_level set, level n costs n * xp_per_level; a level adds hp / 6 (at least 3) to health, and attack and defence grow with it in battle (Realm::fighter_of). A companion joins at the leader’s level.

realm_game on one entity, in a "version": 2 scene.

"realm_game": {
"tables": { "maps": "data/realm_maps", "people": "data/realm_people",
"party": "data/realm_party", "foes": "data/realm_foes" },
"map_entity": "Ground", "map_layer": "ground", "seed": 7
}
CallDoes
Realm.new_game(e)a fresh party at the start
Realm.move(e, dx, dy)walk a tile; a door travels, and the road is dangerous
Realm.talk(e)talk to whoever is next to you
Realm.say(e, word)ask them about a word
Realm.end_talk(e)walk away
Realm.rest(e, hours)sleep: heals the party, moves the clock
Realm.travel(e, map, x, y)go somewhere directly
Realm.battle_move(e, dx, dy)walk, on the battlefield
Realm.battle_attack(e, target)strike, and end the turn
Realm.battle_approach(e, target)one step along the way to it
Realm.battle_guard(e) / Realm.battle_end_turn(e)stand fast / pass
Realm.battle_flee(e)leave, from the edge of the field
Realm.battle_ai(e)let the monster whose turn it is take it
Realm.battle_auto(e)let whoever is up take their turn - the party too (an “auto” button)
Realm.pause(e, on)hold the clock and the walking (a pause menu)
Realm.effect(e, "a;b")apply effects from the script (the words above)
Realm.load(e, json)restore a save
  • RealmState every frame: the clock and its daylight, where the party is, the party’s health (and level, xp, attack, defense, reach), gold, the flags it has collected, virtues ([{ name, value }]) and virtue_max, the heard words, marks (pickups and guarded tiles still on this map), paused, who is being talked to - with topics (what the conversation offers), all (every word they know) and last (the last reply) - and the whole battle when there is one, including can_act, nearest and ground, so a game never has to work out whether a turn can be taken.
  • RealmPeople: everyone on this map, and whether they are awake.
  • RealmMap: once per map, as rows of characters.
  • RealmEvents.events: travel, hour, battle, hit, down, over, battle_over, spoils, flag, rested, and reply (who, word, text, known), heard, virtue (virtue, delta, value), joined, left, party_full, level, found, locked, gold, healed, hurt, waited, event.

Note reply: before it existed the package worked a conversation’s answer out and dropped it, so a script had no way to show what a person said.