Dungeon 134
A good idea from Bruce. Let’s move toward it. I have no real idea how to start.
Bruce Onder offered an idea via Twitter:
Super weird idea… What if ☁️ monster is stuck inside a potion in a chest? And every other monster wants to stop it from finding the WayDown, so they attack it? And P must defend it while it moves to its goal. And WayDown is invisible until ☁️ touches it? Action puzzle!
I wonder if his friends call him “Wild Blue” Onder? This idea is pretty wild. I’m not sure if it makes me blue to think of implementing it or not. But I like a lot of the idea if not all.
I’m trying to move the game away from combat, so I’m a little troubled by the need to defend the cloud, but I like the idea that PathFinders come out of bottles (or genie lamps if I can find some), and I think the notion of the WayDown either being invisible, or refusing to open, until a PathFinder has opened it, is interesting.
Something along these lines would be an interesting puzzle, and there are variations one could imagine readily.
So let’s work in that direction. We already have dealing with inventory on our radar, and that’s how one would open the bottle or rub the lamp.
Now I have to remind myself what’s up with Inventory. There’s that row of stuff at the top of the screen, but I have no recollection of how much is done I wonder who implemented that.
Inventory
At present we have one test, a class called Inventory, and a class called InventoryItem. The class knows how to draw its icon, and the former class, intended to be used uninstantiated, draws the inventory rectangle and each of the items.
I think the next step will be for these classes to deal with touch. I think what I want is for the Inventory to have a touched
function, as one does, and for it to give the items a chance to respond to the touch. In due time, the items will contain an object, perhaps a Loot or something else representing the item, and touching it will do whatever. In our case, it’ll rez a PathFinder.
For touch to be at all reasonable, the inventory classes are going to want to know more than they currently do about where they are on the screen.
In particular, Inventory centers the Items at the top of the screen. As inventory goes up and down, their positions change. Right now, it looks like this:
function Inventory:draw()
pushMatrix()
pushStyle()
rectMode(CENTER)
spriteMode(CENTER)
if self:count() == 0 then
self:add(InventoryItem("snake_staff"))
self:add(InventoryItem("blue_bottle"))
self:add(InventoryItem("skull_staff"))
self:add(InventoryItem("green_flask"))
self:add(InventoryItem("gold_bag"))
end
local drawPos = self:startingPos()
for i,item in ipairs(inventory) do
item:draw(drawPos)
drawPos.x = drawPos.x + 64
end
popStyle()
popMatrix()
end
function Inventory:startingPos()
return vec2(ItemWidth+WIDTH/2-ItemWidth/2*self:count(), HEIGHT-ItemWidth/2)
end
The item just draws a filled rectangle and the icon, centered at the point given.
What is neat about this, but also weird, is that after great struggle, I have that startingPos
method implemented so that an odd number of items centers an item at screen center, and an even number centers a boundary, so the inventory is always centered at the top of the screen.
I honestly have no idea how that method works. And I strongly suspect that all those divisions /
should be //
integer divisions, though since all the numbers in question are even, it’s OK. This probably needs to be made more understandable. However it should suffice for touch testing to let the Item know its current center position, which can be used to decide whether it has been touched.
Let’s make startingPos
into a function of the item number and the total number, so that iconPos(i, count)
returns the value that we’re sending in there now.
I think maybe I can TDD this, in the classical start with nothing form.
_:test("item x position", function()
local x
x = Inventory:xPos(1,1)
_:expect(x).is(WIDTH//2)
end)
When there’s one, it’s centered. Whee. Fake it till you make it:
function Inventory:xPos(itemNr, count)
return WIDTH//2
end
I have a good feeling about this. Not just this test, but the process. The test, I have a very good feeling about. And even with that call for karma, it does run. We could do two items next, or three. I think three is easier.
_:test("item x position", function()
local x
local w = WIDTH//2
x = Inventory:xPos(1,1)
_:expect(x).is(W)
x = Inventory:xPos(1,3)
_:expect(x).is(w-64)
x = Inventory:xPos(2,3)
_:expect(x).is(w)
x = Inventory:xPos(3,3)
_:expect(x).is(w+64)
end)
Let me rephrase that before seeing it fail. I can’t refer to ItemWidth directly, as it is local to the class, but I can at least make it explicit what’s going on:
_:test("item x position", function()
local x
local w = WIDTH//2
local itemWidth = 64
x = Inventory:xPos(1,1)
_:expect(x).is(w)
x = Inventory:xPos(1,3)
_:expect(x).is(w-itemWidth)
x = Inventory:xPos(2,3)
_:expect(x).is(w)
x = Inventory:xPos(3,3)
_:expect(x).is(w+itemWidth)
end)
Two of those will fail, and one will pass by accident since we always return WIDTH//2.
Fails as expected. Now to implement …
I can’t quite type this in correctly. What I want to do is to adjust from the center position, downward if the item is lower than the middle, and upward if it’s higher than the middle. Essentially I want to translate 1,2,3 into -1,0,1. Looks a lot like subtracting count-1.
That works for the new tests, but breaks the 1,1 test. Maybe this clever translation isn’t a good idea.
At this point I chose to do something really odd. I expanded the test:
_:test("item x position", function()
local x
local w = WIDTH//2
local itemWidth = 64
x = Inventory:xPos(1,1)
_:expect(x).is(w)
x = Inventory:xPos(1,3)
_:expect(x).is(w-itemWidth)
x = Inventory:xPos(2,3)
_:expect(x).is(w)
x = Inventory:xPos(3,3)
_:expect(x).is(w+itemWidth)
x = Inventory:xPos(1,5)
_:expect(x).is(w-2*itemWidth)
x = Inventory:xPos(3,5)
_:expect(x).is(w)
x = Inventory:xPos(5,5)
_:expect(x).is(w+2*itemWidth)
end)
And I wrote the code out longhand:
function Inventory:xPos(itemNr, count)
local center = WIDTH//2
if count == 1 then adjustment = 1
elseif count == 3 then adjustment = 2
elseif count == 5 then adjustment = 3
elseif count == 7 then adjustment = 4
end
return center + (itemNr - adjustment)*ItemWidth
end
I’ll turn this into a calculation in a moment, but now I think I can see how the even ones should work. Let’s add to the test:
_:test("item x even position", function()
local x
local w = WIDTH//2
local itemWidth = 64
x = Inventory:xPos(1,2)
_:expect(x).is(w - itemWidth//2)
x = Inventory:xPos(2,2)
_:expect(x).is(w + itemWidth//2)
end)
And enhance the code:
function Inventory:xPos(itemNr, count)
local center = WIDTH//2
if count == 1 then adjustment = 1
elseif count == 2 then adjustment = 1.5
elseif count == 3 then adjustment = 2
elseif count == 4 then adjustment = 2.5
elseif count == 5 then adjustment = 3
elseif coujt == 6 then adjustment = 3.5
elseif count == 7 then adjustment = 4
end
return center + (itemNr - adjustment)*ItemWidth
end
I am, of course, sure of the pattern, although my tests aren’t yet covering all those values, but I can also now see what the function is. It’s (count+1)/2, with real division, not integer.
function Inventory:xPos(itemNr, count)
local center = WIDTH//2
local adjustment = (count+1)/2
return center + (itemNr - adjustment)*ItemWidth
end
Now you probably saw more directly that that’s what we wanted to do, and if you were here, you could have told me, we’d have tried it, and there we’d be. But I wasn’t seeing it, I didn’t have a card handy to scribble on, and I wanted to do the whole solution in code rather than on paper.
So I enumerated some cases, started to see a pattern, enumerated a few more, saw the pattern clearly, and coded it.
I’m kind of happy with that.
Now let’s use that function to draw the inventory. I need to make one irritating adjustment to get the look the way I want it. Tiles have their corner at 64 pixel intervals, and our inventory things are drawn from center. It would be ideal to do everything the same way, but that’s a pervasive change that I don’t want to look at right now. So I had to adjust the xPos
result a bit.
function Inventory:draw()
pushMatrix()
pushStyle()
rectMode(CENTER)
spriteMode(CENTER)
if self:count() == 0 then
self:add(InventoryItem("snake_staff"))
self:add(InventoryItem("blue_bottle"))
self:add(InventoryItem("skull_staff"))
self:add(InventoryItem("green_flask"))
self:add(InventoryItem("gold_bag"))
end
for i,item in ipairs(inventory) do
item:draw(self:drawingPos(i, self:count()))
end
popStyle()
popMatrix()
end
function Inventory:drawingPos(i, count)
return vec2(ItemWidth//2 + self:xPos(i,count), HEIGHT-ItemWidth//2)
end
function Inventory:xPos(itemNr, count)
local center = WIDTH//2
local adjustment = (count+1)/2
return center + (itemNr - adjustment)*ItemWidth
end
Now let’s see about touch.
Our scheme right now looks like this:
function touched(aTouch)
CodeaTestsVisible = false
Runner:touched(aTouch)
end
function GameRunner:touched(aTouch)
for i,b in ipairs(self.buttons) do
b:touched(aTouch, self.player)
end
end
Note that we don’t early-out. We just give everyone a shot. We’ll continue that practice:
function GameRunner:touched(aTouch)
for i,b in ipairs(self.buttons) do
b:touched(aTouch, self.player)
end
Inventory:touched(aTouch)
end
function Inventory:touched(aTouch)
for i,item in ipairs(inventory) do
item:touched(aTouch, self:drawingPos(i, self:count()))
end
end
function InventoryItem:touched(aTouch, pos)
if manhattan(aTouch.pos,pos) < ItemWidth then
print(self.name, " touched")
end
end
Of course they don’t have a name yet. It happens that their icon is a string, so let’s do this:
function InventoryItem:init(icon, name)
self.name = name or icon
self.icon = icon
end
We’ll either provide a name or use the (name of the) icon.
I think this may work. Well, it would work better if I used width over two.
Works perfectly. We should really check for touch state ENDED, or maybe BEGAN. I prefer ENDED, because then you ca touch the wrong thing and slide your finger to the right thing and then lift.
function InventoryItem:touched(aTouch, pos)
if aTouch.state == ENDED and manhattan(aTouch.pos,pos) < ItemWidth//2 then
print(self.name, " touched")
end
end
This works as intended. I can touch an icon and its name prints. This is good, and sufficient to the day.
Commit: Inventory items recognize when touched.
Summary
This has gone smoothly, even with time out for a lovely Sunday breakfast with my wife.
I’d be interested in feedback on what I did to figure out the coordinates for xPos
. It seemed to me to be a very naive discovery of the algorithm, almost by rote rather than doing a bunch of integer math which I’d probably get wrong.
But it was odd, because usually I’d do the integer math and get it wrong, then bash it until it wasn’t wrong any more. Tweet me up or email me or Slack me about what you think of that bit of the process.
Or anything else, really.
One way or another, we’re ready now to use our inventory items as buttons. That will surely lead to some fun.
See you next time!