Dungeon 52
Character sheet for the player today, then a daunting bug, er, defect.
The monsters now have varying strength and health. Player’s health goes up and down, while her strength, so far, does not change. Her health is displayed by an integer floating over her head: not as immersive as it might be.
Let’s give the player a character sheet like the monster ones, displayed down beside the arrow buttons. I do have a general concern with the sheets: they don’t have room for very high numbers, only about 5 or 6. So I’m thinking about how we might change them to display a different kind of value, a power bar or something. We’ll save that for after we get the player set up with a sheet.
Bruce Onder listed a few additional attributes that we might want to provide for, including inventory items like potions and experience points. The character sheet will have to be modified every time we add something that needs displaying, unless we generalize it somehow. Naturally, being who we are, we’ll do that generalization only in the presence of a real need for it.
Let’s look at the MonsterSheet. It’s a bit long, like most report generators, but really quite simple:
-- MonsterSheet
-- RJ 20201228
MonsterSheet = class()
function MonsterSheet:init(monster)
self.monster = monster
end
function MonsterSheet:draw()
pushMatrix()
pushStyle()
resetMatrix()
rectMode(CORNER)
textMode(CORNER)
textAlign(LEFT)
self:drawParchment()
fill(0)
self:drawText(self.monster:name())
self:newLine(2)
self:drawText("Strength")
self:drawIcons(self.monster:strength(), asset.builtin.Small_World.Sword)
self:newLine()
self:drawText("Health")
self:drawIcons(self.monster:health(), asset.builtin.Small_World.Heart)
self:drawPhoto(self.monster:photo())
popStyle()
popMatrix()
end
function MonsterSheet:drawIcons(count, icon)
pushStyle()
tint(255)
spriteMode(CENTER)
for i = 1, count do
sprite(icon, 50+i*30, 10)
end
popStyle()
end
function MonsterSheet:drawParchment()
local sheetW = 250
local sheetH = 200
local margin = 20
local corner = vec2(WIDTH-sheetW-margin, margin)
fill(136, 129, 107)
stroke(0,0,0,0)
rect(corner.x, corner.y, sheetW, sheetH)
noFill()
stroke(0)
strokeWidth(2)
rect(corner.x + 5, corner.y + 5, sheetW-10, sheetH-10)
translate(corner.x + 10, corner.y + sheetH-30)
end
function MonsterSheet:drawPhoto(aSprite)
self:newLine(4)
tint(255)
sprite(aSprite, 80, 0)
end
function MonsterSheet:drawText(aString)
text(aString)
end
function MonsterSheet:newLine(count)
for i = 1,count or 1 do
translate(0,-fontSize()-5)
end
end
The fanciest bit is that the sheet is coded to allow sequential drawing, which is done by translating the drawing origin downward a bit whenever the newLine
method is called.
I don’t see much reason why the same sheet couldn’t display the player. We might have to move it, and we might want to color it differently but if Monsters and Players respond to the same protocol to fetch the values, it should “just work”.
Let’s “just try it”.
function Player:init(tile, runner)
self.alive = true
self.tile = tile
self.tile:illuminate()
self.tile:addContents(self)
self.runner = runner
self.keys = 0
self.healthPoints = 5
self.strengthPoints = 2
self.playerSheet = MonsterSheet(self)
end
function Player:draw(tiny)
local dx = -2
local dy = -3
pushMatrix()
pushStyle()
spriteMode(CORNER)
local center = self:graphicCorner()
if not self.alive then tint(0) end
if tiny then
tint(255,0,0)
sx,sy = 180,272
else
sx,sy = 80,136
end
sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,center.x+dx,center.y+dy, sx,sy)
fontSize(fontSize()*2)
text(self.healthPoints, center.x+dx, center.y+dy+ 100)
popStyle()
popMatrix()
if not tiny then self.playerSheet:draw() end
end
I think this will barf looking for name and photo or something but we’ll just let the failures guide our additions.
MonsterSheet:19: attempt to call a nil value (method 'name')
stack traceback:
MonsterSheet:19: in method 'draw'
Player:70: in method 'draw'
Tile:107: in method 'drawContents'
Tile:100: in method 'draw'
GameRunner:130: in method 'drawMap'
GameRunner:120: in method 'drawLargeMap'
GameRunner:112: in method 'draw'
Main:22: in function 'draw'
Player needs a name. Presumably in a real game, the human player could choose the player name and picture. For now, responding to all the tracebacks:
function Player:name()
return "Princess"
end
function Player:health()
return self.healthPoints
end
function Player:photo()
return asset.builtin.Planet_Cute.Character_Princess_Girl
end
That gives us this display:
Nearly good. Her picture is too large and not well situated, and there seems to be a duplicate trying to be drawn down below the parchment. Let’s deal with the size issue first. It turns out that you can get the size of a sprite by calling spriteSize
. The monster sprites are of various sizes, but all around 50 wide. The princess is twice that wide. Let’s try some simple scaling as we draw the photo:
function MonsterSheet:drawPhoto(aSprite)
self:newLine(4)
tint(255)
sprite(aSprite, 80, 0, 50)
end
The added parameter, 50, sets the width of the sprite, and its height will be scaled accordingly. That looks good:
We still have that second princess down at the bottom of the screen. I thought the check on tiny
would ensure we only drew the sheet once, but this may not be the sheet at all. It might be something else.
After some searching, this turns out to be a rather strange defect. The princess is always drawn twice, once when tile contents are drawn, and once with a special call in GameRunner:draw
:
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.player:draw(tiny)
end
Something in the drawing of the player sheet leaves things such that the duplicate player is drawn down below the one in the sheet. I’m not going to work out the details. It does appear that the MonsterSheet may be missing some pushes and pops. I will check that.
If we allow the tile drawing to draw the player, and do not do the special call shown above, she gets truncated, because she’s currently larger than a tile:
I think we should scale her into a tile and let the regular drawing do the job.
function Player:draw(tiny)
local dx = -2
local dy = -3
pushMatrix()
pushStyle()
spriteMode(CORNER)
local center = self:graphicCorner()
if not self.alive then tint(0) end
if tiny then
tint(255,0,0)
sx,sy = 180,272
else
sx,sy = 80,136
end
sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,center.x+dx,center.y+dy, sx,sy)
fontSize(fontSize()*2)
text(self.healthPoints, center.x+dx, center.y+dy+ 100)
popStyle()
popMatrix()
if not tiny then
self.playerSheet:draw()
end
end
We need to scale her down to a reasonable size. Her full size is 101x171. We need her to fit into a 64x64 tile. It turns out that her sprite size is 101x171 all right, but her actual size is smaller. There’s blank space all around her. After some fiddling, I get her fitting inside a tile and her sheet appearing over by the arrows:
So that’s nearly good. However, if she’s showing up at all on the small map, I can’t see her. That turns out to be because the tiny
flag is not propagated down to contents drawing. So …
function Tile:draw(tiny)
pushMatrix()
pushStyle()
spriteMode(CORNER)
self:updateContents()
tint(self:getTint(tiny))
self:drawSprites()
self:drawContents(tiny)
popStyle()
popMatrix()
end
function Tile:drawContents(tiny)
for k,c in pairs(self.contents) do
c:draw(tiny)
end
end
And now, since the player sizes herself larger in tiny mode, she should show up. And she does:
However, a close examination shows that some other items also show up on the tiny map. Chests and keys and healths, for example. Monsters already know not to draw in tiny mode. I guess I’ll just condition the others not to draw but that’s leading to duplication all over. We’ll worry about that after things work as intended.
function Key:draw(tiny)
if tiny then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Key,g.x,g.y, 50,50)
popStyle()
popMatrix()
end
function Chest:draw(tiny)
if tiny then return end
pushMatrix()
pushStyle()
spriteMode(CORNER)
local g = self.tile:graphicCorner()
sprite(self.pic,g.x + 7,g.y,50,85)
popStyle()
popMatrix()
end
function Health:draw(tiny)
if tiny then return end
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y+10, 35,60)
popStyle()
end
That’s messy but correct. We do have a remaining issue that we spoke about near the top of this article:
One wants one’s princesses to be robust, but her character sheet overfloweth. She has huge tracts of health. Let’s commit what we have and then see about changing the sheet somehow.
Commit: princess has character sheet. adjusted drawing logic.
Displaying Wider Ranges
We clearly need attributes like heath and strength to be able to get up to at least 1d20, which is surely way too much to display in a bar of icons.
Perhaps a simple but still attractive sheet would show the icon once, where the text is now, and then a number representing the current value. Let’s try that:
Arrgh. Somewhere along the way, the monster sheets have stopped turning up. Player’s sheet still displays. I’ve reverted, and it’s still happening. I’ve tweaked something too hard.
A quick check tells me that the sheet is being drawn for the monsters as well as the princess. Maybe it has somehow moved to the wrong location? An adjustment to my print tells me that it’s being drawn at the right location:
Serpent (1096.000000, 20.000000)
The only thing I can think of is that possibly tiles outside the range of visibility, which are tinted black, are being drawn on top of the sheets. But why would that have suddenly started happening?
I’ll turn off that tinting and see what I get. I still don’t see the sheet.
This is truly bizarre. I turned off the player’s sheet, just to be sure that the monster ones weren’t underneath. They still don’t display.
I changed the monster sheet to draw where the player one does … and in that position it does display. What is even going on? That single change makes it appear.
Oh this is weird. Let me see if I can get a movie of this:
Somehow the drawing of the creature is erasing everything to its right, in the monster sheet. It’s as if the sheet is being clipped by the monster’s x coordinate.
I wouldn’t know how to do that if I wanted to! OK, nothing for it but to revert to last commit, then check out the previous one to see what we did wrong, and/or when all this happened.
OK, that tells me that the error is in that last large commit. And it does have that cryptic comment “adjusted drawing logic”. I’ll go through the diff, see if I can see anything obvious.
By the way, I hate this. I’ve broken something and the behavior I see I couldn’t implement if I tried. Really weird.
I’ve found that there is a call, clip
that can set the clipping rectangle. With that, I could replicate this behavior. However, there are no calls to clip
in this program.
I took a wild leap, and commented out the check for tiny here:
function Monster:draw(tiny)
--if tiny then return end
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if self.alive then
sprite(self.moving[self.movingIndex], 0,0)
else
tint(0,128,0,175)
sprite(self.dead)
end
translate(-center.x,-center.y)
popStyle()
popMatrix()
self:drawSheet()
end
And that tells me, after a brief period of hitting my head with the heel of my hand, what is going on.
The big map is drawn translated in some weird way to get the player centered on screen. That translation is set at the very top of the draw. When we draw in full scale mode. the monster sheet is off the screen. When we draw in tiny mode, we are at scale 1, and it is on the screen.
Therefore we need to clear the matrix before we draw the sheets. I have no explanation offhand for the weird scrolling aspect.
Wow. Weird.
Also, no, I’m still wrong: we have a resetMatrix in the sheet drawing:
function MonsterSheet:draw()
pushMatrix()
pushStyle()
resetMatrix()
rectMode(CORNER)
textMode(CORNER)
textAlign(LEFT)
self:drawParchment()
fill(0)
self:drawText(self.monster:name())
self:newLine(2)
self:drawText("Strength")
--sprite(asset.builtin.Small_World.Sword)
self:drawIcons(self.monster:strength(), asset.builtin.Small_World.Sword)
self:newLine()
self:drawText("Health")
self:drawIcons(self.monster:health(), asset.builtin.Small_World.Heart)
self:drawPhoto(self.monster:photo())
popStyle()
popMatrix()
end
Now I am quite confused. I’ve checked the matrix before drawing, after the reset, and it is the unit matrix, so the drawing should be where I think it is.
Ah. At last I understand. We draw the tile with the monster in it, then we draw the monster, then we draw the sheet. Then we continue to draw the other tiles … to the right of the monster. Those tiles are wiping out the sheet drawing.
If z level works, we can use that. But z level is iffy. Let’s see. It does seem to work. Let me revert and just put that in.
And we’re good. Amazing. I must think about this a bit. And there is a related issue, which is that the monster sheet is drawn for the western-most monster in range. It’s drawn for all of the monsters in range, of course, but the western one is the last one drawn and so it’s the one you see.
Arguably, we should draw the dungeon tiles first, and only then draw the contents. Were we to do that, the level problem would not be there. However, the western-most monster getting all the publicity still would be.
For now, commit: monster sheets are drawn at level 1.
Thinking
I need to settle down a bit and think about what has just happened, and what, if anything, to do about it. The zLevel
is supposed to work, and in this instance it does. Arguably, it’s a justified use of a documented feature, to be sure that certain things get drawn on top. That’s a very common thing to want to do.
However, I’ve rarely needed to use the feature in Codea, and when I used it in Invaders, I turned up an issue about transparency. If you draw something at a high zLevel, when something is later drawn “behind” it, any transparency in the forward item is lost: it behaves as if it were opaque. In Invaders, that gave asteroids a square black background when I used zLevel to try to put them on top of each other.
Here, it will mean that we won’t be able to use transparency in the monster sheets to allow for seeing through them if the player view goes down that far on the screen. We’ll have to figure out something else for that situation. One possibility would be to reserve one side of the screen for displayed information like player status, monster status, control buttons, and so on. If we want to do anything with text messages or the like, we could do that.
I think I could readily reserve the left part of the screen.
function GameRunner:scaleForLocalMap()
local center = self.player:graphicCorner()
local LOWX,LOWY = self:maxScrollValues()
translate(clamp(LOWX, WIDTH/2-center.x, 0), clamp(LOWY, HEIGHT/2-center.y, 0))
scale(1)
end
If we clamp this to a different x value than zero, it should scroll the whole picture to the right:
Setting the background to black would make that look almost sensible. So that’s a possibility if the status info gets to be too much trouble. Of course, on a phone screen, we’d be out of luck already. I don’t think I’m remotely interested in making this work on a phone screen.
More generally, though, what made this little problem so hard to spot? Originally, the monster sheet was being drawn twice, once in large mode and once again in tiny mode. The tiny mode version, drawn last, was overwriting the tiles that had overwritten the large mode sheet. So it looked fine until I took the reasonable step of not drawing tile contents in the tiny map, except for the princess.
With that change in place, the monster was never simultaneously close enough to the player to be drawn, and far enough away to show any of the monster sheet. So the sheet just disappeared. It was only after I started moving it left that I discovered the strange overwriting. Even then, I didn’t recognize it as overwriting.
Obvious in retrospect. This is a common characteristic of mysterious defects. They are almost always obvious in retrospect.
Is there a design issue here that should be addressed? I argue that in principle there is not. It makes perfectly good sense to go through the entire matrix, drawing each tile, and drawing its contents at that time. It’s a simple one-pass algorithm. Nothing can go wrong.
Unless some tile tries to draw outside its legitimate boundaries, in which case it has a good chance of having that drawing overwritten. I had actually seen that today, when the overly-robust princess was getting clipped. I spotted that, fixed it, moved on … and when the other more serious clipping problem arose, I didn’t see it.
I don’t see a handle for this one. I don’t see a design change that should have been there all along, nor do I feel badly about not using zLevel right along, since it hasn’t been needed often in the past, and when it was, it did’t serve my purpose very well. So it’s deep enough in the bag of tricks that it might reasonably not come up.
The only advice I can give myself is “be smarter”, and that’s not a very good form of advice at all. So, chalk the frustration up to one of those things, unless one of my dear readers has a suggestion for a sensible thing to have done or tried or thought that would have helped.
This has gone on long enough, before and after our lovely New Year’s brunch. Happy New Year to you, and we’ll see you next time!