Skip to content

jengine-goldbox

A party of six across three packages - the first package here that depends on another.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-goldbox
Version0.1.0
Entry pointjengine_goldbox::GoldboxPackage
Script namespaceGoldbox
Depends onjengine-crawler, jengine-realm
Listed byprojects/arcade-classics

List it in the top-level "packages" array of project.jengine, together with every package it depends on: a crate dependency compiles a package in but does not register it, and packages_a_project_lists_cover_their_dependencies fails a project that lists only the outer one:

{
"packages": [
{ "name": "jengine-goldbox", "source": { "type": "path", "path": "../../packages/jengine-goldbox" } },
{ "name": "jengine-crawler", "source": { "type": "path", "path": "../../packages/jengine-crawler" } },
{ "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 Goldbox == nil then return end.

Scene keyRust type
gb_gameGoldBoxpackage 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
Goldbox.restore(e, party)gb_restore
Goldbox.walk(e, dx, dy)gb_walk
Goldbox.turn(e, steps)gb_turn
Goldbox.fight_move(e, dx, dy)gb_fight_move
Goldbox.new_party(e)gb_new_party
Goldbox.enter(e)gb_enter
Goldbox.leave(e)gb_leave
Goldbox.use(e)gb_use
Goldbox.rest(e)gb_rest
Goldbox.fight_attack(e)gb_fight_attack
Goldbox.fight_approach(e)gb_fight_approach
Goldbox.fight_guard(e)gb_fight_guard
Goldbox.fight_flee(e)gb_fight_flee
Goldbox.fight_ai(e)gb_fight_ai
  • projects/arcade-classics/assets/scenes/goldbox.scene

From packages/jengine-goldbox/src/lib.rs.

Gold Box - one party, three packages.

Not a genre. Every piece of a Gold Box game already exists in this arcade: the first-person grid view is Umber Warren’s (jengine-crawler), the tactical grid battle is Hollow Crown’s (jengine-realm), the party and bag are Monster Town’s (jengine-rpg). A row claiming this breaks new renderer or simulation ground would be wrong.

What it breaks is structural, and it is the shape the bugs that actually reach players come from. Audited Sep 2026:

  • No package in this repo depends on another. All thirty-eight depend on jengine-core and nothing else. Two scenes do use two packages at once (Loam Hollow, Steel Rain), but those are two independent packages on one entity list, not one package using another.
  • No package has ever read a resource another package inserted. Every ResourceMap read is a package reading its own, or the frame the host lends (InputState, FrameCamera; see docs/writing-packages.md).
  • Each of the three owns a whole party of its own. They were written as complete games, not as parts, so composing them means deciding who owns the party - which is the question this package exists to answer.

The rule that comes with a crate dependency

Section titled “The rule that comes with a crate dependency”

A crate dependency compiles a package in. It does not register it.

jengine-builder writes one host.add_package(Box::new(..)) per package listed in the project’s top-level "packages" array. A package reached only as a crate dependency never appears in that list, so its components are never registered and its script commands are never installed - the scene loads, the key lands in CustomComponents, and the first sign is attempt to call a nil value. That is how Dusk Rampart shipped broken, one level further out.

So: depend on the crate, and list both packages. tests/composing_packages.rs says it out loud, and packages_a_project_lists_cover_their_dependencies in jengine-core fails a project that lists one and not the other.

This package does, in Party, carried on the GoldBox (gb_game) component beside the run it is on. The crawler and the realm keep their own parties for their own games; Gold Box hands each of them the state it needs and reads the result back, because teaching two shipped games to share a party they know nothing about would change Umber Warren and Hollow Crown to serve something neither has heard of.

gb_game on one entity. It names Umber Warren’s tables, because the dungeon is Umber Warren’s - not a copy of it.

"gb_game": {
"tables": { "levels": "data/crawl_levels", "monsters": "data/crawl_monsters",
"crawlers": "data/crawl_party" },
"frame": { "cell_w": 420.0, "cell_h": 380.0, "falloff": 0.56, "gloom": 0.82 },
"seed": 20260922
}
CallDoes
Goldbox.new_party(e)roll a fresh four and forget any run
Goldbox.restore(e, party)put back a party a script saved - see below
Goldbox.enter(e)go down. A party already in a dungeon stays where it is
Goldbox.leave(e)come back up, keeping what it came out with
Goldbox.walk(e, dx, dy)a step: dy forward and back, dx sideways. A step into a monster opens a fight
Goldbox.turn(e, steps)turn right by that many quarters
Goldbox.use(e)use what is in front, or the stairs underfoot
Goldbox.rest(e)make camp: heal a little, at the cost of four ticks
Goldbox.fight_move(e, dx, dy)step the acting character on the battle grid
Goldbox.fight_attack(e)strike whatever it can reach
Goldbox.fight_approach(e)one tile along the shortest way to the nearest foe
Goldbox.fight_guard(e)stand fast, and end the turn
Goldbox.fight_flee(e)run for the edge. The monster stays in the corridor
Goldbox.fight_ai(e)take one foe’s turn

Three of those are easy to leave out and each one costs the game:

  • gb_fight_ai. Nothing advances a foe’s turn on its own. A game that never calls this opens a fight, hands the first turn to a rat, and waits forever - which is a hung game rather than a lost one. It is one turn per call so a script can pace it and the player can watch the rat cross the grid; calling it every frame is fine, it does nothing on the party’s move.
  • gb_fight_guard. The only action that always ends a turn. A character out of moves with nothing in reach cannot step and cannot strike, so without a pass the fight stops on it.
  • gb_fight_approach. gb_fight_move refuses a blocked tile without spending a move, so a script that walks toward a foe by comparing coordinates loops forever the first time a body is in the way.

One gb_game bag, republished every frame: phase (town, crawling, fighting, won, lost), party, level, x, y, facing, quads, battle and events.

quads is the crawler’s own picture, from Crawl::picture - every wall face, thing and monster, sorted back to front, so a script draws the list in order and gets depth for free. battle carries side_to_act and moves_left as well as the fighters, because a game needs both: whether to accept a keypress at all, and whether it is time to pace a foe’s turn.

events carries this package’s own (battle_started, battle_won, battle_fled, party_wiped, escaped, crawl), the crawler’s passed through, and the battle’s tagged "from": "battle" - a crawl and a fight both have a hit and they carry different fields.

A package cannot persist itself: data::global() hands packages an immutable registry, and every write in the package contract goes through a script or a runtime. So the party crosses a scene swap the way everything else does - read party out of the published bag, save_set it, and hand it back to gb_restore on the far side. Once per swap, not per frame.