Dungeon 178
Variations on an Idea from the Wild Blue Onder. No success.
Responding to this morning’s article about the DevMap, Bruce Onder tweeted:
What do you think of this idea: pull out the drawing stuff into its own rendering object, initialized with a “null object” which draws the game in default mode (mini map, zoomed in level).
Then when the ShowMap is disabled, call the renderer, replacing with the NoMap object?
That got me thinking instead of napping, as had been my intention, so now I’m here at 1630, thinking I’ll consider some ways of getting rid of those if statements.
Here are all the references to DevMap:
function setup()
if CodeaUnit then
runCodeaUnitTests()
CodeaTestsVisible = true
end
parameter.boolean("ShowMap",false)
parameter.boolean("DevMap", false)
...
function GameRunner:drawTinyMapOnTopOfLargeMap()
if DevMap then return end
pushMatrix()
self:scaleForTinyMap()
Runner:drawMap(true)
popMatrix()
end
function GameRunner:scaleForLocalMap()
if DevMap then
self:getDungeon():setVisibility()
translate(0,0)
local hs = WIDTH/(self.tileSize*(1+self.tileCountX))
local vs = HEIGHT/(self.tileSize*(1+self.tileCountY))
local s = math.min(vs,hs)
scale(s)
else
local center = self.player:graphicCorner()
translate(WIDTH/2-center.x, HEIGHT/2-center.y)
scale(1)
end
end
function Player:drawExplicit(tiny)
if tiny and DevMap then return end
local sx,sy
...
function AttributeSheet:draw()
if DevMap then return end
local m = self.monster
...
Now I’m not certain exactly what Bruce was suggesting, but the basic idea seems to be some pluggable behavior. And as I look at these changes, it seems to me that we’d do better to move them upward in the decision hierarchy, since the decision we’re implementing is a high-level one.
Let’s do one at a time. I’d like to move the attribute sheet drawing up, but it’s called from two places:
function Monster:drawSheet()
if self:manhattanDistanceFromPlayer() <= 5 then
self.attributeSheet:draw()
end
end
function Player:drawExplicit(tiny)
if tiny and DevMap then return end
local sx,sy
local dx = 0
local dy = 30
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self:graphicCenter() + vec2(dx,dy)
translate(center.x,center.y)
if self:isDead() then tint(0) end
sx,sy = self:setForMap(tiny)
self:drawSprite(sx,sy)
popStyle()
popMatrix()
if not tiny then
self.attributeSheet:draw()
end
end
The nest gets more complex:
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,
}
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
That last bit is particularly weird, displaying only the sheets of monsters who aren’t tinted. That’s old code: we used to tint them green when they were dead.
We could certainly put a more fundamental sheet-drawing method in the common superclass of Player and Monster, Entity, and then plug it there. We’d enjoy that particularly, because of folks telling us not to put behavior in superclasses.
I think I want to try something easier. Let’s do this one:
function GameRunner:scaleForLocalMap()
if DevMap then
self:getDungeon():setVisibility()
translate(0,0)
local hs = WIDTH/(self.tileSize*(1+self.tileCountX))
local vs = HEIGHT/(self.tileSize*(1+self.tileCountY))
local s = math.min(vs,hs)
scale(s)
else
local center = self.player:graphicCorner()
translate(WIDTH/2-center.x, HEIGHT/2-center.y)
scale(1)
end
end
This code needed an extract method anyway:
function GameRunner:scaleForLocalMap()
if DevMap then
self:scaleForDevMap()
else
self:scaleForPlayerMap()
end
end
function GameRunner:scaleForDevMap()
self:getDungeon():setVisibility()
translate(0,0)
local hs = WIDTH/(self.tileSize*(1+self.tileCountX))
local vs = HEIGHT/(self.tileSize*(1+self.tileCountY))
local s = math.min(vs,hs)
scale(s)
end
function GameRunner:scaleForPlayerMap()
local center = self.player:graphicCorner()
translate(WIDTH/2-center.x, HEIGHT/2-center.y)
scale(1)
end
Now we could plug one or the other of these into a GameRunner member:
function GameRunner:init()
self.tileSize = 64
self.tileCountX = 85 -- if these change, zoomed-out scale
self.tileCountY = 64 -- may also need to be changed.
self:createNewDungeon()
self.cofloater = Floater(50,25,4)
self.musicPlayer = MonsterPlayer(self)
self:initializeSprites()
self.dungeonLevel = 0
self.requestNewLevel = false
self.playerRoom = 1
self.mapScaler = "scaleForPlayerMap"
Bus:subscribe(self, self.createNewLevel, "createNewLevel")
end
function GameRunner:scaleForLocalMap()
self[self.mapScaler](self)
end
This should work now for the player map, and the dev map will be broken as to scaling. And that’s what happens. Now let’s extend the code for the parameter a bit:
parameter.boolean("DevMap", false, function()
if DevMap then
Runner.mapScaler = "scaleForDevMap"
else
Runner.mapScaler = "scaleForPlayerMap"
end
end)
This is wicked, but it might well work. And in fact it does.
We’ve replaced one of the if statements with a pluggable method. Of course we have a new if statement in the parameter, but now that is easily replaced:
local scaleOptions = {}
scaleOptions[false] = "scaleForPlayerMap"
scaleOptions[true] = "scaleForDevMap"
parameter.boolean("DevMap", false, function()
Runner.mapScaler = scaleOptions[DevMap]
end)
This works. I’ll explain it in a moment, but it’s supper time.
I think I’ll commit this. We can back it out if we need to, but it’s a lot like I had in mind.
Commit: map scaling is pluggable based on DevMap parameter.
After Supper
Right, I don’t like that well enough. Let’s try this another way. How about the EventBus?
...
local modeOptions = {}
modeOptions[false] = "playerMode"
modeOptions[true] = "devMode"
parameter.boolean("DevMode", false, function(mode)
Bus:publish(modeOptions[mode])
end)
...
function GameRunner:init()
self.tileSize = 64
self.tileCountX = 85 -- if these change, zoomed-out scale
self.tileCountY = 64 -- may also need to be changed.
self:createNewDungeon()
self.cofloater = Floater(50,25,4)
self.musicPlayer = MonsterPlayer(self)
self:initializeSprites()
self.dungeonLevel = 0
self.requestNewLevel = false
self.playerRoom = 1
self:setPlayerMode()
Bus:subscribe(self, self.createNewLevel, "createNewLevel")
Bus:subscribe(self, self.setDevMode, "devMode")
Bus:subscribe(self, self.setPlayerMode, "playerMode")
end
function GameRunner:setDevMode()
self.mapScaler = self.scaleForDevMap
end
function GameRunner:setPlayerMode()
self.mapScaler = self.scaleForPlayerMap
end
function GameRunner:scaleForLocalMap()
self:mapScaler()
end
That works just fine, and it keeps knowledge of what’s to be done in the object that knows. Now we can find the other users of DevMap and fix them up. First we’ll change them to refer to DevMode.
function GameRunner:drawTinyMapOnTopOfLargeMap()
if DevMode then return end
pushMatrix()
self:scaleForTinyMap()
Runner:drawMap(true)
popMatrix()
end
function Player:drawExplicit(tiny)
if tiny and DevMap then return end
...
function AttributeSheet:draw()
if DevMap then return end
...
Now we’re fully set up. Let’s improve the others, if improvement it is, the same way.
function AttributeSheet:init(monster, inward)
self.inward = inward or 0
self.monster = monster
self.healthIcon = AdjustedSprite(asset.builtin.Small_World.Heart)
self.speedIcon = AdjustedSprite(asset.builtin.Space_Art.Green_Explosion, vec2(0.4,0.4))
self.strengthIcon = AdjustedSprite(asset.builtin.Small_World.Sword, vec2(0.8,0.8), 45)
self.keyIcon = AdjustedSprite(asset.builtin.Planet_Cute.Key, vec2(30/100,30/100))
self:doDraw()
if Bus then
Bus:subscribe(self, self.doDraw, "playerMode")
Bus:subscribe(self, self.dontDraw, "devMode")
end
end
function AttributeSheet:doDraw()
self.drawSheet = self.doTheDraw
end
function AttributeSheet:dontDraw()
self.drawSheet = self.doNothing
end
function AttributeSheet:draw()
self:drawSheet()
end
function AttributeSheet:doNothing()
end
function AttributeSheet:doTheDraw()
local m = self.monster
if not m:displaySheet() then return end
pushMatrix()
pushStyle()
That’s a lot of darn code to replace one if statement. And it’s far less clear.
This isn’t a good idea for AttributeSheet and it’s not a great idea for the scaling, either.
I’m going to revert this and pull out the change I did before supper. Done. We’re back as of this morning, using DevMap as a flag in a few different spots.
Maybe I’ll have a better idea tomorrow. Or maybe I’ll move on to something useful. Conceivably both but preferably the latter if I can.
And to all a good night!