jengine-rpg
Monster-collecting RPG: grid overworld, NPCs, tall-grass encounters, turn-based battles.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-rpg |
| Version | 0.1.0 |
| Entry point | jengine_rpg::RpgPackage |
| Script namespace | Rpg |
| Listed by | projects/arcade-classics, projects/engine-test-lab, projects/rpg-classics |
Using it
Section titled “Using it”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.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
rpg_world | RpgWorld | package only: needs a v2 scene |
rpg_walker | RpgWalker | package only: needs a v2 scene |
rpg_npc | RpgNpc | package only: needs a v2 scene |
rpg_battle | RpgBattle | package only: needs a v2 scene |
Script commands
Section titled “Script commands”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.
| Call | Flat 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 |
Scenes that use it
Section titled “Scenes that use it”projects/arcade-classics/assets/scenes/rpg_battle.sceneprojects/arcade-classics/assets/scenes/rpg_world.sceneprojects/engine-test-lab/assets/scenes/76-rpg.sceneprojects/engine-test-lab/assets/scenes/76b-battle.sceneprojects/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.
| Module | What it is |
|---|---|
creature | Species, 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. |
battle | The turn-based battle: the player’s party against one wild monster or a trainer’s team. |
world | The overworld components. |
Content in data tables
Section titled “Content in data tables”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):
| Table | A row is |
|---|---|
<prefix>_species | a 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>_moves | a MoveDef: name, kind, power, accuracy, pp, effect, chance |
<prefix>_types | optional: { attack, defend, mul } - pairs not listed are 1; with no rows the triangle stays |
<prefix>_items | optional: { name, kind, amount, catch_mul, price, about } - kind is heal, cure, revive or orb |
<prefix>_trainers | optional: { 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.
Battles
Section titled “Battles”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 itscatch_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_atevolves - anevolvestep, beforeend. - Shared experience: with
xp_share, every other able party member gets that share of a win’s experience. - Rewards: a trainer’s
rewardis 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.
The world and the battle
Section titled “The world and the battle”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):
- The world’s script
Scene.pushs the battle. - The battle’s script
Scene.pops back when the battle ends. - The engine’s scene stack restores the world as it was left, and the
world script’s
initruns 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:
| Key | Holds |
|---|---|
rpg.party | the party and bag |
rpg.encounter | who the next battle is against (a wild monster, or a trainer’s team) |
rpg.result | how 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.
The overworld
Section titled “The overworld”-
rpg_worldgoes on the entity with the map’s enginetilemap. It names two layers:- a collision layer, which is solid wherever it has a tile (mark it
collisionso it isn’t drawn) - a grass layer, where each step rolls
encounter_ratepercent against theencounterstable
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 thedex(or itsdex_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 onatputs the walker onto, so interiors can live on the same map.
- a collision layer, which is solid wherever it has a tile (mark it
-
rpg_walkeris the player:- It moves a whole tile per step, smoothly, facing where it goes.
- The
input_axison 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 itsinputis off, for dialogue or menus.
-
rpg_npcis someone standing on the map:- They block walking, and turn to face you when you talk to them.
- They say
lines, orafter_linesonce their trainer battle is won. healsrestores the party, and atrainerteam (or the dex’strainer_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 thetalkthat 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_battlegoes 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.
Script commands
Section titled “Script commands”| Call | Does |
|---|---|
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 }).
What scripts read
Section titled “What scripts read”RpgWorldState on the world entity has these fields:
events, each with akind: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)
partyandbox(each monster’s name, level, hp, max_hp, xp, type, status, region, stats, moves withpp/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,fledorcaughtturn, which goes up each turnsteps, the last turn, to play back. A step’skindistext,send,attack,damage,heal,status,asleep,faint,xp,levelup,throw,money,evolveorend.me,foe(withtypeandstatus),moves(withpp,max_pp,effect),party,active,items(withusable),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.rsalso: PP, evolution, learning a fifth move, items, shopping, the box, a type chart, and a dex read from data tables.src/battle.rsalso: 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