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.