Dungeon 111
Will this thing never end?? Let’s take a small cut at inventory.
It’s 0800 and I’m already back with chai and cat food. So that’s good.
I had a few random ideas yesterday, including the possibility that the princess could have in her inventory some kind of monster-deterring objects. Monsters won’t cross spikes, or enter cells with chests and such, so it should be possible to have some other kinds of monster repellant.
I was also thinking about drarkness and light, in particular that there might be dark areas in the dungeon that might require that the princess have a torch. And there are already keys, and she does know how many she has, although that information isn’t displayed.
I’ve been holding back from inventory because it will require me to do some things that I don’t particularly enjoy. I’ll have to enhance her attribute sheet, and perhaps even move the sheets around so that we can see the important bits of the screen.
(There is an alternative here. When a room is close to the edge of the official tile array, the player moves over up against the edge instead of staying centered as usual. If I were to provide real or virtual tiles out beyond the edges of where I place rooms, I think she might just stay centered.)
One more thing …
Thanks, Columbo. There’s one more thing. Codea has just upgraded to Lua 5.4. Lua 5.4 has two incompatibilities with 5.3 that will affect us. For reasons sufficient to its creators at the Pontifical Catholic University in Rio de Janeiro. Those are that the unpack
function now generally requires that it be prefixed as table.unpack
, and that the loadstring
function has been renamed to load
. There are a few other changes, but none that should affect us.
The loadstring
issue is in CodeaUnit:
CodeaUnit.execute = function()
CodeaUnit._summary = ""
for i,v in pairs(listProjectTabs()) do
local source = readProjectTab(v)
for match in string.gmatch(source, "function%s-(test.-%(%))") do
print("loading", match)
loadstring(match)()
end
end
return CodeaUnit._summary
end
I’ll change that and leave a comment for the day when someone finds themselves back in some version less than 5.4:
CodeaUnit.execute = function()
CodeaUnit._summary = ""
for i,v in pairs(listProjectTabs()) do
local source = readProjectTab(v)
for match in string.gmatch(source, "function%s-(test.-%(%))") do
print("loading", match)
load(match)() -- loadstring pre Lua 5.4
end
end
return CodeaUnit._summary
end
That should let the tests mostly run. I say mostly because I expect to have to fix some unpack
calls. I guess I’ll just search for them first, like a grownup.
Now this is odd. I’m sure that last night I found an unpack
that needed changing. Maybe that was in some other program. I see only two uses, in tests:
dx,dy = runner:playerDirection(tile):unpack()
These will work, because the type of the return is vec2, a kind of table, so unpack
will be looked up in the right place “automatically”.
Tests are green. We’re good to go. I wonder where that other unpack
was. I’ll try to remember to check the other iPad later.
Let’s try a very simple first “feature”. Let’s require a key to enter a WayDown. That may turn out to be interesting enough for a first thing.
Things will start out in the TileArbiter, which starts the interaction between tile contents and players and monsters. Most tile contents just refuse entry, but monsters allow entry if they are dead and not otherwise. The code for that is here:
function TileArbiter:refuseMoveIfResidentAlive()
if self.resident:isAlive() then
return self:refuseMove()
else
return self:acceptMove()
end
end
Let’s do something similar for the WayDown. Its entry is this:
t[WayDown][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithWayDown}
Now the way the arbiter protocol works, we’ll always get the start action, whether the move is accepted or not:
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)
return result
end
We do the action first and then the move.
Hmm. The WayDown is kind of special, in that you don’t really move. We’ll deal with that when it comes up. For now, we will start here:
function Player:startActionWithWayDown(aWayDown)
aWayDown:actionWith(self)
end
~~~lua
function WayDown:actionWith(aPlayer)
self.runner:createNewLevel()
end
I think we can do without making the entry conditional. If you don’t have a key, you could just step onto the WayDown as if it were a tile. That’s what will happen here:
function WayDown:actionWith(aPlayer)
if aPlayer:hasWayDownKey() then
self.runner:createNewLevel()
else
self.runner:addTextToCrawl("You must have a key to go further!")
end
end
Let’s try this. To make it easier, I’ll place a WayDown in room one again. Currently we place it far away:
function GameRunner:placeWayDown()
local r1 = self.rooms[1]
local target = r1:centerTile()
local tile = self:getDungeon():farawayOpenRoomTile(target)
WayDown(tile,self)
end
We can put it in the room with the princess:
function GameRunner:placeWayDown()
local tile, target
local r1 = self.rooms[1]
target = r1:centerTile()
--tile = self:getDungeon():farawayOpenRoomTile(target)
rcx,rcy = r1:center()
tile = self:getTile(vec2(rcx-2,rcy-2))
WayDown(tile,self)
end
Now in player:
function Player:hasWayDownKey()
return self.keys > 0
end
So that works. However, once she has the key, she has it. We should consume the key. Let’s change the protocol to this:
function WayDown:actionWith(aPlayer)
if aPlayer:provideWayDownKey() then
self.runner:createNewLevel()
else
self.runner:addTextToCrawl("You must have a key to go further!")
end
end
function Player:provideWayDownKey()
if self.keys > 0 then
self.keys = self.keys - 1
return true
else
return false
end
end
This should do what we need. And it does:
Commit: WayDown requires and consumes a key. (I reverted out the patch to put the WayDown in room 1.)
Now I guess we’d better display keys In the Attribute sheet. That goes like this:
function AttributeSheet: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.4,0.4))
self.strengthIcon = AdjustedSprite(asset.builtin.Small_World.Sword, vec2(0.8,0.8), 45)
end
function AttributeSheet:draw()
local m = self.monster
if not m:displaySheet() then return end
pushMatrix()
pushStyle()
resetMatrix()
zLevel(10-m:distanceFromPlayer())
rectMode(CORNER)
textMode(CORNER)
textAlign(LEFT)
self:drawParchment()
fill(0)
self:drawText(m:name())
self:newLine(2)
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)
self:drawPhoto(m:photo())
popStyle()
popMatrix()
end
We’ll need an icon:
function AttributeSheet: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.4,0.4))
self.strengthIcon = AdjustedSprite(asset.builtin.Small_World.Sword, vec2(0.8,0.8), 45)
self.keyIcon = AdjustedSprite(asset.builtin.Planet_Cute.Key, vec2(0.4,0.4))
end
And let’s just display a row of keys after the last line of the attribute sheet, if the entity has any.
...
self:newLine()
self:drawText("Strength")
self:drawBarFraction(self.strengthIcon, m:strength(), 20)
self:drawKeys()
self:drawPhoto(m:photo())
popStyle()
popMatrix()
end
And:
function AttributeSheet:drawKeys()
local icon = self.keyIcon
local k = self.monster:keyCount()
if k > 0 then
self:newLine()
for i = 1, k do
translate(32,0)
icon:draw()
end
end
end
I randomly guessed at a translation of 32 pixels per key. It looks somewhat OK:
You can see that we’ll be running out of vertical space soon. Let’s make the key smaller.
I hate this kind of fiddly thing. I imagine that if I had a layout sheet of paper with scale, I could do this more readily.
I do know that the key is 101x171, so suppose we wanted it to be 20 wide, we’d scale it by 0.2. Let’s try that. Too small. I hate this fiddling.
I wind up with that, this way:
self.keyIcon = AdjustedSprite(asset.builtin.Planet_Cute.Key, vec2(30/100,30/100))
function AttributeSheet:drawKeys()
local const icon = self.keyIcon
local const k = self.monster:keyCount()
if k > 0 then
self:newLine()
for i = 1, k do
translate(20,0)
icon:draw()
end
end
end
I think we should probably max this at some number of keys, just so that we don’t draw keys all across the screen if someone goes crazy collecting them.
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 should mention the <const>
. That’s a new feature of Lua 5.4, which declares that the variable in question is constant. This helps the compiler make better decisions. It also makes the code a bit more clear, so I’ll start using it when I think of it.
I think this feature is nearly good. We’re going to have to do more, but now at least there is an indication that one has keys.
We can even watch them go away, I imagine.
Commit: Keys are displayed in AttributeSheet.
What Else?
Is there some other nice little thing we could do this morning? I’d like to wrap this up but not quite yet.
OK, here’s something. Let’s change things so that no two placed objects can go onto the same tile. I think we can do that in the logic that finds an open tile.
function Dungeon:randomRoomTile(roomToAvoid)
while true do
local pos = vec2(math.random(1, self.tileCountX), math.random(1,self.tileCountY))
local tile = self:getTile(pos)
if tile:getRoomNumber() ~= roomToAvoid and tile:isOpenRoom() then return tile end
end
end
That leads here:
function Tile:isOpenRoom()
local r
for x = -1,1 do
for y = -1,1 do
r = self:getNeighbor(vec2(x,y))
if r.kind ~= TileRoom then return false end
end
end
return true
end
I think we want a new method, isEmpty().
function Tile:isEmpty()
return self.contents:isEmpty()
end
And then:
function DeferredTable:isEmpty()
self:update()
return next(self.contents) == nil
end
This is the official Lua idiom for whether a keyed table is empty. I just learned that the other day. Amazing what you learn if you read the documents.
I have no test for this code, but if we manage to initialize the dungeon without exploding, I think we’ll be fine. I do want to make the same change to our hallway finder:
function Tile:isOpenHallway(up,down,left,right)
if not self:isRoom() then return false end
return up:isRoom() and down:isRoom() and not left:isRoom() and not right:isRoom()
or left:isRoom() and right:isRoom() and not up:isRoom() and not down:isRoom()
end
function Tile:isOpenHallway(up,down,left,right)
if not self:isRoom() or not self:isEmpty() then return false end
return up:isRoom() and down:isRoom() and not left:isRoom() and not right:isRoom()
or left:isRoom() and right:isRoom() and not up:isRoom() and not down:isRoom()
end
Extensive user testing tells me that this is probably maybe more or less could be working as expected. I don’t see much point in testing it, as I don’t think it can go wrong. We’ll see whether I fall dead in the dungeon because of this.
Commit: Only one initial item per tile. Why do we have this deferred collection then?
(The answer is that it deals with creatures entering tiles with contents during move cycles.)
Is that enough? It’s 1036 and I started at 0800. I guess we’ll wrap up.
That’s a Wrap
Not much summarizing needed this morning. Everything went in readily. Some remarks, though:
I liked that the question of whether the room was empty got pushed clear down to the DeferredTable, that’s how that sort of thing is best done, and it’s helpful when you hae a class of your own down there, so that you can control its protocol.
I don’t like the keys on the AttributeSheet, but that’s because the whole sheet is rather ad-hoc, just display this and that and move on down the page. It would be better if it had some kind of structure and intelligence. Perhaps we should work on that.
I do like the key on the WayDown, and I’ll probably put keys on the Chest as well. That’s kind of fun.
And enough for now. I like it when a day goes well.