I reckon it’s time to kill the ship when it’s hit by an asteroid. I have a clever way to do it and a direct way. Which is best?

We have the ship colliding with asteroids and saying “BLAMMO”. (Perhaps it should have said “OUCH!”) But the ship merrily carries on after the collision. What is supposed to happen is that it’s “destroyed” by the collision, disappears except for some debris (BLAMMO) and then, if you have lives left, a new ship spawns and you carry on.

I don’t remember whether a new wave of asteroids starts, or whether you spawn in the middle of an existing onslaught. I’ll watch a video about that, and/or read the code, when I need to know. For now, I’d like the ship to “go away” and after a while, come back.

I can think of at least two ways to accomplish the basic go-away/come-back behavior. One would be to put a flag or timer in the ship and while it is counting down, the ship is invisible and unconsidered by the buttons and asteroids and whatever cares about the ship. Another way would be to destroy the ship and have a timer elsewhere, presumably in Universe, that spawns a ship when the time is up. In that form, some ship-related code just wouldn’t happen, but the findCollision code needs to know not to look, and other

But I have a clever idea lurking in my head as well. Remember tween, that function that we use to make the Splat expand? It takes a table as a parameter and tweaks values in that table, and can optionally call a callback function when the tween ends. So when we kill the ship, whether we really destroy it or just hide it, we could start up a tween that would call back when the time is up.

I really love clever ideas, and I’ve tried to train myself never to put them into the code. But here, I think I’ll give it a go. It should be straightforward enough. We’ll decide after we do it.

So here goes. It’s 0955, by the way, and I’m typing on Slack a bit as well as doing this. That may be a bad idea.

Kill the Ship

We have an empty function killShip, I believe, which is called when the ship is hit by an asteroid:

function Universe:checkShipCollision(asteroid)
    if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
        scoreAsteroid(asteroid)
        splitAsteroid(asteroid, self.asteroids)
        killShip()
    end
end

I think I’ll try actually destroying the ship object. That will require us to deal with ship being nil … unless …

If there are no asteroids, findCollision works just fine: it loops over all the asteroids, of which there are none, and nothing happens. If we put the ship in a collection of ships and looped over it, when the ship is gone, nothing will happen. Let’s explore that approach.

In Universe:

function Universe:init()
    self.score = 0
    self.missileVelocity = vec2(MissileSpeed,0)
    self.button = {}
    createButtons()
    self.ship = createShip()
    self.processorRatio = 1.0
    self.asteroids = {}
    self.missiles = {}
    self.explosions = {}
end

If we make a collection self.ships instead, and put the ship into it in the way we do asteroids, that might be a good thing.

Now here is a place where if our Ship were an object, things might go better. As it is, we have Universe calling drawShip, which is a function (not method) in the Ship tab, and it goes ahead and draws:


function drawShip()
    local sx = 10
    local sy = 6
    pushStyle()
    pushMatrix()
    translate(Ship.pos.x, Ship.pos.y)
    rotate(math.deg(Ship.radians))
    strokeWidth(2)
    stroke(255)
    line(sx,0, -sx,sy)
    line(-sx,sy, -sx,-sy)
    line(-sx,-sy, sx,0)
    popMatrix()
    popStyle()
end

Note that that code refers to the variable Ship, which is local to Ship tab, not to U.ship as it arguably should. This is the kind of thing that happens when you have a procedural design and when you let it get a bit out of hand.

And, fact is, we almost always do let it get a bit out of hand. We don’t even realize at the time we do something like that that it’s not the best way, and when we do find a better way, we may or may not remember to go back and fix the not so better ways. This, by the way, is the original meaning of “technical debt” as Ward Cunningham originally spoke of it. It’s the inevitable difference in the code between what we knew then and what we know now.

We reduce that debt, sometimes in big collapses as we apply some new understanding most everywhere, and sometimes in small improvements as we run across them.

Remind me to talk about cleaning code we’re not working on, in Summing Up.

Anyway, I don’t like that global, and there’s no better time than now to get rid of it.

Looking at Ship tab, I think we can rather quickly convert Ship to an object, and I think we’ll be glad we did. Let’s see if I’m right about either of those ideas. Here’s what it looks like now:

-- Ship
-- RJ 20200520

local Ship = {}

local rotationStep = math.rad(1) -- one degree in radians

function createShip()
    Ship.pos = vec2(WIDTH, HEIGHT)/2
    Ship.radians = 0
    Ship.step = vec2(0,0)
    return Ship
end

function drawShip()
    local sx = 10
    local sy = 6
    pushStyle()
    pushMatrix()
    translate(Ship.pos.x, Ship.pos.y)
    rotate(math.deg(Ship.radians))
    strokeWidth(2)
    stroke(255)
    line(sx,0, -sx,sy)
    line(-sx,sy, -sx,-sy)
    line(-sx,-sy, sx,0)
    popMatrix()
    popStyle()
end

function moveShip()
    if U.button.left then Ship.radians = Ship.radians + rotationStep end
    if U.button.right then Ship.radians = Ship.radians - rotationStep end
    if U.button.fire then if not Ship.holdFire then fireMissile() end end
    if not U.button.fire then Ship.holdFire = false end
    actualShipMove()
end

function actualShipMove()
    if U.button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.step = Ship.step + accel
        Ship.step = maximize(Ship.step, 3)
    end
    finallyMove(Ship)
end

function finallyMove(ship)
    U:moveObject(ship)
end

function maximize(vec, size)
    local s = vec:len()
    if s <= size then
        return vec
    else
        return vec*size/s
    end
end

function fireMissile()
    Ship.holdFire = true
    Missile(Ship)
end

I’m just going to tick right down through those functions and convert them, and then see what needs changing. The additional changes should all be in Universe if we’re at all lucky.

Ship = class()

local rotationStep = math.rad(1) -- one degree in radians

function Ship:init()
    self.pos = vec2(WIDTH, HEIGHT)/2
    self.radians = 0
    self.step = vec2(0,0)
end

That’ll create the ship and automatically return it. Universe has one call to createShip and the tests have three. I think I’ll fix all of those right now:

function Universe:init()
    self.score = 0
    self.missileVelocity = vec2(MissileSpeed,0)
    self.button = {}
    createButtons()
    self.ship = Ship() -- CHANGED
    self.processorRatio = 1.0
    self.asteroids = {}
    self.missiles = {}
    self.explosions = {}
end

The tests go like this:

        _:test("Missile fired at rest", function()
            local ship = Ship()
            local missile = Missile(ship)
            _:expect(missile.step).is(U.missileVelocity)
        end)
        
        _:test("Missile fired north", function()
            local ship = Ship()
            ship.radians = math.pi/2
            local missile = Missile(ship)
            local mx = missile.step.x
            local my = missile.step.y
            _:expect(mx).is(0, 0.001)
            _:expect(my).is(U.missileVelocity.x, 0.001)
        end)
        
        _:test("Missile fired from moving ship", function()
            local ship = Ship()
            ship.step = vec2(1,2)
            local missile = Missile(ship)
            local mx = missile.step.x
            local my = missile.step.y
            _:expect(mx).is(U.missileVelocity.x + 1, 0.001)
            _:expect(my).is(U.missileVelocity.y + 2, 0.001)
        end)

All the references to ship used to be U.ship. Because the ship is nearly an object, we can use a local one. This is actually better than it was before. The tests run. The game does not, which is no surprise. It’s useful to look at why, since it will point to things that need to be changed.

The error is in drawShip, which is accessing capital-S Ship, which is now the class rather than the former ship. drawShip was next on our list to convert anyway:

function Ship:draw()
    local sx = 10
    local sy = 6
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    rotate(math.deg(self.radians))
    strokeWidth(2)
    stroke(255)
    line(sx,0, -sx,sy)
    line(-sx,sy, -sx,-sy)
    line(-sx,-sy, sx,0)
    popMatrix()
    popStyle()
end

I renamed it draw, which is our convention, and changed the ship references to self. Now to change the call to drawShip in Universe:

    self.ship:draw()

Right after that in Universe is:

    moveShip()

I’ll change that too:

    self.ship:move()

And convert moveShip:

function Ship:move()
    if U.button.left then self.radians = self.radians + rotationStep end
    if U.button.right then self.radians = self.radians - rotationStep end
    if U.button.fire then if not self.holdFire then fireMissile() end end
    if not U.button.fire then self.holdFire = false end
    actualShipMove()
end

I’m just going to go right ahead and change all these calls to methods. I could leave them in place and pass self as an argument but in for a penny:

-- Ship
-- RJ 20200520

Ship = class()

local rotationStep = math.rad(1) -- one degree in radians

function Ship:init()
    self.pos = vec2(WIDTH, HEIGHT)/2
    self.radians = 0
    self.step = vec2(0,0)
end

function Ship:draw()
    local sx = 10
    local sy = 6
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    rotate(math.deg(self.radians))
    strokeWidth(2)
    stroke(255)
    line(sx,0, -sx,sy)
    line(-sx,sy, -sx,-sy)
    line(-sx,-sy, sx,0)
    popMatrix()
    popStyle()
end

function Ship:move()
    if U.button.left then self.radians = self.radians + rotationStep end
    if U.button.right then self.radians = self.radians - rotationStep end
    if U.button.fire then if not self.holdFire then self:fireMissile() end end
    if not U.button.fire then self.holdFire = false end
    self:actualShipMove()
end

function Ship:actualShipMove()
    if U.button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        self.step = self.step + accel
        self.step = maximize(self.step, 3)
    end
    self:finallyMove()
end

function Ship:finallyMove()
    U:moveObject(self)
end

function maximize(vec, size)
    local s = vec:len()
    if s <= size then
        return vec
    else
        return vec*size/s
    end
end

function Ship:fireMissile()
    self.holdFire = true
    Missile(self)
end

Note that I left maximize as a function. It’s a utility and not properly part of the ship at all.

The game runs properly. Commit: “ship is an object”. It’s 1033. That took 38 minutes start to finish, and now we have a nice clean ship object to work with.

Killing, Remember?

We are here to kill the ship. All this – thewhole 33 minutes of it – was cleaning things up so that we can deal with the ship being gone.

There are now three places that I can think of where we’ll have to be sensitive to the ship being gone.

We can’t call draw or move if we don’t have a ship. And wee can’t check collisions if we don’t have a ship. Let’s first just do the obvious check:

function Universe:draw()
    --displayMode(FULLSCREEN_NO_BUTTONS)
    pushStyle()
    background(40, 40, 50)
    self.processorRatio = DeltaTime/0.0083333
    self:drawAsteroids()
    self:drawExplosions()
    checkButtons()
    drawButtons()
    if self.ship then self.ship:draw() end
    if self.ship then self.ship:move() end
    self:drawMissiles()
    drawSplats()
    U:drawScore()
    popStyle()
    U:findCollisions()
end

function Universe:findCollisions()
    for i,a in pairs(self.asteroids) do
        self:checkMissileCollisions(a)
        if self.ship then self:checkShipCollision(a) end
    end
end

This should run just fine. And it does. Now, at last, we can do something about killShip, which looks like this:

function killShip()
    Explosion(U.ship)
end

It's not a member function on Universe, because I wasn't comfortable with that. In any case, we'll now just nil out the ship:

~~~lua
function killShip()
    Explosion(U.ship)
    U.ship = nil
end

That should make the ship disappear at the time of BLAMMO. And it does:

ship gone

Now to make it come back, I’ll try the tween trick, and I think I’ll put it up in the checkShipCollision function, though I am not entirely happy with that.

No. On second thought, I’ll promote killShip to a method and put it there. Here’s my first try:

function Universe:killShip()
    local f = function()
        self.ship = Ship()
    end)
    Explosion(U.ship)
    U.ship = nil
    self.dummy = 0
    tween(6, self, {dummy=10}, tween.easing.linear, f)
end

The tween will count dummy from 0 to 10, then call f, which will create a new ship. The delay is 6 seconds, which should be enough to be scary. I put the dummy in there because I believe the tween won’t run if there isn’t anything to change. We’ll see.

Now to test. And it works. The ship goes away and comes back about six seconds later. I’ll try it with an empty change set just to be sure. I’d rather it worked that way.

Ah, it does work. My experiment the other day must have been wrong. Here we are now:

function Universe:killShip()
    local f = function()
        self.ship = Ship()
    end
    Explosion(U.ship)
    U.ship = nil
    tween(6, self, {}, tween.easing.linear, f)
end

The BLAMMO stays on the screen. Let’s make it disappear also, while we’re here. No, first let’s commit: “respawn”. Now then:

function Explosion:init(ship)
    self.pos = ship.pos
    self.step = vec2(0,0)
    U.explosions[self] = self
end

We can put a tween in here too, I’d think:

function Explosion:init(ship)
    local f = function()
        U.explosions[self] = nil
    end
    self.pos = ship.pos
    self.step = vec2(0,0)
    U.explosions[self] = self
    tween(4, self, {}, tween.easing.linear, f)
end

That ought to make the BLAMMO go away in four seconds.

WHOA! Guess what! In my superficial testing, I never moved the ship, I just watched asteroids go by and shot them. When I go to accelerate, it breaks:

Bad argument to rotate in actualShipMove. Let’s see:

function Ship:actualShipMove()
    if U.button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        self.step = self.step + accel
        self.step = maximize(self.step, 3)
    end
    self:finallyMove()
end

Sure enough, a reference to Ship that should be self. That fixes that little problem. Better think about that in Summing Up as well.

Anyway things are working well now. Here’s a video of me killing asteroids and them killing me:

kills

Wow, that was a good run. Every time the ship respawned an asteroid hit it before I could get near a button. Good test and things seem to be working as intended.

Let’s sum up.

Summing Up

There are a couple of promised topics. Let me get those out of the way first:

Cleaning code we’re not working on

Throughout this project, I have often made improvements to the code based on what I saw in the design and what I thought would be better. The point of this was to demonstrate that incremental design improvements are always possible and that we can do them in small enough bites that it doesn’t slow us down. When the design is better, subsequent work goes faster.

However, on a real project, I wouldn’t recommend just going in willy-nilly and improving code. The reason is that our management looks to us for new and improved capability in the product, not nicer and nicer code. So on a real project, what I would do would be to improve code only when we were going to work on it anyway, to add a new capability or change an old one per our product leadership.

The reasoning is pretty simple. Code that we are not working in doesn’t hurt us, no matter how ugly it is. We don’t see the ugliness and can ignore it until we have to go into that area. So time spent working on that code is wasted, unless and until we actually have to change it for product reasons rather than reasons of code clarity.

Naturally, ugly code is more likely to have defects in it, so we may be called upon to fix those defects, and then we have a good reason to improve that code. I’ve written a separate article about this: Refactoring - Not On the Backlog.

Of course you get to make your own decisions about what to work on, but in general I think we do best to let the product definition process point us into the code and then wherever we are pointed, we improve things incrementally as needed.

Missed converting Ship to self.

Then there was today’s surprise defect. For a “long time”, perhaps as long as a half an hour, there was a defect in the program such that the ship could not accelerate, because in converting the Ship to a class, I had missed a reference to the former variable that pointed to the Ship instance. That reference needed to say self.

Codea does have a sort of global replace function, and had I trusted it, I could have bulk-converted all the “Ship” references at once. But Codea will convert “Shipshape” to “selfshape” if you’re not careful, and it doesn’t show very well where it’s going to make the next change, nor is there a way to skip a change as far as I know.

So I don’t use that capability and I make mistakes.

What’s to do? It’s sure to happen again. It might be possible to write a microtest to move the ship, now that it’s an object. That might be a fun exercise for the next article. Maybe. Or I could “remember” always to fly the ship around more extensively during testing.

Yeah, remember. I remember when I used to actually remember stuff. Even then I never remembered to do the good things.

At this moment, I don’t really see a good way to better ensure that I don’t make mistakes like these. And that’s troubling, because it means they’ll continue. All I can do is take a moment to reflect, like we’re doing now, and sometimes I get an idea for an improvement to my practice that will actually work. When I get an idea like that, I try it. When I don’t, I forgive myself and move on.

Today, it’s forgive and move on, unless a reader comes up with a good idea.

What else?

Well, we set out to implement the ship killing and respawning, and we did just that. We used a moderately clever thing, the tween, to implement our timing. It turned out rather well. Here’s why I like it:

If we did timing in the ordinary way, we’d set a timer somewhere, probably in Universe, and then we’d put in code to tick and check the timer, and code to do things when the timer expired. And there would be more and more timers. We already have three tweens doing timing, so there would be at least three timers.

And things that happened when the timers expired would be all over the universe, in explosions, in splats, in Universe itself, and so on.

The use of tween to do our timing lets us put the timer where the action is.

One improvement we will surely want to make is to put the various timing constants into Universe for safe keeping. But overall, I like how the tween handled our timing concerns.

And I like how the morning went. Today I’ll remember to provide the code, whose final commit comment is “blammo disappears, ship comes back”.

Bye for now!



--# Main
-- Asteroids
-- RJ 20200511

Touches = {}

function setup()
    U = Universe()
    U:createAsteroids()
    Score = 0
end

function draw()
    U:draw()
end

function touched(touch)
    if touch.state == ENDED or touch.state == CANCELLED then
        Touches[touch.id] = nil
    else
        Touches[touch.id] = touch
    end
end

--# TestAsteroids
-- TestAsteroids
-- RJ 20200511
    
function testAsteroids()
    CodeaUnit.detailed = true

    _:describe("Asteroids First Tests", function()

        _:before(function()
            -- Some setup
        end)

        _:after(function()
            -- Some teardown
        end)
        
        _:test("Hookup", function()
            _:expect( 2+1 ).is(3)
        end)
        
        _:test("Random", function()
            local min = 100
            local max = 0
            for i = 0,1000 do
                local rand = math.random()*2*math.pi
                if rand < min then min = rand end
                if rand > max then max = rand end
            end
            _:expect(min < 0.01).is(true)
            _:expect(max > 6.2).is(true)
        end)
        
        _:test("Rotated Length", function()
            for i = 0, 1000 do
                local rand = math.random()*2*math.pi
                local v = vec2(1.5,0):rotate(rand)
                local d = v:len()
                _:expect(d > 1.495).is(true)
                _:expect(d < 1.505).is(true)
            end
        end)
        
        _:test("Some rotates go down", function()
            local angle = math.rad(-45)
            local v = vec2(1,0):rotate(angle)
            local rvx = v.x*1000//1
            local rvy = v.y*1000//1
            _:expect(rvx).is(707)
            _:expect(rvy).is(-708)
        end)
        
        _:test("Bounds function", function()
            _:expect(U:keepInBounds(100,1000)).is(100)
            _:expect(U:keepInBounds(1000,1000)).is(0)
            _:expect(U:keepInBounds(1001,1000)).is(1)
            _:expect(U:keepInBounds(-1,1000)).is(999)
        end)
        
        _:test("Missile fired at rest", function()
            local ship = Ship()
            local missile = Missile(ship)
            _:expect(missile.step).is(U.missileVelocity)
        end)
        
        _:test("Missile fired north", function()
            local ship = Ship()
            ship.radians = math.pi/2
            local missile = Missile(ship)
            local mx = missile.step.x
            local my = missile.step.y
            _:expect(mx).is(0, 0.001)
            _:expect(my).is(U.missileVelocity.x, 0.001)
        end)
        
        _:test("Missile fired from moving ship", function()
            local ship = Ship()
            ship.step = vec2(1,2)
            local missile = Missile(ship)
            local mx = missile.step.x
            local my = missile.step.y
            _:expect(mx).is(U.missileVelocity.x + 1, 0.001)
            _:expect(my).is(U.missileVelocity.y + 2, 0.001)
        end)

    end)
end

--# Shapes
RR1 = {
  vec4(0.000000, 2.000000, 2.000000, 4.000000),
  vec4(2.000000, 4.000000, 4.000000, 2.000000),
  vec4(4.000000, 2.000000, 3.000000, 0.000000),
  vec4(3.000000, 0.000000, 4.000000, -2.000000),
  vec4(4.000000, -2.000000, 1.000000, -4.000000),
  vec4(1.000000, -4.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, -4.000000, -2.000000),
  vec4(-4.000000, -2.000000, -4.000000, 2.000000),
  vec4(-4.000000, 2.000000, -2.000000, 4.000000),
  vec4(-2.000000, 4.000000, 0.000000, 2.000000)
}

RR2 = {
  vec4(2.000000, 1.000000, 4.000000, 2.000000),
  vec4(4.000000, 2.000000, 2.000000, 4.000000),
  vec4(2.000000, 4.000000, 0.000000, 3.000000),
  vec4(0.000000, 3.000000, -2.000000, 4.000000),
  vec4(-2.000000, 4.000000, -4.000000, 2.000000),
  vec4(-4.000000, 2.000000, -3.000000, 0.000000),
  vec4(-3.000000, 0.000000, -4.000000, -2.000000),
  vec4(-4.000000, -2.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, -1.000000, -3.000000),
  vec4(-1.000000, -3.000000, 2.000000, -4.000000),
  vec4(2.000000, -4.000000, 4.000000, -1.000000),
  vec4(4.000000, -1.000000, 2.000000, 1.000000)
}

RR3 = {
  vec4(-2.000000, 0.000000, -4.000000, -1.000000),
  vec4(-4.000000, -1.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, 0.000000, -1.000000),
  vec4(0.000000, -1.000000, 0.000000, -4.000000),
  vec4(0.000000, -4.000000, 2.000000, -4.000000),
  vec4(2.000000, -4.000000, 4.000000, -1.000000),
  vec4(4.000000, -1.000000, 4.000000, 1.000000),
  vec4(4.000000, 1.000000, 2.000000, 4.000000),
  vec4(2.000000, 4.000000, -1.000000, 4.000000),
  vec4(-1.000000, 4.000000, -4.000000, 1.000000),
  vec4(-4.000000, 1.000000, -2.000000, 0.000000)
}

RR4 = {
  vec4(1.000000, 0.000000, 4.000000, 1.000000),
  vec4(4.000000, 1.000000, 4.000000, 2.000000),
  vec4(4.000000, 2.000000, 1.000000, 4.000000),
  vec4(1.000000, 4.000000, -2.000000, 4.000000),
  vec4(-2.000000, 4.000000, -1.000000, 2.000000),
  vec4(-1.000000, 2.000000, -4.000000, 2.000000),
  vec4(-4.000000, 2.000000, -4.000000, -1.000000),
  vec4(-4.000000, -1.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, 1.000000, -3.000000),
  vec4(1.000000, -3.000000, 2.000000, -4.000000),
  vec4(2.000000, -4.000000, 4.000000, -2.000000),
  vec4(4.000000, -2.000000, 1.000000, 0.000000)
}

Rocks = {RR1,RR2,RR3,RR4}
--# Ship
-- Ship
-- RJ 20200520

Ship = class()

local rotationStep = math.rad(1) -- one degree in radians

function Ship:init()
    self.pos = vec2(WIDTH, HEIGHT)/2
    self.radians = 0
    self.step = vec2(0,0)
end

function Ship:draw()
    local sx = 10
    local sy = 6
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    rotate(math.deg(self.radians))
    strokeWidth(2)
    stroke(255)
    line(sx,0, -sx,sy)
    line(-sx,sy, -sx,-sy)
    line(-sx,-sy, sx,0)
    popMatrix()
    popStyle()
end

function Ship:move()
    if U.button.left then self.radians = self.radians + rotationStep end
    if U.button.right then self.radians = self.radians - rotationStep end
    if U.button.fire then if not self.holdFire then self:fireMissile() end end
    if not U.button.fire then self.holdFire = false end
    self:actualShipMove()
end

function Ship:actualShipMove()
    if U.button.go then
        local accel = vec2(0.015,0):rotate(self.radians)
        self.step = self.step + accel
        self.step = maximize(self.step, 3)
    end
    self:finallyMove()
end

function Ship:finallyMove()
    U:moveObject(self)
end

function maximize(vec, size)
    local s = vec:len()
    if s <= size then
        return vec
    else
        return vec*size/s
    end
end

function Ship:fireMissile()
    self.holdFire = true
    Missile(self)
end

--# Button
-- Button
-- RJ 20200520

local Buttons = {}

function createButtons()
    local dx=50
    local dy=200
    table.insert(Buttons, {x=dx, y=dy, name="left"})
    table.insert(Buttons, {x=dy, y=dx, name="right"})
    table.insert(Buttons, {x=WIDTH-dx, y=dy, name="fire"})
    table.insert(Buttons, {x=WIDTH-dy, y=dx, name = "go"})
end

function checkButtons()
    U.button.left = false
    U.button.right = false
    U.button.go = false
    U.button.fire = false
    for id,touch in pairs(Touches) do
        for i,button in ipairs(Buttons) do
            if touch.pos:dist(vec2(button.x,button.y)) < 50 then
                U.button[button.name]=true
            end
        end
    end
end

function drawButtons()
    pushStyle()
    ellipseMode(RADIUS)
    textMode(CENTER)
    stroke(255)
    strokeWidth(1)
    for i,b in ipairs(Buttons) do
        pushMatrix()
        pushStyle()
        translate(b.x,b.y)
        if U.button[b.name] then
            fill(128,0,0)
        else
            fill(128,128,128,128)
        end
        ellipse(0,0, 50)
        fill(255)
        fontSize(30)
        text(b.name,0,0)
        popStyle()
        popMatrix()
    end
    popStyle()
end

--# Asteroid
-- Asteroid
-- RJ 20200520

local DeadAsteroids = {}
local Vel = 1.5

Asteroid = class()

function Asteroid:init()
    self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    self.shape = Rocks[math.random(1,4)]
    self.scale = 16
    local angle = math.random()*2*math.pi
    self.step = vec2(Vel,0):rotate(angle)
end

function Asteroid:killDist()
    local s = self.scale
    if s == 16 then return 64 elseif s == 8 then return 32 else return 16 end
end

function killDeadAsteroids(asteroids)
    for k,a in pairs(DeadAsteroids) do
        asteroids[a] = nil
    end
    DeadAsteroids = {}
end

function scoreAsteroid(asteroid)
    local s = asteroid.scale
    local inc = 0
    if s == 16 then inc = 20
    elseif s == 8 then inc = 50
    else inc = 100
    end
    Score = Score + inc
end

function splitAsteroid(asteroid, asteroids)
    if asteroid.scale == 4 then
        Splat(asteroid.pos)
        DeadAsteroids[asteroid] = asteroid
        return
    end
    asteroid.scale = asteroid.scale//2
    asteroid.angle = math.random()*2*math.pi
    local new = Asteroid()
    new.pos = asteroid.pos
    new.scale = asteroid.scale
    asteroids[new] = new
    Splat(asteroid.pos)
end

function Asteroid:draw()
    pushMatrix()
    pushStyle()
    translate(self.pos.x, self.pos.y)
    scale(self.scale)
    strokeWidth(1/self.scale)
    for i,l in ipairs(self.shape) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

function Asteroid:move()
    U:moveObject(self)
end


--# Splat
-- Splat
-- RJ 20200521

local Splats = {}

local Vecs = {
vec2(-2,0), vec2(-2,-2), vec2(2,-2), vec2(3,1), vec2(2,-1), vec2(0,2), vec2(1,3), vec2(-1,3), vec2(-4,-1), vec2(-3,1)
}

function drawSplats()
    for k, splat in pairs(Splats) do
        splat:draw()
    end
end

Splat = class()

function Splat:init(pos)
    local die = function()
        Splats[self] = nil
    end
    self.pos = pos
    Splats[self] = self
    self.size = 2
    self.diameter = 6
    self.rot = math.random(0,359)
    tween(4, self, {size=10, diameter=1}, tween.easing.linear, die)
end

function Splat:draw()
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    fill(255)
    stroke(255)
    rotate(self.rot)
    local s = self.size
    for i,v in ipairs(Vecs) do
        ellipse(s*v.x, s*v.y, self.diameter)
    end
    popMatrix()
    popStyle()
end

--# Missile
-- Missile
-- RJ 20200522

Missile = class()

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.step = U.missileVelocity:rotate(ship.radians) + ship.step
    U.missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

function Missile:die()
    U.missiles[self] = nil
end

function Missile:draw()
    ellipse(self.pos.x, self.pos.y, 6)
end

function Missile:move()
    U:moveObject(self)
end

--# Universe
-- Universe
-- RJ 20200523

Universe = class()

local MissileSpeed = 2.0

function Universe:init()
    self.score = 0
    self.missileVelocity = vec2(MissileSpeed,0)
    self.button = {}
    createButtons()
    self.ship = Ship()
    self.processorRatio = 1.0
    self.asteroids = {}
    self.missiles = {}
    self.explosions = {}
end

function Universe:draw()
    --displayMode(FULLSCREEN_NO_BUTTONS)
    pushStyle()
    background(40, 40, 50)
    self.processorRatio = DeltaTime/0.0083333
    self:drawAsteroids()
    self:drawExplosions()
    checkButtons()
    drawButtons()
    if self.ship then self.ship:draw() end
    if self.ship then self.ship:move() end
    self:drawMissiles()
    drawSplats()
    U:drawScore()
    popStyle()
    U:findCollisions()
end

function Universe:createAsteroids()
    for i = 1,4 do
        local a = Asteroid()
        self.asteroids[a] = a
    end
end

function Universe:findCollisions()
    for i,a in pairs(self.asteroids) do
        self:checkMissileCollisions(a)
        if self.ship then self:checkShipCollision(a) end
    end
end

function Universe:checkShipCollision(asteroid)
    if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
        scoreAsteroid(asteroid)
        splitAsteroid(asteroid, self.asteroids)
        self:killShip()
    end
end

function Universe:checkMissileCollisions(asteroid)
    for k,m in pairs(self.missiles) do
        if m.pos:dist(asteroid.pos) < asteroid:killDist() then
            scoreAsteroid(asteroid)
            splitAsteroid(asteroid, self.asteroids)
            m:die()
        end
    end
end

function Universe:killShip()
    local f = function()
        self.ship = Ship()
    end
    Explosion(U.ship)
    U.ship = nil
    tween(6, self, {}, tween.easing.linear, f)
end

function Universe:moveObject(anObject)
    local pos = anObject.pos + self.processorRatio*anObject.step
    anObject.pos = vec2(self:keepInBounds(pos.x, WIDTH), self:keepInBounds(pos.y, HEIGHT))    
end

function Universe:keepInBounds(value, bound)
    return (value+bound)%bound
end

function Universe:drawExplosions()
    for k,e in pairs(self.explosions) do
        e:draw()
    end
end

function Universe:drawMissiles()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    for k, missile in pairs(self.missiles) do
        missile:draw()
    end
    popMatrix()
    popStyle()
    for k, missile in pairs(self.missiles) do
        missile:move()
    end
end

function Universe:drawAsteroids()
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(self.asteroids) do
        asteroid:draw()
        asteroid:move()
    end
    popStyle()
    killDeadAsteroids(self.asteroids)
end

function Universe:drawScore()
    local s= "000000"..tostring(self.score)
    s = string.sub(s,-5)
    fontSize(100)
    text(s, 200, HEIGHT-60)
end

--# Explosion
Explosion = class()

function Explosion:init(ship)
    local f = function()
        U.explosions[self] = nil
    end
    self.pos = ship.pos
    self.step = vec2(0,0)
    U.explosions[self] = self
    tween(4, self, {}, tween.easing.linear, f)
end

function Explosion:draw()
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    fontSize(30)
    text("BLAMMO", 0, 0)
    popMatrix()
    popStyle()
end