Actions
Actions trigger game functionality from json scripts.
They can do useful things such as spawning objects, triggering visual effects, changing the size of the arena, and more.
They can be run from stages, units, bullets, patterns, status effects, and even guns.
State Machine Control
These actions are used to handle logic and flow through a behaviour state machine.
Wait
Wait for an amount of time before continuing.
1 2 3 | "state 0": [ { "action": "Wait", "time":0.5, }, ], |
Tip
To wait indefinitely, don't provide a value for time
.
1 2 3 | "state_0": [ { "action": "Wait", }, ], |
Warning
For best practices, only use Wait
actions in behaviour state machines, not in handlers like onHitPixel
, onPlayerHit
, etc.
Condition
Use Condition
actions like if-else statements. Based on whether the condition
is true
or false
, call certain other actions.
1 2 3 4 5 6 7 8 9 10 | "state_1":[ { "action": "Condition", "condition": "playerPos.y > 0f", "true": [ { "action": "CallMethod", "target": "player", "method": "Vibrate", "params": { "strength": 1.5, "time": 0.25, }}, { "action": "Goto", "state": "state_2" }, ], "false":[ { "action": "Goto", "state": "state_3" }, ]}, ], |
Tip
You can omit the false
branch if it's not needed (or even omit the true
branch).
1 2 3 4 5 6 7 8 | "5b":[ { "action": "Condition", "condition": "player.GetStatusLevel('status/passive/shield') > 0", "true": [ { "action": "CallMethod", "target": "stage", "method": "SpawnPattern", "params": { "path": "octopus/pattern/bubbleGrid.1", }}, { "action": "CallMethod", "target":"player", "method": "Vibrate", "params": { "strength":0.1, "time":1, }}, { "action": "CallMethod", "target":"stage", "method": "ShakeCamera", "params": { "strength":0.25, "time":0.1, }}, ],}, ], |
Tip
Conditions can be nested as much as you want.
1 2 3 4 5 6 7 8 9 10 11 12 | "state 0": [ { "action": "Condition", "condition": "rand.Float(0f, 1f) < 0.25f", "true": [ { "action": "Condition", "condition": "playerPos.y < 0f", "true": [{ "action": "Goto", "state": "state 1" },], "false":[{ "action": "Goto", "state": "state 2" },]}, ], "false":[ { "action": "Condition", "condition": "playerPos.x < 0f", "false": [{ "action": "Goto", "state": "state 3" },],}, ]}, ], |
Switch
Use a Switch
action when your branching logic has too many conditions.
The value
parameter should be an integer variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | { "properties": { "attack_num": { "type": "Int", "value":1 }, }, // ... "fsm": { "form0": { "choose_next_attack":[ { "action": "Switch", "value": "attack_num", "cases": { "0": [{ "action": "Log", "message": "Shoot small attack!", },], "1": [{ "action": "Log", "message": "Shoot medium attack!!", },], "2": [ { "action": "Log", "message": "Shoot big attack!!!", }, { "action": "CallMethod", "target":"stage", "method": "PlaySfx", "params": { "sfxType":"BombExplode", "pos":"unitPos", }}, ], }}, ], } }, } |
Repeat
Use the Repeat
action to loop actions multiple times.
The count
parameter controls the number of times to loop the actions.
The delay
parameter is the time to wait after each loop.
The inner
parameter is the list of actions to loop.
1 2 3 4 5 | "state_6":[ { "action": "Repeat", "count": "6 + min(loopNum, 4)", "delay": "map(loopNum, 0, 6, 1.5f, 1.25f, 'QuadIn')", "inner": [ { "action": "CallMethod", "method": "ChargePattern", "params": { "path": "claw/pattern/orbiter.1", "partType": "core", "chargeTime":1, "dir":"(playerPos + playerVel * 2f) - partPos", }}, ]}, ], |
Expanded for clarity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | "state_6":[ { "action": "Repeat", "count": "6 + min(loopNum, 4)", "delay": "map(loopNum, 0, 6, 1.5f, 1.25f, 'QuadIn')", "inner": [ { "action": "CallMethod", "method": "ChargePattern", "params": { "path": "claw/pattern/orbiter.1", "partType": "core", "chargeTime":1, "dir":"(playerPos + playerVel * 2f) - partPos", } }, ]}, ], |
Goto
The Goto
action lets you change states.
In the following example, normally the state would loop from "1" back to the beginning state "initial delay". Instead it's told to go back to state "0".
1 2 3 4 5 6 7 8 9 10 11 12 13 | "fsm": { "form0": { "initial delay":[ // do this only one time ], "0":[ // do this every time ], "1":[ { "action": "Goto", "state":"0" }, ], } }, |
Tip
You can also be more specific and pass in a state of "form0.0" (or even "form1.0", if that exists).
SetValue
Set the value of a custom variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | { "properties":[ // ... "lastTauntTime": { "type": "Float", "value":-60, }, ], // when the player takes damage, boss should consider taunting them if it's been long enough since last taunt "baseForm": [ "onHitPlayer":[ { "action": "Condition", "condition": "(stageTime - lastTauntTime) > 5f", "true": [ // display a speech bubble taunt here // set value of lastTauntTime to current time { "action": "SetValue", "name": "lastTauntTime", "value": "stageTime" }, ],}, ], ], } |
When setting the value of another object's property, you must specify the target
object and the type
of the property.
1 | { "action": "SetValue", "target":"stage", "name": "cloudFlashGlowPercent", "type": "Float", "value": "clamp01(cloudFlashGlowPercent + 0.75f)" }, |
Expanded for clarity:
1 2 3 4 5 6 7 8 9 | [ { "action": "SetValue", "target":"stage", "name": "cloudFlashGlowPercent", "type": "Float", "value": "clamp01(cloudFlashGlowPercent + 0.75f)" }, ], |
CallMethod
The CallMethod
action is used to run whitelisted C# methods.
Specifying the target
is only necessary if you are calling a method on a different json object.
1 | { "action": "CallMethod", "target":"stage", "method": "AddTimeScale", "params": { "scale": 0.33, "time": 0.25, "easingType": "CubicIn" }}, |
Here are some of the most useful methods:
Stage Methods
SpawnUnit
AddTimeScale
LerpBounds
ShakeCamera
SpawnPattern
AffectBulletsInRadius
AffectTouchingBullets
AffectUnitsInRadius
AffectTouchingUnits
AddPattern
DespawnAddedPatterns
Player Methods
AddForce
AddPhysicsForce
Vibrate
ReplaceMainGun
RestoreDefaultMainGun
AddStatus
GetStatusLevel
Pattern Methods
Bullet Methods
AddPattern
Flash
Redirect
Reflect
AffectTouchingBullets
AffectTouchingUnits
SetFloatVar
SetVectorVar
SetIntVar
Transform
Unit Part Methods
ChargePattern
AddPattern
Flash
Look
SpawnBubble
StopChargingPatterns
StopPatterns
SetCore
Status Effect Methods
AddBullet
DespawnAddedBullets
AddGun
DespawnGuns
SetCharge
Execute
LevelUp
LevelDown
Disable
Return Values
When using CallMethod, or when calling a whitelisted method directly from a scriptfunc, it's possible to store the return value (output) of the method.
For example, SpawnUnit has a return value of type Unit (which will be equal to the unit spawned if successful, or null
if unsuccessful).
1 2 3 4 5 6 7 | { "action": "CallMethod", "target":"stage", "method": "SpawnUnit", "params": { "name": "egg", "pos": "vec2(0f, 0f)" }, "return":"eggUnit", }, |
In that example, egg
refers to the name of the unit in the stage's units
property...
1 2 3 4 | "units": { // ... "egg": { "config": "misc/unit/eggShield", "count":1, }, }, |
...and eggUnit
refers to the custom variable of type Unit
defined in the stage's properties
:
1 2 3 4 | "properties": { // ... "eggUnit": { "type": "Unit" }, }, |
By setting the return value to eggUnit
, the variable can now be used to call actions on the unit we just spawned.
1 2 3 4 5 | { "action": "CallMethod", "target":"stage", "method": "SpawnUnit", "params": { "name": "egg", "pos": "vec2(0f, 0f)" }, "return":"eggUnit" }, { "action": "Condition", "condition": "eggUnit != null", "true": [ { "action": "CallMethod", "target":"eggUnit", "method": "Shake", "params": { "strength":2, "time":0.5, }}, ],}, |
Tip
You can also use the return value of methods called within scriptfuncs.
1 2 3 4 5 6 7 8 9 10 11 12 13 | "properties":{ // ... "pixel": { "type": "PixelData", }, }, // ... "baseForm":{ // ... "onSpawn": [ { "action": "SetValue", "name": "pixel", "value": "unit.GetRandomPixel()" }, ], }, |
Subroutines
Subroutines allow you to create your own functions (essentially, a sequence of actions).
The following example calls the speech.sub
subroutine when the player gets hit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | { "onHitPlayer":[ { "action": "Condition", "condition": "(stageTime - lastTauntTime) > 5f", "true": [ { "action": "CallSubroutine", "target":"unit", "path": ".speech.sub", "params": { "message": "#octopus.boss.player_hit_pixel", "chance":"map(stageTime - lastTauntTime, 0f, 60f, 0f, 0.45f, 'QuadIn') * map(stageTime - lastSpeechTime, 0f, 4f, 0f, 1f, 'ExpoIn')", }}, { "action": "SetValue", "name": "lastTauntTime", "value": "stageTime" }, ],}, ], "speech.sub": { "parameters": { "this": { "type": "Unit" }, "message": { "type": "String" }, "chance": { "type": "Float" }, }, "actions": [ { "action": "Condition", "condition": "rand.Float(0f, 1f) < chance * map(stageTime - lastSpeechTime, 0f, 4f, 0f, 1f, 'ExpoIn') && !isImploding", "true": [ { "action": "CallMethod", "target":"unit.GetPart('core')", "method": "SpawnBubble", "ignoreNullRef": true, "params": { "text": "${message}", "partType":"core", "lifetime": 4, "fillColor": "color(0.2f, 0.1f, fastSin(stageTime * PI * 2f) * 0.2f + 0.5f, 0.8f)", "borderColor": "color(0.75f, 0.75f, fastSin((stageTime + 0.5f) * PI * 2f) * 0.05f + 0.95f, 0.66f)", "borderWidth": "fastSin(stageTime * PI * 3f) * 2f + 4f", "textColor":"lerp(color(1f, 1f, 1f), color(0.75f, 0.75f, 1f), 0.5f + fastSin(stageTime * PI * 12f) * 0.5f)", "fontSize":33, }}, { "action": "CallMethod", "target":"stage", "method": "PlaySfx", "params": { "sfxType": "OctopusBossSpeech", "pos":"unitPos", }}, { "action": "SetValue", "name": "lastSpeechTime", "value": "stageTime" }, ],}, ] }, }, |
When calling the subroutine, you should specify the target
, even if you're calling the subroutine from an object on itself.
1 2 3 4 5 6 7 8 9 | { "action": "CallSubroutine", "target":"unit", "path": ".speech.sub", "params": { "message": "#octopus.boss.player_hit_pixel", "chance":"map(stageTime - lastTauntTime, 0f, 60f, 0f, 0.45f, 'QuadIn') * map(stageTime - lastSpeechTime, 0f, 4f, 0f, 1f, 'ExpoIn')", }, }, |
The subroutine definition should include a this
parameter, set to the type of object the subroutine will be run on.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | { "tauntSpeech.sub": { "parameters": { "this": { "type": "Unit" }, }, "actions": [ { "action": "Condition", "condition": "(stageTime - lastTauntTime) > 10f && rand.Float(0f, 1f) < map(stageTime - lastTauntTime, 0f, 60f, 0f, 0.35f, 'QuadIn') * map(stageTime - lastSpeechTime, 0f, 4f, 0f, 1f, 'ExpoIn')", "true": [ { "action": "Condition", "condition": "player.GetStatusLevel('status/passive/shield') < 1", "true": [{ "action": "CallSubroutine", "target":"unit", "path": ".speech.sub", "params": { "message": "#octopus.boss.player_hit_bullet_no_shield", "chance":1, }},], "false":[{ "action": "CallSubroutine", "target":"unit", "path": ".speech.sub", "params": { "message": "#octopus.boss.player_hit_bullet", "chance":1, }}, ]}, { "action": "SetValue", "name": "lastTauntTime", "value": "stageTime" }, ],}, ] }, } |
Tip
The name of subroutines don't need to include .sub
, we just do it for clarity.