After getting my blood flowing with a bit of a rant, let’s see if we can get a venomous creature up in this … place.

0905

We have a way to poison our princess, which drains her charming attributes in a most cruel fashion, until she finds and takes an antidote. We even have messages that come out when it happens. Last time I listed a number of things that we need, including a better display of multiple items in the inventory, interactive decor, and so on.

Now I can imagine two ways to poison the poor dear. One is via a venomous bite, but another might be fiddling about with the decor. It would be nice if when you mess with a skeleton or a jar or a crate, you might find something of value, or you might suffer some kind of damage, even poisoning.

It might be quite good if skeletons and other decor were both very dangerous and very productive.

I’ve just talked myself into working with Decor for poison rather than with monsters. I do plan to make some of those venomous as well. We may have to adjust how bad poison is if this keeps up. But we have that ability if we need it.

Let’s look at Decor. And we should look at Loot as well, and ask ourselves why they’re different.

local DecorSprites = { Skeleton1=asset.Skeleton1, Skeleton2=asset.Skeleton2,
s11=asset.Skeleton1, s12=asset.Skeleton1, s13=asset.Skeleton1, s14=asset.Skeleton1,
s21=asset.Skeleton2, s22=asset.Skeleton2, s23=asset.Skeleton2, s24=asset.Skeleton2,
BarrelEmpty=asset.barrel_empty, BarrelClosed=asset.barrel_top, BarrelFull=asset.barrel_full,
Crate=asset.Crate, PotEmpty=asset.pot_empty
}

local ScaleX = {-1,1}

function Decor:init(tile,runner,kind)
    self.sprite = kind and DecorSprites[kind] or Decor:randomSprite()
    tile:moveObject(self)
    self.runner = runner
    self.scaleX = ScaleX[math.random(1,2)]
end

function Decor:randomSprite()
    local keys = self:getDecorKeys()
    local key = keys[math.random(1,#keys)]
    return DecorSprites[key]
end

function Decor:getDecorKeys()
    local keys = {}
    for k,v in pairs(DecorSprites) do
        table.insert(keys, k)
    end
    return keys
end

I notice that we could cache the decor keys. Not very important.

So what we have here is that Decor items are only decor: they have no in-game function other than to display. That’s managed in TileArbiter:

    t[Decor] = {}
    t[Decor][Player] = {moveTo=TileArbiter.acceptMove}

The player can enter a Decor tile. Monsters cannot, because the default is refuseMove. This means that you can sometimes avoid monsters by dodging around a Decor.

Compare this with Loot:

    t[Loot] = {}
    t[Loot][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithLoot}

When the player tries to move to a Loot tile, she is refused entry but the Loot interacts with her:

function Player:startActionWithLoot(aLoot)
    aLoot:actionWith(self)
end

function Loot:actionWith(aPlayer)
    self.tile:removeContents(self)
    if self.kind == "Pathfinder" then
        self:addPathfinderToInventory(aPlayer)
    elseif self.kind == "Antidote" then
        self:addAntidoteToInventory(aPlayer)
    else
        aPlayer:addPoints(self.kind, math.random(self.min, self.max))
    end
end

We see here that we have immature code to deal with the inventory-item kind of loots. They are all handled individually. Fortunately there are only two kinds right now, but I think we have in mind making many if not most of the things you find go into inventory rather than being instant power-up kinds of things.

As I think about it, a kind of classification begins to form in my mind:

There are objects in the dungeon. Each object is in a particular tile. The player attempts to interact with any object by moving onto its tile. The object may variously:

  • Permit or refuse entry;
  • Try to initiate combat;
  • Add or remove points to one or more attributes;
  • Poison the player;
  • Add something to the player’s inventory;
  • Remain in the tile, or disappear.

At present, a tile can contain more than one object. I think we have code that tends to prevent that from happening, as in when we place Loot or Decor, but the Tile has a collection for its contents, so more than one thing can in principle be in there. We might want to change that. On the other hand, maybe we don’t care.

Tile entry invokes a TileArbiter (TA) for each combination of entrant and contents item. The TA checks its tables to decide whether entry is permitted or not. I think the rule is that if any content item refuses entry, you can’t enter. And each item can have an action, which typically starts an interaction with the player. That was originally in place to support combat.

There is a double-dispatch flavor here. The content item initiates action, sending a message to the Player, saying what it is: startActionWithMonster or whatever. Then the Player typically calls back. Now each side knows who and what it’s dealing with, so that they can initiate a CombatRound or whatever.

This is probably too much complexity for most cases, which will come down to adding or removing points, or adding something to inventory.

Recursive Digression

Let me drill down on that for a moment. Looking at it today, I think the interaction between dungeon contents and player, at the code level, is probably too complicated. This is “technical debt”, a difference between the design we have, and the design we might have, knowing what we know now. When we first discover technical debt, it looks like a decent design, but one that feels like there is a simpler, better idea, that we wish we had already done and might well do in the future.

Technical debt is not bad design or bad code. Those are called bad design or bad code. Technical debt is decent design and code such that now, we think we have, or could have, a better idea.

I think we have that here.

I think that one motivation for the current design was the observation that there were many kinds of monsters, but that they all did roughly the same kind of thing, and many kinds of treasure, but they all did roughly the same thing, so we created the TileArbiter and its table to manage the similarity and the differences.

It does seem to me that we want the Player object to know, at least sometimes, what it’s interacting with. The cases are:

function Player:startActionWithChest(aChest)
    aChest:open()
end

function Player:startActionWithKey(aKey)
    self.characterSheet:addKey(aKey)
    sound(asset.downloaded.A_Hero_s_Quest.Defensive_Cast_1)
    aKey:take()
end

function Player:startActionWithLoot(aLoot)
    aLoot:actionWith(self)
end

function Player:startActionWithMonster(aMonster)
    self.runner:initiateCombatBetween(self,aMonster)
end

function Player:startActionWithSpikes(spikes)
    spikes:actionWith(self)
end

function Player:startActionWithWayDown(aWayDown)
    aWayDown:actionWith(self)
end

Clearly we’re not taking much advantage of the double dispatch here. There’s nothing that would prevent us from renaming aChest:open and aKey:take to actionWith, and then all the calls would be the same. (We’d move the sound over into the key.) (And, anyway, keys should probably be in inventory, shouldn’t they?)

Except for the combat interaction, we might just as well have told the object in the tile to do its thing, and passed it the player for reference. Even the combat can probably be done directly.

Is it possible that we can entirely remove the TileArbiter class? Is it completely unnecessary?

Wow, I’m glad we had this little chat.

Pop up from digression

We now have a decision to make. Our mission, and let’s accept it, is to implement something in the dungeon that poisons the Player. That will be Yet Another Interaction, and in thinking about how to do it, and how other interactions work, we’ve come to suspect that the TileArbiter object may be unnecessary.

The question is what to do about this. TA works fine. There’s no story that requires us to remove it, or even work with it very much: the poison can surely be done without touching TA’s code, depending on how we go about it. But TA does slow us down whenever we do interactions, because we have to deal with it, at least editing its table, and often adding methods to it.

So it is tempting to refactor to make a place for poison, with an eye to removing TA from the picture, making a nicer landing field for whoever does the poisoning.

Yet, we can almost certainly do this without the refactoring. That will surely go faster, and surely won’t make future changes any more difficult as regards TA itself.

Now, in these articles, I can justify a refactoring by demonstrating how easy or hard it is to do. In a real product development, I need to think more about the overall flow of features into the product. Too much refactoring will slow down feature delivery, just as too little will slow us down and cause more defects.

I think in the product situation, as much as I’d like to dive into refactoring TA out, I’d say we should do our feature.

But we can at least do our feature in a way that is as independent of TileArbiter as possible. Let’s try that.

Dangerous Decor

Let’s start by making Decor potentially poisonous. We’ll make some fraction of them “dangerous”. I think I’ll go for half, because it will make testing more fun.

Deadly Decor

function Decor:init(tile,runner,kind)
    self.sprite = kind and DecorSprites[kind] or Decor:randomSprite()
    tile:moveObject(self)
    self.runner = runner
    self.scaleX = ScaleX[math.random(1,2)]
    self.dangerous = math.random() > 0.5
end

Let’s tint them for fun:

function Decor:draw(tiny)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    translate(g.x, g.y)
    scale(self.scaleX, 1)
    if self.dangerous then tint(255, 0, 0)   end
    sprite(self.sprite,0,0, 50,50)
    popStyle()
    popMatrix()
end

danger

That’s sweet. Now let’s see how to interact. That starts here:

function Tile:attemptedEntranceBy(enteringEntity, oldRoom)
    local ta
    local acceptedTile
    local accepted = false
    local residents = 0
    for k,residentEntity in pairs(self.contents) do
        residents = residents + 1
        ta = TileArbiter(residentEntity,enteringEntity)
        acceptedTile = ta:moveTo()
        accepted = accepted or acceptedTile==self
    end
    if residents == 0 or accepted then
        return self
    else
        return oldRoom
    end
end

We see by inspecting that code that the rule is that if anyone accepts the entry, it is accepted. So be it, I don’t think we care.

Now we have no chance of avoiding the use of a TileArbiter here. So we’ll have to push on with that scheme. I change the TA entry:

    t[Decor] = {}
    t[Decor][Player] = {moveTo=TileArbiter.refuseMove, action=Decor.startActionWithPlayer}

Now we need startActionWithPlayer on Decor:

function Decor:startActionWithPlayer(aPlayer)
    if self.dangerous then aPlayer:poison() end
end

Is that all there is? I think yes. I’m wrong, though. The method isn’t getting called on Decor.

Now, for sure, TileArbiter is slowing me down. I make a mental note of that. A black check mark against it.

OK, the issue is that the message is always sent to the entrant, not the content item.

    t[Decor][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithDecor}

function Player:startActionWithDecor(aDecor)
    aDecor:actionWith(self)
end

function Decor:actionWith(aPlayer)
    if self.dangerous then aPlayer:poison() end
end

This works. There is no message about being poisoned. We need one.

function Player:curePoison()
    self.runner:addTextToCrawl("You have been cured of poison!")
    self.characterSheet:stopPoison()
end

function Player:poison()
    self.runner:addTextToCrawl("You have been poisoned!")
    self.characterSheet:startPoison()
end

poisoned and cured

So that works nicely. I think I’d prefer to allow the player to step into the tile. I’ll make that change:

    t[Decor][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithDecor}

Now I’ll remove the tint and commit: some decor poisons the player.

I’ve already written one article this morning, and I need to make a run to the grocery store, so let’s sum up.

Summary

This went nicely. With five lines of operating code, plus a little boilerplate, we have made half the Decor items poison the player. We’ll of course want to make many of them quite valuable, though I’m not sure just what we’ll put in there to do that. When we do, it’s pretty clear exactly where we’ll put that code.

We did get slowed down a tiny bit by TileArbiter but I “should” have known better. I do still feel that we can do without it, but there’s no rush.

A decent morning and it’s warm enough now to put the top down on the way to the store. And the way back.

See you next time!


D2.zip