Stashing treasure

In this chapter we’ll use the optional DropTable component to implement chests and have kobolds drop items.

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.

First, register the inventory module in main.lua. While we’re here, register the drop table module as well.

prism.loadModule("prism/extra/inventory")
prism.loadModule("prism/extra/droptable")

Then 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 { char = "%", color=prism.Color4.RED },
      prism.components.Item{
         stackable = "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 a future chapter!

Getting the drop on it

Now we can give modules/game/actors/kobold.lua a DropTable. We’ll give them a 30% chance to drop one of our meat bricks.

prism.components.DropTable {
   chance = 0.3,
   entry = prism.actors.MeatBrick,
}

If you were to kick a few kobolds you’d notice nothing is happening! That’s because the drop table needs to be hooked into the game logic. Open modules/game/actions/die.lua and we’ll add drops to the level when an actor dies.

function Die:perform(level)
   local x, y = self.owner:getPosition():decompose()
   local dropTable = self.owner:get(prism.components.DropTable)

   if dropTable then
      local drops = dropTable:getDrops(level.rng)
      for _, drop in ipairs(drops) do
         level:addActor(drop, x, y)
      end
   end

   -- rest of Die:perform
end

If they have a drop table, we use DropTable:getDrops() to roll the drop table, and add each item to the level at the actor’s position.

To ensure dropped items don’t float over pits, we can add the System:onActorAdded() callback to modules/game/systems/fallsystem.lua.

function FallSystem:onActorAdded(level, actor)
   level:tryPerform(prism.actions.Fall(actor))
end

Boot up the game and kick a few kobolds around. They should start dropping meat!

Creating containers

For chests, we’ll start with a new tag. Navigate modules/game/components and create a new file there called container.lua.

--- @class Container : Component
--- @overload fun(): Container
local Container = prism.Component:extend "Container"

function Container:getRequirements()
   return prism.components.Inventory
end

return Container

Next we’ll define a new action for opening these. Head over to modules/game/actions and create a new file called opencontainer.lua. For our target, we want a container within range 1 that we can see.

local Name = prism.components.Name
local Log = prism.components.Log

local OpenContainerTarget = prism.Target()
   :with(prism.components.Container)
   :range(1)
   :sensed()

In perform, we grab every item in the target container’s inventory and dump them on the ground, before removing the container and logging messages.

--- @class OpenContainer : Action
local OpenContainer = prism.Action:extend "OpenContainer"
OpenContainer.targets = { OpenContainerTarget }
OpenContainer.name = "Open"

--- @param level Level
--- @param container Actor
function OpenContainer:perform(level, container)
   local inventory = container:expect(prism.components.Inventory)
   local x, y = container:expectPosition():decompose()

   inventory:query():each(function(item)
      inventory:removeItem(item)
      level:addActor(item, x, y)
   end)

   level:removeActor(container)

   local containerName = Name.get(container)
   Log.addMessage(self.owner, "You kick open the %s.", containerName)
   Log.addMessageSensed(level, self, "The %s kicks open the %s.", Name.get(self.owner), containerName)
end

return OpenContainer

Now that we’re all set up with our container logic we need to actually make a container to try this with. Let’s create a new file in modules/game/actors called chest.lua. We’ll accept a contents parameter to define the items in the chest and use Inventory:addItems().

prism.registerActor("Chest", function(contents)
   local inventory = prism.components.Inventory()
   local chest = prism.Actor.fromComponents {
       prism.components.Name("Chest"),
       prism.components.Position(),
       inventory,
       prism.components.Drawable("(", prism.Color4.YELLOW),
       prism.components.Container(),
       prism.components.Collider()
   }
   --- @cast contents Actor[]
   inventory:addItems(contents or {})
   return chest
end)

Note

To support Geometer, parameters passed to actor and cell factories must be optional.

Cracking a cold one

If you launch the game and bump into a chest you’ll notice you kick it, which is fun but not exactly what we want. We’ll have to change to logic in GameLevelState. In GameLevelState:updateDecision add the following right above where we try to kick:

function GameLevelState:updateDecision(dt, owner, decision)
   -- yada yada
   if controls.move.pressed then
      -- blah blah

      local openable = self.level
         :query(prism.components.Container)
         :at(destination:decompose())
         :first()

      local openContainer = prism.actions.OpenContainer(owner, openable)
      if self:setAction(openContainer) then return end

      -- kick stuff
   end
end

Okay! When you walk into a chest now you should pop that sucker open! Congratulations! Wait, nothing was inside the chest though. That’s not very fun. Let’s take care of that.

Spicing up level generation

Let’s first create a new top level folder, loot and within that folder a new file chest.lua. Let’s keep it simple for now and give chests a guaranteed drop of meat.

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

At the end of levelgen.lua, we’ll spawn a chest in the middle of a random room.

local chestRoom = availableRooms[rng:random(1, #availableRooms)]
local center = chestRoom:center():floor()
local drops = prism.components.DropTable(chestloot):getDrops(rng)

builder:addActor(prism.actors.Chest(drops), center:decompose())

return builder

The chest will overlap with a kobold, but we’ll deal with that when we revisit level generation. You’ll see now that when we open the chest we get a meat brick!

In the next chapter

We’ve used the DropTable component to add drops to kobolds and added chests. In the next chapter we’ll start on an inventory system.