Just arriving ‘at work’ this morning, I’m thinking we’ll make the Ship move. We need a safe place to land first.

See the end of the article for news of a released defect! Thanks, Dave1707!

“At work” refers to my computer desk in what would be the living room of the house if we were rational people. My dear wife Ricia has gone off to forage for herbs and bananas, so there’s a bit of time before our traditional Sunday breakfast.

It’s about time to make the Ship move, and that’s my plan at this moment, before reviewing the code. I know I would like to convert the ship to an object, because I find that structure to be more clean and easy to manage. Whether we need to do that now, however, is an open question.

Let’s see where we stand. In the Main tab, we do call moveShip:

function draw()
    Ratio = DeltaTime/0.0083333
    --displayMode(FULLSCREEN_NO_BUTTONS)
    checkButtons()
    pushStyle()
    background(40, 40, 50)
    U:draw()
    drawButtons()
    drawShip()
    moveShip()
    drawMissiles()
    drawSplats()
    drawScore()
    popStyle()
    U:findCollisions()
end

That’s not exactly where it belongs, but it tells me we must have an empty function over in Ship tab. Oh, right, not empty, because it handles turning and firing:

function moveShip()
    if Button.left then Ship.ang = Ship.ang + 1 end
    if Button.right then Ship.ang = Ship.ang - 1 end
    if Button.fire then if not Ship.holdFire then fireMissile() end end
    if not Button.fire then Ship.holdFire = false end
end

This code is a bit “iffy”, but it’s pretty clear where we can put our motion code. That’s clean enough that I’m inclined to go ahead. Let’s think about …

How To Move

Well. In the original game, the ship accelerates in the direction its pointing, up to some maximum speed. According to my reading on the old program, it also slows down when the power is off. Apparently the ether is sticky in this universe.

So let’s think … the ship will have a velocity in whatever direction it is presently moving. Each cycle, friction will shorten that velocity a bit. And, each cycle, if the power is on, a small increment of velocity will be added to the current velocity, and, if we remember, the velocity will be limited to its maximum. That seems simple enough.

We know that a speed of 1.5 is pretty decent for the asteroids, so that gives us a sense of what kind of numbers we’re dealing with. I’ve not read the 6502 code to get a sense of what the ship’s acceleration rate is, but watching the old videos, it’s clear that it can fly at least twice as fast as the asteroids. Let’s start with a maximum speed of 3.0.

Now asteroids move themselves like this:

function moveAsteroid(asteroid)
    local step = Ratio*vec2(Vel,0):rotate(asteroid.angle)
    local pos = asteroid.pos + step
    asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end

That’s really not ideal, since asteroids never change direction, so that rotate could be folded into Asteroid shape. That’s for another day. For the ship, we’ll need to handle its angle separately from its velocity, since it can slide sideways or backward just fine.

So let’s store ship velocity as a vector representing the step to be taken to move one cycle’s worth. This vector will have a length less than or equal to 3. When the go button is down, we’ll add a small increment to the velocity, pointing in the direction the ship is pointing.

Sounds easy, let’s do it.

Move The Ship

Because of how iffy that move method is, I’ve decided to break out actual ship motion. We’ll need to improve these names, but that’s not for now

function moveShip()
    if Button.left then Ship.ang = Ship.ang + 1 end
    if Button.right then Ship.ang = Ship.ang - 1 end
    if Button.fire then if not Ship.holdFire then fireMissile() end end
    if not Button.fire then Ship.holdFire = false end
    actualShipMove()
end

Now we “just” have to write actualShipMove. OK, in a fit of creativity I’ve typed this in:

function actualShipMove()
    if Button.go then
        local accel = vec2(0.15,0):rotate(ship.ang)
        ship.velocity = ship.velocity + accel
    end
    ship.pos = ship.pos + ship.velocity
end

It seems to me that this ought to work. I need to initialize velocity, however, up in the create function:

function createShip()
    Ship.pos = vec2(WIDTH, HEIGHT)/2
    Ship.ang = 0
    ship.velocity = vec2(0,0)
end

I’m going to just run this and see what happens. Hold my water bottle.

Yeah, well, it’s capital S ship not lower case all over. Duh.

It might be a good idea to think about that mistake. I actually typed lower-case ship right after two lines with the capital letter, because I was already locked in on the lower case from the actualShipMove function. Why did I type it there?

Because lower case letters are for instances and upper case are for classes and globals, and I think of the ship as an instance. (The story is a bit more complex than that, because when it becomes an object we’ll be saying self there, but I think what has bitten me is that I am thinking locally and the Ship is written globally. Another hint that we might be wise to convert it.)

But I digress. NOW I think the ship might fly.

Well, it does. A few things happen. First of all, it goes way too fast, so the 0.15 is too large an acceleration. Second, it doesn’t wrap around: we didn’t keep Ship.pos in bounds yet. Third, it goes off at weird angles. Left facing left, it goes left. Aimed up, it goes right. In between, it does other random things. I am confuse.

Ah. While the screen rotate function is in degrees, the vector rotate expects radians. But the asteroids seem not to reflect that!

function moveAsteroid(asteroid)
    local step = Ratio*vec2(Vel,0):rotate(asteroid.angle)
    local pos = asteroid.pos + step
    asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end

What’s up with that? Well, sir, asteroids are given an angle in radians when created:

function createAsteroid()
    local a = {}
    a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    a.angle = math.random()*2*math.pi
    a.shape = Rocks[math.random(1,4)]
    a.scale = 16
    return a
end

Since they don’t rotate or change direction, we didn’t stumble on this issue before.

I think the best thing to do is to convert Ship to use radians as well, which will mean we need to adjust its draw to expect that. We need to go one way or the other and I’d rather be consistent between asteroids and the ship.

I decided to rename the variable as radians which might serve as a clue. I’ll show you all the code in a moment but I might as well run it to see what I missed.

OK, after dialing down acceleration to 0.015 the ship moves somewhat reasonably. Let me put in keeping in bounds and then show what we’ve got.

Asteroids do it like this:

function moveAsteroid(asteroid)
    local step = Ratio*vec2(Vel,0):rotate(asteroid.angle)
    local pos = asteroid.pos + step
    asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end

I’ll just do similarly for the ship:

function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.velocity = Ship.velocity + accel
    end
    Ship.pos = Ship.pos + Ship.velocity
    Ship.pos = vec2(keepInBounds(Ship.pos.x, WIDTH), keepInBounds(Ship.pos.y, HEIGHT))
end

And it moves as intended!

it moves

I’ve not handled maximizing the speed yet. Here’s my plan for maximizing to three. I’ll write it out longhand first:

function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.velocity = Ship.velocity + accel
    end
    Ship.velocity = maximize(Ship.velocity, 3)
    Ship.pos = Ship.pos + Ship.velocity
    Ship.pos = vec2(keepInBounds(Ship.pos.x, WIDTH), keepInBounds(Ship.pos.y, HEIGHT))
end

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

We get the length of the velocity vector, compare it to the desired maximum, and if it’s larger, we scale it down by the ratio between the desired size and the current size. If it were trying to go twice as fast, that ratio would be 3/6, or 1/2, and it would scale right back where it’s supposed to be.

And, perhaps more significant, it seems to work on the screen as well.

It appears to me that we can move the call to maximize up into the if statement that adjusts velocity:

function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.velocity = Ship.velocity + accel
        Ship.velocity = maximize(Ship.velocity, 3)
    end
    Ship.pos = Ship.pos + Ship.velocity
    Ship.pos = vec2(keepInBounds(Ship.pos.x, WIDTH), keepInBounds(Ship.pos.y, HEIGHT))
end

The code might be made a bit faster with fewer accesses to Ship, but for now, we have things working. And my dear wife is home from foraging, so it’s probably time to break.

Summing Up

I’ll dump all the code below, so you can scan it and make your own assessment. Here’s the relevant stuff from Ship, which is the only tab we’ve edited:

function createShip()
    Ship.pos = vec2(WIDTH, HEIGHT)/2
    Ship.radians = 0  -- <-------
    Ship.velocity = vec2(0,0)
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 Button.left then Ship.radians = Ship.radians + rotationStep end
    if Button.right then Ship.radians = Ship.radians - rotationStep end
    if Button.fire then if not Ship.holdFire then fireMissile() end end
    if not Button.fire then Ship.holdFire = false end
    actualShipMove() -- <-------
end

function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.velocity = Ship.velocity + accel
        Ship.velocity = maximize(Ship.velocity, 3)
    end
    Ship.pos = Ship.pos + Ship.velocity
    Ship.pos = vec2(keepInBounds(Ship.pos.x, WIDTH), keepInBounds(Ship.pos.y, HEIGHT))
end

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

Our changes were’t very invasive, mostly just additions to give moving a place to stand and to do the work. And it went smoothly other than needing to train myself to type “Ship” instead of “ship”, which I surely did about 61 times.

The code is not lovely, and could perhaps be optimized a bit, but it works nicely. We’ll need to tune the magic numbers, and that reminds me of what’s not to like about what we have so far.

Magic Numbers
I’ve mentioned before that all these magical 1.5s and 0.0015s and the like need to be centralized and made consistent. Every time we write another function with a constant in it, we are making the code harder to modify.
Classes and Objects
I prefer how the code looks for Splats and Missiles, which are defined in classes rather than just a bunch of functions arranged in a tab somewhere. I’d like to convert Ship to that form, and will probably make that the task for next time. Doing so should provide some insights, I suspect.
Degrees and Radians
Codea has done it to us, in its use of degrees in graphics and radians in vectors. We just have to do our best to be internally consistent. And I suspect that naming angles things like “radians” and “degrees” will help with that.
Consistent Move Functions
The asteroid motion and ship motion are a bit different in style, and I’ve mentioned that the asteroid computes its motion vector more frequently than it needs to. If we initialize asteroids a bit differently, I think we can make them look more similar to ships. That may lead to some consolidation and should at least reduce confusion
Friction!
I just realized, while reviewing the article before shipping it, that I didn’t handle friction. Remind me to do that.
Ratio!!!
And did you notice that I didn’t scale the velocity to DeltaTime? Another oversight. How could we build this code so that oversights like that are less likely? We’ll have to think about that.

There’s probably more than that to be discovered. We’ll keep our eyes open.

I have a more general comment. On one of the private Slack groups I belong to, a member commented that when they write “small” programs, they like to keep them all in one file, but larger programs they like to separate out.

This program was just under 500 lines when we started today and I have it broken out into 9 tabs already, so it’s broken into chunks of around 50 or 60 lines already. I believe that big programs get messy a little bit at a time, so I try to work “in the small” much as I would work “in the large”, observing the little things and fixing them.

I try to keep the program well designed, not for the future, but for today. I try never to put something in because I’ll need it tomorrow. I do things because they’re already wrong and need to be better, or because I’m about to do something that won’t fit in well, and I need to give it a place to be.

It works for me, and it’s kind of fun to do things in these tiny steps. I’m having a good time, and I hope that if you’re reading, you are as well.

See you next time!

A Defect! I shipped a defect!

Dave1707 has been following along, and he tried something that I didn’t try in my zeal to get to breakfast: he tried firing a missile. That caused the program to explode rather than a rock, because of this line in Missile:

    self.vel = vec2(MissileVelocity,0):rotate(math.rad(ship.ang))

We changed the ship from using the angle to using a new variable, radians. Well, OK, I did that. And I didn’t change the line that decides where to aim the Missile. It should be:

    self.vel = vec2(MissileVelocity,0):rotate(ship.radians)

I’ve already been thinking about how to centralize the decisions about the member variables on moving objects, with my thought being that if we can get it done in just one place, we should be able to keep it more nearly right.

There’s also a case to be made for some kind of an automated test. That needs some consideration as well.

For now, apologies for the defect and thanks to Dave!

The Code


--# Main
-- Asteroids
-- RJ 20200511

Touches = {}
Ratio = 1.0 -- draw time scaling ratio
Score = 0

function setup()
    U = Universe()
    U:createAsteroids()
    Score = 0
    --displayMode(FULLSCREEN_NO_BUTTONS)
    createButtons()
    createShip()
end

function draw()
    Ratio = DeltaTime/0.0083333
    --displayMode(FULLSCREEN_NO_BUTTONS)
    checkButtons()
    pushStyle()
    background(40, 40, 50)
    U:draw()
    drawButtons()
    drawShip()
    moveShip()
    drawMissiles()
    drawSplats()
    drawScore()
    popStyle()
    U:findCollisions()
end

function drawScore()
    local s= "000000"..tostring(Score)
    s = string.sub(s,-5)
    fontSize(100)
    text(s, 200, HEIGHT-60)
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(keepInBounds(100,1000)).is(100)
            _:expect(keepInBounds(1000,1000)).is(0)
            _:expect(keepInBounds(1001,1000)).is(1)
            _:expect(keepInBounds(-1,1000)).is(999)
        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

local Ship = {}

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

function createShip()
    Ship.pos = vec2(WIDTH, HEIGHT)/2
    Ship.radians = 0
    Ship.velocity = vec2(0,0)
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 Button.left then Ship.radians = Ship.radians + rotationStep end
    if Button.right then Ship.radians = Ship.radians - rotationStep end
    if Button.fire then if not Ship.holdFire then fireMissile() end end
    if not Button.fire then Ship.holdFire = false end
    actualShipMove()
end

function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.velocity = Ship.velocity + accel
        Ship.velocity = maximize(Ship.velocity, 3)
    end
    Ship.pos = Ship.pos + Ship.velocity
    Ship.pos = vec2(keepInBounds(Ship.pos.x, WIDTH), keepInBounds(Ship.pos.y, HEIGHT))
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

--# Button
-- Button
-- RJ 20200520

Button = {}
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()
    Button.left = false
    Button.right = false
    Button.go = false
    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
                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 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

function createAsteroids(asteroids)
    for i = 1,4 do
        local a = createAsteroid()
        asteroids[a] = a
    end
end

function createAsteroid()
    local a = {}
    a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    a.angle = math.random()*2*math.pi
    a.shape = Rocks[math.random(1,4)]
    a.scale = 16
    return a
end

function drawAsteroids(asteroids)
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(asteroids) do
        drawAsteroid(asteroid)
        moveAsteroid(asteroid)
    end
    popStyle()
    killDeadAsteroids(asteroids)
end

function killDist(asteroid)
    local s = asteroid.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 deathSize()
    local i = 0
    for k, a in pairs(DeadAsteroids) do
        i = i + 1
    end
    return i
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 = createAsteroid()
    new.pos = asteroid.pos
    new.scale = asteroid.scale
    asteroids[new] = new
    Splat(asteroid.pos)
end

function drawAsteroid(asteroid)
    pushMatrix()
    pushStyle()
    translate(asteroid.pos.x, asteroid.pos.y)
    ellipse(0,0,2*killDist(asteroid))
    scale(asteroid.scale)
    strokeWidth(1/asteroid.scale)
    for i,l in ipairs(asteroid.shape) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

function moveAsteroid(asteroid)
    local step = Ratio*vec2(Vel,0):rotate(asteroid.angle)
    local pos = asteroid.pos + step
    asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end

function keepInBounds(value, bound)
    return (value+bound)%bound
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

Missiles = {}

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

Missile = class()

local MissileVelocity = 2.0

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.vel = vec2(MissileVelocity,0):rotate(ship.radians)
    Missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

function Missile:die()
    Missiles[self] = nil
end

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

function Missile:move()
    self.pos = self.pos + Ratio*self.vel
    self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end

--# Universe
-- Universe
-- RJ 20200523

Universe = class()

function Universe:init()
    self.asteroids = {}
end

function Universe:draw()
    drawAsteroids(self.asteroids)
end

function Universe:createAsteroids()
    createAsteroids(self.asteroids)
end

function Universe:findCollisions()
    for i,a in pairs(self.asteroids) do
        for k,m in pairs(Missiles) do
            if m.pos:dist(a.pos) < killDist(a) then
                scoreAsteroid(a)
                splitAsteroid(a, self.asteroids)
                m:die()
            end
        end
    end
end