Skip to content

Bullet Patterns

A bullet pattern is made up of volleys, each shooting a number of bullets from a certain position and angle.

A Simple Pattern

1
2
3
4
5
6
7
8
"simpleAttack": {
  "numVolleys": 15,
  "numBulletsInVolley":5,
  "shootDelay": 0.1,
  "rotationSpeed":-150,
  "sfxVolley": "OctopusSmallSpewShoot",
  "bullets": [".bullet"],
},

This pattern shoots 15 times, with 5 bullets each. By default, the bullets in each volley are spread evenly around a full 360° circle.

After the initial volley, there's a 0.1 second delay until the next.

The pattern rotates clockwise with a speed of 150 units (basically equivalent to 150° per second). Negative is clockwise, positive is counterclockwise.

bullets defines what this pattern shoots - since only one type is referenced, every bullet in the pattern will be the same. The path can be absolute, but usually it's easiest to reference definitions in the same json object - in this case, .bullet refers to a bullet defined at the same json level as the simpleAttack pattern definition.

More About Patterns

Script Functions

Most of the properties in the pattern can be expressed as a string function that is continuously updated, instead of a static value. For example, we could increase the number of bullets in each subsequent volley by changing the numBulletsInVolley property:

1
"numBulletsInVolley":"5 + volleyNum",
The volleyNum parameter is a value that all patterns can use in functions. The first volley will have 5 bullets, the second 6 bullets, and so on.

Instead of an unchanging rotation speed, we could make it oscillate between -150 and 150:

1
"rotationSpeed":"sin(patternTime * 5f) * 150f",
patternTime simply tracks how long this pattern has been running for.

Info

The 'f's after the numbers denote them as floating-point numbers instead of integers.

Script Parameters

These parameters can be used inside any scriptfunc in a Pattern.

Aiming

By default, patterns simply rotate according to their rotationSpeed property, but we can instead set a target angle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
"simpleAttack": {
    "numVolleys": 15,
    "numBulletsInVolley":5,
    "shootDelay": 0.1,
    "bullets": [".bullet"],
    "sfxVolley": "ShootSoftWomp",
    "aimMode":"Target",
    "targetAimAngle":"vecToAngle(playerPos - patternPos)",
    "aimSpeed":0.1,
    "arcAngle":90,
  },
Setting "aimMode":"Target" means the pattern will ignore rotationSpeed and ease toward targetAimAngle instead. In this case the pattern wants to aim directly at the player's position.

The aimSpeed is a value from 0-1. With a value of 0.1 it will ease 10% of the way to the target each frame, so it will lag behind a bit.

With "arcAngle":90 we changed the angle that all the bullets are distributed between from 360° to 90°.

The entire json file for our example pattern, located at octopus/pattern/simplePattern.json

When the pattern refers to the bullet, it can simply use .bullet since it's in the same json object (the braces that enclose the whole file), though octopus/pattern/simplePattern.bullet would work as well. Any name can be used instead of "bullet".

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
  "simpleAttack": {
    "numVolleys": 15,
    "numBulletsInVolley":5,
    "shootDelay": 0.1,
    "bullets": [".bullet"],
    "sfxVolley": "ShootSoftWomp",
    "aimMode":"Target",
    "targetAimAngle":"vecToAngle(playerPos - patternPos)",
    "aimSpeed":0.1,
    "arcAngle":90,
  },

  "bullet": {
    "keyframes": [
    {
      "duration":0.5,
      "acceleration":5,
      "frictionPercent":0.25,
      "radius":2,
      "sprite":"sprites/circle/simple",
      "colorA1":{ "value": {"r":1,"g":0,"b":0,"a":0.9}},
      "colorA2":{ "value": {"r":1,"g":0.1,"b":0,"a":0.7}},
      "colorB1":{ "value": {"r":0.8,"g":0,"b":0,"a":0.8}},
      "colorB2":{ "value": {"r":0.6,"g":0,"b":0,"a":0.6}},
      "colorC1":{ "value": {"r":1,"g":0.1,"b":0,"a":0.2}},
      "colorC2":{ "value": {"r":0.8,"g":0.2,"b":0,"a":0.1}},
      "glowA": 4,
      "glowB": 0.5,
      "glowC": 0,
      "colorBlinkTime":0.15,
      "opacity":0,
      "easingType":"QuadOut",
    },
    {
      "duration":1.5,
      "acceleration":15,
      "frictionPercent":0.05,
      "glowA": 3.5,
      "opacity":1,
      "acceleration":66,
    },
    ],
    "startSpeed":120,
    "lifetime":30,
    "depthLevel": "Bullet",
    "shapeType": "Circle",
    "despawnTime":0.15,
  },
}

Spawning a Pattern

Once you've created a bullet pattern definition, you'll need to spawn it in-game using an Action.

1
{ "action": "CallMethod", "target":"stage", "method": "SpawnPattern", "params": { "path": "octopus/pattern/simplePattern.simpleAttack", "pos":"playerPos + vec2(0f, 10f)", "dir":"vec2(0f, 1f)", }},

Let's expand that line to make it more clear:

1
2
3
4
5
6
7
8
9
{ "action": "CallMethod", // type of action
    "target":"stage", // call method on the stage object
    "method": "SpawnPattern", // name of the method to call
    "params": { // parameters to pass into this method
        "path": "octopus/pattern/simplePattern.simpleAttack", // the location of the pattern definition, inside our custom campaign folder
        "pos":"playerPos + vec2(0f, 10f)", // the position to spawn the pattern at (in this case, 10 units above the player, with 1 unit equaling the size of 1 boss pixel)
        "dir":"vec2(0f, 1f)", // the direction the pattern should begin facing (in this case, straight upward)
    }
},
Common methods for spawning patterns

These are the most common use-cases for making bullet patterns appear. There are more optional parameters than shown here, however.

Spawning anywhere

This method, the same as seen above, can be called from anywhere actions are valid: from a unit, the player, the stage itself, etc.

1
{ "action": "CallMethod", "target":"stage", "method": "SpawnPattern", "params": { "path": "octopus/pattern/simplePattern.simpleAttack", "pos":"playerPos + vec2(0f, 10f)", "dir":"vec2(0f, 1f)", }},

Charging from a unit part

This example doesn't specify the target, as it's not necessary when called from a unit itself.

1
{ "action": "CallMethod", "method": "ChargePattern", "params": { "path": "octopus/pattern/simplePattern.simpleAttack", "partType": "core", "chargeTime":1, "dir":"(playerPos + playerVel * 1f) - partPos", }},
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{ "action": "CallMethod",
    "method": "ChargePattern", 
    "params": { 
        "path": "octopus/pattern/simplePattern.simpleAttack", 
        "partType": "gun", // the name of the unit part to charge the pattern on
        "partSelect":"All" // if multiple parts of the same name exist, charge from all of them
        "chargeTime":1, // how long the pattern will charge up before shooting
        "dir":"(playerPos + playerVel * 1f) - partPos", // aim at the player, and lead them a bit
    }
},

Anchoring a pattern to a bullet

The easiest way to make a pattern move around the way you want is to anchor it to a bullet. The bullet anchor can be invisible and non-colliding, and simply function as a vehicle to move the pattern around. When calling this from a bullet it's unnecessary to specify the target.

1
{ "action": "CallMethod", "method": "AddPattern", "params": { "path": ".myChildPattern", "pos":"bulletPos", "angle":"vecToAngle(playerPos - bulletPos)" }},
1
2
3
4
5
6
7
8
{ "action": "CallMethod", 
    "method": "AddPattern", 
    "params": { 
        "path": ".myChildPattern",
        "pos":"bulletPos", // the position of the parent bullet
        "angle":"vecToAngle(playerPos - bulletPos)"
    }
},

Other Pattern Shapes

Instead of having patterns shoot outward from a central point, you may want to spawn bullets in precise or odd configurations.

Custom

Setting the patternShape to Custom lets you decide the position and orientation of each bullet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
"1": {
  "patternShape":"Custom",

  // spawn the bullets in a circle arount the player
  "customBulletPos": "playerPos + angleToVec((volleyNum / float(numVolleys)) * 360f) * 40f",

  // spawn them aiming towards the player
  "customBulletAngle":"vecToAngle(playerPos - bulletPos)",

  "numVolleys": 40,
  "numBulletsInVolley":1,
  "shootDelay": 0.01,
  "bullets": [".bullet"],
  "sfxVolley": "ShootSoftWomp",
},

Manual

Setting the patternType to Manual means the pattern will wait for you to manually call Actions to shoot volleys or end the pattern.

That above pattern would have been challenging to express as a single customBulletPos function, so the actions were called manually.

Full json file
  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
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
{
  "1":{
    "patternShape": "Manual",
    "properties": {
      "size": { "type": "Float", },
      "startPos": { "type": "Vector2", },
      "pos": { "type": "Vector2", },
      "count": { "type": "Int", },
      "amount": { "type": "Int", },
    },
    "behaviour": ".fsm",
    "bulletIndex": 0,
    "arcAngle": "360f",
    "dragBullets": false,
    "chargeColor": "color(1f, 0f, 0f) * 10f",
    "shootColor": "color(1f, 0f, 0f) * 8f",
    "shootColorTime":0.25,
    "shootColorEasingType": "QuadIn",
    "vibrationStrength":"0.33f",
    "maxVibrationDist":10,
    "vibrationHorizThreshold":5,
    "sfxVolley": "ShootPump",
    "numVolleysPerShootSfx": 1,
  },

  // =============================================================================================================================================================================
  "bullet": {
    "keyframes": [
    {
      "duration":0.5,
      "acceleration":0,
      "frictionPercent":0,
      "colorA1":{"r":0,"g":0,"b":1,"a":0.9},
      "colorA2":{"r":0,"g":0,"b":1,"a":0.7},
      "colorB1":{"r":0,"g":0,"b":1,"a":0.8},
      "colorB2":{"r":0,"g":0,"b":1,"a":0.6},
      "colorC1":{"r":0,"g":0,"b":1,"a":0.8},
      "colorC2":{"r":0,"g":0,"b":1,"a":0.6},
      "colorBlinkTime":0.5,
      "easingType":"QuadOut",
      "loopEnd": 1,
      "glowA": 6,
      "glowB": 6,
      "glowC": 6,
      "opacity":0,
      "facingSpeedPercent": 0.5,
      "sprite":"sprites/circle/thinEdge",
      "ignorePlayerCollision":true,
      "radius":4,
    },
    {
      "duration":1.5,
      "frictionPercent":0.035,
      "radius": { "mode": "PerUpdate", "value": "1.66f + fastSin(bulletTime * 25f) * map(lifetimeProgress, 0f, 1f, 0f, 0.2f, 'ExpoOut')" },
      "glowA": 0.66,
      "glowB": 0.33,
      "glowC": 0.5,
      "loopEnd": 1,
      "opacity":0.75,
      "ignorePlayerCollision":false,
      "colorA1":{"r":1,"g":0.5,"b":0.25,"a":0.9},
      "colorA2":{"r":1,"g":0.25,"b":0.25,"a":0.7},
      "colorB1":{"r":0.8,"g":0.4,"b":0.25,"a":0.8},
      "colorB2":{"r":0.9,"g":0.2,"b":0.25,"a":0.6},
      "colorC1":{"r":1,"g":0.25,"b":0.25,"a":0.8},
      "colorC2":{"r":1,"g":0,"b":0.25,"a":0.6},
    },
    {
      "duration":0.15,
      "colorBlinkTime":0.05,
      "glowA": 2.66,
      "glowB": 0.33,
      "glowC": 0.33,
      "colorA1":{"r":1,"g":0.15,"b":0,"a":0.9},
      "colorA2":{"r":1,"g":0.25,"b":0,"a":0.8},
      "colorB1":{"r":0.8,"g":0.4,"b":0,"a":0.8},
      "colorB2":{"r":0.9,"g":0.2,"b":0,"a":0.6},
      "colorC1":{"r":1,"g":0.25,"b":0,"a":0.8},
      "colorC2":{"r":1,"g":0,"b":0,"a":0.6},
      "onKeyframe":[
        { "action": "CallMethod", "target":"stage", "method": "PlaySfx", "params": { "sfxType":"InvasionXAttack", "pos":"bulletPos", }},
      ],
    },
    {
      "duration":0,
      "acceleration":300,
      "moveAngle":"vecToAngle(playerPos - bulletPos)",
    },
    ],
    "startSpeed":0,
    "lifetime":30,
    "shouldLoop":false,
    "despawnAfterKeyframes":false,
    "depthLevel": "BulletBottom",
    "shapeType": "Circle",
    "impulseFrictionPercent":0.05,
    "despawnTime":0.15,
    "useAbsoluteAngles":true,
    "circleSkew":0.5,
    "circleSkewDist":1,
  },

  // =============================================================================================================================================================================
  "fsm": {
    "0": [
      { "action": "SetValue", "name": "size", "value": 3 },
      { "action": "SetValue", "name": "startPos", "value": "playerPos + (playerPos.y < 0f ? vec2(0f, 20f) : vec2(0f, -20f))" },
      { "action": "SetValue", "name": "amount", "value": "roundToInt(map(patternNum, 0, 7, 6f, 12f, 'SineInOut'))" },

      { "action": "SetValue", "name": "pos", "value": "startPos" },
      { "action": "SetValue", "name": "count", "value": 0 },
      { "action": "Repeat", "count": "amount", "delay": 0.02, "inner": [
        { "action": "SetValue", "name": "pos", "value": "pos + rotateAround(vec2(0.7f, 0.7f), (map((nspc * patternNum) % 5, 0, 4, 0f, 45f) + rand.Float(0f, map(patternNum, 0, 6, 0f, 100f))) * (patternNum % 2 == 0 ? -1f : 1f)) * size" },
        { "action": "CallMethod", "method": "AddBullet", "params": { "pos": "pos", "path": ".bullet", }}, 
        { "action": "CallMethod", "target":"stage", "method": "PlaySfx", "params": { "sfxType":"InvasionXSpawn", "pos":"pos", "pitchModifier":"map(count, 0, float(amount), 0.8f, 1.5f, 'QuadIn')" }},
        { "action": "SetValue", "name": "count", "value": "count + 1" },
      ]},

      { "action": "SetValue", "name": "pos", "value": "startPos" },
      { "action": "SetValue", "name": "count", "value": 0 },
      { "action": "Repeat", "count": "amount", "delay": 0.02, "inner": [
        { "action": "SetValue", "name": "pos", "value": "pos + rotateAround(vec2(0.7f, -0.7f), (map((nspc * patternNum) % 5, 0, 4, 0f, 45f) + rand.Float(0f, map(patternNum, 0, 6, 0f, 100f))) * (patternNum % 2 == 0 ? -1f : 1f)) * size" },
        { "action": "CallMethod", "method": "AddBullet", "params": { "pos": "pos", "path": ".bullet", }},
        { "action": "CallMethod", "target":"stage", "method": "PlaySfx", "params": { "sfxType":"InvasionXSpawn", "pos":"pos", "pitchModifier":"map(count, 0, float(amount), 0.8f, 1.5f, 'QuadIn')" }},
        { "action": "SetValue", "name": "count", "value": "count + 1" },
      ]},

      { "action": "SetValue", "name": "pos", "value": "startPos" },
      { "action": "SetValue", "name": "count", "value": 0 },
      { "action": "Repeat", "count": "amount", "delay": 0.02, "inner": [
        { "action": "SetValue", "name": "pos", "value": "pos + rotateAround(vec2(-0.7f, 0.7f), (map((nspc * patternNum) % 5, 0, 4, 0f, 45f) + rand.Float(0f, map(patternNum, 0, 6, 0f, 100f))) * (patternNum % 2 == 0 ? -1f : 1f)) * size" },
        { "action": "CallMethod", "method": "AddBullet", "params": { "pos": "pos", "path": ".bullet", }},
        { "action": "CallMethod", "target":"stage", "method": "PlaySfx", "params": { "sfxType":"InvasionXSpawn", "pos":"pos", "pitchModifier":"map(count, 0, float(amount), 0.8f, 1.5f, 'QuadIn')" }},
        { "action": "SetValue", "name": "count", "value": "count + 1" },
      ]},

      { "action": "SetValue", "name": "pos", "value": "startPos" },
      { "action": "SetValue", "name": "count", "value": 0 },
      { "action": "Repeat", "count": "amount", "delay": 0.02, "inner": [
        { "action": "SetValue", "name": "pos", "value": "pos + rotateAround(vec2(-0.7f, -0.7f), (map((nspc * patternNum) % 5, 0, 4, 0f, 45f) + rand.Float(0f, map(patternNum, 0, 6, 0f, 100f))) * (patternNum % 2 == 0 ? -1f : 1f)) * size" },
        { "action": "CallMethod", "method": "AddBullet", "params": { "pos": "pos", "path": ".bullet", }},
        { "action": "CallMethod", "target":"stage", "method": "PlaySfx", "params": { "sfxType":"InvasionXSpawn", "pos":"pos", "pitchModifier":"map(count, 0, float(amount), 0.8f, 1.5f, 'QuadIn')" }},
        { "action": "SetValue", "name": "count", "value": "count + 1" },
      ]},

      { "action": "CallMethod", "method": "Finish", },
    ],
  },
}

Subdividing

Bullets spawned by a pattern can be subdivided into several copies of themself. This can allow you to easily create more complex patterns.

Fan

A fanSubdivide will replace each bullet with a fan of bullets.

1
2
3
4
5
6
7
8
{
    "patternShape": "Spokes",
    "numVolleys": 3,
    "numBulletsInVolley":3,
    // ...
    "fanSubdivide":4, // replace each bullet with 4
    "fanAngle":45, // aim the bullets in a 45 degree angle fan
  },

Parallel

A parallelSubdivide will instead replace with bullets facing the same direction.

1
2
3
4
5
6
7
8
{
    "patternShape": "Spokes",
    "numVolleys": 5,
    "numBulletsInVolley":4,
    // ...
    "parallelSubdivide":5,
    "parallelForce":12, // the amount of force applied to bullets to spread them out
}

The two types of subdivides can be combined, and can use scriptfuncs instead of static values:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "patternShape": "Spokes",
    "numVolleys": 5,
    "numBulletsInVolley":4,
    // ...
    "fanSubdivide":"1 + volleyNum",
    "fanAngle":"75f - volleyNum * 10f",
    "parallelSubdivide":"5 - volleyNum",
    "parallelForce":"5f + volleyNum * 2f",
}

Tip

Patterns can use the fanNum or parallelNum properties to access the index of a bullet in a subdivision.

Affecting Child Bullets

Patterns have their own position and angle, and their movement can be applied to bullets they've fired.

1
2
3
4
5
6
7
"childPattern": {
    // ...
    "dragBullets": true, // affect bullet position as pattern position changes
    "rotateBullets": true, // affect bullet position as pattern rotation changes
    "rotateBulletAngles":true, // affect bullet rotation as pattern rotation changes
    "waitForBulletsToDespawn":true, // dont despawn until all bullets have despawned
},

On the left, dragBullets is true.
In the center, dragBullets and rotateBullets are true.
On the right, dragBullets and rotateBullets and rotateBulletAngles are true.


To produce the attack in the image above:

1) A very simple pattern shoots the faded blue bullet, then the pattern despawns

2) On its first keyframe, the blue bullet shoots a second pattern and anchors it to itself

3) The second pattern fires the red diamond bullets, and drags them along as the blue bullet drags it

Full json file
  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
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
{
    "1":{
        "patternShape": "Spokes",
        "numVolleys": 1,
        "numBulletsInVolley": "1",
        "shootDelay": "0f",
        "rotationSpeed": "-80f - ((patternNum % 3) * 20f)",
        "arcAngle": "360f",
        "bullets": [".parentBullet1"],
        "dragBullets": false,
        "chargeColor":"color(1f, 0f, 0f) * 8f",
        "shootColor":"color(1f, 0f, 0f) * 10f",
        "shootColorTime":0.25,
        "shootColorEasingType": "QuadIn",
        "vibrationStrength":"0.33f",
        "maxVibrationDist":10,
        "vibrationHorizThreshold":5,
        "sfxCharge":"OctopusF1OrbCharge",
        "sfxVolley": "OctopusF1OrbShoot",
        "numVolleysPerShootSfx": 1,
    },

    "2":{
        "#include":".1",
        "bullets": [".parentBullet2"],
    },

    "3":{
        "#include":".1",
        "bullets": [".parentBullet3"],
    },

    "parentBullet1": {
        "keyframes": [
        {
        "duration":0,
        "acceleration":6,
        "frictionPercent":0.025,
        "opacity":0.2,
        "radius":5,
        "sprite":"sprites/circle/simple",
        "colorA1":{"r":1,"g":0,"b":1,"a":0.9},
        "colorA2":{"r":1,"g":0,"b":1,"a":0.8},
        "colorB1":{"r":0,"g":0,"b":1,"a":0.33},
        "colorB2":{"r":0,"g":0,"b":1,"a":0.5},
        "colorC1":{"r":1,"g":0.33,"b":1,"a":0.5},
        "colorC2":{"r":1,"g":0.22,"b":1,"a":0.66},
        "onKeyframe":[{ "action": "CallMethod", "method": "AddPattern", "params": { "path": ".childPattern1", }},],
        },
        ],
        "startSpeed":0,
        "lifetime":200,
        "shouldLoop":false,
        "despawnAfterKeyframes":false,
        "depthLevel": "Bullet",
        "shapeType": "Circle",
        "despawnTime":0.05,
        "outOfBoundsRadiusFactor":5,
        "mass":2,
        "ignorePlayerCollision":true,
    },

    "parentBullet2":{
        "#include":".parentBullet1",
        "keyframes": [
            {
            "#include": ".parentBullet1.keyframes[0]",
            "onKeyframe":[{ "action": "CallMethod", "method": "AddPattern", "params": { "path": ".childPattern2", }},],
            },
        ],
    },

    "parentBullet3":{
        "#include":".parentBullet1",
        "keyframes": [
            {
            "#include": ".parentBullet1.keyframes[0]",
            "onKeyframe":[{ "action": "CallMethod", "method": "AddPattern", "params": { "path": ".childPattern3", }},],
            },
        ],
    },

    "childPattern1": {
        "patternShape": "Custom",
        "numVolleys": 1,
        "numBulletsInVolley": "5",
        "customBulletPos": "patternPos + vec2(0f, 1f) * bulletNum * 5f",
        "customBulletAngle": 0,
        "shootDelay": "0f",
        "bulletIndex": 0,
        "rotationSpeed": "-50f",
        "arcAngle": "360f",
        "bullets": [".childBullet"],
        "dragBullets": true,  
        "rotateBullets": false,
        "rotateBulletAngles":false,
        "waitForBulletsToDespawn":true,
        "constantRotation":true,
        "chargeColor":"color(1f, 0f, 0f) * 8f",
        "shootColor":"color(1f, 0f, 0f) * 10f",
        "shootColorTime":0.25,
        "shootColorEasingType": "QuadIn",
        "vibrationStrength":"0.33f",
        "maxVibrationDist":10,
        "vibrationHorizThreshold":5,
        "sfxVolley": "ShipShoot",
        "numVolleysPerShootSfx": 1,
        "debugVector":"patternDir * 5f",
    },

    "childPattern2":{
        "#include":".childPattern1",
        "dragBullets": true,  
        "rotateBullets": true,
        "rotateBulletAngles":false,
    },

    "childPattern3":{
        "#include":".childPattern1",
        "dragBullets": true,  
        "rotateBullets": true,
        "rotateBulletAngles":true,
    },

    "childBullet": {
        "keyframes": [
        {
        "duration":0.5,
        "frictionPercent":0.05,
        "colorA1":{"r":1,"g":0,"b":0.1,"a":0.9},
        "colorA2":{"r":1,"g":0,"b":0.05,"a":0.8},
        "colorB1":{"r":0,"g":0,"b":0.075,"a":0.33},
        "colorB2":{"r":0,"g":0,"b":0.1,"a":0.5},
        "colorC1":{"r":1,"g":0.33,"b":0,"a":0.5},
        "colorC2":{"r":1,"g":0.22,"b":0,"a":0.66},
        "colorBlinkTime":0.5,
        "easingType":"ExpoOut",
        "loopEnd": 1,
        "glowA": 8,
        "glowB": 8,
        "glowC": 8,
        "opacity":0,
        "sprite":"sprites/diamond/simple",
        },
        {
        "duration":2,
        "length":5,
        "crossWidth":4,
        "crossDistance":0.66,
        "easingType":"ExpoInOut",
        "glowA": 3.75,
        "glowB": 0.33,
        "glowC": 0.33,
        "opacity":1,
        },
        ],
        "startSpeed":0,
        "lifetime":200,
        "shouldLoop":false,
        "despawnAfterKeyframes":false,
        "depthLevel": "Bullet",
        "shapeType": "Diamond",
        "despawnTime":1,
        "mass":99,
        "anchored":true,
    },
}

Linking Volley Bullets

A pattern can choose to link all bullets in each volley.

1
2
3
"linkedPattern": {
    "linkVolleyBullets":true,
},

If one linked bullet despawns, all other bullets in the linked volley will despawn - this is useful for powerup patterns, snake-like patterns, and other things.

Visual Effects

Patterns can declare another pattern to be spawned as their "shoot effect":

1
2
3
4
"attack":{
    // ...
    "shootEffectPattern": "misc/pattern/shoot/fanRedRound",
},

This will spawn the effect at the position of the shooting pattern, and pointed in the same direction.

Handlers

You may want to call Actions at certain points during a pattern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
"pattern":{
    // ...
    "onStart":[
        // called when the pattern begins
        { "action": "CallMethod", "target":"stage", "method": "ShakeCamera", "params": { "strength": 2, "time": 0.75, "easingType": "QuadOut" }},
    ],    

    "onVolley":[ /* called when the pattern starts a volley */ ],
    "onBullet":[ /* called when the pattern fires a bullet */ ],
    "onUpdate":[ /* called each frame the pattern is active */ ],
    "onFinish":[ /* called when the pattern despawns */ ],
},

Custom Variables

Custom variables can be defined in the properties section of the pattern config. They can be used in other script funcs throughout the pattern, and can be modified with SetValue.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    "properties": {
        "nextBulletPos": { "type": "Vector2" },
    },

    "patternShape":"Custom",
    "customBulletPos": "nextBulletPos",

    // ...

    "onVolley":[
        { "action": "SetValue", "name": "nextBulletPos", "value": "lerp(nextBulletPos, playerPos, 0.5f)" },
    ],
}

Workflow

Extending other patterns

Patterns can include other patterns and change only specifed properties, keeping the rest the same as the original.

1
2
3
4
5
"simpleAttack_rng_bs": {
  "#include":".simpleAttack",
  "numBulletsInVolley":"rand.Int(1, 30)",
  "arcAngle":"rand.Float(10f, 360f)",
},

Debugging

debugVector: draws a 2d vector from the pattern position
debugText: displays a string above the pattern position (use ${NAME} for properties)

1
2
3
4
5
6
"simpleAttack": {
  ...
  "debugVector":"patternDir * 30f",
  "debugText":"volleyNum: ${volleyNum}, angle: ${patternAngle}",
  "waitForBulletsToDespawn":true, // pattern persists until all its bullets have despawned (normally, patterns end when all have been fired)
},