Tutorial 5: Difficulty Controls
By default, the engine has the following difficulty levels: Easy, Normal, Hard, and Lunatic.
You can add or remove levels by modifying the FixedDifficulty enum in Danmaku/Core/Enums.cs, and also adjusting the values in the enum helpers in that file.
Start with this code:
pattern { } {
phase 0 {} {
paction 0 {
shiftphaseto(1)
}
}
phase 40 {
type(non, "Difficulty")
hp(14000)
root(0, 1)
} {
paction 0 {
async "arrow-red/w" <> gcrepeat {
wait(120)
times(inf)
} gsrepeat {
times(30)
circle
} s tprot cx 2
}
}
}
This should all be familiar.
In the earlier tutorials, I stressed using the R key to instantaneously reload scripts. There are in fact other script reload keys, and you may have pressed a few of them by accident. They are defined in Danmaku/Core/GameManagement.cs:TryTriggerLocalRestart as follows:
- T: Reload on Easy
- Y: Reload on Normal
- U: Reload on Hard
- I: (i) Reload on Lunatic
Try pressing some of these keys while the script is running. You should see the difficulty marker in the top right of the game UI change, although the pattern will stay the same.
Now, let's make some difficulty variation. Change the times
line to times(30 * dl)
.
Reload the script in several difficulties. You'll see that the number of bullets changes every time. What's this magic number dl
?
Internally, each difficulty has an associated counter and value. The counter is 0 on Easy, 1 on Normal, 2 on Hard, and 3 on Lunatic. The value is a multiplier which is roughly equal to sqrt(2)^(counter-1)
, ie. each difficulty is approximately 40% greater than the previous difficulty. You don't need to remember this, and if you think these numbers are bad, you can change them in Danmaku/Core/Enums.cs.
The function dl
returns the ratio CurrentDifficultyValue / LunaticDifficultyValue
. Effectively, we are using 30 as the reference value on Lunatic difficulty, and scaling it multiplicatively for all other difficulties.
If you have multiple layers of bullets, then using dl
on everything will probably make the layers unbalanced amongst each other on other difficulties. To resolve this, you can tweak the difficulty variable by raising it to a power. For example, (30 * dl ^ 0.8)
makes the difficulty vary less than (30 * dl)
. You can use a different adjustment for each layer.
We could also scale the waiting time using dl
, although we should divide it, since higher difficulties have lower waiting times:
async "arrow-red/w" <> gcrepeat {
wait(120 / dl)
times(inf)
} gsrepeat {
times(30)
circle
} s tprot cx 2
In addition to dl
, there are also dn
, and dh
(guess what they do?), and you can add any that you need to ExMDifficulty.
However, sometimes we want an additive modifier instead of a multiplicative one. In this case, we should use dc:
sync "gdlaser-blue/b" <> gsrepeat {
times(6 + dc)
circle
} laser none 1 2 {
dsfx
}
This will spawn 6 lasers on Easy and 1 more for each difficulty above that.
There are also some convenience methods for grouping difficulties. For example, d3d2 returns -2 for Easy, 0 for Normal/Hard, and 2 for Lunatic. You can always add new convenience functions to ExMDifficulty.
async "triangle-green/w" <> gcrepeat {
wait(8)
times(10)
target ang Lplayer
} gsrepeat {
times(5 + d3d2)
spread <80>
center
} s tprot cx 2
The last standard method for difficulty controls is using selectdc
, which requires manually setting values for all difficulties (easy, normal, hard, and lunatic).
async "arrow-red/w" <> gcrepeat {
wait(120)
times(inf)
} gsrepeat {
times(selectdc 5 10 20 30)
circle
} s tprot cx 2