I guess we’ll work on the Learning Level, but honestly I’m getting a bit tired of this project. Must think about that.

Historically, and at my age I can say that judiciously, I get bored with projects as they get closer to “done”. There are probably at least two reasons for that, one good and one not so good.

The not so good reason is that toward the end of a project, the work tends to get tedious, and I’m not good at pushing through tedious work.

The not so bad reason is that toward the end of a project, the learning seems to get more sparse, and I’m most excited when I’m learning something.

Anyway, it’s possible that we’ve squeezed most of the juice out of this program and should move on to something else. And yet, there are certainly interesting problems to solve, including the learning level, and other designed dungeons. We still could use some set-pieces, puzzles, and so on.

So, no, there’s juice in here. Maybe it’s just Monday, although these days it’s hard to tell Monday from any other day.

Meh. Let’s get to it.

Making a Designed Level

We aren’t going to divert to build a dungeon-building app: that would delay features too long, and our management wants to see the product improving, not to hear about how easy it is for the design team.

One thing that we do need, however, is a better way to find out if the dungeon is what we intended than to walk through it. (I do have a trick to make the game display the full map right at the beginning. Let’s bring that up to an actual feature, right now.)

When the small map is being drawn, each Tile gets sent this message:

function Tile:drawMapCell(center)
    if self.kind ~= TileRoom or not self.seen then return end
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    translate(center.x, center.y)
    fill(255)
    rect(0,0,TileSize,TileSize)
    popStyle()
    popMatrix()
end

We only display the room-type tiles, the floor bits, because displaying the walls didn’t look good on the small map. When I want to display the map right at the beginning, I edit that first line:

    if self.kind ~= TileRoom --[[or not self.seen--]] then return end

Codea has those parameter things, which add button and display objects to the console area. Let’s use one of those:

function setup()
    if CodeaUnit then 
        runCodeaUnitTests() 
        CodeaTestsVisible = true
    end
    parameter.boolean("ShowMap",false)
...

Now we’ll change that method to use the new flag ShowMap. This is trickier than it looks, or my brain isn’t awake yet. I reversed the sense of the if and got it right after only two tries:

function Tile:drawMapCell(center)
    if self.kind == TileRoom and (ShowMap or self.seen) then
        pushMatrix()
        pushStyle()
        rectMode(CENTER)
        translate(center.x, center.y)
        fill(255)
        rect(0,0,TileSize,TileSize)
        popStyle()
        popMatrix()
    end
end

Here’s a little movie of the game displaying the small map, the whole map, then creating the learning level and displaying again:

maps display

Now as soon as I see the learning level compared to that other level, I notice that it is much smaller. That may be OK: there’s no good reason to make the learning level involve a lot of walking about. The individual rooms 12x8, seem of a reasonable size.

I’m thinking that we may want another display of the dungeon for developers, where the whole area is displayed, essentially full screen. We’d probably like that view to display all the dungeon items. Let’s think about that.

The drawing all pretty much starts here:

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

Let’s commit the map flag and then try a spike for the developer map: Switch to show all tiles in small map.

I think I’ll just add another boolean parm, DevMap:

    parameter.boolean("ShowMap",false)
    parameter.boolean("DevMap", false)
...

I’m not sure where all to patch this in. We could create a whole new developer drawing method, or we could probably just mess about with a few things and let it happen.

The main issues will be, first, scaling to show the whole map, and, second, marking all the tiles as visible. Let’s see what drawLargeMap does now:

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

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

Hm. If we fiddle that scaleForLocalMap method, we should be nearly there. I try this:

function GameRunner:scaleForLocalMap()
    if DevMap then
        self:getDungeon():setVisibility()
        translate(WIDTH/2, HEIGHT/2)
        scale(0.25)
    else
        local center = self.player:graphicCorner()
        translate(WIDTH/2-center.x, HEIGHT/2-center.y)
        scale(1)
    end
end

function Dungeon:setVisibility()
    for i,row in ipairs(self.tiles) do
        for j,tile in ipairs(row) do
            tile:setVisible()
        end
    end
end

I don’t expect the scale to be right, but I expect it to be small enough to show something.

first dev map

Not bad. The translate wasn’t needed.

dev map

Wow! That’s super. Was 0.25 the right scale? It sure seems so. Let’s see, 64*64 is 4096, and the screen is 1024 high in Codea coordinates. How about that? We could scale a bit more down, to allow for the fact that the dungeon space is actually 65 high by 86 wide. Yes, let’s do.

I did this:

function GameRunner:scaleForLocalMap()
    if DevMap then
        self:getDungeon():setVisibility()
        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

The value of s is about 0.246, not surprising.

This seems to mostly work … but sometimes in playing with it, I’ve seen dungeon objects out in the open area, or on a wall. There may be something else needing patching here. And, for sure, I want not to display the attribute sheets and the tiny map. It’s tempting to play on this map, but the idea is to see what’s going on, not to play the game.

I am, however, tempted to make the switch into a scale slider.

Let’s complete this experiment and commit to it.

function AttributeSheet:draw()
    if DevMap then return end
    local m = self.monster
    if not m:displaySheet() then return end
    pushMatrix()
...

bugged

There! There’s an example of the dungeon contents being all over the place. What’s up with that?

It’s very strange: it happens randomly. In this case, it happened three times in a row and now it isn’t happening. Very strange. One more thing, the tiny map:

function GameRunner:drawTinyMapOnTopOfLargeMap()
    if DevMap then return end
    pushMatrix()
    self:scaleForTinyMap()
    Runner:drawMap(true)
    popMatrix()
end

no tiny map

OK. I’m not sure what that misplacement thing is. At a random guess, I change the scaling function to include a translate to 0,0:

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

Again I see items out of place in the dev map for a couple of runs, and then they’re OK.

I run again, same thing. Out of place twice, then OK.

Very strange. It doesn’t affect the actual game, so I’m going to let it be. Commit: DevMap switch shows complete dungeon map.

I think we’ll wrap up here, because we have a couple of lessons to talk about.

Summary

First the good news: with a few very small changes, we’ve improved a dungeon developer’s ability to see what has been wrought. We can even see the play, monsters, and dungeon contents in place. That’s quite a nice little step in our “Making App”, and at very low cost.

The not quite so good news is those items displaying off in space. It’s particularly disconcerting because it doesn’t always happen.

Break

A short break has ensued, while I took my turn at the hot water. With my hair freshly washed, I look even more like the Utopian than usual. One of these first years, I should get a haircut.

The value of a break is that often ideas will come to us. The effect we’re seeing, when we see it, is that some of the dungeon objects aren’t in the right place on the DevMap view. But they do seem to be at the right scale. What could cause that effect?

If someone were doing a translate call outside a pushMatrix/popMatrix bracket, that would shift everything that drew after that point. So it’s worth a quick search for translate:

Here’s one:

function Lever:draw(tiny, center)
    pushStyle()
    spriteMode(CENTER)
    translate(center.x,center.y)
    LeverSprites[self.position]:draw()
    popStyle()
end

Let’s fix that. Can’t hurt, might help.

function Lever:draw(tiny, center)
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    translate(center.x,center.y)
    LeverSprites[self.position]:draw()
    popStyle()
    popMatrix()
end

There’s some tricky translation stuff going on in the AttributeSheet, as we draw the text and graphs, but it’s all enclosed here:

function AttributeSheet:draw()
    if DevMap then return end
    local m = self.monster
    if not m:displaySheet() then return end
    pushMatrix()
    pushStyle()
    resetMatrix()
    zLevel(10-m:manhattanDistanceFromPlayer())
    rectMode(CORNER)
    textMode(CORNER)
    textAlign(LEFT)
    self:drawParchment()
    self:drawPhoto(m:photo())
    fill(0)
    self:drawText(m:name())
    self:newLine(2)
    self:drawText("Health")
    self:drawAttribute(self.healthIcon, m:healthAttribute())
    self:newLine()
    self:drawText("Speed")
    self:drawAttribute(self.speedIcon, m:speedAttribute())
    self:newLine()
    self:drawText("Strength")
    self:drawAttribute(self.strengthIcon, m:strengthAttribute())
    self:drawKeys()
    popStyle()
    popMatrix()
end

I’m a bit concerned by the resetMatrix but it’s enclosed in the push-pop, and anyway we aren’t displaying attributes in the DevMap mode. The only one I found is the Lever.

At first I thought that had fixed it, but now I’m seeing the Princess’s red dot outside the dungeon. Oh! That’s not a problem, it’s the small map dot. We just didn’t turn that off.

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

I think maybe we’ve got it. And in any case, the changes are righteous. Commit: Level properly saves matrix. Don’t display tiny princess on DevMap.

Now where were we? Oh, yes …

We have a nice little feature here, the ability to display the entire game map, with all its contents, and it has only taken a few lines of code. Perhaps unfortunately, those lines are if statements, some of them a bit complicated, and they occur in four different places. We have duplication, references to DevMap, and even in this small program we could have missed one, or may miss one if we change how this feature works.

At this moment, I don’t see a good way to improve this. And honestly it’s the sort of thing that makes up “legacy code”, code that is too weird to change without fear. At the moment, though, I don’t have a better idea. Maybe in a day or so.

However you cut it, we’ve made a fairly simple change to the system to make it easier to design and test dungeons. Here’s a photo of the learning level … and it teaches us something:

learning

The buttons are on top of the dungeon, because it’s down at the lower left. But we do want the buttons, I’m sure the level designers will want to play in this mode. Let’s do this: let’s move the dungeon.

function GameRunner:createLearningRooms()
    local w = 12
    local h =  8
    local t = {
        {2,2, w,h},
        {15,2, w,h},
        {28,2, w,h},
        {41,2, w,h},
        {2,11, w,h},
        {15,11, w,h},
        {28,11, w,h},
        {41,11, w,h},
        {2,20, w,h},
        {15,20, w,h},
        {28,20, w,h},
        {41,20, w,h},
    }
    for i,r in ipairs(t) do
        r[1] = r[1]+24
        r[2] = r[2]+24
    end
    self.rooms = self:makeRoomsFromXYWH(t)
end

This will move everything 24 tiles up and to the right. The result:

learn offset

Nice. Bit of a hack, but helpful. Commit: move learning level out from under buttons.

OK, we’re good. A decent feature, and I’m about 85% certain that that change to Lever has fixed the offset dungeon objects. We’ll see going forward.


D2.zip