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:

simple

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:

tiny square

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:

square

Now to improve it.

parchment

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:

death fly

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 clean 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 cleaning 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:

text

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:

packed

I need to space the icons out a bit more:

better

Now one more thing. Let’s display the monster’s icon in the character sheet. After some tuning, we get this:

tuned

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 clean 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!


D2.zip