Systems & Mechanics

Rules prompts, action modes, dice expressions, and the sandboxed JavaScript mechanics engine.

A System defines how actions are resolved, what dice are rolled, how characters progress, and how rules are enforced.

System Structure

systems/classic-d20/
├── system.yaml         # System metadata, modes, and dice definitions
├── mechanics.js        # Sandboxed JavaScript mechanics engine
└── prompts/
    └── rules.md        # Natural language instructions for the GM agent

Action Modes

LocalRPG supports four fundamental action modes configured in system.yaml:

  • do: Physical or active interventions ("I leap across the chasm").
  • say: Direct dialogue or social interactions ("I ask the merchant about the lost amulet").
  • story: Narrative establishment or background declarations ("Ten years ago, my guild swore an oath…").
  • roll: Explicit rules checks evaluated against the mechanics engine ("Roll Athletics DC 15").

Dice Expressions

LocalRPG features a built-in dice evaluation engine supporting standard tabletop notations:

  • 1d20 + 5: Roll a 20-sided die and add 5.
  • 2d6: Roll two six-sided dice and sum the results.
  • 4dF: Fate/Fudge dice (values -1, 0, +1).
  • 1d100 / d%: Percentile dice.
  • 3d6kh2: Keep highest 2 of 3 six-sided dice.

Resolution Profiles

A system can declare named resolution profiles under mechanics.checks.profiles, so the GM can name how a check resolves. A profile maps a roll to an outcome. The supported kinds are threshold ladders, difficulty classes, success-count pools, and position/effect pairs.

mechanics:
  checks:
    notation: 2d6
    profiles:
      pbta:
        ladder:
          - { min: 10, outcome: strong }
          - { min: 7, outcome: weak }
          - { min: 0, outcome: miss }
      d20:
        notation: 1d20
        dc: 15
      pool:
        notation: 5d10
        success_on: ">=8"
        outcomes:
          - { min: 3, max: -1, outcome: strong }
          - { min: 1, max: 2, outcome: weak }
          - { min: 0, max: 0, outcome: miss }

Ladders resolve highest-first. Difficulty classes pass when the total meets them. Pools count the dice that meet success_on. A check without a profile resolves through the system's default conventions, exactly as before.

Opposed Checks

A profile can declare that a check is opposed. The opponent rolls too, and the higher total decides:

mechanics:
  checks:
    profiles:
      grapple:
        notation: 2d6
        dc: 10
        opposed: might    # the opponent rolls Might
        ties: opponent    # an equal total goes to the opponent; the default is actor

A check is opposed when the GM sets target to the opponent and opposed to a stat. When the profile declares opposed, it supplies the stat and the GM sets only the target. The opponent rolls the same notation and adds its opposed stat to the roll. An unknown opponent rolls flat, and a mistyped target resolves against that flat roll. The profile's best outcome applies when the actor's total is higher, and its worst when the opponent's is.

Rolling at the Table

With mechanics.engagement: ask, the GM proposes a check instead of resolving it. The Roll card states the stakes, the notation, the bonuses, and the possible outcomes, and offers three ways to settle it:

  • Roll rolls the dice in the app.
  • Enter a roll takes the dice you rolled at the table. Type 4 3 for two dice, or 9 for one total. The card shows the notation's range and previews the total with the system's bonuses, and a double-count is visible before you send it. The entry is recorded as manual, and an implausible number is flagged rather than refused.
  • Argue counter-proposes the check. Give a different approach, a restatement of the stakes, or a difficulty, and the GM rules on it: it accepts, adjusts, or holds with a reason. On accept or adjust the card shows the agreed terms, and you roll those. A hold leaves the check as it was. The chronicle records each counter and its ruling, and a check takes a few counters rather than an unlimited argument.

A check the player already rolled refuses a counter. An argument cannot rewrite a settled roll.

Sandboxed JavaScript Mechanics Engine (mechanics.js)

Custom systems export JavaScript functions that execute inside an isolated Goja runtime, a JavaScript engine embedded in the binary itself. The engine passes a small context and receives a structured resolution:

// Example mechanics.js
onAction('roll', function (ctx) {
  // ctx.player is the player's entity id, and ctx.action is the player's input.
  const athletics = getStat(ctx.player, 'athletics') || 0;
  const result = roll('1d20 + ' + athletics);
  const targetDC = 12;
  const success = result.total >= targetDC;

  return {
    success: success,
    roll: result,
    message: success ? 'Feat succeeded with style.' : 'Complication arises.',
  };
});

The mechanics engine exposes hooks including onAction, onTurnBegin, onTurnEnd, onWorldTick, onCheck, and onHealthZero, plus helpers such as roll, getStat, setStat, and injectGMDirection.

Edit this page on GitHub