Brewing potions¶
Note
This chapter hasn’t had a second pass! Beware explosive brews!
In this chapter we’ll use the StatusEffect component included in prism/extra
to create a potion of vitality that heals the drinker
and increases their hitpoint maximum by 5, temporarily. We’ll go over creating a buff, making our Health component respect it, and
ticking down the duration on status effects.
Adding status effects to the player¶
Make your way to modules/game/actors/player.lua
and add the following component.
prism.components.StatusEffects(),
Getting a brew going¶
The first thing we’re going to want to do is head over to main.lua
and load the statuseffects
module. Let’s add this line
right above where we’re loading our game module.
prism.loadModule("prism/extra/statuseffects")
Okay with that done the next thing we’re going to want to do is create a subclass of the StatusEffectInstance that will serve as a base prototype for all of our buffs/debuffs in the game. This is where we’ll define stuff that’s game specific to our status effects like durations.
Head over to modules/game
and create a new file named statusinstance.lua
.
--- @class GameStatusInstance : StatusEffectsInstance
--- @field duration integer?
local GameStatusInstance = prism.components.StatusEffects.Instance:extend "GameStatusInstance"
--- @class GameStatusInstanceOptions : StatusEffectsInstanceOptions
--- @field duration integer
--- @param options GameStatusInstanceOptions
function GameStatusInstance:__new(options)
prism.components.StatusEffects.Instance.__new(self, options)
self.duration = options.duration or nil
end
return GameStatusInstance
Our only addition for our usecase is going to be a duration since we’ll want timed buffs. Now head over to module.lua
in the same folder.
We’re going to load this file and inject our status instance into the global namespace simply for convenience sake.
This Instance is a collection of Modifiers. A status effect instance might be the entire buff a potion gives you like +5 maxhp, +2 strength. A modifier is the individual pieces like +5 maxhp. You can subclass this to define a new named instance or use it anonymously.
--- @module "modules.game.statusinstance"
prism.GameStatusInstance = require ("modules.game.statusinstance")
The path string manipulation is just so that this file loads correctly now matter which folder our module is loaded from, that won’t matter here, but it’s a good idea to do this.
Modifying health¶
Let’s head back to modules/game/components/health.lua
and take a look at our health component. At the top of the file let’s add
the following code.
local StatusEffects = prism.components.StatusEffects
Let’s quickly alias the StatusEffects component, we’re going to use one of it’s static methods later in the file.
--- @class HealthModifier : StatusEffectsModifier
--- @field maxHP integer
local HealthModifier = prism.components.StatusEffects.Modifier:extend "HealthModifier"
function HealthModifier:__new(delta)
self.maxHP = delta
end
This defined a new StatusEffectsModifier. We’ll leave the constructor as it is, but let’s set maxHP to private.
--- @class Health : Component
--- @field private maxHP integer
--- @field hp integer
--- @overload fun(maxHP: integer)
Next let’s create a getMaxHP function that will take our new modifier into account.
--- @return integer maxHP
function Health:getMaxHP()
local modifiers = StatusEffects.getActorModifiers(self.owner, HealthModifier)
local modifiedMaxHP = self.maxHP
for _, modifier in ipairs(modifiers) do
modifiedMaxHP = modifiedMaxHP + modifier.maxHP
end
return modifiedMaxHP
end
We loop through each modifier, add it to our base maxHP, and return the modified value. While we’re here we’ll need to change a few more things. First let’s change heal to use our new getter function.
--- @param amount integer
function Health:heal(amount)
self.hp = math.min(self.hp + amount, self:getMaxHP())
end
Next we’ll add a small function that will clamp hp to maxhp for a little bit later in the tutorial.
function Health:enforceBounds()
self.hp = math.min(self.hp, self:getMaxHP())
end
And finally we’ll set Health.Modifier
to the modifier we’ve just created for this component so that we can access it from a convenient place.
Health.Modifier = HealthModifier
Now in gamelevelstate.lua
we’ll have to make a small change. We’re drawing maxHP, but we’re accessing it directly let’s change this line in draw
:
if health then self.display:putString(1, 1, "HP: " .. health.hp .. "/" .. health.maxHP) end
To use the new getter:
if health then self.display:putString(1, 1, "HP: " .. health.hp .. "/" .. health:getMaxHP()) end
Drinking¶
Let’s create a new file in modules/game/components
called drinkable.lua
.
--- @class DrinkableOptions
--- @field healing integer?
--- @field status StatusEffectsInstance?
--- @class Drinkable : Component
--- @field healing integer?
--- @field status StatusEffectsInstance?
--- @overload fun(options: DrinkableOptions): Drinkable
local Drinkable = prism.Component:extend "Drinkable"
function Drinkable:__new(options)
self.healing = options.healing
self.status = options.status
end
return Drinkable
We create a simple component with an optional healing value, and an optional status effect.
Now let’s create a new file in modules/game/actions
called drink.lua
.
local DrinkTarget = prism.InventoryTarget()
:inInventory()
:with(prism.components.Drinkable)
First we define our target an item in the actor’s inventory with the Drinkable component.
--- @class Drink : Action
local Drink = prism.Action:extend "Drink"
Drink.targets = {
DrinkTarget
}
--- @param level Level
function Drink:perform(level, drink)
local drinkable = drink:expect(prism.components.Drinkable)
local statusComponent = self.owner:get(prism.components.StatusEffects)
if statusComponent and drinkable.status then
statusComponent:add(drinkable.status)
end
Then if we’ve got a status effects component and our drink applies a status effect we add that to the status effects component.
local health = self.owner:get(prism.components.Health)
if health and drinkable.healing then
health:heal(drinkable.healing)
end
end
return Drink
Finally we’ll heal the actor for the amount of the drinkable’s healing, if any.
Brewing the potion¶
Create a new file in modules/game/actors
called vitalitypotion.lua
.
prism.registerActor("VitalityPotion", function()
return prism.Actor.fromComponents {
prism.components.Name("Potion of Vitality"),
prism.components.Drawable("!", prism.Color4.RED),
prism.components.Item(),
prism.components.Drinkable{
healing = 5,
status = prism.GameStatusInstance{
duration = 10,
modifiers = {
prism.components.Health.Modifier(5)
}
}
}
}
end)
You’ve seen most of this before, except the Drinkable component. Here we’re saying that this potion should heal for 5 and modify the actor’s maxHP by +5 for 10 turns.
If we go into the game now and drink the potion everything should work, but you’ll notice the buff doesn’t expire after 10 turns! Let’s fix that!
Ticking down durations¶
Head over to modules/game/actions
and create a new file called tick.lua
.
--- @class Tick : Action
local Tick = prism.Action:extend "Tick"
Tick.requiredComponents = { prism.components.StatusEffects }
Our tick action can only be taken by actors who have a status effect component.
--- @param level Level
function Tick:perform(level)
-- Handle status effect durations
local statusComponent = self.owner:expect(prism.components.StatusEffects)
local expired = {}
for handle, status in statusComponent:pairs() do
--- @cast status GameStatusInstance
if status.duration then
status.duration = status.duration - 1
if status.duration <= 0 then
table.insert(expired, handle)
end
end
end
First we loop through all of the status effects currently applied to our actor, ticking down their durations and keeping track of which ones have expired.
for _, handle in ipairs(expired) do
statusComponent:remove(handle)
end
Then we remove the expired status effects.
-- Validate components
local health = self.owner:get(prism.components.Health)
if health then health:enforceBounds() end
end
return Tick
Finally we clamp our hp to maxHP by calling enforceBounds
from earlier. This is where you’d enforce minimums or maximums that might change.
Without this if the player ends the duration of the buff with 15 health they’d end up keeping that health total and only see a reduction in their
maximum.
Now head over to modules/game/systems
and create a new file called tick.lua
.
--- @class TickSystem : System
local TickSystem = prism.System:extend "TickSystem"
function TickSystem:onTurn(level, actor)
level:tryPerform(prism.actions.Tick(actor))
end
return TickSystem
Each turn we try to perform tick action on the actor. If we head back into the game and spawn a new Potion of Vitality with Geometer and drink it we’ll see that our health and max health both go up by 5, and then after 10 turns our max health returns to it’s original value, success!
Wrapping up¶
In the next chapter we’ll make a wand and write some targetting code.