Skip to content

jengine-adventure

Point-and-click adventure - verbs, rooms that remember, and a game proved winnable from every state it can reach.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-adventure
Version0.1.0
Entry pointjengine_adventure::AdventurePackage
Script namespaceAdventure
Listed byprojects/arcade-classics

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

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

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 Adventure == nil then return end.

Scene keyRust type
cl_gameDrivepackage 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
Adventure.start(e)cl_start
Adventure.quit(e)cl_quit
Adventure.end_talk(e)cl_end_talk
Adventure.verb(e, verb)cl_verb
Adventure.point(e, x, y)cl_point
Adventure.click(e, x, y)cl_click
Adventure.say(e, line)cl_say
  • projects/arcade-classics/assets/scenes/lane.scene

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

Crooked Lane - a point-and-click adventure.

  • [world]: the game as a state machine - a room, an item bitset, a flag bitset.
  • [solve]: proving it winnable from every state it can reach.
  • [talk]: conversations, and the one hole in that proof.
  • [content]: loading it from data tables, ids resolved to indices.

Cursor work. A point-and-click is a game about pointing: what is under the cursor, what the current verb would do to it, and the sentence that describes it. All of that lives here, in Drive::look_at, and it is one hit test over one list - hotspots, exits and inventory icons together.

Keeping them on one path was a deliberate choice against the obvious alternative, which is hotspots in the world and the inventory in a .gui. Two hit-testing paths means two sets of coordinate conventions, two z orders, and two places for “what is under the cursor” to disagree - and the player cannot see any of that, they just find that clicking the crowbar sometimes does nothing. So the GUI owns the verb bar, the status line and the conversation; everything you can point at a thing with is a sprite.

A hint that cannot lie. Because solve knows the real distance from every reachable state to a win, the game can say how many moves are left and be right. The usual version of this is an author writing “you’re getting warmer” next to a puzzle and hoping.

Rooms are not scenes, and that is on purpose

Section titled “Rooms are not scenes, and that is on purpose”

It would be tempting to make each room a scene, and it would prove nothing: Brisk Errands already loads a scene every four seconds and already found the bug that lives there (BUGS #149). A room here is a background swap inside one scene, which is also what most of the genre actually does - a room is not a level. The state therefore lives in the component and needs nothing special to survive walking about.

cl_game, on an entity with a transform.

"cl_game": {
"tables": { "rooms": "data/cl_rooms", "items": "data/cl_items",
"flags": "data/cl_flags", "rules": "data/cl_rules",
"talk": "data/cl_talk" },
"sheet": "atlases/lane.png"
}
CallDoes
Adventure.start(e)begin
Adventure.verb(e, verb)choose a verb: look, take, use, talk, open
Adventure.point(e, x, y)the cursor moved - resolves what is under it
Adventure.click(e, x, y)act with the current verb at that point
Adventure.say(e, line_id)choose a line in a conversation
Adventure.end_talk(e)leave one
Adventure.quit(e)give up