Having a snack¶
Note
This section hasn’t had a second pass! Eat at your own peril! Beware food poisoning!
In this section of the tutorial we’re going to add an Eat action that works on inventory items and a user interface to let us actually choose to eat the items in our inventory.
Modifying health¶
Let’s head over to module/game/components/health.lua
and make a small addition.
--- @param amount integer
function Health:heal(amount)
self.hp = math.min(self.hp + amount, self.maxHP)
end
This should only be called from actions because it modifies the component! This will make it easy for us to apply healing without going over max HP.
Making it edible¶
Let’s create a new component to define what’s edible. We’ll take a single integer in
and set that to the component’s healing field. Head over to module/game/components
and
create a new file edible.lua
.
--- @class Edible : Component
--- @field healing integer
--- @overload fun(healing: integer)
local Edible = prism.Component:extend("Edible")
--- @param healing integer
function Edible:__new(healing)
self.healing = healing
end
return Edible
Now we’ll create an action to use on the edible actor. Head to module/game/actions
and
create a new file eat.lua
.
local EatTarget = prism.InventoryTarget(prism.components.Edible)
:inInventory()
The inventory module injects a new subclass of Target into the prism namespace as a convenience. This saves us from having to write our own filter to figure out if something is in the actor’s inventory.
---@class Eat : Action
---@overload fun(owner: Actor, food: Actor): Eat
local Eat = prism.Action:extend("Eat")
Eat.requiredComponents = {
prism.components.Health
}
Eat.targets = {
EatTarget
}
Now we create our eat action. You need a health component to eat, and the target has to be something edible in your inventory as we defined above.
--- @param level Level
--- @param food Actor
function Eat:perform(level, food)
local edible = food:expect(prism.components.Edible)
local health = self.owner:expect(prism.components.Health)
health:heal(edible.healing)
local inventory = self.owner:expect(prism.components.Inventory)
inventory:removeQuantity(food, 1)
Log.addMessage(self.owner, sf("You eat the %s", Name.get(food)))
Log.addMessageSensed(level, self, sf("%s eats the %s", Name.get(self.owner), Name.get(food)))
end
return Eat
And for the perform function we just add to the eater’s health the amount of the edible’s healing. Now let’s head back over to
modules/game/actors/meat.lua
and add the edible component to the meat actor.
prism.components.Edible(1)
Modifying the interface¶
Okay with the actual mechanics out of the way it’s time for us to flesh out our inventory menu a little more. Create a new file at
gamestates/inventoryactionstate.lua
and let’s create a new GameState.
local keybindings = require "keybindingschema"
local Name = prism.components.Name
First we’re going to require our keybinding schema, and then we’re going to alias the name component for the sake of brevity.
--- @class InventoryActionState : GameState
--- @field decision ActionDecision
--- @field previousState GameState
--- @overload fun(display: Display, decision: ActionDecision, level: Level, item: Actor)
local InventoryActionState = spectrum.GameState:extend "InventoryActionState"
--- @param display Display
--- @param decision ActionDecision
--- @param level Level
--- @param item Actor
function InventoryActionState:__new(display, decision, level, item)
self.display = display
self.decision = decision
self.level = level
self.item = item
self.actions = {}
for _, Action in ipairs(self.decision.actor:getActions()) do
local action = Action(self.decision.actor, self.item)
if self.level:canPerform(action) then
table.insert(self.actions, action)
end
end
end
Next we’ll create a new GameState and in the constructor we’ll loop through all the actions the active actor can do, and assign those to a letter if the actor can take that action with the inventory item as the first target.
function InventoryActionState:load(previous)
--- @cast previous InventoryState
self.previousState = previous.previousState
end
Then we’re going to store the LevelState in our previousState field so that we can draw the level under this menu.
function InventoryActionState:draw()
self.previousState:draw()
self.display:clear()
self.display:putString(1, 1, Name.get(self.item), nil, nil, 2, "right")
for i, action in ipairs(self.actions) do
local letter = string.char(96 + i)
local name = string.gsub(action.className, "Action", "")
self.display:putString(1, 1 + i, string.format("[%s] %s", letter, name), nil, nil, nil, "right")
end
self.display:draw()
end
Then we can set up our draw function to loop through all of the possible actions and enumerate them to the user, drawing the letter used to take that action next to the item name.
function InventoryActionState:keypressed(key)
for i, action in ipairs(self.actions) do
print(key, string.char(i + 96))
if key == string.char(i + 96) then
self.decision:setAction(action)
self.manager:pop()
end
end
local binding = keybindings:keypressed(key)
if binding == "inventory" or binding == "return" then self.manager:pop() end
end
Then we’ll handle the user selecting the action. If the user hits the letter that matches an action we set the decision to that action. Now we’ll need to head over to gamestates/inventorystate.lua
and let’s push this new InventoryActionState onto the stack when a user selects an item instead of dropping it. Let’s modify our
InventoryState:keypressed
to look like this.
function InventoryState:keypressed(key)
for i, letter in ipairs(self.letters) do
if key == letter then
self.manager:push(InventoryActionState(self.display, self.decision, self.level, self.items[i]))
return
end
end
local binding = keybindings:keypressed(key)
if binding == "inventory" or binding == "return" then self.manager:pop() end
end
We’ve got one last thing to handle. When we press a letter right now we just go back to the inventory screen and nothing happens until we leave that screen. A little strange to say the least! The next thing we need to do is get the inventory menu to get out of the way when a decision is set.
function InventoryState:resume()
if self.decision:validateResponse() then
self.manager:pop()
end
end
In the next chapter¶
In the next chapter we’ll go over drop tables and containers like chests, populating the dungeon with delicious meat and shiny trinkets!