Dungeon 289
Another run at the current Big Story. Need to find something that will lead me away from confusion rather than into it. Might have to think. Might even have to draw a picture.
The Big Story is something like this:
Better control over placing DungeonObjects. Counts, etc.
My rough end goal will be some kind of table-driven, text-file driven thing that lets someone specify both general rules for placement (these objects in these containers, randomly) and specific placement (exactly one of these, in this container, right here). There’s just about no end to what one might want to do with that problem. My purpose will be to learn about it. It’s part of what Hill would call the Making App, at the end of the day.
Right now, I have a much smaller story in mind. I changed the Chest, which used to always show a Health powerup when opened, which you could take by trying to move onto the Chest again. I changed it to accept an arbitrary item, and to add that item to your inventory immediately upon opening the Chest.
function Chest:open()
if self:isOpen() then return end
self.opened = true
self.pic = self.openPic
self.item:addToInventory()
end
I’ve decided that I don’t like that behavior. I want to go back to displaying the item and giving it when you bump the Chest again. But there are issues, and so far I’ve not found a way through them that I really like. Let me try to explain why it’s harder than it should be. Then I’ll try to find a way to make it easier.
Loot
There is a class called Loot
that serves as a container for DungeonObjects that can be found lying about in the open. This is as opposed to Decor, which appear as skeletons or barrels or something, and which,if they contain something, give it to you when you bump them.
The difficulty comes about because none of these schemes is done as neatly as it might be. I have the deplorable habit of setting myself some new problem, like “place some treasures lying about”, and once I’ve essentially cracked it, I tend to move on to something else, without really sorting out making the code clear and clean, and integrating it smoothly with related things.
I do that for two reasons, one of them pretty good. The pretty good one is that I want to emulate an ordinary product implementation, where nothing is perfect and we are under pressure to move on as soon as something works. The not so good one is that this is also a bad habit of mine: when something is basically cracked, I want to work on a new problem.
So, whether it’s a good thing, because I want to show you what real programming is like, or it’s a bad thing, the fact is, there are a lot of pretty tenuous connections between all these cooperating objects, and they’re causing me trouble.
There’s enough of it that I’m not really able to describe things accurately without looking at the code, but here are some things that I think are going on:
- InventoryItems
-
InventoryItems have associated values including the message to publish when they are used, and additional values such as the attribute to improve, e.g. Health, and the amount to improve it by, e.g. 4 points. So when you’re given such an item, a Bus publication triggers a subscriber to the message to check the attributes and increment your CharacterSheet.
-
I am not sure whether the specific value are retained in Inventory. I know that the inventory display shows a count, so if you have found three Health things, it’ll show the bottle and a “3”. But whether behind that are the actual three healths with their three different values, I am not sure.
- Creation
-
InventoryItems are defined in a single table somewhere, listing all the items that can be created. Those, of course, have a default value for the number of points they add, if that’s the kind of thing they are. So in some cases when code creates such an item, it sets the desired value into the table, replacing the default.
-
I am not sure whether that modifies the single instance of that Item, or whether it has made a new one. The latter is what it should do.
- Name References
-
Some objects, and I think Loot may be one of these, are created with nothing but the name of the Item they should give. The name is looked up in some table, probably the “single table” referred to above, and the item (or a copy?) is referred to in the Loot.
What is supposed to be happening right now is that all the Loots, Decor and Chests and such are created randomly, and as such can contain any kind of treasure that exists. What’s really happening is different from that, and it’s different for Loot from Decor from Chests. I think. I don’t have all this memorized, and it’s not consistent.
Therefore …
What needs to be done is to get our stuff together up in this dungeon. In my prior two or three attempts at the simple story of getting the Chest to display its gift before giving it, I’ve crashed and burned through confusion or though winding up with code that just didn’t seem right.
So today’s mission is to take very small steps, everyone of which makes the overall design better, until we are in a good enough position to more to a bigger part of the overall Big Story.
I think what I’d like to do is arrange things so that however you create an InventoryItem, the powerup ones will be created with a random add-value, between some standard values. Once that is done, I want to look at providing an override value instead, so that you can request a +8 Health if you want one.
And if I recall how Loot works, it is just given the name of the gift, so we’ll want that to continue to be possible.
I think I know one thing almost for sure, and that’s how to do the Chest story:
A Chest is created containing an InventoryItem. When the Chest is opened, it will create a Loot containing that item, and place it on the Chest’s own tile. That will make it appear. Then the Loot logic will provide the item to the Player when next bumped.
I’m glad we had this little chat. Why should we provide an Item to the Chest and require it to create a Loot? Why don’t we give it the darn Loot to begin with? Among other good things, that would uncouple Chest from knowing anything about Loot. So now the plan is:
A Chest is created containing a Loot (containing an InventoryItem). When the Chest is opened, it will place the Loot on the Chest’s own tile. That will make it appear. Then the Loot logic will provide the item to the Player when next bumped.
Where should I start?
I could either start working on making Loot better, or I could change Chest (and those who create them) to have a Loot. Then when Loot is better, Chest wouldn’t need to change (though Chest creation might).
I’m inclined to finish Chest’s new little story and then move one. That way I have a legitimate visible improvement to ship.
Here we go.
Chest
Here are the key methods in Chest that we need to deal with:
function Chest:init(tile, item)
tile:moveObject(self)
self.closedPic = "mhide01"
self.openPic = asset.open_chest
self.pic = self.closedPic
self.opened = false
self.item = item or InventoryItem("health")
end
function Chest:open()
if self:isOpen() then return end
self.opened = true
self.pic = self.openPic
self.item:addToInventory()
end
We want to provide a Loot now, not an item. And I don’t think we want to allow a default. (There might be an EmptyLoot someday, but if people aren’t creating Chests the way we need, let’s not fix it for them.)
function Chest:init(tile, loot)
tile:moveObject(self)
self.closedPic = "mhide01"
self.openPic = asset.open_chest
self.pic = self.closedPic
self.opened = false
self.loot = loot or error("Chest must be given a Loot")
end
Now in open …
function Chest:open()
if self:isOpen() then return end
self.opened = true
self.pic = self.openPic
self:currentTile():moveObject(self.loot)
end
I think this should work. We tell our own tile to move the loot to itself. We’ll need to be sure that Loots can be created with no tile, I guess. Let’s find where we create our Chests and give them a loot.
function DungeonBuilder:placeChests(numberToCreate)
local sourceItems = {
InventoryItem("cat_persuasion"),
InventoryItem("strength"),
InventoryItem("health"),
InventoryItem("nothing")
}
for i = 1,numberToCreate do
local item = sourceItems[1+i%#sourceItems]
local tile = self:randomRoomTileAvoidingRoomNumber(self.playerRoomNumber)
Chest(tile,item)
end
end
While finding this I realize that there are quite a few tests that are going to explode due to requiring a Loot. I’m tempted to change the plan, but let’s not.
Can I “just” change this to create a Loot and pass that to the Chests I create? Let’s see how to create a Loot:
function Loot:init(tile, kind, min, max)
self.kind = kind
self.icon = self:getIcon(self.kind)
self.min = min
self.max = max
self.desc = LootDescriptions[self.kind] or self.kind
self.message = "I am a valuable "..self.desc
if tile then tile:moveObject(self) end
end
Now here’s a crock. A Loot gets a “kind”, which it uses to look up some values. Then later …
function Loot:actionWith(aPlayer)
local item
self:currentTile():removeContents(self)
item = self:createInventoryItem(aPlayer)
item:addToInventory()
end
function Loot:createInventoryItem(aPlayer)
return InventoryItem(self.kind, math.random(self.min,self.max))
end
It creates the inventory item on the fly. I don’t like this. However, to make the Chest work, I guess I don’t care. Loots can currently accept these types:
local LootIcons = {strength="blue_pack", health="red_vial", speed="green_flask",
pathfinder="blue_jar", curePoison="red_vase"}
local LootDescriptions = {strength="Pack of Steroidal Strength Powder", health="Potent Potion of Health", speed="Potion of Monstrous Speed" }
So let’s just make Chest provide one of those, curePoison
. I’ll try this. I’m feeling this is an experiment but that it is close to right enough.
Lots of tests will fail. Game might run. Test. It nearly works. The random
failed, because I didn’t pass in any numbers. Let’s fix Loot:
function Loot:createInventoryItem(aPlayer)
local value
if self.min and self.max then
value = math.random(self.min,self.max)
end
return InventoryItem(self.kind, value)
end
Test again. Seems to work, with one odd bit. If you look carefully at the item in the Inventory bar, there is a shadow of a skeleton behind it. It is possible but unlikely that the Vase has that picture. I’ll try some other gift.
Before bumping the Chest:
After first bump: the thing is visible!
After second bump: we got the thing!
Another gift does not show that shadow … and when I find a Red Vase free-standing, it also does not show that shadow in Inventory. I’ve made a sticky for it and plan to move on.
So things are working. Let’s fix the tests. They all look about like this:
_:test("player can't enter chest tile", function()
local chestTile = Tile:room(10,10,runner:tileFinder())
local chest = Chest(chestTile)
local playerTile = Tile:room(11,10,runner:tileFinder())
local player = Player(playerTile,runner)
_:expect(chest:isOpen()).is(false)
local chosenTile = playerTile:validateMoveTo(player, chestTile)
_:expect(chosenTile).is(playerTile)
_:expect(chest:isOpen()).is(true)
end)
I decide to back down from my prior rule that Chest requires a Loot, and to allow it to be nil.
function DungeonContentsCollection:moveObjectToTile(object,tile)
--requires DungeonObject to be defined way forward in tabs.
if object then
self.contentMap[object] = tile
self:dirty()
end
end
The change there means that a nil object can be moved to a tile … and will ultimately be ignored. So no one has to check further up in the chain.
Also I figured out what the shadow was. The Inventory background is slightly transparent, and there was a skeleton on the tile that was under the window, so it showed through a bit. Nothing to worry about.
Tests are good. Let’s make Chests contain all the objects we originally intended:
function DungeonBuilder:placeChests(numberToCreate)
local sourceItems = {
"cat_persuasion",
"strength",
"health",
"nothing"
}
for i = 1,numberToCreate do
local kind = sourceItems[1+ i%#sourceItems]
local loot = Loot(nil, kind, 4,10)
local tile = self:randomRoomTileAvoidingRoomNumber(self.playerRoomNumber)
Chest(tile,loot)
end
end
I’m not sure about the “nothing”, but it used to work. We’ll see …
Loot:49: expect userdata, got nil
stack traceback:
[C]: in function 'sprite'
Loot:49: in method 'draw'
DungeonContentsCollection:113: in method 'draw'
GameRunner:164: in method 'drawMapContents'
GameRunner:130: in method 'draw'
Main:102: in function 'draw'
That’s probably the nothing right there. I’ll test a bit more to be sure that doesn’t always happen. It seems to always happen. We need to make Loot smarter.
I am not worried at this point. I’m feeling that we’re on a decent track and we’re just sorting the relationship between Loot and Chest out a bit. We will have more changes to do later, I expect.
Right, Loot doesn’t understand my requests … let’s just pick ones that it does understand.
local LootIcons = {strength="blue_pack", health="red_vial", speed="green_flask",
pathfinder="blue_jar", curePoison="red_vase"}
local LootDescriptions = {strength="Pack of Steroidal Strength Powder", health="Potent Potion of Health", speed="Potion of Monstrous Speed" }
function DungeonBuilder:placeChests(numberToCreate)
local sourceItems = {
"curePoison",
"strength",
"health",
"pathfinder",
"speed"
}
for i = 1,numberToCreate do
local kind = sourceItems[1+ i%#sourceItems]
local loot = Loot(nil, kind, 4,10)
local tile = self:randomRoomTileAvoidingRoomNumber(self.playerRoomNumber)
Chest(tile,loot)
end
end
Test. We’re good. All those items have turned up in Chests. Commit: Chests now created with various gifts. Show gift. Bump again to receive gift.
Retro- and Prospective
This went smoothly. It wasn’t what I’d call planned. As usual, I just followed my nose to make things work, but that, it seems to me, is the way we mostly use in real code.
We have deferred creation of InventoryItems to Loot:
function Loot:actionWith(aPlayer)
local item
self:currentTile():removeContents(self)
item = self:createInventoryItem(aPlayer)
item:addToInventory()
end
function Loot:createInventoryItem(aPlayer)
local value
if self.min and self.max then
value = math.random(self.min,self.max)
end
return InventoryItem(self.kind, value)
end
And it is still happening when, and only when, you actually bump the Loot. Meanwhile, the Loot knows what icon to display. What we ought to do, it seems to me, is create the item when we create the Loot, and have the Loot look through the item to find the icon to display. I’ll make a couple of cards (yellow stickies):
Loot should create Item on init, use Item for display.
Loot able to create all Items? by name
Loot needs a “nothing” item.
That should be enough to get us started tomorrow. Since this is the second article for today, let’s call it a day.
Things are going more smoothly today. Partly due to prior experience, partly due to more carefully picking a small step. Either way. a good day. See you next time!