Packing your bags¶
In this chapter we’ll implement a simple inventory with the optional module.
Head over to main.lua and add the inventory module from extra.
prism.loadModule("prism/extra/inventory")
Giving the player an inventory¶
Let’s head over to modules/game/actors/player.lua
and add our inventory component.
We’ll limit the size to the alphabet.
prism.components.Inventory{
limitCount = 26,
},
Adding a keybinding¶
Let’s head to keybindingschema.lua
and add a few entries.
-- inventory
{ key = "tab", action = "inventory", description = "Opens the inventory screen." },
{ key = "backspace", action = "return", description = "Moves back a level in a substate." },
{ key = "p", action = "pickup", description = "Picks up an inventory item." },
We’ll use these later in the tutorial to open the inventory and return to the main game state.
Creating an inventory screen¶
Navigate to gamestates
and create a new file inventorystate.lua
.
We create a new GameState
and we pass in the display, decision, level,
and the inventory. We get all the items at the time of instantiation and store them in the items field for convenience.
Finally we create a mapping of letters from 1-26 corresponding to a-z which we’ll use during input handling.
local utf8 = require "utf8"
local keybindings = require "keybindingschema"
--- @class InventoryState : GameState
--- @overload fun(display: Display, decision: ActionDecision, level: Level, inventory: Inventory)
local InventoryState = spectrum.GameState:extend "InventoryState"
--- @param display Display
--- @param decision ActionDecision
--- @param level Level
--- @param inventory Inventory
function InventoryState:__new(display, decision, level, inventory)
self.display = display
self.decision = decision
self.level = level
self.inventory = inventory
self.items = inventory.inventory:getAllActors()
self.letters = {}
for i = 1, #self.items do
self.letters[i] = utf8.char(96 + i) -- a, b, c, ...
end
end
We also want to keep a reference to the previous GameState
, so we’ll use GameState:load()
to capture it.
function InventoryState:load(previous)
self.previousState = previous
end
Now we’ll draw the inventory. To show the inventory on top of the level, we’ll first draw the previous state. Then we clear the display and draw a simple header, aligned to the right side of the screen. Finally, we loop through each item in our inventory, assign it a letter based on its index, and draw it to the screen.
function InventoryState:draw()
self.previousState:draw()
self.display:clear()
self.display:putString(1, 1, "Inventory", nil, nil, 2, "right")
for i, actor in ipairs(self.items) do
local name = actor:getName()
local letter = self.letters[i]
local item = actor:expect(prism.components.Item)
local countstr = ""
if item.stackCount and item.stackCount > 1 then
countstr = ("%sx "):format(item.stackCount)
end
local itemstr = ("[%s] %s%s"):format(letter, countstr, name)
self.display:putString(1, 1 + i, itemstr, nil, nil, 2, "right")
end
self.display:draw()
end
Now we handle keypresses. For the items we loop through our letters to find which one matches our keypress
and for now we just try to drop the item when we hit that button. Drop
’s canPerform()
will return false
if given a nil
target.
function InventoryState:keypressed(key)
for i, letter in ipairs(self.letters) do
if key == letter then
local pressedItem = self.items[i]
local drop = prism.actions.Drop(self.decision.actor, pressedItem)
if drop:canPerform(self.level) then
self.decision:setAction(drop)
end
self.manager:pop()
return
end
end
Then we check if the user hit the inventory or return key, and if so we call GameStateManager:pop()
,
returning us to the previous state.
local binding = keybindings:keypressed(key)
if binding == "inventory" or binding == "return" then
self.manager:pop()
end
end
return InventoryState
Complete inventorystate.lua
local keybindings = require "keybindingschema"
--- @class InventoryState : GameState
--- @field previousState GameState
--- @overload fun(display: Display, decision: ActionDecision, level: Level, inventory: Inventory)
local InventoryState = spectrum.GameState:extend "InventoryState"
--- @param display Display
--- @param decision ActionDecision
--- @param level Level
--- @param inventory Inventory
function InventoryState:__new(display, decision, level, inventory)
self.display = display
self.decision = decision
self.level = level
self.inventory = inventory
self.items = inventory.inventory:getAllActors()
self.letters = {}
for i = 1, #self.items do
self.letters[i] = string.char(96 + i) -- a, b, c, ...
end
end
function InventoryState:load(previous)
self.previousState = previous
end
function InventoryState:draw()
self.previousState:draw()
self.display:clear()
self.display:putString(1, 1, "Inventory", nil, nil, 2, "right")
for i, actor in ipairs(self.items) do
local name = actor:getName()
local letter = self.letters[i]
local item = actor:expect(prism.components.Item)
local countstr = ""
if item.stackCount and item.stackCount > 1 then countstr = ("%sx "):format(item.stackCount) end
local itemstr = ("[%s] %s%s"):format(letter, countstr, name)
self.display:putString(1, 1 + i, itemstr, nil, nil, 2, "right")
end
self.display:draw()
end
function InventoryState:keypressed(key)
for i, letter in ipairs(self.letters) do
if key == letter then
local pressedItem = self.items[i]
local drop = prism.actions.Drop(self.decision.actor, pressedItem)
if drop:canPerform(self.level) then self.decision:setAction(drop) end
self.manager:pop()
return
end
end
local binding = keybindings:keypressed(key)
if binding == "inventory" or binding == "return" then self.manager:pop() end
end
return InventoryState
Opening the inventory¶
With the inventory state complete it’s time to glue things together. Head back to gamelevelstate.lua
and
let’s add some input handling to get the InventoryState
to pop up.
First require
access our InventoryState
at the top of the file.
local InventoryState = require "gamestates.inventorystate"
Then at the bottom of GameLevelState:keypressed
, just above the wait action, we’ll check for the inventory key
and push the InventoryState
, if the current actor (owner
) has an inventory.
function MyGameLevelState:keypressed(key, scancode)
-- ...
if action == "inventory" then
local inventory = owner:get(prism.components.Inventory)
if inventory then
local inventoryState = InventoryState(self.display, decision, self.level, inventory)
self.manager:push(inventoryState)
end
end
-- Handle waiting
if action == "wait" then decision:setAction(prism.actions.Wait(self.decision.actor)) end
end
Now we can run the game and hit tab. The inventory menu will show up (but won’t do anything)!
Creating an item¶
Our kobold kicking hero needs something to chew on, and a way to regain health! Let’s add a Meat Brick that they can pick up and eat to restore their health.
Create a new file in modules/game/actors
called meatbrick.lua
and register the following actor.
prism.registerActor("MeatBrick", function ()
return prism.Actor.fromComponents{
prism.components.Name("Meat Brick"),
prism.components.Position(),
prism.components.Drawable("%", prism.Color4.RED),
prism.components.Item{
stackable = prism.actors.MeatBrick,
stackLimit = 99
}
}
end)
We give it the Item
component to indicate it can be held in an Inventory
.
We’ll make it consumable in the next chapter!
Picking things up¶
Now to be able to pick these things up we’ll need to hook up the Pickup
action.
if action == "pickup" then
local target = self.level:query(prism.components.Item)
:at(owner:getPosition():decompose())
:first()
local pickup = prism.actions.Pickup(owner, target)
if self.level:canPerform(pickup) then
decision:setAction(pickup)
return
end
end
We grab the first item on the tile and use it as the target for Pickup
.
Boot up the game and draw in a few meat bricks with Geometer. You should be able to
pick up and drop them now!
Fixing the draw order¶
You might notice that now when the player moves on top of the food sometimes the player is drawn underneath the food.
We can fix this by changing the depth or ‘layer’ the player’s drawable is drawn at.
Go ahead and navigate back to modules/game/actors/player.lua
and change the following line from
prism.components.Drawable("@", prism.Color4.GREEN),
to
prism.components.Drawable("@", prism.Color4.GREEN, nil, math.huge),
We’re setting the background color to nil
so that it still defaults to transparent, but we’re setting our draw priority
to math.huge
so the player will always draw on top of everything else.
In the next chapter¶
We’ve implemented a simple inventory with the provided inventory module. In the next chapter we’ll make the bricks consumable, get them to drop from kobolds, and add user interface elements to allow the user a choice between dropping and eating the meat.