Stashing treasure

Note

This section hasn’t had a second pass! Some chests might be mimics!

In this chapter we’ll add a drop table to the kobolds, and create chests the player can open like a loot pinata. To do this we’ll be making heavy use of the DropTable component included in extra/droptable. How to write your own drop tables will be covered in a future how-to.

Getting the drop on it

Let’s head over to main.lua where you’ll add the following line to your module loading before modules/game.

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

Now let’s make our way over to modules/game/actors/kobold.lua and add the droptable component.

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

This is a really simple drop table, and it just says there’s a 30% chance for the kobold to drop a meat brick. If you were to go into the game and kick a few kobolds now you’d notice nothing is happening! That’s because the drop table needs to be hooked into the game logic. Let’s head over to modules/game/actions/die.lua where we’ll change the beginning the Die action.

local walkmask = prism.Collision.createBitmaskFromMovetypes{"walk"}
function Die:perform(level)
   local x, y = self.owner:getPosition():decompose()
   local dropTable = self.owner:get(prism.components.DropTable)
   local cellmask = level:getCell(x, y):getCollisionMask()

   if prism.Collision.checkBitmaskOverlap(walkmask, cellmask) and 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

First we make our walkmask, a simple collision mask saying an actor with the walk movetype can move onto this tile. Then within perform we’ll get the Die’ers position, drop table, and the cell they’re standing on’s mask.

If the cell is walkable and they have a drop table we roll on the table and then add all of the results to the the level at the tile which they died. We check walkability so we don’t spawn items over a pit. Now if you boot up the game and kick a few kobolds around you should start getting some meat bricks as drops, success!

Creating containers

Many games won’t settle just for drop tables. You might want a zelda style chest the player can crack open and get some goodies! So let’s add that. The first step we’ll need to take is adding a new tag component. Navigate to 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

This is a really simple component that just marks an actor with an inventory as a container that can be opened. Next up let’s head over to modules/game/actions where we’ll create a new file called opencontainer.lua.

local sf = string.format
local Name = prism.components.Name
local Log = prism.components.Log

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

Let’s start with our target. We specify it must be a container, at range 1, and sensed by the actor.

--- @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 items = inventory:query():gather()

   local x, y = container:expectPosition():decompose()
   for _, item in ipairs(items) do
      inventory:removeItem(item)
      level:addActor(item, x, y)
   end

   level:removeActor(container)

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

return OpenContainer

Then we get to the perform action. We know that the container has to have an inventory because it’s required by the container component. So we grab the inventory and get a list of the items it contains. Then we loop through that list and remove them from the container’s inventory while adding them to the level. Finally, we remove the container itself from the level.

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.

prism.registerActor("Chest", function(contents)
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)

Let’s break this down a little since this is the first time we’re really making use of the factory to take optional parameters for an actor. We accept a contents argument to the Chest constructor. All parameters to the factory function MUST be optional! In this case it’d set items = nil and inventory wouldn’t see the field. If this parameter is not optional Geometer will crash on startup!

Cracking a cold one

If you go ingame now and bump the chest you’ll notice you kick it, that’s definitely not what we want. We’ll have to change to logic in GameLevelState. Let’s find out way to GameLevelState:keypressed and add the following right above where we try to kick:

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 really simple for now.

return {
   {
      entry = prism.actors.MeatBrick
   }
}

This defines a single gauranteed drop of a meatbrick. We’ll flesh this out a lot more when we create more stuff for chests to drop. Next let’s head over to levelgen.lua and let’s spawn a chest using this level generation.

At the end of the anonymous function we return in this file just above return builder let’s add the following.

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

local mf = math.floor
builder:addActor(prism.actors.Chest(drops), mf(center.x), mf(center.y))

return builder

The chest will overlap with a kobold which we’re also spawning in the center of the room, but that’s fine we’ll deal with that when we revisit the level generation in the future. You’ll see now that when we open the chest we get a meat brick!

In the next chapter

In the next chapter we’ll create some more items to add to our loot tables and make the game more interesting. We’ll add a potion and go over making buffs using the StatusEffect component included in /extra!