-- game name: Vectoroids -- author: David Jalbert -- date: 2017/01/23 -- description: An Asteroids clone made for the VectorBoy minimalist vectorial fantasy console. -- Defining the tunes and sound effects tuneStart = { {audio.g4, 1, 0.1}, {0, 0, 0.2}, {audio.a4, 1, 0.1}, {audio.b4, 1, 0.1}, {0, 0, 0.1}, {audio.g4, 1, 0.1}, {0, 0, 0.1}, {audio.fs5, 1, 0.1}, {audio.e5, 1, 0.1}, {audio.d5, 1, 0.1}, {0, 0, 0.1}, {audio.g5, 1, 0.1}} soundLaser = { {audio.c6, 1, 0.025}, {audio.b5, 1, 0.025}, {audio.a5, 1, 0.025}, {audio.g5, 1, 0.025}, {audio.f5, 1, 0.025}, {audio.e5, 1, 0.025}, {audio.d5, 1, 0.025}, {audio.c5, 1, 0.025}} soundExplosion = { {audio.c2, 1, 0.025}, {audio.a1, 1, 0.025}, {audio.f1, 1, 0.025}, {audio.d1, 1, 0.025}, {audio.b0, 1, 0.025}, {audio.g0, 1, 0.025}, {audio.e0, 1, 0.025}, {audio.c0, 1, 0.025}} soundEngine = { {audio.c2, 1, 0.025}, {0, 0, 0.025}} -- Defining shapes to use as sprites shapeTitle = { {-0.9375, 0.1250, -0.8750, -0.1250}, {-0.8750, -0.1250, -0.8125, 0.1250}, {-0.6250, 0.1250, -0.7500, 0.1250}, {-0.7500, 0.1250, -0.7500, -0.1250}, {-0.7500, -0.1250, -0.6250, -0.1250}, {-0.7500, 0.0000, -0.6875, 0.0000}, {-0.4375, 0.1250, -0.5000, 0.1250}, {-0.5000, 0.1250, -0.5625, 0.0625}, {-0.5625, 0.0625, -0.5625, -0.0625}, {-0.5625, -0.0625, -0.5000, -0.1250}, {-0.5000, -0.1250, -0.4375, -0.1250}, {-0.3750, 0.1250, -0.2500, 0.1250}, {-0.3125, 0.1250, -0.3125, -0.1250}, {-0.1250, 0.1250, -0.1875, 0.0625}, {-0.1875, 0.0625, -0.1875, -0.0625}, {-0.1875, -0.0625, -0.1250, -0.1250}, {-0.1250, -0.1250, -0.0625, -0.0625}, {-0.0625, -0.0625, -0.0625, 0.0625}, {-0.0625, 0.0625, -0.1250, 0.1250}, {0.0000, 0.1250, 0.0625, 0.1250}, {0.0625, 0.1250, 0.1250, 0.0625}, {0.1250, 0.0625, 0.0625, 0.0000}, {0.0625, 0.0000, 0.0000, 0.0000}, {0.0000, 0.1250, 0.0000, -0.1250}, {0.0000, 0.0000, 0.1250, -0.1250}, {0.2500, 0.1250, 0.3125, 0.0625}, {0.3125, 0.0625, 0.3125, -0.0625}, {0.3125, -0.0625, 0.2500, -0.1250}, {0.2500, -0.1250, 0.1875, -0.0625}, {0.1875, -0.0625, 0.1875, 0.0625}, {0.1875, 0.0625, 0.2500, 0.1250}, {0.3750, 0.1250, 0.5000, 0.1250}, {0.4375, 0.1250, 0.4375, -0.1250}, {0.3750, -0.1250, 0.5000, -0.1250}, {0.5625, 0.1250, 0.6250, 0.1250}, {0.6250, 0.1250, 0.6875, 0.0000}, {0.6875, 0.0000, 0.6250, -0.1250}, {0.6250, -0.1250, 0.5625, -0.1250}, {0.5625, -0.1250, 0.5625, 0.1250}, {0.8750, 0.1250, 0.8125, 0.1250}, {0.8125, 0.1250, 0.7500, 0.0625}, {0.7500, 0.0625, 0.7500, 0.0000}, {0.7500, 0.0000, 0.8750, 0.0000}, {0.8750, 0.0000, 0.8750, -0.0625}, {0.8750, -0.0625, 0.8125, -0.1250}, {0.8125, -0.1250, 0.7500, -0.1250}} shapeAsteroid = { {0.0000, 1.0000, 0.5625, 0.8125}, {0.5625, 0.8125, 0.4375, 0.4375}, {0.4375, 0.4375, 0.8125, 0.5625}, {0.8125, 0.5625, 1.0000, 0.0000}, {1.0000, 0.0000, 0.7500, -0.7500}, {0.7500, -0.7500, 0.0000, -1.0000}, {0.0000, -1.0000, 0.0000, -0.5000}, {0.0000, -0.5000, -0.3750, -0.8750}, {-0.3750, -0.8750, -0.7500, -0.7500}, {-0.7500, -0.7500, -1.0000, 0.0000}, {-1.0000, 0.0000, -0.8125, 0.5625}, {-0.8125, 0.5625, -0.5625, 0.5625}, {-0.5625, 0.5625, -0.7500, 0.7500}, {-0.7500, 0.7500, 0.0000, 1.0000}} shapeParticle = { {-1, 1, 1, 1}, {1, 1, 1, -1}, {1, -1, -1, -1}, {-1, -1, -1, 1}} shapeShip = { {0.1250, 0.5000, 0.5000, -0.2500}, {0.5000, -0.2500, 0.2500, -0.5000}, {0.2500, -0.5000, 0.0000, -0.4375}, {0.0000, -0.4375, -0.2500, -0.5000}, {-0.2500, -0.5000, -0.5000, -0.2500}, {-0.5000, -0.2500, -0.1250, 0.5000}, {-0.1250, 0.5000, -0.1875, 0.0000}, {-0.1875, 0.0000, 0.0000, -0.2500}, {0.0000, -0.2500, 0.1875, 0.0000}, {0.1875, 0.0000, 0.1250, 0.5000}} shapeFlames = { {-0.2500, -0.6875, -0.1250, -0.5625}, {-0.1250, -0.5625, 0.0000, -0.6875}, {0.0000, -0.6875, 0.1250, -0.5625}, {0.1250, -0.5625, 0.2500, -0.6875}, {-0.1250, -0.5625, 0.0000, -1.0000}, {0.0000, -1.0000, 0.1250, -0.5625}} shapeLaser = {{-0.25,-1,-0.25,1}, {0.25,-1,0.25,1}} -- constants maxPositionX = 1.1 maxPositionY = 1.1 minPositionX = -1.1 minPositionY = -1.1 laserSpeed = 4 laserScale = 0.05 laserRadius = 0.05 playerMaxSpeed = 2 playerAcceleration = 2 playerFriction = 0.25 playerFlameTimer = 0 playerFlameSpeed = 0.1 playerParticleScale = 0.05 playerFlashDelay = 3 playerFlashSpeed = 0.1 playerFlashTimer = 0 playerScale = 0.15 playerRadius = 0.05 particleNum = 10 particleMinSpeed = 1 particleMaxSpeed = 2 particleMinDelay = 0.25 particleMaxDelay = 0.5 asteroidScaleBig = 0.2 asteroidScaleMedium = 0.1 asteroidScaleSmall = 0.05 asteroidRadiusBig = 0.2 asteroidRadiusMedium = 0.1 asteroidRadiusSmall = 0.05 asteroidMinSpeed = 0.25 asteroidMaxSpeed = 0.5 asteroidMinTorque = -90 asteroidMaxTorque = 90 titleFlashDelay = 0.5 levelEndDelay = 2 playerDeathDelay = 2 showLevelDelay = 3 gameOverDelay = 3 levelMultiplier = {1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.8, 2, 2.25, 2.5, 2.8, 3.2, 3.5, 4} stateNone = 0 stateMenu = 1 statePlay = 2 stateGameOver = 3 -- global variables showLevelTimer = 0 score = 0 highScore = 0 lives = 0 state = 0 level = 0 timer = 0 asteroids = {} player = {} lasers = {} particles = {} -- start function; this is called at the start of the game function start() -- initializes the game variables and starts at the main menu score = 0 highScore = 0 state = stateMenu timer = 0 end -- update function; this is called once per frame function update() local i = 0 local j = 0 local k = 0 graphics.clearScreen() if (state == stateMenu) then -- draws the highscore, copyright, and title banner graphics.drawText("Highscore : " .. highScore, -1, 1) graphics.drawText("(c) 2017 Melloland Games", 0, -1, 0, -1) graphics.drawShape(shapeTitle, 0, 0.25) -- makes the text "press any button to start" blink timer = timer + graphics.getDeltaTime() if (timer > titleFlashDelay) then timer = 0 end if (timer < titleFlashDelay / 2) then graphics.drawText("Press any button to start", 0, -0.25, 0, 0) end -- starts the game at level 1 when any button is pressed if (input.getButtonDown()) then timer = 0 lives = 3 level = 1 startLevel() audio.playSequence(audio.square, tuneStart) end elseif (state == stateGameOver) then -- displays the game over screen for a few seconds, then return to the main menu graphics.drawText("GAME OVER\n\nFinal score : " .. score, 0, 0, 0, 0) timer = timer + graphics.getDeltaTime() if (timer >= gameOverDelay) then state = stateMenu end elseif (state == statePlay) then -- shows the score, and the level if it has just started graphics.drawText("Score : " .. score, -1, 1) if (showLevelTimer > 0) then showLevelTimer = showLevelTimer - graphics.getDeltaTime() graphics.drawText("Level " .. level, 0, -0.25, 0, 0) end -- shows the lives remaining as tiny player ships for i = 0, lives - 2 do graphics.drawShape(shapeShip, -0.9 + i * 0.2, 0.75, 0.1, 0) end for i = #asteroids, 1, -1 do -- moves asteroids and draws them on the screen asteroids[i]["position"] = vector.translate(asteroids[i]["position"], asteroids[i]["direction"], asteroids[i]["velocity"] * graphics.getDeltaTime()) clampPosition(asteroids[i]["position"]) asteroids[i]["angle"] = asteroids[i]["angle"] + asteroids[i]["torque"] * graphics.getDeltaTime() graphics.drawShape(shapeAsteroid, asteroids[i]["position"][1], asteroids[i]["position"][2], asteroids[i]["scale"], asteroids[i]["angle"]) -- kills the player if it collides with an asteroid if (player["enabled"] and player["flashtimer"] <= 0 and vector.distance(player["position"], asteroids[i]["position"]) < asteroids[i]["radius"] + player["radius"]) then player["enabled"] = false addParticles(player["position"], playerParticleScale) audio.playSequence(audio.noise, soundExplosion) end end -- shoots a laser when button 0 is pressed if (input.getButtonDown(0) and player["enabled"]) then addLaser(player["position"], player["angle"], laserScale, laserSpeed) audio.playSequence(audio.sawtooth, soundLaser) end for i = #lasers, 1, -1 do -- moves the lasers lasers[i]["position"] = vector.translate(lasers[i]["position"], lasers[i]["angle"], lasers[i]["velocity"] * graphics.getDeltaTime()) if (isOutside(lasers[i]["position"])) then -- removes lasers that are off screen table.remove(lasers, i) else -- draws the lasers on the screen graphics.drawShape(shapeLaser, lasers[i]["position"][1], lasers[i]["position"][2], lasers[i]["scale"], lasers[i]["angle"]) laserHit = false for j = #asteroids, 1, -1 do -- if a laser hits an asteroid, destroys the asteroid and removes the laser if (not laserHit and vector.distance(lasers[i]["position"], asteroids[j]["position"]) < asteroids[j]["radius"] + lasers[i]["radius"]) then addParticles(asteroids[j]["position"], asteroids[j]["scale"] / 4) -- spawns smaller asteroids depending on the size of the original asteroid, and increments the score if (asteroids[j]["size"] == 1) then addScore(50) for k = 1, 2 do addAsteroid(asteroids[j]["position"], 2, asteroids[j]["velocity"]) end elseif (asteroids[j]["size"] == 2) then addScore(25) for k = 1, 2 do addAsteroid(asteroids[j]["position"], 3, asteroids[j]["velocity"]) end elseif (asteroids[j]["size"] == 3) then addScore(10) end table.remove(lasers, i) table.remove(asteroids, j) audio.playSequence(audio.noise, soundExplosion) laserHit = true end end end end for i = #particles, 1, -1 do particles[i]["timer"] = particles[i]["timer"] - graphics.getDeltaTime() if (particles[i]["timer"] <= 0) then -- removes old particles table.remove(particles, i) else -- moves the particles and draws them on the screen particleRatio = particles[i]["timer"] / particles[i]["delay"] particles[i]["position"] = vector.add(particles[i]["position"], vector.multiply(particles[i]["velocity"], {graphics.getDeltaTime(), graphics.getDeltaTime()})) graphics.drawShape(shapeParticle, particles[i]["position"][1], particles[i]["position"][2], particles[i]["scale"] * particleRatio, particles[i]["angle"]) end end if (player["enabled"]) then showFlames = false showPlayer = true -- makes the player flash for a while after spawning playerFlashTimer = playerFlashTimer + graphics.getDeltaTime() if (playerFlashTimer > playerFlashSpeed) then playerFlashTimer = 0 end if (player["flashtimer"] > 0) then player["flashtimer"] = player["flashtimer"] - graphics.getDeltaTime() if (playerFlashTimer < playerFlashSpeed / 2) then showPlayer = false end end -- rotates the player when the joystick is moved on the x axis player["angle"] = player["angle"] + input.getAxisX() * graphics.getDeltaTime() * 180 if (input.getAxisY() > 0) then -- makes the player accelerate when the joystick is moved on the y axis player["velocity"] = vector.add(player["velocity"], vector.fromAngle(player["angle"], playerAcceleration * input.getAxisY() * graphics.getDeltaTime())) -- displays a flashing flame behind the player's ship when it's accelerating playerFlameTimer = playerFlameTimer + graphics.getDeltaTime() if (playerFlameTimer > playerFlameSpeed) then playerFlameTimer = 0 end if (playerFlameTimer < playerFlameSpeed / 2) then showFlames = true end -- plays a tone while the flames are on if (not audio.isChannelPlaying(audio.sine)) then audio.playSequence(audio.sine, soundEngine, true) end else audio.stop(audio.sine) end -- applies the player's acceleration to its current velocity if (vector.magnitude(player["velocity"]) > playerMaxSpeed) then player["velocity"] = vector.multiply(vector.normalize(player["velocity"]), {playerMaxSpeed, playerMaxSpeed}) end -- applies friction to the player's velocity frictionVelocity = 1 - math.min(1, playerFriction * graphics.getDeltaTime()) player["velocity"] = vector.multiply(player["velocity"], {frictionVelocity, frictionVelocity}) -- applies the player's velocity to its current position player["position"] = vector.add(player["position"], vector.multiply(player["velocity"], {graphics.getDeltaTime(), graphics.getDeltaTime()})) clampPosition(player["position"]) -- displays the player's ship and flames on the screen if (showPlayer) then graphics.drawShape(shapeShip, player["position"][1], player["position"][2], player["scale"], player["angle"]) end if (showFlames) then graphics.drawShape(shapeFlames, player["position"][1], player["position"][2], player["scale"], player["angle"]) end else audio.stop(audio.sine) end if (#asteroids == 0) then -- goes to the next level if all asteroids have been destroyed timer = timer + graphics.getDeltaTime() if (timer >= levelEndDelay) then level = level + 1 startLevel() timer = 0 end elseif (not player["enabled"]) then -- either spawns a new player ship or goes to the game over screen after the ship has been destroyed timer = timer + graphics.getDeltaTime() if (timer >= playerDeathDelay) then timer = 0 lives = lives - 1 if (lives <= 0) then state = stateGameOver else spawnPlayer() end end end end end -- adds a value to the current score function addScore(s) score = score + s if (highScore < score) then highScore = score end end -- initializes the asteroids field, empties the lasers and particles arrays, and spawns a new player on the field function startLevel() local i = 0 local multiplier = 1 asteroids = {} lasers = {} particles = {} showLevelTimer = showLevelDelay if (level > #levelMultiplier) then multiplier = levelMultiplier[#levelMultiplier] else multiplier = levelMultiplier[level] end for i = 1, 10 * multiplier do addAsteroid({math.random() * 2 - 1, math.random() * 2 - 1}, math.random(1,3), (math.random() * (asteroidMaxSpeed - asteroidMinSpeed) + asteroidMinSpeed) * multiplier) end spawnPlayer() state = statePlay end -- spawns a new player in the middle of the field function spawnPlayer() player["position"] = {0, 0} player["angle"] = 0 player["scale"] = playerScale player["velocity"] = {0, 0} player["direction"] = 0 player["torque"] = 0 player["enabled"] = true player["flashtimer"] = playerFlashDelay player["radius"] = playerRadius end -- adds a new asteroid on the field function addAsteroid(position, size, speed) local newAsteroid = {} newAsteroid["position"] = position newAsteroid["size"] = size newAsteroid["angle"] = math.random(0,360) if newAsteroid["size"] == 1 then newAsteroid["scale"] = asteroidScaleBig newAsteroid["radius"] = asteroidRadiusBig elseif newAsteroid["size"] == 2 then newAsteroid["scale"] = asteroidScaleMedium newAsteroid["radius"] = asteroidRadiusMedium elseif newAsteroid["size"] == 3 then newAsteroid["scale"] = asteroidScaleSmall newAsteroid["radius"] = asteroidRadiusSmall end newAsteroid["velocity"] = speed newAsteroid["direction"] = math.random(0,360) newAsteroid["torque"] = math.random() * (asteroidMaxTorque - asteroidMinTorque) + asteroidMinTorque table.insert(asteroids, newAsteroid) end -- if the player is outside the level's bounds, makes him appear on the other side of the screen function clampPosition(position) if (position[1] < minPositionX) then position[1] = maxPositionX end if (position[1] > maxPositionX) then position[1] = minPositionX end if (position[2] < minPositionY) then position[2] = maxPositionY end if (position[2] > maxPositionY) then position[2] = minPositionY end end -- adds a laser on the field function addLaser(position, angle, scale, velocity) local newLaser = {} newLaser["position"] = position newLaser["angle"] = angle newLaser["velocity"] = velocity newLaser["scale"] = scale newLaser["radius"] = laserRadius table.insert(lasers, newLaser) end -- adds a particle on the field function addParticles(position, scale) local i = 0 for i = 1, particleNum do newParticle = {} newParticle["position"] = position newParticle["angle"] = math.random() * 360 newParticle["scale"] = scale newParticle["velocity"] = vector.fromAngle(math.random() * 360, math.random() * (particleMaxSpeed - particleMinSpeed) + particleMinSpeed) newParticle["delay"] = math.random() * (particleMaxDelay - particleMinDelay) + particleMinDelay newParticle["timer"] = newParticle["delay"] table.insert(particles, newParticle) end end -- returns true if the position is outside the level's bounds function isOutside(position) if (position[1] < minPositionX or position[1] > maxPositionX or position[2] < minPositionY or position[2] > maxPositionY) then return true else return false end end