Brewing potions

In this chapter we’ll use the ConditionHolder component included in prism/extra to create a potion that heals the drinker and increases their health temporarily. We’ll go over creating a buff, making our Health component respect it, and ticking down the duration on status effects.

Setting up

First, load the conditions module.

prism.loadModule("prism/extra/condition")

Then make your way to modules/game/actors/player.lua and add the following component.

prism.components.ConditionHolder(),

Modifying health

Let’s head back to modules/game/components/health.lua and define a new ConditionModifier to represent a change to our max health.

--- @class HealthModifier : ConditionModifier
--- @field maxHP integer
--- @overload fun(delta: integer): HealthModifier
local HealthModifier = prism.condition.Modifier:extend "HealthModifier"

function HealthModifier:__new(delta)
   self.maxHP = delta
end

prism.register(HealthModifier)

Tip

Every object with a registry can be registered manually with prism.register()!

On the Health component we’ll set maxHP to private, since we won’t want to access it directly now.

--- @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. We use ConditionHolder.getActorModifiers() to retrieve all health modifiers and apply each of them to our maxHP.

--- @return integer maxHP
function Health:getMaxHP()
   local modifiers = ConditionHolder.getActorModifiers(self.owner, HealthModifier)

   local modifiedMaxHP = self.maxHP
   for _, modifier in ipairs(modifiers) do
      modifiedMaxHP = modifiedMaxHP + modifier.maxHP
   end

   return modifiedMaxHP
end

We change heal to use our new function.

--- @param amount integer
function Health:heal(amount)
   self.hp = math.min(self.hp + amount, self:getMaxHP())
end

We’ll add a small function that to clamp our health to the maximum. We’ll use this later.

function Health:enforceBounds()
   self.hp = math.min(self.hp, self:getMaxHP())
end

In gamelevelstate.lua we’ll make sure to draw our health with the new function as well. Change the following line:

if health then self.display:print(1, 1, "HP: " .. health.hp .. "/" .. health.maxHP) end

To use the new getter:

if health then self.display:print(1, 1, "HP: " .. health.hp .. "/" .. health:getMaxHP()) end

Drinking

Let’s create a new component in modules/game/components/drinkable.lua that we’ll give to our potions. For now, let’s give it an optional healing amount and an optional condition.

--- @class DrinkableOptions
--- @field healing integer?
--- @field condition Condition?

--- @class Drinkable : Component
--- @field healing integer?
--- @field condition Condition?
--- @overload fun(options: DrinkableOptions): Drinkable
local Drinkable = prism.Component:extend "Drinkable"

function Drinkable:__new(options)
   self.healing = options.healing
   self.condition = options.condition
end

return Drinkable

Now let’s create a new action in modules/game/actions/drink.lua. First we define our target to be an item in the actor’s inventory with a Drinkable component.

local DrinkTarget = prism.targets.InventoryTarget(prism.components.Drinkable)

Then if we have a condition holder and our drink applies a condition we add that condition.

--- @class Drink : Action
local Drink = prism.Action:extend "Drink"
Drink.targets = {
   DrinkTarget
}

--- @param drink Actor
function Drink:perform(level, drink)
   self.owner:expect(prism.components.Inventory):removeItem(drink)
   local drinkable = drink:expect(prism.components.Drinkable)

   local conditions = self.owner:get(prism.components.ConditionHolder)
   if conditions and drinkable.condition then
      conditions:add(drinkable.condition)
   end

Finally we’ll heal the actor for the amount of the drinkable’s healing, if there is any.

   local health = self.owner:get(prism.components.Health)
   if health and drinkable.healing then
      health:heal(drinkable.healing)
   end
end

return Drink

Brewing the potion

Create a new file in modules/game/actors called vitalitypotion.lua. We register a new item with our new Drinkable component. We’ll make it heal for 5 and apply a health bonus of 5 as well.

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.Position(),
      prism.components.Drinkable {
         healing = 5,
         condition = prism.condition.Condition(prism.modifiers.Health(5))
      }
   }
end)

To have it appear in game, let’s include it in loot/chest.lua.

--- @type DropTableOptions
return {
   {
      entry = "VitalityPotion"
   }
}

Start the game and try drinking a potion. It should heal us for 5 points and increase our maximum health by 5!

Ticking down durations

To make our health bonus a temporary effect, we’ll extend Condition to include a duration.

Create a new directory modules/game/conditions/ and create a new file named tickedcondition.lua. We’re just adding a duration field to indicate how many turns the condition lasts.

--- @class TickedCondition : Condition
--- @field duration integer
--- @overload fun(duration: integer, ...: ConditionModifier): TickedCondition
local TickedCondition = prism.condition.Condition:extend "TickedCondition"

function TickedCondition:__new(duration, ...)
   self.super.__new(self, { ... })
   self.duration = duration
end

return TickedCondition

To use it, edit vitalitypotion.lua to the following.

prism.components.Drinkable {
   healing = 5,
   condition = prism.conditions.TickedCondition(10, prism.modifiers.HealthModifier(5))
},

Head over to modules/game/actions and create a new file called tick.lua. Our tick action can only be taken by actors who have a condition holder.

--- @class Tick : Action
local Tick = prism.Action:extend "Tick"
Tick.requiredComponents = { prism.components.ConditionHolder }

Next, we want to iterate over all of the performer’s timed conditions and tick them down by 1. We can use ConditionHolder:each() to simplify this.

--- @param level Level
function Tick:perform(level)
   -- Handle status effect durations
   self.owner
      :expect(prism.components.ConditionHolder)
      :each(function(condition)
         if prism.conditions.TickedCondition:is(condition) then
            --- @cast condition TickedCondition
            condition.duration = condition.duration - 1
         end
      end)

Then we want to remove any that have expired. We can similarly use ConditionHolder:removeIf().

:removeIf(function(condition)
   --- @cast condition TickedCondition
   return prism.conditions.TickedCondition:is(condition)
      and condition.duration <= 0
end)

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.

   -- Validate components
   local health = self.owner:get(prism.components.Health)
   if health then health:enforceBounds() end
end

return Tick

Now head over to modules/game/systems and create a new file called tick.lua. Each turn we try to perform Tick on the actor.

--- @class TickSystem : System
local TickSystem = prism.System:extend "TickSystem"

function TickSystem:onTurn(level, actor)
   level:tryPerform(prism.actions.Tick(actor))
end

return TickSystem

Don’t forget to add our system to the level in gamelevelstate.lua:

builder:addSystems(
   prism.systems.SensesSystem(),
   prism.systems.SightSystem(),
   prism.systems.FallSystem(),
   prism.systems.TickSystem()
)

Head back into the game and quaff some more potions. The maximum health increase should now end after 10 turns!

Wrapping up

In the next chapter we’ll make a wand and write some targetting code.