Dungeon 205
I just want to program a bit after all the screed-writing. No promises on what I’ll do or whether it’ll be any good at all.
“Retreat to the known”, they call it, the behavior of, say, a manager who goes back to programming, or an NCIS agent who builds a boat in his basement. It’s returning to a place of relative comfort. And, in my view, there’s no harm in it. It might be a message from the universe as to where we actually belong.
What’s before us in the Dungeon? Here are a few things that come to me as I review the “contents” of my “mind”, and the cards that serve as auxiliary storage.
We have a very simple Darkness thing, no more than a proof of concept. It might want to be a toggle. It might want to have a delayed effect. It might want to turn off the lights elsewhere in the dungeon. Hmm, that last would be interesting: how could we even do that? There might be a lever somewhere that turned the lights back on. The player might be able to use a torch. We’d probably have to devise some interesting things to make that work.
We have the notion that using more Publish / Subscribe, through the EventBus, might reduce coupling and improve the program overall. We might learn something from exploring that.
We could improve general game play, or the learning level, or add more interesting treasures, perhaps ones that interact with the environment more. Are Death Flies susceptible to any kind of bug spray? Would a bright light scare away a Ghost? Could we throw down a brick and dull the blade of an Ankle Biter?
Should there be treasure of monetary value and something to spend it on?
Puzzles, we keep talking about puzzles.
There’s duplication of “Magic Jar” in there somewhere. It reflects the existence of ideas that probably need merging, or at least a different kind of distinction.
There are methods that certain objects “must” implement. For example, anything that can be in the Dungeon must implement “query”. Is there some way we could enforce that, ideally before we ship this baby?
Hmm. That one’s interesting. Could we extend CodeaUnit somehow to help with that?
There’s duplication in the sprite notion, including AdjustedSprite and other approaches.
OK, what’s for today? Something easy. Something relaxing.
What’s for Today?
By golly, I’m going to do a brick. Really. Why not? Let’s see if we can find some brick art.
I haven’t found any but I think I’ve found something just as good: a rock. That’ll be just fine. I’ve moved the Rock graphic into D2, and now we’ll make a Rock kind of InventoryItem.
I think this all starts when we create Decor:
function GameRunner:createDecor(n)
local sourceItems = {
InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
InventoryItem{ icon="blue_jar", name="Magic Jar", object=self.player, method="spawnPathfinder", description="Magic Jar to create a Pathfinding Cloud Creature" },
InventoryItem{object=self.player},
InventoryItem{object=self.player},
InventoryItem{object=self.player},
}
local items = {}
for i = 1,n or 10 do
table.insert(items, sourceItems[1 + i%#sourceItems])
end
Decor:createRequiredItemsInEmptyTiles(items,self)
end
We do have another way that things come into being:
local RandomLootInfo = {
{"Strength", 4,9},
{"Health", 4,10},
{"Speed", 2,5 },
{"Pathfinder", 0,0},
{"Antidote", 0,0}
}
function GameRunner:createLoots(n)
for i = 1, n or 1 do
local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
local tile = self:randomRoomTile(self.playerRoom)
Loot(tile, tab[1], tab[2], tab[3])
end
end
These two approaches do need to be converged. But not today. Today I’m here to do something easy. So:
function GameRunner:createDecor(n)
local sourceItems = {
InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
InventoryItem{ icon="blue_jar", name="Magic Jar", object=self.player, method="spawnPathfinder", description="Magic Jar to create a Pathfinding Cloud Creature" },
InventoryItem{ icon="rock", name="Rock", object=self.player, method="dullSharpness", description="Mysterious Rock of Dullness" },
InventoryItem{object=self.player},
InventoryItem{object=self.player},
InventoryItem{object=self.player},
}
local items = {}
for i = 1,n or 10 do
table.insert(items, sourceItems[1 + i%#sourceItems])
end
Decor:createRequiredItemsInEmptyTiles(items,self)
end
function Sprites:initializeSprites()
...
sheet = asset.Rock
names = {"rock"}
Sprites:add(names,sheet,0,0,0,0)
This actually works, in that you can now find a rock.
Of course it doesn’t do anything if you touch it, since it’ll try to send the undefined message “dullSharpness”:
Inventory:268: attempt to call a nil value (field '?')
stack traceback:
Inventory:268: in method 'touched'
Inventory:206: in method 'touched'
GameRunner:567: in method 'touched'
Main:97: in function 'touched'
That’s here:
function InventoryItem:touched(aTouch, pos)
if aTouch.state == ENDED and manhattan(aTouch.pos,pos) < ItemWidth//2 then
Inventory:remove(self)
self:informObjectRemoved()
self.object[self.message](self.object, self.attribute, self.value)
end
end
So we’re trying to send dullSharpness
to the object, which is the Player. (This may not be ideal, but it’s how things work right now.)
function Player:dullSharpness()
local msg = "You have dulled all the sharp objects near by."
Bus:publish("addTextToCrawl", self, {text=msg})
end
The upshot now is an encouraging message:
How might we actually dull things? Well, what if the Ankle Biter subscribed to a message that the Player sent?
Looking into the future, arguably objects, when used, might publish on their own, rather than call someone who will then publish. For now, however:
function Player:dullSharpness()
local msg = "You have dulled all the sharp objects near by."
Bus:publish("addTextToCrawl", self, {text=msg})
Bus:publish("dullSharpness")
end
Getting a specific monster to subscribe will be a bit more difficult, as they are all just defined in the big monster table.
function Monster:initAttributes()
local health = CharacterAttribute(self:roll(self.mtEntry.health))
local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
local attrs = { _health=health, _strength=strength, _speed=speed }
self.characterSheet = CharacterSheet(attrs)
self.movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
local behavior = self.mtEntry.behaviorName or "normalBehavior"
self.behavior = Monster[behavior]
self:initSubscriptions()
end
New method: initSubscriptions.
function Monster:initSubscriptions()
if self.name == "Ankle Biter" then
Bus:subscribe(self, self.becomeDull, "dullSharpness")
end
end
Now the rubber meets the road. How do we do becomeDull
? What we need is for the monster to no longer do damage when it attacks. And, ideally, it would have new attacking messages, like “buzzes fruitlessly” instead of “saws at” or whatever it says now.
One thing at a time. I fear that the damage done is all wrapped up in CombatRound at present:
function CombatRound:rollDamage()
local damage = self.random(1,6)
self:applyDamage(damage)
end
There’s no provision at present for asking the fighting Entity how much damage it can do. Who even is calling this thing, and what does it know?
CombatRound has an attacker and a defender. So its attacker variable is the one we should ask. What if we did this:
function CombatRound:rollDamage()
if attacker.rollDamage then
damage = attacker:rollDamage()
else
local damage = self.random(1,6)
end
self:applyDamage(damage)
end
And this:
function Monster:rollDamage()
return math.random(self.damageRange[1], self.damageRange[2])
end
And this:
function Monster:initAttributes()
local health = CharacterAttribute(self:roll(self.mtEntry.health))
local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
local attrs = { _health=health, _strength=strength, _speed=speed }
self.characterSheet = CharacterSheet(attrs)
self.movementStrategy = self:selectMovementStrategy(self.mtEntry.strategy)
local behavior = self.mtEntry.behaviorName or "normalBehavior"
self.behavior = Monster[behavior]
self:initSubscriptions()
self.damageRange = {1,6}
end
And this:
function Monster:becomeDull()
self.damageRange = {0,0}
end
Now I’ve set Ankle Biter to be a level one monster. Let’s see if this works. I have doubts.
First problem, no self
:
function CombatRound:rollDamage()
if self.attacker.rollDamage then
damage = self.attacker:rollDamage()
else
local damage = self.random(1,6)
end
self:applyDamage(damage)
end
A few tests fail but I don’t think they’re germane, we’ll address them later.
Well, that didn’t work. The AB continued to do damage despite all our best efforts so far.
I need to make it a lot easier to find a rock, and to find an Ankle Biter. I’ll set all the other L1 monsters to L10 for now.
Ah. Monsters don’t have a .name
, they respond to :name()
, so this does the trick:
function Monster:initSubscriptions()
if self:name() == "Ankle Biter" then
Bus:subscribe(self, self.becomeDull, "dullSharpness")
end
end
I back out all my debug settings of monster level and decor setup, and now commit: Mysterious Rock of Dullness can dull an Ankle Biter attack to zero.
Let’s sum up. We need it.
Summary
My intention going in was just to pop something fun into the dungeon, and that was accomplished. But there are things to observe here.
One is that creating an inventory item involves creating a Sprite, adding initialization to the kinds of Decor that can appear (which are not dependent on level), creating an InventoryItem with a new method. The new method is probably on Player, although it may have little to do with the actual state of the Player.
Then the Player object needs to take some action based on the message from the deployment of the InventoryItem, which probably involves at least two steps, probably saying something, and certainly triggering action.
Triggering the action via Bus publication seems useful. That means that some other object or Entity needs to subscribe to the (probably new) publication.
We find that Monsters, in particular, are all one class, and that, therefore, if they need to condition what they subscribe to based on what kind of monster they are, the only means we have for that right now is to use the monster’s name property and to conditionally execute relevant subscribes. Just now, there’s only one but that won’t long endure.
We could generalize the fielding of the subscription by sending the same message all the time, with a detailed parameter saying what the issue really is, or we can add a new method to monster for every subscription. Probably the right thing is a combination of these, and maybe additional options.
We’d like for all this behavior to be controlled by and expressed in the monster definition table, the mtEntry table. At present, some is, and some isn’t. The mass of odd elements in those table entries suggests that there’s room for rationalizing and better arranging those items.
So … with this little tiny, nearly simple implementation, we’ve uncovered a lot of things under the rock.
Lots to learn about, lots of opportunities to improve the program.
We’ll do that soon! See you then!