Stashing treasure

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

Getting the drop on it

Head over to main.lua and load the drop table module.

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

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.

prism.registerActor("Chest", function(contents)
   --- @cast contents Actor[]
   return prism.Actor.fromComponents {
       prism.components.Name("Chest"),
       prism.components.Position(),
       prism.components.Inventory{items = contents},
       prism.components.Drawable("(", prism.Color4.YELLOW),
       prism.components.Container(),
       prism.components.Collider()
   }
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 modules/gamestates/gamelevelstate.lua GameLevelState:keypressed and add the following right above where we try to kick:

function GameLevelState:keypressed(key, scancode)
   -- yada yada
   if keybindOffsets[action] then
      -- blah blah

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

      local openContainer = prism.actions.OpenContainer(owner, openable)
      if self.level:canPerform(openContainer) then
         decision:setAction(openContainer)
         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.

return {
   {
      entry = prism.actors.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 add a potion and go over making buffs.