jengine-cards
Deckbuilding roguelike: data-driven cards, enemy intents, statuses, a run map with rewards, rests and shops.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-cards |
| Version | 0.1.0 |
| Entry point | jengine_cards::CardsPackage |
| Script namespace | Cards |
| Listed by | projects/arcade-classics, projects/engine-test-lab |
Using it
Section titled “Using it”List it in the top-level "packages" array of project.jengine:
{ "packages": [ { "name": "jengine-cards", "source": { "type": "path", "path": "../../packages/jengine-cards" } } ]}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 Cards == nil then return end.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
card_game | CardGame | 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 |
|---|---|
Cards.new_run(e, seed) | cards_new_run |
Cards.choose(e, node) | cards_choose |
Cards.play(e, uid, target) | cards_play |
Cards.end_turn(e) | cards_end_turn |
Cards.pick(e, index) | cards_pick |
Cards.rest(e, choice, uid) | cards_rest |
Cards.buy(e, kind, index) | cards_buy |
Cards.leave(e) | cards_leave |
Cards.auto(e, on, delay) | cards_auto |
Scenes that use it
Section titled “Scenes that use it”projects/arcade-classics/assets/scenes/deckbuilder.sceneprojects/engine-test-lab/assets/scenes/79-cards.scene
From packages/jengine-cards/README.md.
A deckbuilding roguelike in the style of Slay the Spire. You climb a map of rooms, fight with a deck of cards, and make the deck stronger (and thinner) as you go.
Used by projects/arcade-classics (Ashen Deck) and Engine Test Lab
scene 79.
| Module | What it is |
|---|---|
content | Cards, enemies, relics and encounters, read from the project’s data tables. A new card is a new row, not new code. |
combat | A fight: the piles, energy, effects, statuses and enemy intents. |
run | The map, rewards, rest sites, shops and treasure. |
bot | A player that plays itself, for attract mode and the balance test. |
Content is data
Section titled “Content is data”The package reads four .data tables through the engine’s published
data registry, jengine_core::data::global(). These are the same tables
scripts read with Data.get, and you edit them in the editor’s table
editor. The native runner publishes them when it loads them, and the editor
viewport when the editor seeds them. Until they’re published, the game
waits (CardsState.phase is loading).
The tables to read are named on the component. The defaults are
data/cards, data/enemies, data/relics and data/encounters:
"card_game": { "seed": 0, "auto_delay": 0.45, "tables": { "cards": "data/cards", "enemies": "data/enemies", "relics": "data/relics", "encounters": "data/encounters" } }Loading checks every reference: an encounter naming a missing enemy, or an
effect adding a missing card, is an error at load (CardsState.load_error),
not a gap in the middle of a run.
Effects
Section titled “Effects”Cards, enemy moves and relics share one effect shape:
{ "op": "damage", "amount": 6, "times": 1, "status": "", "target": "", "card": "", "pile": "" }op | Does |
|---|---|
damage | amount damage, times times, after strength, weak and vulnerable. Block absorbs it first. |
block | Block for the doer, after dexterity and frail. |
apply | Adds amount of status (see the statuses below). |
draw, energy | For the player. |
heal, lose_hp | Heal, or lose HP. Losing HP ignores block. |
add_card | Puts card into a pile (discard by default, or draw or hand). Enemies use it to slime your deck. |
gold | Gold found in a fight. |
Targets. An effect with no target goes where it naturally would:
- a player’s damage goes to the enemy the card was aimed at (or every enemy
for an
allcard) - a player’s debuff goes to that enemy, and a buff to the player
- an enemy’s damage and debuffs go to the player, and its block and buffs to itself
Set a target to override this: self, all, random, or opponent,
which aims any status at the other side (an enemy taking your strength).
Statuses.
| Status | Does |
|---|---|
| strength | Adds to every hit. |
| dexterity | Adds to every block. |
| vulnerable | Takes 50% more attack damage. Runs down at the end of its owner’s turn. |
| weak | Deals 25% less attack damage. Runs down at the end of its owner’s turn. |
| frail | Gains 25% less block. Runs down at the end of its owner’s turn. |
| poison | Loses that much HP at the start of its turn, then 1 less. |
| thorns | Attackers take that much damage back. |
| ritual | Gains that much strength at the end of each turn. |
| metallicize | Gains that much block at the end of each turn. |
Card rows have these fields:
name,type(attack,skill,powerorstatus),rarity,cost,target(enemy,self,allornone) andtext.texthas{d}(damage),{b}(block),{m}(the other number) and{x}(hits) filled in, as the numbers would land now.effects, and the upgrade’splus_effects,plus_cost(-1 keeps the cost) andplus_text.exhaust,unplayableandart.
Enemy rows have these fields:
hp_minandhp_max, andmoves. Each move has anintentshown to the player, aweightand itseffects.pattern:randomby weight, never the same move three times running; orcycle, in order.first_move, andstarts_withfor the statuses it starts with.
Relic rows have a trigger (combat_start, first_turn, turn_start,
combat_end, attack_played or pickup) and effects, or extra energy
each turn.
Encounter rows name a pool (easy, normal, elite or boss) and
their enemies.
The run
Section titled “The run”- The map has twelve floors in seven lanes, built from four paths that merge. Row 0 is all fights, row 6 is treasure, row 10 is rest and row 11 is the boss. There are no elites or rest sites before row 4.
- Fights start with 3 energy and a 5-card draw.
- After a fight: gold and a choice of three cards. An elite also gives a relic.
- Rest sites heal 30% of max HP, or upgrade a card.
- Shops sell five cards and a relic, and remove a card for 75 gold.
- The start: 70 HP, 99 gold, five Strikes, four Defends, a Bash and the Ember Heart, which heals 6 after each fight.
Script commands
Section titled “Script commands”| Call | Does |
|---|---|
Cards.new_run(e, seed) | a fresh run (seed 0 picks one) |
Cards.choose(e, node) | go to a room on the map |
Cards.play(e, uid, target) | play a card from the hand, at an enemy (its index; -1 for none) |
Cards.end_turn(e) | end the turn: the enemies act, and the next turn begins |
Cards.pick(e, index) | take reward card index, or -1 to skip it |
Cards.rest(e, choice, uid) | heal, or upgrade card uid |
Cards.buy(e, kind, index) | card index, relic, or remove card uid |
Cards.leave(e) | leave a shop or treasure room |
Cards.auto(e, on, delay) | the bot plays, one action every delay seconds; it starts a new run when one ends |
Requests are carried out in the next package step, in order. A refused one
(a card not in hand, not enough energy) sets CardsState.error and changes
nothing.
What scripts read
Section titled “What scripts read”CardsState is rewritten when anything changes, and its version goes
up. It has these fields:
phase:map,combat,reward,rest,shop,treasure,victoryordefeat.floor,hp,max_hp,gold,relics,deck,seed,errorandload_error.map: the rooms, each withid,row,col,kind,next,reachableandvisited.combat:turn,energyandmax_energyplayer(hp,block,statuses)hand, and the pile sizesdraw,discardandexhaustedenemies, each withhp,block,statusesand anintent(kind,name,amount,times)incoming
reward,shop,restandtreasure, in their phases.
A card as shown has these fields:
uid,id,name,cost,type,rarity,targetandarttext, with its numbers as they would land nowplayable, andupgrade(its smithed name, cost and text, for a preview)
CardsEvents.events lists what happened this frame, in order:
- in a fight:
play,draw,damage(from,to,amount,blocked),block,status,heal,hp_loss,death,turn,enemy_move,shuffle,exhaust,add_card,energy,victoryanddefeat - in a run:
enter,relic,relic_gained,card_gained,upgraded,bought,removed,restedandrun_over
An enemy turn resolves all at once, so a screen that wants it to read one step at a time spaces the events out itself. Ashen Deck does this.
src/combat.rs:- damage and block modifiers
- a turn: energy, a play, the enemy’s hit through block, the next intent and a fresh hand
- poison through block, and thorns
tests/balance.rs: the arcade’s real tables.- Every reference resolves, and the text placeholders are known.
- The bot plays 40 runs and must win between a quarter and four fifths of them. Today it wins about half.
- The same seed plays the same run.
tests/deck_scene.rs: the arcade’s scene.- A run driven by commands visits every kind of room and reports every kind of fight event.
- A refused play sets
errorand spends nothing; a real one hits.
tests/test_lab_scene.rs: scene 79, on the lab’s own tables.