Dungeon 67
Some display work. Then let’s start on treasures. I have a cunning plan …
I hope to get to putting in some treasures, either this morning, or tomorrow. I expect that there will be a fairly large number of different kinds of treasures, with varying properties. Possibilities include:
- Gold, which can be used to buy things;
- Enhancements to player attributes, like speed or health;
- Weapons, to enhance performance in battle;
- Armor, for more resilience in battle;
- Magic, for sussing out enemies or paths to glory;
Within those vague classes, we can already see that a treasure can have a wide range of impacts. We’d hope that most of those can come down to player attributes, but even so, their impact on the game might occur in almost any fashion at all.
If you have the XYZZY gem, and you touch it in a certain room, you’ll be teleported to another specific room. If you touch it again, you’ll be teleported back.
If you have the caged bird, and let it free, it will drive away the deadly poisonous snake.
If you place the goblet on the altar, the door will open.
It could be anything. The possibilities are endless, and I have little idea about how to make it all make sense.
And that is my cunning plan … I’m going to work without a plan.
That may come as no surprise to constant readers. My general practice in these exercises is not to create a lot of clever infrastructure and then use it, it is to create some specific cases and then refactor them to create multiple elements of code that are commonly useful … that is … infrastructure.
This generally works rather well, although we are always left with parts of the code that are rather well organized, and others that are less so. As we pass over the less organized one, we try to make a point of refining them, so that the system is always working itself toward better design.
As I say, I think this generally works rather well. You may not agree, in which case you’re probably just reading this to see me get in more trouble. That’s OK too.
But first …
Attribute Sheets
We need to improve the attribute sheets a bit Right now they display Strength, Health, and Courage. Courage isn’t even a thing, I just pasted it into the sheet when I was working on the bar graph part of the display. Strength is used to roll damage, which is a value from zero through the character’s strength. Health is basically hit points. A strike’s damage value is subtracted from the defender’s Health.
We use a variable speed.
function firstAttack(attacker,defender, random)
yield(attacker:name().." strikes!")
local attackerSpeed = rollRandom(attacker:speed(), random)
local defenderSpeed = rollRandom(defender:speed(), random)
if defenderSpeed > attackerSpeed then
...
Everyone presently has speed 5.
For today’s “easy” task, I propose to remove Courage from the sheet, and add speed. As a random design decision, I think we should put Health at the top of the display, then Speed and then Strength.
Here’s the sheet:
function MonsterSheet:draw()
local m = self.monster
pushMatrix()
pushStyle()
resetMatrix()
zLevel(1)
rectMode(CORNER)
textMode(CORNER)
textAlign(LEFT)
self:drawParchment()
fill(0)
self:drawText(m:name())
self:newLine(2)
self:drawText("Strength")
self:drawBarFraction(asset.builtin.Small_World.Sword, m:strength(), m:strengthMax())
self:newLine()
self:drawText("Health")
self:drawBarFraction(asset.builtin.Small_World.Heart, m:health(), m:healthMax())
self:newLine()
self:drawText("Courage")
self:drawBarFraction(asset.builtin.Small_World.Heart_Glow, 3,10)
self:drawPhoto(m:photo())
popStyle()
popMatrix()
end
Nothing very fancy, just long and repetitive. I recall that I created the drawing methods so that they just sort of “carriage return” down the page.
So …
self:drawText(m:name())
self:newLine(2)
self:drawText("Health")
self:drawBarFraction(asset.builtin.Small_World.Heart, m:health(), m:healthMax())
self:newLine()
self:drawText("Speed")
self:drawBarFraction(asset.builtin.Space_Art.Green_Explosion, m:speed(), 20)
self:newLine()
self:drawText("Strength")
self:drawBarFraction(asset.builtin.Small_World.Sword, m:strength(), m:strengthMax())
self:drawPhoto(m:photo())
That nearly works but the explosion thing is too large:
This shows up a general issue in the system. We use things like sprites willy-nilly, from all different sources, and they come in various sizes. There’s code spread around the system to scale them, and to adjust their position if necessary. The princess, for example:
function Player:draw(tiny)
local sx,sy
local dx = 0
local dy = 10
pushMatrix()
pushStyle()
spriteMode(CENTER)
local center = self:graphicCenter() + vec2(dx,dy)
translate(center.x,center.y)
if not self.alive then tint(0) end
sx,sy = self:setForMap(tiny)
self:drawSprite(sx,sy)
popStyle()
popMatrix()
if not tiny then
self.playerSheet:draw()
end
end
function Player:drawSprite(sx,sy)
if self.showDamage then
self:selectDamageTint()
sprite(asset.Character_Princess_Girl_Hit,0,0,sx,sy)
else
sprite(asset.Character_Princess_Girl,0,0,sx,sy)
end
end
function Player:setForMap(tiny)
if tiny then
tint(255,0,0)
return 180,272
else
return 66,112
end
end
We translate to a seemingly random location, up 10 pixels from graphics center, and then scale to two seemingly random scale values, depending on whether we’re drawing the real princess or the little dot on the small map.
There are other such messes. They’re spread around, and we may never run across them again. But here we’re about to propagate the evil one more time. Let’s try to do better.
Let’s create a new class, AdjustedSprite, that knows an asset, a scale vector, and an offset vector. It will have only a draw method. In that method, it will first translate by the offset vector, then scale by the scaling vector. (We may find that we want to reverse these. Time will tell.) Then it will draw.
In the fullness of time, we should be thinking in terms of having all our sprites embedded in these AdjustedSprites, and probably centralized in some single location. For now, we’ll use the thing ad hoc, right here.
function MonsterSheet:init(monster, inward)
self.inward = inward or 0
self.monster = monster
self.healthIcon = AdjustedSprite(asset.builtin.Small_World.Heart)
self.speedIcon = AdjustedSprite(asset.builtin.Space_Art.Green_Explosion, vec2(0.5,0.5))
self.strengthIcon = AdjustedSprite(asset.builtin.Small_World.Sword)
end
I have in mind defaulting the arguments.
To use them, we’ll just pass them to our drawing method:
self:drawText(m:name())
self:newLine(2)
self:drawText("Health")
self:drawBarFraction(self.healthIcon, m:health(), m:healthMax())
self:newLine()
self:drawText("Speed")
self:drawBarFraction(self.speedIcon, m:speed(), 20)
self:newLine()
self:drawText("Strength")
self:drawBarFraction(self.strengthIcon, m:strength(), m:strengthMax())
self:drawPhoto(m:photo())
And drawBarFraction
looks like this now:
function MonsterSheet:drawBarFraction(icon, current, currentMax)
pushStyle()
tint(255)
spriteMode(CENTER)
rectMode(CORNER)
sprite(icon, 80, 10)
stroke(255)
fill(150,150,150)
rect(100,0,120,20)
--fill(255,0,0)
rect(100,0,120*currentMax/20,20)
fill(255,255,0)
rect(100,0,120*current/20,20)
popStyle()
end
We don’t really want to tell the AS where to draw, we want to be positioned there already. Let’s do this for now:
function MonsterSheet:drawBarFraction(icon, current, currentMax)
pushStyle()
tint(255)
spriteMode(CENTER)
rectMode(CORNER)
pushMatrix()
translate(80,10)
icon:draw()
popMatrix()
stroke(255)
fill(150,150,150)
rect(100,0,120,20)
--fill(255,0,0)
rect(100,0,120*currentMax/20,20)
fill(255,255,0)
rect(100,0,120*current/20,20)
popStyle()
end
We probably should improve that method, shouldn’t we? But not now, we’re making something work.
New class:
-- AdjustedSprite
-- RJ 20210117
AdjustedSprite = class()
function AdjustedSprite:init(icon,scaleVec)
self.icon = icon
self.scaleVec = scaleVec or vec2(1,1)
end
function AdjustedSprite:draw()
pushMatrix()
scale(self.scaleVec.x, self.scaleVec.y)
sprite(self.icon)
popMatrix()
end
I decided not to put in the offset, as it is speculative and even when discussing it I wasn’t clear on whether it should be applied before or after the scaling. We’ll put it in if and when we need it.
Now things look like this:
I think I’ll make the explosion thing even smaller, and scale down the sword now that I can.
This is better, but you know what would be nice? It would be nice if the sword were rotated a bit. That’s easy now, we’ll just add a rotation parameter.
self.strengthIcon = AdjustedSprite(asset.builtin.Small_World.Sword, vec2(0.8,0.8), 45)
I chose degrees because 45, and rotate
wants degrees anyway.
function AdjustedSprite:init(icon,scaleVec, degrees)
self.icon = icon
self.scaleVec = scaleVec or vec2(1,1)
self.degrees = degrees or 0
end
function AdjustedSprite:draw()
pushMatrix()
scale(self.scaleVec.x, self.scaleVec.y)
rotate(self.degrees)
sprite(self.icon)
popMatrix()
end
And the result:
Now I call that fine.
A nice result in time for Sunday brekkers. Commit: improved attribute sheet, uses AdjustedSprites.
Monday, Monday
Here we are, Monday, Monday. What shall we do now?
I think what needs to be done is to specify all the monster attributes, health, speed, and strength, and the same with the Player. I have in mind a new scheme for the monsters. the monster definition table entries look like this:
m = {name="Pink Slime", health=1, strength={1,2,3},
dead=asset.slime_squashed, hit=asset.slime_hit,
moving={asset.slime, asset.slime_walk, asset.slime_squashed}}
table.insert(MT,m)
And they are processed like this:
function Monster:init(tile, runner, mtEntry)
if not MT then self:initMonsterTable() end
self.alive = true
self.tile = tile
self.tile:addContents(self)
self.runner = runner
self.mtEntry = mtEntry or self:randomMtEntry()
self.dead = self.mtEntry.dead
self.hit = self.mtEntry.hit
self.moving = self.mtEntry.moving
local attributeTable = self:getTable(self.mtEntry.health)
self.healthPoints = attributeTable[1]
self.healthPointsMax = attributeTable[2]
self.healthPointsPossible = attributeTable[3]
attributeTable = self:getTable(self.mtEntry.strength)
self.strengthPoints = attributeTable[1]
self.strengthPointsMax = attributeTable[2]
self.strengthPointsPossible = attributeTable[3]
self.movingIndex = 1
self.swap = 0
self.move = 0
self.monsterSheet = MonsterSheet(self)
end
The getTable
function is presently:
function Monster:getTable(mtTable)
if type(mtTable) == "table" then return mtTable end
return {mtTable, mtTable, mtTable}
end
I used to think we’d store the attribute, its current maximum, and a possible maximum. I have a new plan, which will be to use the standard D&D range of 1d20, one through twenty, for all attributes. (There may be additions in the future.)
For our monsters, I want to let them roll their attributes in a range that will be provided in the input table. If we provide a single value, that will be the value of that attribute. So the new entries will either be a constant, or a two-entry table. We’ll convert to the two entry table with getTable, and roll against it.
Let’s work from the init:
self.healthPoints = self:roll(self.mtEntry.health)
self.strengthPoints = self:roll(self.mtEntry.strength)
self.speedPoints = self:roll(self.mtEntry.speed)
Then …
function Monster:roll(entry)
local range = self:getTable(entry)
return math.random(range[1], range[2])
end
I think this obviously works, and since it’s random, I’m not of a mind to microtest it. I’ll ask the Zoom Ensemble about it if I remember. Let’s set up the monsters. I’ll include the code and some commentary. I think I’ll add a ``level` value as well, though we won’t be using it.
self.level = self.mtEntry.level or 1
self.healthPoints = self:roll(self.mtEntry.health)
self.strengthPoints = self:roll(self.mtEntry.strength)
self.speedPoints = self:roll(self.mtEntry.speed)
I think we want to allow a wider range of speed than we’ve been using. I figure the Player will start all their attributes at about 10, so the monsters should have speeds around that value as well.
function Monster:initMonsterTable()
local m
MT = {}
m = {name="Pink Slime", level = 1, health={1,2}, speed = {4,10}, strength=1,
dead=asset.slime_squashed, hit=asset.slime_hit,
moving={asset.slime, asset.slime_walk, asset.slime_squashed}}
table.insert(MT,m)
m = {name="Death Fly", level = 1, health={2,3}, speed = {8,12}, strength=1,
dead=asset.fly_dead, hit=asset.fly_hit,
moving={asset.fly, asset.fly_fly}}
table.insert(MT,m)
m = {name="Toothhead", level = 2, health={4,6}, speed = {8,15}, strength={1,2},
dead=asset.barnacle_dead, hit=asset.barnacle_hit,
moving={asset.barnacle, asset.barnacle_bite}}
table.insert(MT,m)
m = {name="Murder Hornet", level = 2, health={2,3}, speed = {8,12}, strength={2,4},
dead=asset.bee_dead, hit=asset.bee_hit,
moving={asset.bee, asset.bee_fly}}
table.insert(MT,m)
m = {name="Ghost", level=1, health={1,5}, speed={5,9},strength={1,1},
dead=asset.ghost_dead, hit=asset.ghost_hit,
moving={asset.ghost, asset.ghost_normal}}
table.insert(MT,m)
m = {name="Serpent", level=3, health={8,14}, speed={8,15}, strength={8,12},
dead=asset.snake_dead, hit=asset.snake_hit,
moving={asset.snake, asset.snake_walk}}
table.insert(MT,m)
m = {name="Vampire Bat", level=2, health={3,8}, strength={8,10},
dead=asset.bat_dead, hit=asset.bat_hit,
moving={asset.bat, asset.bat_fly}}
table.insert(MT,m)
m = {name="Yellow Widow", level=3, health={1,4}, speed = {2,5}, strength={9,15},
dead=asset.spider_dead, hit=asset.spider_hit,
moving={asset.spider, asset.spider_walk1,asset.spider_walk2}}
table.insert(MT,m)
m = {name="Poison Frog", level=3, health={4,8}, speed = {2,6}, strength={8,11},
dead=asset.frog_dead, hit=asset.frog_hit,
moving={asset.frog, asset.frog_leap}}
table.insert(MT,m)
m = {name="Ankle Biter", level=4, health={9,18}, speed={3,7}, strength={10,15},
dead=asset.spinnerHalf_dead, hit=asset.spinnerHalf_hit,
moving={asset.spinnerHalf, asset.spinnerHalf_spin}}
table.insert(MT,m)
end
I’m not at all certain of these values, in particular strength. I have in mind a vague plan to change the way the combat works to be a bit more like D&D in terms of the values used.
Now to change the princess’s attributes and see how things look.
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 = 12
self.speedPoints = 8
self.strengthPoints = 2
self.playerSheet = MonsterSheet(self,750)
end
I should mention, lest I forget, that I plan to make it a lot harder to kill the princess. I know that the roguelike tradition is to include permadeath but at least for now, I don’t like it.
Now I need to adjust the attribute sheet to the new scheme of things.
...
self:drawText("Health")
self:drawBarFraction(self.healthIcon, m:health(), 20)
self:newLine()
self:drawText("Speed")
self:drawBarFraction(self.speedIcon, m:speed(), 20)
self:newLine()
self:drawText("Strength")
self:drawBarFraction(self.strengthIcon, m:strength(), 20)
...
OK, remember where I said I wasn’t going to test roll
because it was obviously correct?
Monster:243: bad argument #1 to 'random' (number expected, got nil)
stack traceback:
[C]: in function 'math.random'
Monster:243: in method 'roll'
Monster:21: in field 'init'
... false
end
So … that line is:
self.speedPoints = self:roll(self.mtEntry.speed)
And I suspect that means I have someone who didn’t get speed. Also, let’s bullet-proof the roll/getTable
stuff:
function Monster:getTable(mtTable)
if type(mtTable) == "table" then return mtTable end
return {mtTable or 1, mtTable or 1}
end
OK, now who doesn’t have a speed entry? Vampire Bat. Let’s try again.
I want monster sheets not to appear if the creature is dead. Easily done.
function MonsterSheet:draw()
local m = self.monster
if m:isDead() then return end
...
Princess needs greater strength.
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 = 12
self.speedPoints = 8
self.strengthPoints = 10
self.playerSheet = MonsterSheet(self,750)
end
One more tiny thing: I’d like to display the player’s sheet eve if she’s dead. The operator might like to look at the stats to make decisions about the future. Let’s do this:
function Monster:displaySheet()
return self:isAlive()
end
function Player:displaySheet()
return true
end
function MonsterSheet:draw()
local m = self.monster
if not m:displaySheet() then return end
...
Everything’s working as intended. Monster sheets disappear upon demise, player’s does not.
Commit: attributes for monsters are random. Player attributes increased. Monsters have levels (not yet used.)
Now What?
On the list of things needing doing, we find:
- Levels
- Leveling Up
- Improve Combat
- Combine Monster and Player
- Keep Player Alive
- Reduce pain of having lots of tests
- Add music
- Don’t block hallways
- Better allocation of items, not too near player, not too close together
- More treasures.
I’ve also been thinking about improving the walls and flooring, but that seems mostly boring and probably superfluous at this point.
Let’s look at combat a bit. One decent source is Roll20.net, at least when they have content. The site does often lead you to an opportunity to buy more information. (I wonder if I still have my old D&D books. Probably not.)
D&D Battle is turn-based. The Encounter logic here is turn-based as well, although there are probably more speed-related rolls than in D&D. And, at present, you can make no decisions on your turn. In essence, to attack you try to move onto the tile occupied by an opponent, and the encounter logic runs.
In encounters now, no user or monster actions can take place during the encounter, which basically leaves me banging a key either to ensure I get the first attack–toward the opponent–or to try to run away from them. If I had optional attacks, such as magic, there’d be no way to specify them.
Most of the time, that is, when exploring the level, I just want to be able to walk the Princess around at will. So an overall turn-based approach wouldn’t be good. But I could imagine a turn-based scheme when an encounter occurs. Let’s sketch that out a bit.
Possible Turn-based Encounters
Suppose that most of the time, when there are no monsters around, you can explore at will, pick things up, etc. When there are monsters “within range”, there are a few possibilities:
The monsters might decide to attack. This could be based on some random aspects, an aggression factor or the like. Or, the player might decide to attack. There might be an Attack button to be tapped.
Then we could roll surprise (and there could be some monsters that always have surprise), to see who strikes first. Either way, we could bring up a new set of controls, that would start out simple and become more elaborate as we add aspects to the game, such as magic, healing potions, hand grenades, water pistols, and the like.
When those controls are up, the game would be in a turn-based mode. At first, perhaps the only player options are Attack or Disengage, but others such as Dash, Dodge, Hide, and so on, could be implemented.
I’m not sure quite how to manage the give-and-take of the crawl. At present, since all the action is programmed, when we call back to the encounter coroutine, it just computes a bit and then yields a result:
function attackStrikes(attacker,defender, random)
local damage = rollRandom(attacker:strength(), random)
if damage == 0 then
yield("Weak attack! ".. defender:name().." takes no damage!")
if math.random() > 0.5 then
yield("Riposte!!")
firstAttack(defender,attacker, random)
end
else
defender:displayDamage(true)
yield(attacker:name().." does "..damage.." damage!")
defender:displayDamage(false)
defender:damageFrom(attacker.tile, damage)
if defender:isDead() then
yield(defender:name().." is dead!")
end
end
end
If we move to a turn-based scheme, I think the coroutine approach might handle the monster or mob side, but the player side would have to be event driven, hitting a button or typing a character. It would be ironic–or would it, I’m never really sure about irony–if the coroutine idea were to turn out to be tossed away. And it might. So be it.
I think I’ll need to do some paper design around turn-based combat, to get a sense of what we might want, and probably I’ll do an experiment or two to see what approaches might work. So that’s too big for a closing effort today.
Oh here’s one I didn’t list. The monster sheets should display the sheet for the closest monster. The issue is that monsters are displayed in tile order, left to right bottom to top (or top to bottom, whatever it is). So the right-most monster typically gets its sheet displayed.
But I just got an idea. We use zLevel for the sheets anyway: why not give close monsters better z levels?
Where’s that code?
function Monster:drawSheet()
if self:distanceFromPlayer() <= 5 then
self.monsterSheet:draw()
end
end
function MonsterSheet:draw()
local m = self.monster
if not m:displaySheet() then return end
pushMatrix()
pushStyle()
resetMatrix()
zLevel(1)
rectMode(CORNER)
textMode(CORNER)
...
Let’s try this:
function MonsterSheet:draw()
local m = self.monster
if not m:displaySheet() then return end
pushMatrix()
pushStyle()
resetMatrix()
zLevel(10-m:distanceFromPlayer())
...
Could it be that easy? Apparently not but I’m not clear why:
MonsterSheet:20: attempt to call a nil value (method 'distanceFromPlayer')
stack traceback:
MonsterSheet:20: in method 'draw'
Player:76: in method 'draw'
Tile:107: in method 'drawContents'
Oh. We’re drawing for the player. :) Let’s just give her the method: it won’t matter.
function Player:distanceFromPlayer()
return 0
end
Now we should be good to go. And a quick test … well, a long one, since I had to round up a monster then lead it to where another monster was … a test later, I see that this simple trick works just as I’d hoped.
Commit: closest monster’s sheet displays.
I think that’ll do for today. See you next time, I hope! And if you are following these articles, please let me know. And if you’re not, please let me know also, and also how you knew to let me know.