Skip to content

Calling Behaviors

Quick Start: Spice it up with twists.

Big bundled mods like infection and vip-escort run a fatter pipeline than the simple one in Modifiers: event blocks guard and record shared state, then timer blocks evaluate per member. Here it is with infection:

1. Event block fires (ON_DEATH)
   A speedrunner dies what might be their final death.
        |
        v
2. Guard lines exit early (<if> plus exit)
   Former role SPEEDRUNNER? Eliminated for good?
   Anything else leaves quietly.
        |
        v
3. Shared state remembers (flags)
   <pswitch> the victim to HUNTER, append them
   to the <gflag:infected> list.
        |
        v
4. Timer block evaluates per member (INTERVAL)
   Every 12s per hunter: on the infected list?
   Root hunters exit on line one.
        |
        v
5. Game-world effects, gated by chance
   Sporadic blindness, groceries on luck
   (each candidate fails 80% of the time).
  • vip-escort shows the same shape with four blocks: an ON_START pick into <gflag:vip>, an ON_DEATH block that ends the game when the VIP dies, an ON_START plus ON_RESPAWN block writing one infinite effect entry for the VIP, and an ON_CLEANUP block clearing it when the escort ends.
  • Bingo cards, bounties, and position swaps all follow this pattern: guard, remember, evaluate per member, apply.

<runblock:id> runs another behavior block of the same modifier inline, in the current run: same executor, same event args.

  • The target's console list always runs; its player, hunter, and speedrunner lists run only when a live executor exists.
  • The target keeps its execution mode (IN_ORDER or PICK_RANDOM) and its success chance; its triggers, interval, and delay are ignored, so a called-only block never fires on its own.
  • The classic split is one interval block that refreshes state, checks completions, and calls a broadcast block only when something is due:
behavior:
  eligibility:
    runs-on:
      - INTERVAL
    options:
      interval-settings:
        interval: 8
        deviation: 2
    commands:
      console:
        - "<runblock:refresh>"
        - "<if:<gflag:dirty>==true,<runblock:broadcast>>"
  refresh:
    commands:
      console:
        - "<gflag:dirty,false>"
  broadcast:
    commands:
      console:
        - "<gmessage:standings updated>"
  • Chains share the calling line's step budget with loops and functions, cycles (a calls b calls a) stop with one warning per match, and unknown ids warn once per match and skip. A block that calls itself stops the same way, so prefer one direction: callers call down, called blocks never call back.

<runblock:id,arg1,arg2> passes arguments:

  • Inside the target, <args:0> is arg1, <args:1> is arg2, and so on (override-all: passed args replace the caller event args entirely, so a block that needs both must receive the event values explicitly).
  • Args evaluate in the caller context before the hop, so <i> and <lf> names in arg position resolve per iteration; one surrounding quote layer is stripped ("a, b" arrives as a, b).
  • The one-part form keeps inheriting the caller args, which is how an interval block hands its elapsed time to a refresh block without naming it.
# bingo: eligibility fans out per member with the name as arg
- "<for:<active-players:ALL>,<runblock:check-member,<i>>>"
# check-member completes one slot with WHO and SLOT as args
- "<runblock:announce-slot,<lf:who>,<i>>"
# announce-slot binds them to locals first
- "<lf:who,<args:0>>"
- "<lf:slot,<args:1>>"

One File Per Side Choice (Team Gate)

When a modifier can hit either side, ship one file, not one per role. A team ENUM setting names the side, a short player: gate compares it against <prole:<p>>, and a called-only block holds the shared script (called blocks omit runs-on on purpose):

config:
  settings:
    team:
      def:
        meta:
          name: Team
          desc: Which side this modifier affects.
          icon: SHIELD
        folder:
          - affected_team:
              def:
                meta:
                  name: Affected Team
                  desc: Tunable setting for which team this mod affects.
                  icon: SHIELD
                value:
                  type: ENUM
                  default: BOTH
                  allowed: [BOTH, RUNNER, HUNTER]
behavior:
  main:
    runs-on:
      - INTERVAL
    options:
      interval-settings:
        interval: 2.5
        behavior: PER_INVOKE
    commands:
      player:
        - '<lf:mine,<prole:<p>>>'
        - '<lf:wanted,<setting:team.affected_team>>'
        - '<lf:hit,<if:"<lf:wanted> == BOTH and <lf:mine> != null",true,false>>'
        - '<if:"<lf:wanted> == RUNNER and <lf:mine> == SPEEDRUNNER","<lf:hit,true>">'
        - '<if:"<lf:wanted> == HUNTER and <lf:mine> == HUNTER","<lf:hit,true>">'
        - '<if:"<lf:hit> == true","<runblock:logic>">'
  logic:
    commands:
      player:
        - '<lf:team,<active-players:<prole:<p>>>>'
  • The gate matches RUNNER against the SPEEDRUNNER side (<prole:> only yields HUNTER or SPEEDRUNNER); the null guard keeps BOTH to both teams, never spectators.
  • The one-part <runblock:logic> call inherits the executor (<p>) and the interval seconds (<args:0>), while local flags and <def:> functions stay per block, so the called block re-derives everything from <p>, <args:0>, and settings.

Next Steps