Dungeon 130
I’m starting to wonder if we have all the juice out of this program, but there sure is a lot that we could still do. Let’s consider some options.
If anyone is still reading these articles, I’d welcome some feedback on any subject, including especially these:
- Is it time for the Dung program exercise to be over?
- What interesting and challenging changes might be enjoyable and educational for the Dung program?
- Since we’ve done Spacewar, Asteroids, Space Invaders, and now Dung, what else might be interesting and educational?
- Or anything really.
Tweet me up. Or email me. I always read, and usually reply.
I have some ideas of my own, including:
- Inventory
- I want to make most things found in the dungeon go into the player’s inventory, and show up on the attribute sheet. Then the player can “equip” them by touching the item. For a power-up, she might guzzle down a health potion or take a hit of speed. For an amulet or ring, she might wear it. I have in mind that some amulets, when worn, will move the players maximum stable attributes upward. Her health, for example, converges on 12 now. Once wearing the Amulet of Single Payer Health Care, her stable health attribute might pop up to 15. Other inventory items might be ranged weapons, or provide other services.
-
Probably the biggest issue with this idea will be enhancing the Attribute sheet to have the items in it, and to connect touches back to code. The various code snippets, once we dispatch to them, should be easy enough.
- The Little Death
- Surely you’ve listened to the sounds she makes in battle. But I digress. When the player “dies”, i.e. health goes to zero, I want to send her back to her starting position. I’m not sure whether she should lose some of her possessions or not.
-
Similarly for monsters. When a monster is defeated, I want it to flee rather than die. There’s enough death.
- Bosses
- There are now guardians around the WayDown. They will fight but do not attack unless they’re forced into it. I’d like to add a boss entity that must be defeated in some way to enter the WayDown. (Maybe you have to get the WayDown key from it.) Defeat need not mean battle. Maybe you need to deploy some horrid scent that the monster hates, or bait it out of the room, or answer Riddles Three.1 Maybe there is a puzzle somewhere else that you must solve. Or maybe it’s chained up and just wants to be released.
- Puzzles
- I’d like to have puzzles that need solving. I don’t have any great ideas for these, but I do have a few floor buttons and switches that could be used.
- Doors
- We probably need doors. Puzzles might open doors to good things, or just to the WayDown.
- Special Levels
- It might be enjoyable to have a maze level or a level with an interesting shape. It would certainly be interesting to figure out how to create such a thing. It might not be too difficult.
- Poison
- Venomous creatures might poison the player, draining their health until they collapse or until they apply a antidote.
- Light and Darkness
- The dungeon is well-lighted just now. Perhaps there should be dark areas, and torches or something.
- Decor
- We have decor items, skeletons, boxes, barrels, and the like. We could turn some of them into Loots. Perhaps some are even dangerous Loots. Perhaps if you step into a skeleton’s tile, you might get damaged somehow, or you might get a valued item of armor or a weapon.
Of course the possibilities are endless. I have no intention of turning this into an actual game that could be put on the app store, unless someone out there wants to pair or mob with me on the Mac-side process of building it from the exported XCode. I’d be open to that, probably. I’m not going to do it on my own.
But mostly what I’m doing, other than entertaining myself, is asking for things uncontemplated in the present design, and then discovering how easy it is (or, occasionally, difficult) to put the new idea in. My belief is that with good-enough code, new capabilities, even ones that are a bit off the wall, are usually easy enough when the code is good enough.
Seriously off the wall ideas, of course, may still be too difficult. And plenty of ideas probably just won’t interest me. I’m old, I’m not being paid for this, and I won’t do things that don’t interest me.
I have a couple of things in mind for today.
For Today
I want to change the display so that the player stays centered all the time. I copied the idea of letting the player drift side to side and top to bottom from the Dungeon Shadows game on the Codea Forum. But if I don’t do that, I’ll have better control over areas of the screen for attribute sheets and the like.
I want to change Decor items so that the player can step on them. I’ll leave them so that monsters cannot.
And I think I’ll reformat the player’s attribute sheet a bit, to leave room for inventory.
Let’s get started.
Screen Adjustment
GameRunner has a method to set up for drawing the big map:
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
That clamping stuff is what causes the player to drift to the side. Essentially that code ensures that we never try to draw a tile that is outside the maxScrollValues
:
function GameRunner:maxScrollValues()
local DW,DH = self:dungeonSize()
return WIDTH - DW, HEIGHT - DH
end
If we ignore all that and just translate to the middle of the screen, that should do what we want. The game will of course then try to draw tiles that are outside the bounds of the game, but I think we already return an edge tile for out of bounds access.
So:
function GameRunner:scaleForLocalMap()
local center = self.player:graphicCorner()
translate(WIDTH/2-center.x, HEIGHT/2-center.y)
scale(1)
end
That works exactly as intended. The player now stays centered at all times. Commit: player stays at center screen.
Walk on Decor
Sweet. Now to allow the player to walk on Decor. Those decisions are made in TileArbiter
:
function TileArbiter:createTable()
-- table is [resident][mover]
if TA_table then return end
local t = {}
t[Chest] = {}
t[Chest][Monster] = {moveTo=TileArbiter.refuseMove}
t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
t[Key] = {}
t[Key][Monster] = {moveTo=TileArbiter.acceptMove}
t[Key][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithKey}
t[Player] = {}
t[Player][Monster] = {moveTo=TileArbiter.refuseMove, action=Monster.startActionWithPlayer}
t[Monster]={}
t[Monster][Monster] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Monster.startActionWithMonster}
t[Monster][Player] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Player.startActionWithMonster}
t[Loot] = {}
t[Loot][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithLoot}
t[Loot][FakePlayer] = {moveTo=TileArbiter.refuseMove, action=FakePlayer.startActionWithLoot}
t[WayDown] = {}
t[WayDown][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithWayDown}
t[Spikes] = {}
t[Spikes][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithSpikes}
TA_table = t
end
We just need to add an entry for the Decor-Player interaction.
t[Decor] = {}
t[Decor][Player] = {moveTo=TileArbiter.acceptMove}
That should suffice.
I should have had you hold my beer. It isn’t quite that easy:
TileArbiter:17: attempt to call a nil value (method 'getTile')
stack traceback:
TileArbiter:17: in field 'moveTo'
TileArbiter:28: in method 'moveTo'
Tile:90: in method 'attemptedEntranceBy'
Tile:355: in function <Tile:353>
(...tail calls...)
Player:181: in method 'moveBy'
Player:124: in method 'executeKey'
Player:175: in method 'keyPress'
GameRunner:313: in method 'keyPress'
Main:35: in function 'keyboard'
Let’s see what’s going on in our moveTo
.
function TileArbiter:moveTo()
local entry = self:tableEntry(self.resident,self.mover)
local action = entry.action
if action then action(self.mover,self.resident) end
local result = entry.moveTo(self) -- <-- 28
return result
end
function TileArbiter:acceptMove()
return self.resident:getTile() -- <-- 17
end
This is odd. The message seems to me to be saying that the self.resident
at line 17 found a nil. But we enter things all the time.
Ah. The resident is the Decor, and Decor do not know how to return their tile.
function Decor:getTile()
return self.tile
end
That works fine, we can now trample the Decor:
Commit: Player can enter decor tiles.
This may be telling us that our Decor items should be a kind of Entity. Perhaps Loots should as well. There are certainly common elements and methods that they all share, such as getTile
. So one possibility is that they should all inherit from Entity. Maybe there are other possibilities, like having them all wrapped in a DungeonEntity object or something.
The solution could be any number of things. The message we’re getting, subtle though it may be, is that our design lacks something here that ensures that these disparate objects all have certain protocol elements in common.
Reformat Attribute Sheet.
You can see the attribute sheets in the video above. I want to eliminate the blank line between name and Health, eliminate the cute but meaningless icons, and instead of showing all the keys, show one key and a count, so save space for other icons.
Attributes are drawn by the AttributeSheet
class:
function AttributeSheet:draw()
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()
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()
self:drawPhoto(m:photo())
popStyle()
popMatrix()
end
Nothing terribly fancy here. I’ll just modify it to taste. Since this is a view object, I don’t know how to test it other than to look. Perhaps someone smarter than I am, or more experienced in this area, can tell me a better way.
First, I change the newLine(2)
to newLine()
, eliminating the blank line. Then I move on to the drawAttribute
:
function AttributeSheet:drawAttribute(icon,attribute)
local view = AttributeView(icon, attribute)
view:draw()
end
function AttributeView:draw()
pushStyle()
tint(255)
spriteMode(CENTER)
rectMode(CORNER)
pushMatrix()
translate(80,10)
self.icon:draw()
popMatrix()
stroke(255)
fill(150,150,150)
rect(100,0,120,20)
fill(255,255,0)
rect(100,0,120*self.attr:value()/20,20)
noFill()
stroke(0)
rect(100,0,120*self.attr:nominal()/20,20)
stroke(255)
rect(100,0,120,20)
popStyle()
end
I have a change of heart. Maybe we can get better icons. We don’t need the horizontal space for these, so I’ll leave them for now.
Now to the keys:
function AttributeSheet:drawKeys()
local icon <const> = self.keyIcon
local keyMax <const> = 5
local k <const> = math.min(self.monster:keyCount(), keyMax)
if k > 0 then
self:newLine()
for i = 1, k do
translate(20,0)
icon:draw()
end
end
end
I want to draw just one key, and then the text for k.
(It’s clear that we’re going to have to go to something more robust than this “newLine” idea, which has works so far but may not survive adding inventory. We’ll leave that evil to the day thereof.)
I decided to go with this:
function AttributeSheet:drawKeys()
local icon <const> = self.keyIcon
local keyMax <const> = 5
local k <const> = math.min(self.monster:keyCount(), keyMax)
self:newLine()
if k > 0 then
translate(20,0)
icon:draw()
text(k, 15,-15)
translate(-20,0)
end
end
This is a bit of hackery, or two bits. I unconditionally do the newLine
, so that we are guaranteed to be at the next line when we exit this function. Then I set the margin back to where it was before I drew the key and count. This means that whatever we do next will have a common starting point, no matter whether we have keys or not. This may defer the time when we have to do something more sophisticated about layout in the AttributeSheet.
And it gives me what I wanted. So we can commit: Keys now show a single key and count in the AttributeSheet.
Monster Picture
I noticed that the monster picture no longer shows up in the AttributeSheet. It’s down off the screen or something. Let’s see how that happens.
function AttributeSheet:drawPhoto(aSprite)
self:newLine(4)
sprite(aSprite, 80, 0, 50)
end
Meh. This should go in the top right of the AttributeSheet. I need my newLine back, and to draw this sprite earlier on, right after the name.
We’d really like to draw it at a known position. I see we’re drawing it at size 50 (50x50) so if we draw it at the beginning …
The parchment is 250x200 with a margin of 20, so the right edge is at 230. If we leave 10, we’d put the left margin of the photo at 170? Let’s see:
That’ll do for now. As usual, I just bashed the values until it looks about right.
function AttributeSheet:drawPhoto(aSprite)
sprite(aSprite, 200, 6, 50)
end
function AttributeSheet:draw()
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
Close enough. Commit: Attribute sheet properly shows entity photo.
Summary
I think that’ll do for the morning. We’ve changed the dsplay not to scroll, allowed the player to walk on the Decor, readied the attribute sheet to accept inventory items, and put the mug shots back into the attribute sheet.
Not bad for a couple of hours. Everything went smoothly …
But we are getting a hint of a scent that suggests that things in the dungeon need more common code than they presently have, which leads to necessary duplication of methods like getTile
, which leads to mistakes such as today’s surprise discovery that the Decor couldn’t return that info.
Maybe we’ll fix that. Maybe we don’t. It depends how much pain we suffer. Today’s pain, a three line method, wasn’t enough to push me to refactor.
See you next time!
-
Presumably “What did you do yesterday, what are you going to do today, what is in your way?” ↩