Skip to content

jengine-rpg

Monster-collecting RPG: grid overworld, NPCs, tall-grass encounters, turn-based battles.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-rpg
Version0.1.0
Entry pointjengine_rpg::RpgPackage
Script namespaceRpg
Listed byprojects/arcade-classics, projects/engine-test-lab, projects/rpg-classics

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

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

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

Scene keyRust type
rpg_worldRpgWorldpackage only: needs a v2 scene
rpg_walkerRpgWalkerpackage only: needs a v2 scene
rpg_npcRpgNpcpackage only: needs a v2 scene
rpg_battleRpgBattlepackage 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
Rpg.new_game(world, starter)rpg_new_game
Rpg.interact(e)rpg_interact
Rpg.challenge(npc)rpg_challenge
Rpg.heal(world)rpg_heal
Rpg.set_input(e, on)rpg_set_input
Rpg.walk(e, dx, dy)rpg_walk
Rpg.battle_start(e)rpg_battle_start
Rpg.battle_choose(e, action, index)rpg_battle_choose
Rpg.party_swap(world, a, b)rpg_party_swap
Rpg.box_deposit(world, index)rpg_box_deposit
Rpg.box_withdraw(world, index)rpg_box_withdraw
Rpg.use_item(world, item, target)rpg_use_item
Rpg.give_item(world, item, count)rpg_give_item
Rpg.place(e, x, y, facing)rpg_place
Rpg.buy(world, item, count)rpg_buy
  • projects/arcade-classics/assets/scenes/rpg_battle.scene
  • projects/arcade-classics/assets/scenes/rpg_world.scene
  • projects/engine-test-lab/assets/scenes/76-rpg.scene
  • projects/engine-test-lab/assets/scenes/76b-battle.scene
  • projects/rpg-classics/assets/scenes/tamer.scene

From packages/jengine-rpg/README.md.

A monster-collecting RPG in the style of the early Pokémon games. You walk a tile map, talk to people, meet wild monsters in tall grass, and fight them on a separate battle screen.

Used by projects/arcade-classics (Monster Town), projects/rpg-classics (Mosswood Tamer) and Engine Test Lab scene 76.

ModuleWhat it is
creatureSpecies, moves (with PP), items, trainers and a type chart - the built-in triangle (fire > grass > water > fire) unless a game gives its own. Stats come from base stats and level; moves are learnt by level; species evolve at a level; experience follows a cubic curve. The party, bag, money and box are saved as JSON.
battleThe turn-based battle: the player’s party against one wild monster or a trainer’s team.
worldThe overworld components.

Monster Town’s dex is built in. A game with its own gives rpg_world and rpg_battle a dex_table prefix ("data/tamer"), and the dex is read from data tables (Dex::from_tables):

TableA row is
<prefix>_speciesa SpeciesDef: name (else the row id), kind, base, learns as [{ level, move }], catch_rate, xp_yield, region (else the name in lower case), evolves_at, evolves_to, about
<prefix>_movesa MoveDef: name, kind, power, accuracy, pp, effect, chance
<prefix>_typesoptional: { attack, defend, mul } - pairs not listed are 1; with no rows the triangle stays
<prefix>_itemsoptional: { name, kind, amount, catch_mul, price, about } - kind is heal, cure, revive or orb
<prefix>_trainersoptional: { id (the row id), name, team: [{ species, level }], reward } - an rpg_npc names one by trainer_id

A species learning a move no row defines, or evolving into one that is not there, is refused with the names (logged once). The component’s own dex.xp_scale and dex.xp_share still apply.

The battle module covers:

  • Choices: fight, potion, catch, switch, item and run. You can’t catch or run from a trainer.
  • PP: every move has uses; a move with none left is refused, and a monster with none in any move struggles. A heal restores PP.
  • Statuses: poison (hurts each turn), sleep (one to three turns) and paralysis (slower, and a quarter of turns lost), from status moves or a damaging move’s chance. They last until cured or healed, and make an orb likelier to hold (sleep x2, the others x1.5).
  • Items: heal, cure and throw orbs from the bag (Choice::Item); a better orb multiplies the odds by its catch_mul.
  • Turn order and damage: speed decides who goes first. Damage uses the classic formula, with a same-type bonus, type effectiveness and a random roll.
  • Stat stages: Growl lowers the foe’s attack and Harden raises your defence.
  • Fainting: a faint forces a switch, and a trainer sends out their next monster.
  • Growth: experience, level-ups, and moves learnt on level-up.
  • Catching: the odds depend on the foe’s HP, catch rate, status and the orb. A catch with a full party (six) goes to the box.
  • Evolution: after a battle that is not lost, every party member at its species’ evolves_at evolves - an evolve step, before end.
  • Shared experience: with xp_share, every other able party member gets that share of a win’s experience.
  • Rewards: a trainer’s reward is money for beating them.
  • The auto-player: it drinks a potion when low, switches to a partner that hits the foe much harder, and otherwise uses the strongest move. Tests and demos use it.

Each turn comes out as a list of steps for the battle screen to play back. Each step carries the numbers as they were at that moment, so health bars can drain along with the story.

They can be one scene: make rpg_battle manual, and call Rpg.battle_start(battle) after an encounter or a trainer’s challenge - the battle starts from the stored encounter, and the world hears how it went on the next frame (returned, or a blackout that heals the party and puts the player at heal_point). Mosswood Tamer does this, with the battle on a stage off the map.

Or they can be different scenes (Monster Town):

  1. The world’s script Scene.pushs the battle.
  2. The battle’s script Scene.pops back when the battle ends.
  3. The engine’s scene stack restores the world as it was left, and the world script’s init runs again. A flag in the persistent store tells that return apart from a fresh start.

What crosses between the scenes goes through the engine’s persistent store (scripting::persistent), as JSON the package reads and writes itself:

KeyHolds
rpg.partythe party and bag
rpg.encounterwho the next battle is against (a wild monster, or a trainer’s team)
rpg.resulthow the last battle ended: won, lost, fled, caught
rpg.beaten.<id>1 once that trainer is beaten

The store is the same one in every runtime: scripts read it with persistent_get_*, and the package writes it from Rust. Before BUGS #106, the browser runtimes kept a separate script-only copy.

  • rpg_world goes on the entity with the map’s engine tilemap. It names two layers:

    • a collision layer, which is solid wherever it has a tile (mark it collision so it isn’t drawn)
    • a grass layer, where each step rolls encounter_rate percent against the encounters table

    It also sets where a new game starts, where you wake up after losing everything, the starters, the starting money and bag (start_money, start_bag), and the dex (or its dex_table).

    • zones: named rectangles ({ name, rect: [x0, y0, x1, y1], encounters, encounter_rate }), each with its own wild monsters (a negative rate is the world’s). The player’s zone is published.
    • warps: doors ({ at, to, facing }) - finishing a step on at puts the walker on to, so interiors can live on the same map.
  • rpg_walker is the player:

    • It moves a whole tile per step, smoothly, facing where it goes.
    • The input_axis on the same entity steers it.
    • Its sprite region is <sprite>_<facing>_<frame>, with frame 0 standing and frames 1 and 2 the two steps. A walker is frozen while its input is off, for dialogue or menus.
  • rpg_npc is someone standing on the map:

    • They block walking, and turn to face you when you talk to them.
    • They say lines, or after_lines once their trainer battle is won.
    • heals restores the party, and a trainer team (or the dex’s trainer_id) battles after the lines.
    • sight: an unbeaten trainer sees that many tiles ahead. Stepping into that line freezes the player (spotted), and the trainer walks over and gives the talk that starts the battle.
    • gone_if: a persistent key (rpg.beaten.<id> or a game’s own flag); once it is set the person is hidden and out of the way - a guard who steps aside. A new game puts walked-over trainers back where they stood.
  • rpg_battle goes on one entity in the battle scene:

    • On its first frame it loads the party and the encounter. Opened on its own, it plays a demo battle instead.
    • When the battle ends it writes the party and the result back.
CallDoes
Rpg.new_game(world, starter)a fresh party; trainers unbeaten; back to the start
Rpg.interact(walker)talk to whoever is in front
Rpg.challenge(npc)set up a battle against that NPC’s team
Rpg.heal(world)restore the party
Rpg.set_input(walker, on)freeze or free the player
Rpg.walk(walker, dx, dy)one step without input_axis; one asked for mid-step lasts until that step ends, so send it every frame to keep walking
Rpg.battle_start(battle)begin a battle against the stored encounter (a manual battle)
Rpg.battle_choose(battle, action, index)"move" (index), "potion", "catch", "switch" (index), "item" (dex item index), "run", "auto"
Rpg.party_swap(world, a, b)reorder the party (the first leads)
Rpg.box_deposit(world, i) / Rpg.box_withdraw(world, i)between party and box; one able monster always stays
Rpg.use_item(world, item, target)heal, cure or revive a party member from the bag
Rpg.buy(world, item, count)buy at the item’s price
Rpg.give_item(world, item, count)put items in the bag (found, a gift)
Rpg.place(walker, x, y, facing)put the player on a tile (loading a save)

The party commands answer with an item, buy or party event ({ ok, text }).

RpgWorldState on the world entity has these fields:

  • events, each with a kind:
    • step, bump, warp (x, y, zone)
    • encounter (species, level)
    • spotted (npc, name, x, y)
    • talk (name, lines, heals, trainer, npc)
    • blackout, returned (result)
    • item, buy, party (ok, text)
  • party and box (each monster’s name, level, hp, max_hp, xp, type, status, region, stats, moves with pp / max_pp, evolution), bag (name, kind, count, about, price), money, seen, caught, potions, orbs, has_party, zone, player (x, y, facing, moving, input)

BattleState on the battle entity has these fields:

  • phase: choose, switch, won, lost, fled or caught
  • turn, which goes up each turn
  • steps, the last turn, to play back. A step’s kind is text, send, attack, damage, heal, status, asleep, faint, xp, levelup, throw, money, evolve or end.
  • me, foe (with type and status), moves (with pp, max_pp, effect), party, active, items (with usable), potions, orbs, money, wild, trainer, foes_left, caught_before, catch_chance
  • src/creature.rs: covers stats and moves by level, the type triangle, and the party’s JSON round trip.
  • src/battle.rs:
    • winning and XP, and types mattering
    • catching, running from a trainer, and a trainer’s next monster
    • a forced switch and a loss
    • levelling up and learning a move
  • tests/balance.rs: the arcade’s difficulty.
    • An untrained starter loses to the leader.
    • A trained one wins, for every starter.
    • Embit wins with a caught Mossbug, since the leader’s ace is water.
  • src/creature.rs also: PP, evolution, learning a fifth move, items, shopping, the box, a type chart, and a dex read from data tables.
  • src/battle.rs also: PP spent and refused, struggling, sleep, poison, a status that fails, a catch into the box, evolution, rewards, shared experience and an immunity.
  • tests/world_features.rs: one scene on a dex from data tables - a trainer who sees the player and walks over, a manual battle in the world’s scene, a guard who leaves on a flag, doors, zones with their own monsters, the shop, items, party order, the box, give_item, place.
  • tests/rpg_scenes.rs: a whole adventure through the real scenes.
    • a new game, and a house being solid
    • walking into the grass until a monster appears, and the battle scene playing it out
    • the world hearing how it went, and a trainer being marked beaten
    • the nurse healing, and a blackout waking you at the heal point
    • test-lab scenes 76 and 76b loading