Cooldowns conceptually are fairly simple timers that begin after a spellcast and block casting that spell again until they are over.
You might be asking, why add cooldowns now? Haven’t you been working on this game for years, shouldn’t this have been done ages ago?
Yes, it should have.
— Kittems, 2020
Fine, you got me.
After playing around with blend modes for lighting, I made an invisibility shader. I think it’s awesome, and now I demand that the spell actually work. But I need buffs and cooldowns for that.
Look! It even pulses, wasn’t that worth a few hours? Wasn’t it!? LOOK AT IT! It doesn’t even pulse all at once, it does so directionally. That took math.
And see how it only highlights edges on the pixel art? I had to learn sobel edge detection to achieve that effect.
I love shaders.
This is all a critical part of making a polished vertical slice of gameplay, right? right?
So, cooldowns and buffs…?
All casters have a Spellbook
component, so adding cooldowns was straight forward.
- I added
Map<SpellId, Timer> cooldowns
to the book and inserted a record whenever a mage casted a spell. - A new
CooldownSystem
ticked the timers and removed entries when they reached 0. - The
HasSpellResources
function ensured a spell could not be cast on cooldown, which is used in all the relevant casting systems.
And there, done.
But the player needs an indicator that their spell is on cooldown. Most games (League, Heroes, World of Warcraft) use a spiral clock where part of the icon is shaded to represent this.
You know what that means?
MORE SHADERS
Drawing square clock angle requires a few things:
- UV coordinates to normalize a center.
- A timer uniform to determine the shaded angle.
- A uniform color for darkening the icon.
LibGDX does not pass texture min and max values to their shaders by default and v_texCoords are not normalized for an individual sprite when using spritesheets (the coordinates represent the entire sheet).
For anyone else using LibGDX and spritesheets, I’ve had to use this for other shaders:
public static void SetTexUvUniforms(ShaderProgram shader, TextureRegion region) {
shader.setUniformf("u_texUvMin", region.getU(), region.getV());
shader.setUniformf("u_texUvMax", region.getU2(), region.getV2());
}
uniform vec2 u_texUvMin;
uniform vec2 u_texUvMax;
void main() {
vec2 uv = (v_texCoords - u_texUvMin) / (u_texUvMax - u_texUvMin);
// ...
}
With UV coordinates, I simply determined what percent of 2pi
radians
to rotate from based on the remaining timer, determined the current
frag pixel angle from center, and darkened anything under the timer angle.
Now buffs
Invisibility is a persistent effect that lasts for a set duration. Most of my spells have been independent entities (even the DoT) with a target.
State changes with a timer are nontrivial because of the sheer amount of bugs that can occur:
- Timer/Buff disappates but apply/remove does not properly change state back
- Stacking effects not applying correctly
- Removing buffs might lead to a permanent bad state for stats
Invisibility needed to add the Hidden
component to the mage, but what if
the component is already added? What about removing it, if another effect
also applied it?
I do not have great solutions to offer after hours of thinking, only a sense of diligence and recommendation to calculate as many stats as possible rather than pure mutation. You can’t avoid mutation, just minimize it.
I added a Buffs
component with a Map<BuffType, Buff>
and a BuffsSystem
that applies, unapplies, and updates timers for buffs. In the past, I kept
all my spells data-driven through json, but an enum for defining buffs felt safer for
initial implementation because of all the special cases.
When every problem is a snowflake, you need to build a snow machine.
BuffsSystem
has a large switch-case to handle applying and unapply every buff, so I
can write all the nasty logic for these state mutations in one place.
For having a Spell
apply a buff, I added a new ApplyBuffsEffect
.
All together, the invisibility spell definition looked like this (minus object pooling for better reading).
SpellDef invisibility = new SpellDef("core.spells.invisibility", School.Illusion)
.manaCost(25)
.castTime(0.5f)
.range(5)
.cooldown(60)
.target(new EntityTarget())
.requireLineOfSight()
.setIcon( ... )
.cast((engine, spellDef, caster, target) -> {
return new Entity()
.add(new Trigger()) // triggers immediately upon cast
.add(target)
.add(new ApplyBuffsEffect(BuffType.Invisibility, 60)) // apply buff on target
})
Now, I just need to display to the user what buffs they have active and how long the buffs will last.
…Wait, does that mean…?
MORE SHADERS!!!!!!!
Well, actually, no. I can just reuse my cooldown shader. Buffs have a timer that ticks down instead of up, and the shaded section grows rather than shrinks.
I thought I could just invert the timer math, and that did mostly work.
Unfortunately, cooldowns ticking counterclockwise didn’t feel right. I added an invert
uniform u_useDarkenFill
to determine if we shaded below or shaded the remainder of the angle.
All together, invisibility is now working with buffs and cooldowns!