Dungeon 48
Levels, treasures, or character sheets, that is the question. The answer is only a bit interesting. One good bit tho.
Now that there are a handful of randomly-generated monsters, and now that it’s easy to add in more until I run out of pictures of monsters, it seems to me that there are only about three big missing bits before this is a lot like a real game:
- Treasures to collect
- Our intrepid princess needs an incentive to explore, risking life, limb, and the kingdom for some purpose. Of course, saving the kingdom from monsters is her main purpose, but it’d be nice to collect some gold or other goodies along the way.
- Levels to explore
- Any dungeon worth the name has levels of increasing depth and difficulty, each more dank, more dark, more packed with evil and treasure than the last. We need to provide for that.
- Score-keeping display
- I figure the score-keeping display would be some kind of character sheet, listing attributes and inventory. Maybe there’d need to be more, but my guess is that a character sheet will do the job.
There are also some smaller matters that will need addressing. I try to jot them down when I think of them, but in all the excitement of the holidays, I lost the sticky note I was using to jot them down. One that I recall is that I need to ensure that chests never block hallways, because the princess cannot cross over a chest, at least not as things now stand. We could change that, which is another possibility.
Another idea is to ensure that monsters don’t start out too close to the player, to give her a fair chance to avoid a fight or fight back.
Yet another is never to put anything down to close to any other item. It would be easy to ensure “not directly on top”, but keeping things separated further could get interesting.
Anyway, I’ll start a new list. For today, let’s start on character sheets.
Character Sheets
A “character sheet” is a small table of information about a character, listing whatever their important attributes are in the game: strength, health, wealth, special items that they hold, and so on.
This is a simple game, and I don’t intend to do anything very exotic here, but it seems that both monsters and the player do need some attributes and we need to display what they are.
The Codea Simple Dungeon example displays information like this:
We see here both the player’s status and that of the attacking “Pink Pudding”. I see no reason not to copy those ideas pretty directly. In fact, in the spirit of re-use, let’s examine the code that displays that information.
In Simple Dungeon, the function that displays a monster’s attributes looks like this:
function Monster:drawCard()
if WIDTH < 700 then return end
pushStyle()
dist = math.sqrt((self.x - cx) * (self.x- cx) + (self.y - cy) * (self.y - cy))
if dist > 3 then return end
if self.health > 0 then
monsterClose = true
end
fill(195, 192, 192, 255)
rect(WIDTH - 220, 20, 200, 200)
noFill()
strokeWidth(2)
stroke(0, 0, 0, 255)
rect(WIDTH - 210, 30, 180, 180)
fill(32, 31, 114, 255)
textMode(CENTER)
text(self.name, WIDTH - 120, 190)
textMode(CORNER)
if self.health > 0 then
tint(self.tint)
sprite(self.image1, WIDTH - 120, 145, 64, 64)
else
tint(127, 127, 127, 255)
sprite(self.image2, WIDTH - 160, 145, 64, 64)
end
noTint()
fill(0, 0, 0, 255)
text("Health", WIDTH - 190, 40)
text("Strength", WIDTH - 190, 70)
for i = 1, self.health do
sprite("Planet Cute:Heart",WIDTH - 120 + i * 20,50,22,28)
end
for i = 1, strength do
sprite("Small World:Sword",WIDTH - 120 + i * 22,85,12,32)
end
end
This is rather messier than I’d prefer, but it’s pretty straightforward, displaying a filled rectangle, a rectangle border line, then the text for the attributes of health and strength, and little swords and hearts showing the status. The “monsterClose” flag changes the music to a more ominous tune. Make a note: music.
The code at the top only displays the card if the monster is within 3 cells of the player.
I don’t think I’ll import that draw function, but I will replicate something like it. Our Monster:draw
looks like this:
function Monster:draw()
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if not self.alive then tint(0,100,0) end
sprite(self.moving[self.movingIndex], 0,0)
translate(-center.x,-center.y)
popStyle()
popMatrix()
end
Let’s add a drawSheet
function call to that:
function Monster:draw()
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self.tile:graphicCenter()
translate(center.x,center.y)
self:flipTowardPlayer()
if not self.alive then tint(0,100,0) end
sprite(self.moving[self.movingIndex], 0,0)
translate(-center.x,-center.y)
popStyle()
popMatrix()
self:drawSheet()
end
And then draw the simplest possible character sheet. Let’s right-justify it.
function Monster:drawSheet()
if self:distanceFromPlayer() > 10 then return end
pushMatrix()
pushStyle()
local sheetW = 200
local sheetH = 200
local margin = 20
local corner = vec2(WIDTH-sheetW-margin, margin)
rectMode(CORNER)
fill(255)
stroke(0,0,0,0)
rect(corner.x, corner.y, sheetW, sheetH)
popStyle()
popMatrix()
end
At first I thought this wasn’t working at all. Then I noticed that tiny bright square up by the small map:
That’s my rectangle, being drawn in the zoomed-out mode. Why isn’t it being drawn in the … oh …
In the zoomed-in mode, we are drawing a small segment of a very large map. We are translated and scaled according to princess location:
function draw()
pushMatrix()
if CodeaUnit then showCodeaUnitTests() end
if DisplayToggle then
local center = Runner.player:graphicCorner()
focus(center, 1)
else
scale(0.25)
end
Runner:draw(false)
popMatrix()
Runner:drawButtons()
drawTinyMap()
end
This decision is still made up in the top-level drawing function, including the focus:
function focus(center, zoom)
local LOWX,LOWY = maxScrollValues()
translate(clamp(LOWX, WIDTH/2-center.x, 0), clamp(LOWY, HEIGHT/2-center.y, 0))
end
I think we can safely clear the transform here, since we’re pushing and popping. I’ve not tried that before but I rather expect it to work:
function Monster:drawSheet()
if self:distanceFromPlayer() > 10 then return end
pushMatrix()
pushStyle()
resetMatrix() -- <---
local sheetW = 200
local sheetH = 200
local margin = 20
local corner = vec2(WIDTH-sheetW-margin, margin)
rectMode(CORNER)
fill(255)
stroke(0,0,0,0)
rect(corner.x, corner.y, sheetW, sheetH)
popStyle()
popMatrix()
end
That does the job, giving me a big white square where I expect it:
Now to improve it.
I went for a bit of a parchment color there. Maybe we’ll get more fancy later. Now our monsters need to have some information to display. They need a name, and let’s steal Simple Dungeon’s idea of giving them strength and health.
I can just extend my table with more named variables.
function Monster:initMonsterTable()
local m
MT = {}
m = {dead=asset.slime_squashed, hit=asset.slime_hit, moving={asset.slime, asset.slime_walk, asset.slime_squashed}, name = "Pink Slime"}
table.insert(MT,m)
m = {dead=asset.fly_dead, hit=asset.fly_hit, moving={asset.fly, asset.fly_fly}, name="Death Fly"}
table.insert(MT,m)
m = {dead=asset.barnacle_dead, hit=asset.barnacle_hit, moving={asset.barnacle, asset.barnacle_bite}, name="Toothhead"}
table.insert(MT,m)
m = {dead=asset.bee_dead, hit=asset.bee_hit, moving={asset.bee, asset.bee_fly}, name="Murder Hornet"}
table.insert(MT,m)
m = {dead=asset.ghost_dead, hit=asset.ghost_hit, moving={asset.ghost, asset.ghost_normal}, name="Ghost"}
table.insert(MT,m)
m = {dead=asset.snake_dead, hit=asset.snake_hit, moving={asset.snake, asset.snake_walk}, name="Serpent"}
table.insert(MT,m)
end
Our Monster includes its monster table entry as self.mtEntry
, so we can just fetch the needed info from the table for now:
function Monster:drawSheet()
if self:distanceFromPlayer() > 10 then return end
pushMatrix()
pushStyle()
resetMatrix()
local sheetW = 200
local sheetH = 200
local margin = 20
local corner = vec2(WIDTH-sheetW-margin, margin)
rectMode(CORNER)
fill(136, 129, 107)
stroke(0,0,0,0)
rect(corner.x, corner.y, sheetW, sheetH)
noFill()
stroke(0)
strokeWidth(2)
rect(corner.x + 10, corner.y + 10, sheetW-20, sheetH-20)
translate(corner.x + 20, corner.y + sheetH-30)
textMode(CORNER)
textAlign(LEFT)
fill(0)
text(self.mtEntry.name, 0,0)
popStyle()
popMatrix()
end
I set the font this way, at the top of the system:
font("Optima-BoldItalic")
That looks pretty good but not quite large enough. With a bit of tuning, I get this:
This is nearly good. I suspect I may need a bit more width. Also, my drawSheet method is getting as messy as the one I condemned only a few words ago. Let’s see if we can tidy it up a bit and then go ahead with strength and health.
I think that if I could have my druthers, I’d prefer to think of this sheet as having its origin at top left, not bottom left. I’m not sure if it’s worth it to build that in for a single sheet or not. Probably not. But I am minded to create a new object for this, a MonsterSheet. Let’s do that:
function Monster:drawSheet()
if self:distanceFromPlayer() <= 10 then
self.monsterSheet:draw()
end
end
Of course for this to work, we’d better have a monster sheet:
function Monster:init(tile, runner, mtEntry)
self.mtEntry = mtEntry or self:randomMtEntry()
self.alive = true
self.tile = tile
self.tile:addContents(self)
self.runner = runner
--sprite(asset.slime)
self.dead = self.mtEntry.dead
self.hit = self.mtEntry.hit
self.moving = self.mtEntry.moving
self.movingIndex = 1
self.swap = 0
self.move = 0
self.hitPoints = 2
self.monsterSheet = MonsterSheet(self)
if not MT then self:initMonsterTable() end
end
And now we need a class:
-- MonsterSheet
-- RJ 20201228
MonsterSheet = class()
function MonsterSheet:init(monster)
self.monster = monster
end
function MonsterSheet:draw()
pushMatrix()
pushStyle()
resetMatrix()
local sheetW = 200
local sheetH = 200
local margin = 20
local corner = vec2(WIDTH-sheetW-margin, margin)
rectMode(CORNER)
fill(136, 129, 107)
stroke(0,0,0,0)
rect(corner.x, corner.y, sheetW, sheetH)
noFill()
stroke(0)
strokeWidth(2)
rect(corner.x + 10, corner.y + 10, sheetW-20, sheetH-20)
translate(corner.x + 20, corner.y + sheetH-30)
textMode(CORNER)
textAlign(LEFT)
fill(0)
text(self.monster.mtEntry.name, 0,0)
popStyle()
popMatrix()
end
With just that one tweak to access mtEntry
, the code works fine. We really want to just talk to our monster, however, and in general, to do that through methods.
text(self.monster:name(), 0,0)
function Monster:name()
return self.mtEntry.name
end
I think it’s time to commit this. Monster sheet has name.
That done, let’s print Strength and Health, then their little icons. We’ll let the code get a bit more messy before tidying it up.
function MonsterSheet:draw()
pushMatrix()
pushStyle()
resetMatrix()
local sheetW = 200
local sheetH = 200
local margin = 20
local corner = vec2(WIDTH-sheetW-margin, margin)
rectMode(CORNER)
fill(136, 129, 107)
stroke(0,0,0,0)
rect(corner.x, corner.y, sheetW, sheetH)
noFill()
stroke(0)
strokeWidth(2)
rect(corner.x + 10, corner.y + 10, sheetW-20, sheetH-20)
translate(corner.x + 20, corner.y + sheetH-30)
textMode(CORNER)
textAlign(LEFT)
fill(0)
text(self.monster:name(), 0,0)
translate(0,-30)
text("Strength")
translate(0,-20)
text("Health")
popStyle()
popMatrix()
end
This looks almost OK:
The code is driving me crazy, however. Let’s see if we can set this up so that it’s a bit more readable. OK, How about this:
function MonsterSheet:draw()
pushMatrix()
pushStyle()
resetMatrix()
rectMode(CORNER)
textMode(CORNER)
textAlign(LEFT)
self:drawParchment()
fill(0)
self:drawText(self.monster:name())
self:drawText("")
self:drawText("Strength")
self:drawText("Health")
popStyle()
popMatrix()
end
function MonsterSheet:drawParchment()
local sheetW = 220
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:drawText(aString)
text(aString)
translate(0,-fontSize()-5)
end
This sets up the “parchment”rectangle, and then positions the cursor at the top left of the text area. When you call self:drawText
, that function automatically inserts a newline, by translating you down to the next line. I picked 5 pixels plus the font size because it looks about right.
However, this neatness isn’t going to deal well with my plan to draw little icons representing strength and hit points, is it? For that, I’ll want to do my newlines explicitly. OK, let’s bite that bullet right now:
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)
popStyle()
popMatrix()
end
Now I need some values for strength and health. With those added, I get this:
I need to space the icons out a bit more:
Now one more thing. Let’s display the monster’s icon in the character sheet. After some tuning, we get this:
I spared you most of the details of the adjustment of pixel counts. The final MonsterSheet is this:
-- 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")
--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
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)
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 Monster photo is:
function Monster:photo()
if self.alive then
return self.moving[1]
else
return self.dead
end
end
Let’s commit this: Decent cut at Monster Sheet. And let’s sum up, I’m hungry.
Summary
This was all just grungy, nitty-gritty drawing. I rather like how I managed the margins and typing in the sheet, with the drawText
and newLine
functions. That would have ben even nicer had I been able to type swords, but they are sprites, not emoji. So newLine
isn’t too bad.
MonsterSheet is basically just a view on Monster, with read access to whatever it needs.
There’s some code to fix up now in Monster, but nothing too nasty: we just need to smooth out the interface between the monster and its table entry. And, of course, we’ll want to put hit points and health into the table, and the monster, in some consistent way.
I was bitten by the use of strength
as a member variable and a method. Codea can’t cope with two entries of the same name. Who could, really? So I just renamed strength
the variable to power
for now.
So this went in easily and was mostly boring.
See you next time!