Spikes. My Darling Crawl. And a simple change generates many reverts.

My colleague over on the Codea forum,”Ubergoober”, objects to the spikes giving you a little jab even when they’re down. As we kicked that topic around, he observed that when he’s playing the game, he’s not paying attention to the status bars showing health and such going up and down. And he noted that when you’re scooting through the dungeon, often the messages in the crawl are lagged behind events.

I’ve observed both those things myself. I usually only look at my attributes occasionally, to see if I’m in trouble. And yes, the crawl lags behind events, sometimes far enough that it’s quite weird.

But I love the crawl. It’s cool, and it was hard to do. And now, it’s tangled up in combat in a way that might make removing it difficult.

“Murder your darlings”, they say1.

In writing, I have not followed that rule. If I happen to craft a sentence or paragraph that I like, I publish that sucker. On the other hand, Chet Hendrickson recommends that if anyone calls you over to their desk to show you some really cool code that they have written, you should grab the keyboard and delete the code. Cool code leads to trouble.

And there’s no doubt that the crawl has caused complexity in our program, and I’m sure it has cost me time compared to a simple scrolling text column off to the side somewhere. There’s all that stuff about embedded commands in the crawl, to make things happen on the screen at the “right time”, or to play sounds when the message comes out, and so on.

So … my gut is telling me to murder the crawl. My heart is telling me that it’s actually a nice effect.

I wonder if there’s a compromise to be had. What if the crawl wasn’t slowly scrolling up as it does now? What if it just went full speed? When a new line appears, scroll everything up, and display the line right now.

But then wouldn’t the lines just hang there? A good thing about the crawl is that stuff scrolls off after a while, leaving the screen clear.

Another thing: the messages that appear in the crawl appear where you are looking, right over the Princess. If we moved them over to the side in a console, people wouldn’t see them. They add to the fun, and they provide useful information.

On the other hand, the additional complexity of deferred damage and such, that are part of Combat … we could do without that.

I think for now we’ll leave things as they are, but keep it on the list of things to think about and possibly experiment with. It’s not serving as well as it might. When we have a better idea, we’ll try it.

Spikes

It really bugs Ubergoober that the spikes do a little damage even when they’re down. He thinks they look harmless and that it’s not fair to have dangerous surprises, that it makes the game less fun.

He is also concerned about the Mimic looking exactly like a Chest. He feels that that’s unfair.

I don’t agree. I do think the game should be playable, that there shouldn’t be deadly unavoidable dangers that just kill you off with no hope. But, I argue, the spikes just take one hit point, and the Mimic will not attack you if you move away from it rapidly.

I am toying with the notion that the Mimic might be a “boss”, a monster that you must defeat before you can move on to the next level. In our game, we could do that very simply, by giving the door key to the Mimic.

Should it be possibly to play the whole game with no combat? My colleague Bryan Beecham has floated that idea with his game, and it is an interesting idea.

I’m not sure. From a programming viewpoint, I think providing for combat is an important subject. It requires a kind of interaction between game entities that is tricky to resolve, so we learn things. But there are games that involve no combat, and some of them are quite good.

Most monsters in the game now are “Calm”. They wander, and will follow you, but will not attack unless you corner them. Some, however, are angry, and they will follow you and attack. There is a way to deal with such a monster without fighting it. Maybe there should be more such ways. A monster-repellent spray perhaps.

A cloying scent of Lavender comes from the vase. The Ghost retches and flees in distress.

See, that’s why I love the crawl, so you can say things like that.

OK Spikes. Let’s change their message:

function Spikes:query()
    return "Deadly Spikes do damage to tender feet when down and even more when up!"
end

Now maybe there is a way to make your feet not be tender.

What if strength greater than some amount protected your feet from spikes? That might be amusing …

But not now. We have work to do. Commit: changed Spike message.

Chest

There’s an issue with the Chest. When it is open, the lid doesn’t show up because the tile above cuts it off.

We resolve a similar problem with the monsters by drawing them after the tiles are fully drawn. How might we do that for the Chest, or for all the dungeon items?

Here’s the relevant draw function:

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    self:drawSprites(tiny)
    if not tiny and self.currentlyVisible then self:drawContents(tiny) end
    popStyle()
    popMatrix()
end

function Tile:drawContents(tiny)
    local center = self:graphicCenter()
    for k,c in pairs(self.contents) do
        c:draw(tiny, center)
    end
end

We draw the contents of a tile immediately after drawing it. If the contents overlap a tile that will be drawn later, well, too bad, the picture will be covered.

Suppose we removed the call to ``drawContents from draw, and then later called drawContents? That would be adequate, but it would loop over all the tiles twice in every draw cycle. That might be slow.

But it would be easy. Let’s try it.

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawButtons()
    self:drawTinyMap()
    self:drawMessages()
    self:drawInventory()
end

function GameRunner:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self:drawMap(false)
    self.player:drawExplicit(false)
    popMatrix()
end

function GameRunner:drawMap(tiny)
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:draw(tiny)
        end
    end
    self.monsters:draw()
    self.player:drawExplicit(tiny)
end

function GameRunner:drawTinyMap()
    pushMatrix()
    self:scaleForTinyMap()
    Runner:drawMap(true)
    popMatrix()
end

Sheesh! We’re already drawing the map twice, once large and one tiny.

Let’s see if we can avoid doing it three times. Ah. We have two ways of scaling. Let’s remove the explicit call to drawTinyMap and do the scaling inside the main drawMap.

My first cut at that fails. I’m not sure why. I’ll spare you the mistaken code. Revert, try again in smaller steps.

The draw logic is a bit convoluted. This is never a sign of clear thinking. I wonder who wrote this.

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawButtons()
    self:drawTinyMap()
    self:drawMessages()
    self:drawInventory()
end

function GameRunner:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self:drawMap(false)
    self.player:drawExplicit(false)
    popMatrix()
end

function GameRunner:drawTinyMap()
    pushMatrix()
    self:scaleForTinyMap()
    Runner:drawMap(true)
    popMatrix()
end

function GameRunner:drawMap(tiny)
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:draw(tiny)
        end
    end
    self.monsters:draw()
    self.player:drawExplicit(tiny)
end

OK, let’s see. What I’d like to do, I think, is have two calls to tile:draw there in the drawMap method, one large, one small.

And while we’re at it, maybe we should move the drawing of the monsters and princess up into the main draw function.

We note that the princess is drawn twice, once large once tiny, and she needs the proper scaling for that. The monsters do not show up on the small map, though as I look at this code I wonder why not.

function Monsters:draw(tiny)
    if not tiny then
        for i,m in ipairs(self.table) do
            m:drawExplicit()
        end
    end
end

It seems to me that they should be being drawn. We don’t pass a parameter. It’ll be nil. That’s false. The monsters should draw themselves on the small map.

And, you know what? They do! They’re just very small. If you look really close you can almost make them out.

OK, that encourages me to think I understand this. Anyway, let’s move the player and monster drawing up into the main draw method:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawButtons()
    self:drawTinyMap()
    self:drawMessages()
    self:drawInventory()
    pushMatrix()
    self:scaleForLocalMap()
    self.monsters:draw(false)
    self.player:drawExplicit(false)
    popMatrix()
    pushMatrix()
    self:scaleForTinyMap()
    self.player:drawExplicit(true)
    popMatrix()
end

Let’s see how this goes. Good so far. Now if we remove the call to drawTinyMap, all should be good except no tiny map.

So far so good. The dot representing the princess is still drawn. And the monsters no longer are. I’m OK with that, did it on purpose.

Now let’s draw the tiles twice. We need to be careful with the scaling here.

function GameRunner:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self:drawMap(false)
    self.player:drawExplicit(false)
    popMatrix()
end

Oh and we should remove that call to draw the player. She’s handled.

function GameRunner:drawLargeMap()
    self:drawMap(false)
end

I foresee an optimization here :)

function GameRunner:drawMap(tinyIgnored)
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            pushMatrix()
            self:scaleForLocalMap()
            tile:draw(false)
            popMatrix()
            pushMatrix()
            self:scaleForTinyMap()
            tile:draw(true)
            popMatrix()
        end
    end
end

Lots of pushing, popping, and scaling. Does it still work?

Not quite. The tiny map isn’t drawing properly. It shows cells near the player, but they don’t stick, as if the illumination isn’t remembered.

No, it’s not that. The scaling and translation are off. I wish I had committed it when it was working a moment ago.

OK. I’m going to guess that I’m missing a pop or push somewhere.

No. I don’t see it. Revert again, do again.

This is telling us one thing for sure: the drawing code is too complicated. It “just growed” and is too intricate.

Lets try to go even more slowly and get some save points.

Move the princess and monster drawing up into draw:

function GameRunner:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self:drawMap(false)
    self.player:drawExplicit(false)
    popMatrix()
end

Remove the draw of player and move it to draw:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self.player:drawExplicit(false)
    popMatrix()
    self:drawButtons()
    self:drawTinyMap()
    self:drawMessages()
    self:drawInventory()
end

I put in the scaling as well. Test. Works so far.

Find the other call to draw the princess.

function GameRunner:drawMap(tiny)
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:draw(tiny)
        end
    end
    self.monsters:draw()
    self.player:drawExplicit(tiny)
end

Move that up as well. Put it after the draw tiny just in case it matters.

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self.player:drawExplicit(false)
    popMatrix()
    self:drawButtons()
    self:drawTinyMap()
    pushMatrix()
    self:scaleForTinyMap()
    self.player:drawExplicit(tiny)
    popMatrix()
    self:drawMessages()
    self:drawInventory()
end

When I make that change, the player dot in the small map is smaller. What’s up with that?

Back that out. Commit the first change. Now what makes the player draw larger? Oh, I think I just didn’t pass the flag. Do again:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self.player:drawExplicit(false)
    popMatrix()
    self:drawButtons()
    self:drawTinyMap()
    pushMatrix()
    self:scaleForTinyMap()
    self.player:drawExplicit(true)
    popMatrix()
    self:drawMessages()
    self:drawInventory()
end

OK, that was it. I didn’t say “true” before. Commit: moving player drawing upward in GameRunner draw nest.

Now about the tiny map. Remove the call to draw it: So far so good. Red dot but no map.

Trace carefully what drawTinyMap would have done, so we can do it inside the drawLargeMap:

function GameRunner:drawTinyMap()
    pushMatrix()
    self:scaleForTinyMap()
    Runner:drawMap(true)
    popMatrix()
end

function GameRunner:drawMap(tiny)
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:draw(tiny)
        end
    end
    self.monsters:draw()
end

No one is saving and restoring that fill and stroke stuff. We really should. But it shouldn’t affect out drawing of the tiles, it’s done all the time.

I still think this should work:

function GameRunner:drawMap(tiny)
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            pushMatrix()
            self:scaleForLocalMap()
            tile:draw(false)
            popMatrix()
            pushMatrix()
            self:scaleForTinyMap()
            tile:draw(true)
            popMatrix()
        end
    end
    self.monsters:draw()
end

But we need to do the scaling only there. If we do it above as well, the scales would multiply and that would be bad. So we remove the other scaling.

function GameRunner:drawLargeMap()
    --pushMatrix()
    --self:scaleForLocalMap()
    self:drawMap(false)
    --popMatrix()
end

I’m not going to call the tiny one but I’ll edit it:

function GameRunner:drawTinyMap()
    error("don't call this")
    --pushMatrix()
    --self:scaleForTinyMap()
    --Runner:drawMap(true)
    --popMatrix()
end

I think this is ready:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self.player:drawExplicit(false)
    popMatrix()
    self:drawButtons()
    --self:drawTinyMap()
    pushMatrix()
    self:scaleForTinyMap()
    self.player:drawExplicit(true)
    popMatrix()
    self:drawMessages()
    self:drawInventory()
end

But I also think it’s the same as I had before. Test.

And it’s wrong again in the same way. It looks like the tiny map is in the wrong location.

It looks as if the map is translated off to the side (at least). Here are where the translations take place:

function GameRunner:scaleForLocalMap()
    local center = self.player:graphicCorner()
    translate(WIDTH/2-center.x, HEIGHT/2-center.y)
    scale(1)
end

function GameRunner:scaleForTinyMap()
    local sc = 0.05
    local dw,dh = Runner:dungeonSize()
    translate(WIDTH-sc*dw, HEIGHT-sc*dh)
    scale(sc)
end

What if maybe they were applied on top of each other? Let’s revert to what worked and look again. What is this, three or four reverts on one trivial feature?

Works fine again. Let’s look even more carefully. I really want to unwind this mess. I can see a way to hack the chest, but it ought to be done right.

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self.player:drawExplicit(false)
    popMatrix()
    self:drawButtons()
    self:drawTinyMap()
    pushMatrix()
    self:scaleForTinyMap()
    self.player:drawExplicit(true)
    popMatrix()
    self:drawMessages()
    self:drawInventory()
end

function GameRunner:drawLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    self:drawMap(false)
    popMatrix()
end

function GameRunner:drawMap(tiny)
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:draw(tiny)
        end
    end
    self.monsters:draw()
end

function GameRunner:drawTinyMap()
    pushMatrix()
    self:scaleForTinyMap()
    Runner:drawMap(true)
    popMatrix()
end

I’m pretty much grasping at straws now. The fill and stroke stuff will be done at the current drawing scale, because the matrix is set outside. We could try moving that inside the double-drawing thing.

OK let’s do.

function GameRunner:drawMap(tiny)
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            pushMatrix()
            self:scaleForLocalMap()
            fill(0)
            stroke(255)
            strokeWidth(1)
            tile:draw(false)
            popMatrix()
            pushMatrix()
            self:scaleForTinyMap()
            fill(0)
            stroke(255)
            strokeWidth(1)
            tile:draw(true)
            popMatrix()
        end
    end
    self.monsters:draw()
end

Now remove the call to draw the tiny map.

And again it doesn’t work.

What is it called when you try the same thing over and over again, expecting different results?

Again I revert. I’m going to try to figure out what sequence of scaling and translation is taking place and see if I can see what I’ve missed out. First let’s try moving the calls for the player down to the end of the main draw:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawButtons()
    self:drawTinyMap()
    self:drawMessages()
    self:drawInventory()
    pushMatrix()
    self:scaleForLocalMap()
    self.player:drawExplicit(false)
    popMatrix()
    pushMatrix()
    self:scaleForTinyMap()
    self.player:drawExplicit(true)
    popMatrix()
end

Extract that last bit:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawButtons()
    self:drawTinyMap()
    self:drawMessages()
    self:drawInventory()
    self:drawPlayerOnBothMaps()
end

Reorder to draw both maps together:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawTinyMap()
    self:drawButtons()
    self:drawMessages()
    self:drawInventory()
    self:drawPlayerOnBothMaps()
end

Still good. Just for fun, reverse the two map calls. I bet there’s a clue here … and I might even know what it is!!

Right! No map appears. The main map–of course–draws on the whole screen. The small map is drawn on top of whatever room might otherwise show up on the small map.

So the weird stuff I’ve been seeing is due to the fact that we must draw the maps separately! Be nice if the code had expressed that idea. Let’s fix that right now:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawTinyMapOnTopOfLargeMap()
    self:drawButtons()
    self:drawMessages()
    self:drawInventory()
    self:drawPlayerOnBothMaps()
end

And the appropriate rename:

function GameRunner:drawTinyMapOnTopOfLargeMap()
    pushMatrix()
    self:scaleForTinyMap()
    Runner:drawMap(true)
    popMatrix()
end

Well. That was interesting. Let’s see if there isn’t some more simplification that we can make, moving the monster drawing upward at least.

function GameRunner:drawMap(tiny)
    fill(0)
    stroke(255)
    strokeWidth(1)
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:draw(tiny)
        end
    end
    self.monsters:draw()
end

I’m going to move the monsters:draw up:

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawTinyMapOnTopOfLargeMap()
    self:drawButtons()
    self:drawMessages()
    self:drawInventory()
    self:drawPlayerOnBothMaps()
    self.monsters:draw()
end

This should still be OK. But it isn’t. The scaling isn’t right. The monsters draw in the wrong place or something.

Why didn’t I commit that code when it was working? I need to develop the habit of more frequent commits, but WorkingCopy’s connection to Codea is slow enough to make that tedious. Anyway undo that.

Do again, get the scaling right.

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawTinyMapOnTopOfLargeMap()
    self:drawButtons()
    self:drawMessages()
    self:drawInventory()
    self:drawPlayerOnBothMaps()
    self:drawMonstersOnSomeMaps()
end

function GameRunner:drawMonstersOnSomeMaps()
    pushMatrix()
    self:scaleForLocalMap()
    self.monsters:draw()
    popMatrix()
    -- don't show them on the small map for now
end

OK, that works as advertised.

Commit: reorganize gameRunner drawing code in prep for drawing contents last.

Let’s reflect. I’ve just wasted a couple of hours for want of understanding the code I wrote.

Unclear Code Causes Delays

The code for drawing in GameRunner is by necessity somewhat complex, as it has to orchestrate drawing everything. (There might be some other way to do that, but that’s where we are at present.)

The code has a legitimate need to draw the map in two passes, in order for the small map to show up on top of the large one. (If z-order actually worked, there’d be another easy way to do that, but it doesn’t work as one might think.)

I definitely don’t want to do a third pass over the tiles, and it might even be worth working to make it all be one pass, but I don’t see a clever way to do it …

Although … suppose there was a way to know which tiles the small map would cover. Maybe we could quickly skip those tiles in a single tile-drawing loop like the one I tried Lo! these many tries this morning. We’ll keep that in mind. We have no reason to believe it’s necessary.

But we can certainly decide to do something better than a third full pass over all 8000+ tiles.

The obvious alternative is to have a table of dungeon items that need to be drawn, and to draw them last. We’re already doing that with the monsters and the player.

If we put all the decor and treasures into a collection, we could draw them all. There is a bit of an issue:

We’ve been trying to get objects in the dungeon not to link back to their tile, to reduce the difficulty of creating tests. Decor actually throw away their tile info:

function Decor:init(tile, item, kind)
    self.kind = kind or Decor:randomKind()
    self.sprite = DecorSprites[self.kind]
    if not self.sprite then
        self.kind = "Skeleton2"
        self.sprite = DecorSprites[self.kind]
    end
    self.item = item
    self.tile = nil -- tile needed for TileArbiter and move interaction
    tile:moveObject(self)
    self.scaleX = ScaleX[math.random(1,2)]
    local dt = {self.doNothing, self.doNothing, self.castLethargy, self.castWeakness}
    self.danger = dt[math.random(1,#dt)]
end

What about other things?

There’s this:

function GameRunner:createLevel(count)
    self.dungeonLevel = self.dungeonLevel + 1
    if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
    TileLock=false
    self:createTiles()
    self:clearLevel()
    self:createRandomRooms(count)
    self:connectRooms()
    self:convertEdgesToWalls()
    self:placePlayerInRoom1()
    self:placeWayDown()
    self:placeSpikes(5)
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:createButtons()
    self.cofloater:runCrawl(self:initialCrawl(self.dungeonLevel))
    self:startTimers()
    self.playerCanMove = true
    TileLock = true
end

Note that we create Chests separately,. Also note that createThings returns the collection of things, in case you want it.

Let’s want it.

...
    self.keys = self:createThings(Key,5)
    self.chests = self:createThings(Chest,5)
...

Now we’ve got them. Now we can draw them.

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawTinyMapOnTopOfLargeMap()
    self:drawButtons()
    self:drawMessages()
    self:drawInventory()
    self:drawPlayerOnBothMaps()
    self:drawMonstersOnSomeMaps()
    self:drawChestsOnLargeMap()
end

function GameRunner:drawChestsOnLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    for i,chest in ipairs(self.chests) do
        chest:draw()
    end
    popMatrix()
end

I think this fixes the problem. Let’s go see.

Not so fast, Ron. Chests are told where to draw:

function Chest:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    sprite(Sprites:sprite(self.pic),center.x,center.y, 96,127)
    popStyle()
    popMatrix()
end

And, because I’m unthreading things from linking to tile, the chests don’t even know what tile they are on. We don’t have the information we need.

Let’s look at createThings, maybe it can help us.

function GameRunner:createThings(aClass, n)
    local things = {}
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(self.playerRoom)
        table.insert(things, aClass(tile,self))
    end
    return things
end

Let’s see, what might we do here? We do have the tile and the instance here in hand. How about we have the chest record the tile center when it’s created? Either that or we save it here and use it in the draw.

Let’s save it. We’ll change what createThings creates, and use it differently:

function GameRunner:createThings(aClass, n)
    local things = {}
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(self.playerRoom)
        local item = {thing=aClass(tile,self), center=tile:graphicCemter() }
        table.insert(things, item)
    end
    return things
end

Now we have to use those collections a bit differently. And we use createThings more than once.

function GameRunner:drawChestsOnLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    for i,item in ipairs(self.chests) do
        item.thing:draw(item.center)
    end
    popMatrix()
end

That’s easy enough. We’ll think about whether it’s awful or not in a moment.

Who else uses createThings’ collection? keys.

function GameRunner:drawKeys()
    for i,k in ipairs(self.keys) do
        k:draw()
    end
end

Better check key:draw …

function Key:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    sprite(asset.builtin.Planet_Cute.Key,center.x,center.y, 50,50)
    popStyle()
    popMatrix()
end

How does that even work? Who’s calling drawKeys? No one. They’re in the attribute sheet. We can remove that method.

It would help if I were to use the conventional spelling of “center”, rather than “cemter”:

function GameRunner:createThings(aClass, n)
    local things = {}
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(self.playerRoom)
        local item = {thing=aClass(tile,self), center=tile:graphicCenter() }
        table.insert(things, aClass(tile,self))
    end
    return things
end

It would also help if I were to store the item after I create it. I’m moving too fast.

function GameRunner:createThings(aClass, n)
    local things = {}
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(self.playerRoom)
        local item = {thing=aClass(tile,self), center=tile:graphicCenter() }
        table.insert(things, item)
    end
    return things
end

Most distressing after all this is that it seems not to work.

I’m wondering what I’m missing. Oh, wrong call to draw:

function GameRunner:drawChestsOnLargeMap()
    pushMatrix()
    self:scaleForLocalMap()
    for i,item in ipairs(self.chests) do
        item.thing:draw(false,item.center)
    end
    popMatrix()
end

The Chest expects the tiny flag. We’ll fix that shortly.

That works, but doesn’t work.

chest open

What’s wrong? It’s not showing the chest’s gift, a health. If I try to step in, I don’t see it, but I receive it.

Why? Because the health is being added to the tile’s contents, and it is being drawn at tile time, and then we overwrite it by drawing at chest time.

Meh. This nearly works but nearly isn’t good enough.

Revert again. I’m not entirely happy with this setup anyway, so another cut will likely be better.

OK, I’m Ticked Off Now

We’re going to do this the direct but inefficient way and see what happens.

We’re going to loop over the tiles three times.

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawTinyMapOnTopOfLargeMap()
    self:drawButtons()
    self:drawMessages()
    self:drawInventory()
    self:drawPlayerOnBothMaps()
    self:drawMonstersOnSomeMaps()
end

We’ll add this:

function GameRunner:drawMapContents()
    pushMatrix()
    self:scaleForLocalMap()
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:drawContents(false)
        end
    end
    popMatrix()
end

function GameRunner:draw()
    font("Optima-BoldItalic")
    self:drawLargeMap()
    self:drawTinyMapOnTopOfLargeMap()
    self:drawMapContents()
    self:drawButtons()
    self:drawMessages()
    self:drawInventory()
    self:drawPlayerOnBothMaps()
    self:drawMonstersOnSomeMaps()
end

We’ll modify Tile:draw:

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    self:drawSprites(tiny)
    if not tiny and self.currentlyVisible then self:drawContents(tiny) end
    popStyle()
    popMatrix()
end

To this:

function Tile:draw(tiny)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    self:drawSprites(tiny)
    popStyle()
    popMatrix()
end

And this:

function Tile:drawContents(tiny)
    local center = self:graphicCenter()
    for k,c in pairs(self.contents) do
        c:draw(tiny, center)
    end
end

To this:

function Tile:drawContents(tiny)
    if not self.currentlyVisible then return end
    local center = self:graphicCenter()
    for k,c in pairs(self.contents) do
        c:draw(tiny, center)
    end
end

And I expect this to work. And it does.

open chest with gift

Commit: Open chests display themselves. All contents now drawn in separate pass over tiles.

I’m sitting here chuckling ruefully. Let’s sum up and see if we’ve learned anything.

Summary

Well, the change that worked took A dozen lines of code. It took minutes to install. And it loops over 5440 tiles an additional time, for a total of 16,000 elements. There’s more pushing and popping and function calls. In principle, we could do this with two passes. We could do it with one pass if z-level worked. What’s the deal with z-level? Well, I think it comes down to a long-standing problem in OpenGL, but i’m not sure. I just know that I’ve learned not to use it.

I think it has to do with transparency, so possibly it would work for this program. I suppose we could experiment with that.

So I must have reverted sebenty-leben times this morning. I don’t feel badly about it, but I certainly took more time than it was worth to get this tiny feature working. The problem was due to an interesting fact, which was that what I was trying to do couldn’t possibly work, and I didn’t realize it.

Instead of making the feature work, as we did here at the end, I decided to improve the existing code to do everything in one pass, and that simply cannot work as the code stands today.

It could work, if we were to mask the tiny map’s screen area. And a quick experiment makes me think that it could work with zLevel. I’d want to try more tests before I relied on that but ti may offer a path to simpler and faster code if we need it.

What threw me was that until I remembered that the tiny map had to be drawn last, I concluded that I was missing something in the way I changed the code around, so I tried it so many times. I suspect that in some of those tries, I had to as close to right as I could have asked. It just couldn’t work no matter what.

I’m not drawing any deep lessons here. I did improve the code to mention the “on top of” issue, and possibly if I had done that before, I’d have known my chosen path wasn’t going to work. We do our best to express our ideas in the code. That idea was not expressed, and now it is.

I think we have a sort of “technical debt” issue here. The Tiles are a central idea for my dungeon, and everything tries to revolve around them. As I’ve elaborated the game, things have moved into Tile contents, more capability has been added to Tile, we’ve begun to move some of the global aspects to Dungeon, but some are left in GameRunner.

As I’ve commented before, the divisions are not clear among GameRunner, Tile, Dungeon, and the various denizens of the dungeon, who all reside on a tile.

We’ve been moving to improve things, separating out concerns, but we’re not there yet. There’s a bit too much mud in this ball of mud.

But it’s getting better, and more capable. Can’t complain about that.

As for today … it was fun, if not as productive as it might have been.

See you next time!


D2.zip

  1. Earliest known example; Arthur Quiller-Crouch.