Dungeon 164
Will this thing never end? More on the Mimic. User(!) concerns. Still not a game, really.
164 articles on one little program. Surely more words than two of my books. I’m not sure how many were in the second book, but it was large. Both readers enjoyed it, though.
One(!) of my users(!), Ubergoober by name, objected to the fact that the spikes do some damage even when they are down. As in fact they do. They do more damage when up than when down:
self.damageTable = { down={lo=1,hi=1}, up={lo=4,hi=7}}
So. One point when down, four to seven when up. Dungeons are dangerous, what can I say? Ubergoober is also running on an iPhone, and that gives him trouble seeing things sometimes. I’ve never even tried that. I think of this as an iPad Pro 12.9 game. I’ll try to get psyched to run it on my iPhone and see what happens. I’m sure it will be weird and irritating.
Ubergoober suggested that it should be possible, with careful observation, to tell the difference between a Mimic and a Chest. At this point, my plan is different. I plan to make them indistinguishable, and to give the Mimic a unique treasure that you can only get if you wake it up. I’m not sure if you have to defeat it, or if it just drops the treasure when it wakes. Maybe there are two items.
So one thing here is to make the Chest and the Mimic look the same when the Mimic is hiding.
The dimensions of the “hide” spritesheet are 958x127 (are you serious??), so the individual sizes must be about 96x127. Let’s plug that in for the Chest and see how it goes.
function Chest:draw(tiny, center)
if tiny then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
sprite(Sprites:sprite(self.pic),center.x,center.y-5, 55,75)
popStyle()
popMatrix()
end
We’ll try this:
function Chest:draw(tiny, center)
if tiny then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
sprite(Sprites:sprite(self.pic),center.x,center.y-5, 96,127)
popStyle()
popMatrix()
end
While trying to find a Chest–there’s never treasure when you need it–I notice two bad things. First, the monster music plays and the attribute sheet comes up when you are near a mimic. Second, the mimic turns to face the player, even when it’s hiding. That’s a bit of a tell. But maybe we should leave that feature in …
Pretty good, but the chest and mimic aren’t aligned quite the same. The chest used to use a different graphic and its y was adjusted. Remove that and they’re perfect:
Which is the Mimic and which the Chest? Moohahaha try and find out.
Actually it’s pretty easy to find out. If you hit ?? the one says “I am probably a mysterious chest.” and the other says “I am a mimic”. Let’s do something about that, and let’s try for something better than if statements.
In Monster:
function Monster:query()
return "I'm a "..self:name()
end
Let’s give these guys some pluggable behavior. We’ll call it self.behavior
for now, and there’s just one kind, called NormalMonsterBehavior
.
I’m not going to start this with TDD. I’m not quite sure what I want, so I’m going to sketch in the query and then …
Wait. I was thinking that I’d create a couple of new classes, NormalMonsterBehavior and MimicMonsterBehavior, and populate them with methods that do the different things we want.
Let’s try something different. Let’s make a table of behaviors. It’ll be keyed by the desired behavior, e.g. “query”, and the values will be the method to call.
So …
function Monster:initAttributes()
local health = CharacterAttribute(self:roll(self.mtEntry.health))
local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
local attrs = { _health=health, _strength=strength, _speed=speed }
self.characterSheet = CharacterSheet(attrs)
self._movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
self.behavior = Monster.normalBehavior
end
Monster.normalBehavior = {
query=Monster.queryName,
}
And …
function Monster:query()
return self.behavior.query(self)
end
function Monster:queryName()
return "I'm a "..self:name()
end
I think this ought to work. Doesn’t. I think we can’t define the table that way, we’ll need to define it inside a method. If this works, I’ll explain my “thinking”.
OK, this works as anticipated:
function Monster:initAttributes()
local normalBehavior = {
query=Monster.queryName,
}
local health = CharacterAttribute(self:roll(self.mtEntry.health))
local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
local attrs = { _health=health, _strength=strength, _speed=speed }
self.characterSheet = CharacterSheet(attrs)
self._movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
self.behavior = normalBehavior
end
I “just” defined the table inside the initAttributes
function. I could have defined it anywhere that is sure to get called. The issue, I think, is that when it was set outside a method, the table definition gets run at compile time, and at that point, the queryName
function is not defined, so the table entry is nil, and therefore not there at all.
Just for fun, I’m going to try something else:
Monster.normalBehavior = {
query=function(monster) return monster:queryName() end
}
function Monster:initAttributes()
local health = CharacterAttribute(self:roll(self.mtEntry.health))
local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
local attrs = { _health=health, _strength=strength, _speed=speed }
self.characterSheet = CharacterSheet(attrs)
self._movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
self.behavior = Monster.normalBehavior
end
That works as advertised. Let’s go with this form and see if we wind up liking it.
Now we want this behavior to be pluggable, so we want a new table for the Mimic:
Monster.normalBehavior = {
query=function(monster) return monster:queryName() end
}
Monster.mimicBehavior = {
query=function(mimic) return "I am probably a mysterious chest." end
}
And in the init …
function Monster:initAttributes()
local health = CharacterAttribute(self:roll(self.mtEntry.health))
local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
local attrs = { _health=health, _strength=strength, _speed=speed }
self.characterSheet = CharacterSheet(attrs)
self._movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
local behavior = self.mtEntry.behaviorName or "normalBehavior"
self.behavior = Monster[behavior]
end
And, finally, in the monster definitions:
m = {name="Mimic", level=1, health={10,10}, speed={10,10}, strength=10, facing=-1, strategy=MimicMonsterStrategy, behaviorName="mimicBehavior",
attackVerbs={"bites", "chomps", "gnaws"},
dead={"mdead01","mdead02","mdead03","mdead04","mdead05","mdead06","mdead07","mdead08","mdead09","mdead10"},
...
This one has to be a string to make this work. A bit tricky, perhaps? Let’s make it work tho. And I think it should.
So that’s as intended. I could imagine making him reply with “HAHAHA I’m a Mimic!!!” If he’s awake. Should we?
Yes, let’s.
Monster.mimicBehavior = {
query=function(mimic)
if mimic.awake then
return "HAHAHA!!! I'm a MIMIC!!!"
else
return"I am probably a mysterious chest."
end
end,
}
That does the trick:
OK, now we want to stop the flipping, which is done here:
function Monster:drawMonster()
if not self.tile.currentlyVisible then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
noTint()
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
self.animator:draw()
popStyle()
popMatrix()
end
Here again we want a call to behavior:
function Monster:drawMonster()
if not self.tile.currentlyVisible then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
noTint()
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self.behavior.flip(self)
self.animator:draw()
popStyle()
popMatrix()
end
And in our tables:
Monster.normalBehavior = {
query=function(monster) return monster:queryName() end,
flip=function(monster) monster:flipTowardPlayer() end,
}
Monster.mimicBehavior = {
query=function(mimic)
if mimic.awake then
return "HAHAHA!!! I'm a MIMIC!!!"
else
return"I am probably a mysterious chest."
end
end,
flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end
}
That works as planned, for both Mimics and regular monsters:
OK, now for not displaying the attribute sheet. That’s done here:
function Monster:drawExplicit()
local r,g,b,a = tint()
if r==0 and g==0 and b==0 then return end
self:drawMonster()
self:drawSheet()
end
As before:
function Monster:drawExplicit()
local r,g,b,a = tint()
if r==0 and g==0 and b==0 then return end
self:drawMonster()
self.behavior.drawSheet(self)
end
Monster.normalBehavior = {
query=function(monster) return monster:queryName() end,
flip=function(monster) monster:flipTowardPlayer() end,
drawSheet=function(monster) monster:drawSheet() end,
}
Monster.mimicBehavior = {
query=function(mimic)
if mimic.awake then
return "HAHAHA!!! I'm a MIMIC!!!"
else
return"I am probably a mysterious chest."
end
end,
flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end,
drawSheet = function(mimic) if mimic.awake then mimic:drawSheet() end end,
}
As we see in the movie above, the mimic doesn’t draw its attribute sheet until it wakes up.
This is good. I think we’re due a commit here, and then some reflection. Commit: mimic pretends to be a chest, doesn’t flip when hiding, doesn’t display attributes when hiding.
Reflection
This pluggable behavior is working nicely. A special table can be selected for any monster using the “behaviorName” entry in its definition table. The tables are keyed collections of functions:
Monster.normalBehavior = {
query=function(monster) return monster:queryName() end,
flip=function(monster) monster:flipTowardPlayer() end,
drawSheet=function(monster) monster:drawSheet() end,
}
Monster.mimicBehavior = {
query=function(mimic)
if mimic.awake then
return "HAHAHA!!! I'm a MIMIC!!!"
else
return"I am probably a mysterious chest."
end
end,
flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end,
drawSheet = function(mimic) if mimic.awake then mimic:drawSheet() end end,
}
The normal table just calls the method that was originally used prior to pluggable behavior. The behaviors all expect to be passed the monster as their parameter, so they can call methods on the monster in question.
We use the behavior like this:
function Monster:query()
return self.behavior.query(self)
end
For some reason, we can’t say this instead:
function Monster:query()
return self.behavior:query()
end
I would have said that the latter was just syntactic sugar for the former, but anyway it doesn’t work. I’ll have to think about why. Be that as it may, the former form works for us.
Oh, I understand why. foo:query()
is equivalent to query(foo)
, so the function gets the behavior table as self
. So that can’t work, but the way it does work is consistent.
Is this too obscure?
One might argue that this is a bit too deep in the bag of tricks, but since functions are first-class objects in Lua, I think our Lua-oriented team would be quite ready to use tables of functions in this way.
In fact, an object in Lua is little more than a table of functions.
So I’m arguing that this is a rather elegant approach to specialized behavior in the Monster, and that we might want to consider using it in other places where we want variable behavior in the system.
We have one more issue, which is that the monster music plays if you are near a Mimic. We could leave that, as a clue, or we could turn it off. Let’s turn it off, because that may be a bit tricky, and then we can readily turn it back on if we wish.
The music is controlled here:
MonsterPlayer = class()
function MonsterPlayer:init(runner)
self.runner = runner
local playlist = {battle=asset.downloaded.A_Hero_s_Quest.Battle, dungeon=asset.downloaded.A_Hero_s_Quest.Dungeon}
self.sp = SmoothPlayer(3, playlist)
tween.delay(1, self.everySoOften, self)
end
function MonsterPlayer:checkForDanger()
local close = self.runner:hasMonsterNearPlayer(5)
if close then self.sp:play("battle") else self.sp:play("dungeon") end
end
function MonsterPlayer:everySoOften()
self:checkForDanger()
tween.delay(1, self.everySoOften, self)
end
If we were to do something here, we’d need to get ahold of all the monsters found by hasMonsterNearPlayer
. Let’s look in there:
function GameRunner:hasMonsterNearPlayer(range)
return self.monsters:hasMonsterNearPlayer(range)
end
function Monsters:hasMonsterNearPlayer(range)
for i,m in ipairs(self.table) do
if m:isAlive() and m:manhattanDistanceFromPlayer() <= range then return true end
end
return false
end
Let’s ask a different question:
function Monsters:hasMonsterNearPlayer(range)
for i,m in ipairs(self.table) do
if m:isActive() and m:manhattanDistanceFromPlayer() <= range then return true end
end
return false
end
Now we can do isActive
in Monster.
function Monster:isActive()
return self.behavior.isActive(self)
end
And in the tables:
Monster.normalBehavior = {
query=function(monster) return monster:queryName() end,
flip=function(monster) monster:flipTowardPlayer() end,
drawSheet=function(monster) monster:drawSheet() end,
isAlive=function(monster) return monster:isAlive() end,
}
Monster.mimicBehavior = {
query=function(mimic)
if mimic.awake then
return "HAHAHA!!! I'm a MIMIC!!!"
else
return"I am probably a mysterious chest."
end
end,
flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end,
drawSheet = function(mimic) if mimic.awake then mimic:drawSheet() end end,
isAlive = function(mimic) return mimic.awake and mimic:isAlive() end,
}
I think this probably works. (Probably?? What is this “probably”???)
Also, it doesn’t because I said isAlive
and should have said isActive
:
Monster.normalBehavior = {
query=function(monster) return monster:queryName() end,
flip=function(monster) monster:flipTowardPlayer() end,
drawSheet=function(monster) monster:drawSheet() end,
isActive=function(monster) return monster:isAlive() end,
}
Monster.mimicBehavior = {
query=function(mimic)
if mimic.awake then
return "HAHAHA!!! I'm a MIMIC!!!"
else
return"I am probably a mysterious chest."
end
end,
flip = function(mimic) if mimic.awake then mimic:flipTowardPlayer() end end,
drawSheet = function(mimic) if mimic.awake then mimic:drawSheet() end end,
isActive = function(mimic) return mimic.awake and mimic:isAlive() end,
}
That works. Music plays on regular monsters, doesn’t play for closed Mimics, does play when the Mimic opens.
I notice another issue. When Chests open, we can’t see the top part, which I personally drew at great personal expense. This is because large objects need to be drawn after all the tiles, not just the tile they are on. I think for now, I’ll let that go. I’ll make a yellow bug report for it: can’t see top of open chest.
Commit: Mimics don’t evoke danger music when hiding.
Now we do have an issue that showed up in this most recent change: if we put a typo or other mistake into one or both of the behavior tables, it won’t be detected until the feature is executed. That could mean that a defect could slip in … since I had my “typo” in both tables, it was found as soon as I came upon a monster. But suppose it had only been in the Mimic table. If I tested the game but never encountered a Mimic, the bug could have slipped into the release.
In the case in hand, my method that used the table called one function name and the table had the function stored under another name. There’s really no good way to check that mechanically.
We could, however, at least ensure that all the same names occur in both tables. I’ll make a note of that, but I think we’ll wrap this up for today.
Quick Summary
We’ve come up with a nice form of pluggable behavior that allows us to customize our Mimic monster without subclassing and without a bunch of if statements. (We do still check him for awake, but we aren’t checking the monster type as we would otherwise have to do. That reminds me, was there a check for that somewhere?
function Player:startActionWithMonster(aMonster)
local name = aMonster:name()
if name ~= "Mimic" or (name == "Mimic" and aMonster.awake) then
self.runner:initiateCombatBetween(self,aMonster)
else
aMonster.awake = true
end
end
Indeed there is. This is a tricky one. I’ve made yet another note. I’m sure we can clean that one up, but not today. Today I gotta get outa here.
Anyway, I like this. If you have a view, please let me know.
See you next time!