Music? And then, idk, something else. Maybe refactoring? Poison?

I do think I’d like for the poisonous creatures to set up a health-draining effect on the princess, but I’ve not decided how to fix it. Maybe any health Loot should cure poison?

Anyway, I was going to start with music. I want to provide at least two tunes, one ongoing and another more rousing one when monsters are close by. We’ll evolve our way into that, of course.

Music in Codea is played using the music function, which includes this calling sequence, which I think we’ll use:

    music(tune,loop)

The tune parameter will be the asset to be played, and loop will be set to true to loop the tune.

To begin, I guess I’ll just start it going in createLevel:

...
    music(asset.downloaded.A_Hero_s_Quest.Dungeon,true)

That’s a suitably eerie selection, as you’ll hear in the videos from now on:

sound

Now this is just One More Darn Thing that’s going on inside GameRunner, which is already loaded with all kinds of gadgetry. So let’s quickly move on to a little object to deal with the music. We’ll need some interesting logic to deal with monster proximity.

So let’s assume a little object by intention, changing this:

    music(asset.downloaded.A_Hero_s_Quest.Dungeon,true)

To this:

    self.musicPlayer:dungeonMusic()

I’m just supposing we’ll have a music player that GameRunner knows about. It’s pretty much the central server for info like that. So in its init:

function GameRunner:init()
    self.tileSize = 64
    self.tileCountX = 85 -- if these change, zoomed-out scale 
    self.tileCountY = 64 -- may also need to be changed.
    self.tiles = {}
    for x = 1,self.tileCountX+1 do
        self.tiles[x] = {}
        for y = 1,self.tileCountY+1 do
            local tile = Tile:edge(x,y, self)
            self:setTile(tile)
        end
    end
    self.cofloater = Floater(self, 50,25,4)
    self.musicPlayer = MusicPlayer()
end

Nothing for it but to create a MusicPlayer, I guess. Naively, this:

-- MusicPlayer
-- RJ 20210219

MusicPlayer = class()

function MusicPlayer:init(dungeon,battle)
    self.dungeon = dungeon or asset.downloaded.A_Hero_s_Quest.Dungeon
    self.battle = battle or asset.downloaded.A_Hero_s_Quest.Battle
end

function MusicPlayer:dungeonMusic()
    music(self.dungeon,true)
end

function MusicPlayer:battleMusic()
    music(self.battle, true)
end

This will be enough to play the dungeon music still. But what about the danger / battle music? I think we’ll want it to start whenever a live monster is within range of the player, and to stop a short while after no live monsters are in range. We’ll want some kind of time delay to ensure that we don’t switch back and forth every second, and the Player will need to know what it’s currently playing.

We could TDD this. Honestly, I know I’m a bad person, but I’m just not seeing the value. So sue me, I’m here to show you what I do, and when I get in trouble, you get to see that as well.

So I code this:

function MusicPlayer:init(dungeon,battle)
    self.dungeon = dungeon or asset.downloaded.A_Hero_s_Quest.Dungeon
    self.battle = battle or asset.downloaded.A_Hero_s_Quest.Battle
    self.playing = nil
    self.locked = false
end

function MusicPlayer:dungeonMusic()
    if self.locked or self.playing == self.dungeon then return end
    music(self.dungeon,true)
    self.playing = self.dungeon
    self.locked = true
    tween.delay(3,self.unlock, self)
end

function MusicPlayer:battleMusic()
    if self.locked or self.playing == self.battle then return end
    music(self.battle, true)
    self.playing = self.battle
    self.locked = true
    self.delay(3,self.unlock, self)
end

function MusicPlayer:unlock()
    self.locked = false
end

And that clearly needs some refactoring, so before I go further, I do it.

function MusicPlayer:dungeonMusic()
    if self.locked or self.playing == self.dungeon then return end
    self:play(self.dungeon)
end

function MusicPlayer:battleMusic()
    if self.locked or self.playing == self.battle then return end
    self:play(self.battle)
end

function MusicPlayer:play(tune)
    music(tune,true)
    self.playing = tune
    self.locked = true
    tween.delay(3,self.unlock, self)
end

That looks reasonable. Now let’s see about switching the tune:

How about we turn on the battle music when a monster moves and is in range of the player?

function Monster:moveTowardAvatar()
    local dxdy = self.runner:playerDirection(self.tile)
    if dxdy.x == 0 or dxdy.y == 0 then
        self.tile = self.tile:legalNeighbor(self, dxdy)
    elseif math.random() < 0.5 then
        self.tile = self.tile:legalNeighbor(self,vec2(0,dxdy.y))
    else 
        self.tile = self.tile:legalNeighbor(self,vec2(dxdy.x,0))
    end
end

We can start the music right there. Where can we stop it? In the other move, I guess.

function Monster:makeRandomMove()
    self.runner:playDungeonMusic()
    local moves = {vec2(-1,0), vec2(0,1), vec2(0,-1), vec2(1,0)}
    local move = moves[math.random(1,4)]
    self.tile = self.tile:legalNeighbor(self,move)
end

function Monster:moveTowardAvatar()
    self.runner:playBattleMusic()
    local dxdy = self.runner:playerDirection(self.tile)
    if dxdy.x == 0 or dxdy.y == 0 then
        self.tile = self.tile:legalNeighbor(self, dxdy)
    elseif math.random() < 0.5 then
        self.tile = self.tile:legalNeighbor(self,vec2(0,dxdy.y))
    else 
        self.tile = self.tile:legalNeighbor(self,vec2(dxdy.x,0))
    end
end

And:

function GameRunner:playBattleMusic()
    self.musicPlayer:battleMusic()
end

function GameRunner:playDungeonMusic()
    self.musicPlayer:dungeonMusic()
end

A quick test tells me what’s wrong with this arrangement: there is always a monster out of range, so even if there is one in range, the out-of-range ones will turn the battle music back off. We’re going to have to be more clever than this.

Which may mean being more clever than I am, which I’m not sure I’m up to.

I also noticed that there can be monsters within range of the player but not visible, such as in nearby rooms or hallways. I think I’ll call that OK.

What we need to know is whether there are any monsters within range. That’s something that the GameRunner should figure out for us. So I’ll remove the two calls I just put in, and then:

I think we’ll check in moveMonsters, which goes like this:

function GameRunner:moveMonsters()
    for k,m in pairs(self.monsters) do
        m:chooseMove()
        --if math.random() > 0.67 then m:chooseMove() end
        --if math.random() > 0.67 then m:chooseMove() end
    end
end

I left those two commented lines in to remind me that monsters used to be able to move more than once.

By intention:

function Monster:chooseMove()
    -- return true if in range
    if not self.alive then return false end
    if self:distanceFromPlayer() <= 10 then
        self:moveTowardAvatar()
        return true
    else
        self:makeRandomMove()
        return false
    end
end

And …

function GameRunner:moveMonsters()
    local inRange = false
    for k,m in pairs(self.monsters) do
        if m:chooseMove() then inrange = true end
        --if math.random() > 0.67 then m:chooseMove() end
        --if math.random() > 0.67 then m:chooseMove() end
    end
    if inRange then self:playBattleMusic() else self:playDungeonMusic() end
end

I don’t love this but I think it works. Let me see. It does not. Why not? Oh. That typo:

function GameRunner:moveMonsters()
    local inRange = false
    for k,m in pairs(self.monsters) do
        if m:chooseMove() then inRange = true end
        --if math.random() > 0.67 then m:chooseMove() end
        --if math.random() > 0.67 then m:chooseMove() end
    end
    if inRange then self:playBattleMusic() else self:playDungeonMusic() end
end

Now then.

battle

So that’s nearly good. However, I don’t like requiring the side effect in chooseMove, and I’m not overly fond of the boolean either. Let’s just do the job separately, it won’t take much longer:

function GameRunner:moveMonsters()
    self:selectMusic()
    for k,m in pairs(self.monsters) do
        if m:chooseMove() then inRange = true end
        --if math.random() > 0.67 then m:chooseMove() end
        --if math.random() > 0.67 then m:chooseMove() end
    end
end
function GameRunner:selectMusic()
    local battleDistance = 5
    local tune = self.musicPlayer.dungeonMusic
    for k,m in pairs(self.monsters) do
        local d = m:distanceFromPlayer()
        if m:isAlive() and d <= battleDistance then
            tune = self.musicPlayer.battleMusic
        end
    end
    tune(self.musicPlayer)
end

This works nicely. The monster sheets do appear before the music, and I think we should change that value as well. Let me explain the code above, just in case it’s not clear.

The local tune is initialized to the musicPlayer’s method dungeonMusic, which is a function expecting the music player as its first argument. If we find a live monster within battleDistance, we set the tune to battleMusic. When we’ve checked all the monsters, we call the tune function, passing the expected musicPlayer to it, which plays the tune.

Because of the interlocking in MusicPlayer, we don’t mind the redundant calls to play the same tune, but after the tween delay, we will allow a switch.

Commit: We now have dungeon and battle music.

Let’s change the monster sheets to use the same distance value, if it’s not too difficult:

function Monster:drawSheet()
    if self:distanceFromPlayer() <= 5 then
        self.attributeSheet:draw()
    end
end

That’s already using 5. That’s fine.

Let’s reflect on this code, see if it’s OK.

function GameRunner:selectMusic()
    local battleDistance = 5
    local tune = self.musicPlayer.dungeonMusic
    for k,m in pairs(self.monsters) do
        local d = m:distanceFromPlayer()
        if m:isAlive() and d <= battleDistance then
            tune = self.musicPlayer.battleMusic
        end
    end
    tune(self.musicPlayer)
end

Hm. We have methods of our own in GameRunner to set the music. We should either use them or lose them. Let’s lose them for now. We have no external callers.

I could imagine that MusicPlayer could be made autonomous rather than having GameRunner calling it. That sounds like fun, let’s do it. I’ll create a MusicPlayer and keep it, but let it do its own thing.

In createLevel, I’ll remove the line that started the music and in init save a MusicPlayer linked back to GameRunner.

    self.musicPlayer = MusicPlayer(self)

The music player will need to get info form the GameRunner. And upgrade MusicPlayer:

function MusicPlayer:init(dungeon,battle)
    self.dungeon = dungeon or asset.downloaded.A_Hero_s_Quest.Dungeon
    self.battle = battle or asset.downloaded.A_Hero_s_Quest.Battle
    self.playing = nil
    self.locked = false
    tween.delay(1, self.decide,self)
end

We’ll decide once per second.

function MusicPlayer:decide()
    if self:hasMonsterNearPlayer() then
        self:battleMusic()
    else
        self:dungeonMusic()
    end
    tween.delay(1,self.decide,self)
end

How will we decide? We’ll ask GameRunner:

function MusicPlayer:hasMonsterNearPlayer()
    local range = 5
    return self.runner:hasMonsterNearPlayer(range)
end

And …

function GameRunner:hasMonsterNearPlayer(range)
    for k,m in pairs(self.monsters) do
        if m:isAlive() and m:distanceFromPlayer() <= range then return true end
    end
    return false
end

This works a treat. Very pleasing. Commit: MusicPlayer is now autonomous.

I am pleased with this. One minor objection might be the return of a boolean answering whether there’s a monster near the player but I don’t see anything better to do.

What shall we do now?

One possibility that appeals to me would be to cal it a morning, but let’s take a glance at GameRunner, since we’re here, and see if we can clean it up. I’m sure it at least has some methods out of order.

GameRunner also has a raft of creation methods including:

  • createCombatButtons
  • createInitialRoom
  • createLevel
  • createLoots
  • createRandomRooms
  • createThings
  • createTestRooms

Some of these are used in the creation of the level, others are for testing.

I see that createTestRooms isn’t used other than in a commented-out call in Main. Let’s remove that.

The createInitialRoom is also some testing hokum, so let’s remove that and the reference.

A bit of testing assures me that we’re all solid, so commit: remove unneeded code from GameRunner.

I should probably quit while I’m ahead, but let’s at least think about how poison might work. One possibility is to give the Player some kind of poisoned attribute that she knows about. But it just popped into my head that maybe we could create an invisible “poison” monster that is created when a poisonous creature bites, that follows the player around taking a health point every so often, until she dies or grabs a health heart somewhere.

There should be only one such thing, I suppose, and there’s no real reason for it to follow around, or move at all, since it can have access to the player object and ding her whenever it wants. But a poison object could still be a good way to do it.

We’ll think about that for tomorrow or some future day. For now, a quick summary and let’s ship this baby.

Summary

Again, we did something new and it sent in fine. We wound up with an autonomous music player that changes the music based on the situation. It only considers two aspects now, monsters close to player or not, but since it has access to the GameRunner, there’s no limit to what it might know about.

But we started with a very simple object that played under remote control and only knew one tune. Then we added another tune, and drove it remotely. Finally, we moved the decision into the music player itself.

Could we have gone that way right off the bat? Sure, had we thought of it. But I prefer to do things incrementally, and I generally find that that lets me take smaller steps, with everything working all along. And once we have the thing somewhere, in some kind of decent shape, it’s usually more clear how it might work.

In the case in hand, I had not thought of using a tween delay to lock a tune for a respectable interval, but once timing came to mind, I thought of tween. (I had been thinking of keeping track of time more manually, though I didn’t mention it at all.)

Then, having thought of tween, I guess it was natural to think of using it to check whether we should switch tunes. And there we were.

Then just a bit more cleaning up, to leave the campground better than we found it.

That’ll do for today. See you tomorrow, I imagine.


D2.zip