439 lines
11 KiB
Lua
439 lines
11 KiB
Lua
local paddle_x = nil
|
|
local paddle_y = nil
|
|
local paddle_width = 0.1
|
|
local paddle_height = 0.02
|
|
|
|
local wall_thickness = 0.01
|
|
|
|
local missiles = {}
|
|
local missile_radius = 0.005
|
|
local missile_trail_fade = 0.4
|
|
local missile_trail_min_visibility = 0.03
|
|
local missile_trail_length = math.log(missile_trail_min_visibility, missile_trail_fade)
|
|
|
|
local unreflected_missiles = 0
|
|
local unreflected_missiles_max = 5
|
|
|
|
local cities = {}
|
|
local city_radius = 0.05
|
|
|
|
local explosions = {}
|
|
local explosion_radius = 0.08
|
|
local explosion_duration = 0.4
|
|
|
|
local enemies = {}
|
|
local enemy_radius = 0.025
|
|
local enemy_min_shoot = 2
|
|
local enemy_max_shoot = 20
|
|
|
|
local window_width = nil
|
|
local window_height = nil
|
|
local viewport_x_offset = nil
|
|
local viewport_y_offset = nil
|
|
local scale = nil
|
|
|
|
function setScreenDimensions(width, height)
|
|
window_width = width
|
|
window_height = height
|
|
|
|
viewport_x_offset = 0
|
|
viewport_y_offset = 0
|
|
scale = math.min(width, height)
|
|
viewport_x_offset = (window_width - scale) / 2
|
|
viewport_y_offset = (window_height - scale) / 2
|
|
end
|
|
|
|
function toScreenCoordinates(x, y)
|
|
local screen_x = x * scale + viewport_x_offset
|
|
local screen_y = y * scale + viewport_y_offset
|
|
return screen_x, screen_y
|
|
end
|
|
|
|
function toScreenSize(size)
|
|
return size * scale
|
|
end
|
|
|
|
function fromScreenCoordinate(x, y)
|
|
local logical_x = (x - viewport_x_offset) / scale
|
|
local logical_y = (y - viewport_y_offset) / scale
|
|
return logical_x, logical_y
|
|
end
|
|
|
|
function love.load()
|
|
love.mouse.setVisible(false)
|
|
local width, height = love.graphics.getDimensions()
|
|
setScreenDimensions(width, height)
|
|
movePaddle(0)
|
|
spawnCities()
|
|
spawnEnemy(0.1, 0.1)
|
|
spawnEnemy(0.2, 0.1)
|
|
spawnEnemy(0.3, 0.1)
|
|
spawnEnemy(0.4, 0.1)
|
|
spawnEnemy(0.5, 0.1)
|
|
spawnEnemy(0.6, 0.1)
|
|
spawnEnemy(0.7, 0.1)
|
|
spawnEnemy(0.8, 0.1)
|
|
spawnEnemy(0.9, 0.1)
|
|
spawnEnemy(0.15, 0.2)
|
|
spawnEnemy(0.25, 0.2)
|
|
spawnEnemy(0.35, 0.2)
|
|
spawnEnemy(0.45, 0.2)
|
|
spawnEnemy(0.55, 0.2)
|
|
spawnEnemy(0.65, 0.2)
|
|
spawnEnemy(0.75, 0.2)
|
|
spawnEnemy(0.85, 0.2)
|
|
end
|
|
|
|
function spawnCities()
|
|
local number_of_cities = 7
|
|
cities = {}
|
|
for i = 1, number_of_cities do
|
|
local city_x = ((i - 0.5) / number_of_cities) * (1 - 2*wall_thickness) + wall_thickness
|
|
local city_y = 1
|
|
|
|
table.insert(cities, {
|
|
x = city_x,
|
|
y = city_y,
|
|
alive = true
|
|
})
|
|
end
|
|
end
|
|
|
|
function spawnMissile(x, y, target_x, target_y, speed)
|
|
local dx = target_x - x
|
|
local dy = target_y - y
|
|
local length = math.sqrt(dx * dx + dy * dy)
|
|
local dx = dx / length * speed
|
|
local dy = dy / length * speed
|
|
table.insert(missiles, {
|
|
x = x,
|
|
y = y,
|
|
dx = dx,
|
|
dy = dy,
|
|
reflected = false,
|
|
history = {
|
|
{x = x, y = y}
|
|
},
|
|
trail_length = 0,
|
|
alive = true
|
|
})
|
|
unreflected_missiles = unreflected_missiles + 1
|
|
end
|
|
|
|
function spawnExplosion(x, y)
|
|
table.insert(explosions, {
|
|
x = x,
|
|
y = y,
|
|
radius = 0,
|
|
remaining = explosion_duration,
|
|
})
|
|
end
|
|
|
|
function spawnEnemy(x, y)
|
|
table.insert(enemies, {
|
|
x = x,
|
|
y = y,
|
|
until_shoot = math.random() * (enemy_max_shoot - enemy_min_shoot),
|
|
alive = true
|
|
})
|
|
end
|
|
|
|
function updateMissiles(dt)
|
|
for _, missile in ipairs(missiles) do
|
|
missile.x = missile.x + missile.dx * dt
|
|
missile.y = missile.y + missile.dy * dt
|
|
|
|
if missile.y < wall_thickness + missile_radius then
|
|
-- Collision with top wall
|
|
missile.y = wall_thickness + missile_radius
|
|
missile.dy = -missile.dy
|
|
end
|
|
|
|
if missile.x < wall_thickness + missile_radius then
|
|
-- Collision with left wall
|
|
missile.x = wall_thickness + missile_radius
|
|
missile.dx = -missile.dx
|
|
elseif missile.x > 1 - (wall_thickness + missile_radius) then
|
|
-- Collision with right wall
|
|
missile.x = 1 - (wall_thickness + missile_radius)
|
|
missile.dx = -missile.dx
|
|
end
|
|
|
|
local paddle_left = paddle_x - paddle_width/2
|
|
local paddle_right = paddle_x + paddle_width/2
|
|
local paddle_top = paddle_y - paddle_height/2
|
|
if paddle_left <= missile.x and missile.x <= paddle_right and paddle_top <= missile.y and missile.y <= paddle_y then
|
|
-- Collision with the paddle
|
|
missile.y = paddle_top
|
|
missile.dy = -missile.dy
|
|
if not missile.reflected then
|
|
missile.reflected = true
|
|
unreflected_missiles = unreflected_missiles - 1
|
|
end
|
|
end
|
|
|
|
for _, city in ipairs(cities) do
|
|
local dx = city.x - missile.x
|
|
local dy = city.y - missile.y
|
|
local distance = math.sqrt(dx * dx + dy * dy)
|
|
if city.alive and distance < city_radius then
|
|
spawnExplosion(missile.x, missile.y)
|
|
-- Freeze the missile in-place
|
|
missile.dx = 0
|
|
missile.dy = 0
|
|
end
|
|
end
|
|
|
|
if missile.reflected then
|
|
for _, enemy in ipairs(enemies) do
|
|
local dx = enemy.x - missile.x
|
|
local dy = enemy.y - missile.y
|
|
local distance = math.sqrt(dx * dx + dy * dy)
|
|
if distance < enemy_radius then
|
|
spawnExplosion(missile.x, missile.y)
|
|
-- Freeze the missile in-place
|
|
missile.dx = 0
|
|
missile.dy = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
local dx = missile.history[1].x - missile.x
|
|
local dy = missile.history[1].y - missile.y
|
|
local distance = math.sqrt(dx * dx + dy * dy)
|
|
if distance >= 1 / scale then
|
|
missile.history[1].length = distance
|
|
missile.trail_length = missile.trail_length + distance
|
|
table.insert(missile.history, 1, {
|
|
x = missile.x,
|
|
y = missile.y,
|
|
length = nil,
|
|
})
|
|
-- Remove the oldest segments
|
|
while missile.trail_length > missile_trail_length do
|
|
local length = missile.history[#missile.history].length
|
|
missile.trail_length = missile.trail_length - length
|
|
table.remove(missile.history)
|
|
end
|
|
end
|
|
end
|
|
|
|
local i = 1
|
|
while i <= #missiles do
|
|
if missiles[i].y > 1 + missile_radius or not missiles[i].alive then
|
|
-- Went off the bottom of the screen or exploded, delete
|
|
table.remove(missiles, i)
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
function updateExplosions(dt)
|
|
local i = 1
|
|
while i <= #explosions do
|
|
local explosion = explosions[i]
|
|
|
|
local completeness = (explosion_duration - explosion.remaining) / explosion_duration
|
|
explosion.radius = completeness ^ 1.5 * explosion_radius
|
|
|
|
-- Destroy missiles within range
|
|
for _, missile in ipairs(missiles) do
|
|
local dx = missile.x - explosion.x
|
|
local dy = missile.y - explosion.y
|
|
local distance = math.sqrt(dx * dx + dy * dy)
|
|
if distance < explosion.radius then
|
|
missile.alive = false
|
|
end
|
|
end
|
|
|
|
-- Destroy enemies within range
|
|
for _, enemy in ipairs(enemies) do
|
|
local dx = enemy.x - explosion.x
|
|
local dy = enemy.y - explosion.y
|
|
local distance = math.sqrt(dx * dx + dy * dy)
|
|
if distance < explosion.radius + enemy_radius then
|
|
enemy.alive = false
|
|
end
|
|
end
|
|
|
|
explosion.remaining = explosion.remaining - dt
|
|
|
|
if explosion.remaining < explosion_duration * 0.2 then
|
|
-- Destroy cities within range
|
|
for _, city in ipairs(cities) do
|
|
local dx = city.x - explosion.x
|
|
local dy = city.y - explosion.y
|
|
local distance = math.sqrt(dx * dx + dy * dy)
|
|
if distance < explosion.radius + city_radius then
|
|
city.alive = false
|
|
end
|
|
end
|
|
end
|
|
|
|
if explosion.remaining < 0 then
|
|
table.remove(explosions, i)
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
function updateEnemies(dt)
|
|
local i = 1
|
|
while i <= #enemies do
|
|
local enemy = enemies[i]
|
|
|
|
enemy.until_shoot = enemy.until_shoot - dt
|
|
if enemy.until_shoot < 0 then
|
|
enemy.until_shoot = enemy_min_shoot + math.random() * (enemy_max_shoot - enemy_min_shoot)
|
|
if unreflected_missiles < unreflected_missiles_max then
|
|
local target = cities[math.random(1, #cities)]
|
|
spawnMissile(enemy.x, enemy.y, target.x, target.y, 0.2)
|
|
end
|
|
end
|
|
|
|
if not enemy.alive then
|
|
table.remove(enemies, i)
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
function love.update(dt)
|
|
updateMissiles(dt)
|
|
updateExplosions(dt)
|
|
updateEnemies(dt)
|
|
|
|
if #explosions == 0 and #missiles == 0 and #enemies == 0 then
|
|
love.event.quit()
|
|
end
|
|
end
|
|
|
|
function explodeAllReflected()
|
|
for _, missile in ipairs(missiles) do
|
|
if missile.reflected then
|
|
spawnExplosion(missile.x, missile.y)
|
|
-- Freeze the missile
|
|
missile.dx = 0
|
|
missile.dy = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
function movePaddle(screen_x)
|
|
paddle_x = fromScreenCoordinate(screen_x, 0)
|
|
paddle_x = math.max(paddle_x, paddle_width/2 + wall_thickness)
|
|
paddle_x = math.min(paddle_x, 1 - (paddle_width/2 + wall_thickness))
|
|
paddle_y = 0.9
|
|
end
|
|
|
|
love.mousemoved = movePaddle
|
|
|
|
love.mousepressed = explodeAllReflected
|
|
|
|
love.resize = setScreenDimensions
|
|
|
|
function drawWalls()
|
|
love.graphics.setColor(0, 0, 1)
|
|
local x, y = toScreenCoordinates(0, 0)
|
|
local width = toScreenSize(1)
|
|
local height =toScreenSize(wall_thickness)
|
|
love.graphics.rectangle('fill', x, y, width, height)
|
|
local width = toScreenSize(wall_thickness)
|
|
local height = toScreenSize(1)
|
|
love.graphics.rectangle('fill', x, y, width, height)
|
|
local x, y = toScreenCoordinates(1 - wall_thickness, 0)
|
|
love.graphics.rectangle('fill', x, y, width, height)
|
|
end
|
|
|
|
function drawMissiles()
|
|
love.graphics.setLineWidth(0.001 * scale)
|
|
|
|
-- Trails
|
|
for _, missile in ipairs(missiles) do
|
|
local dx = missile.history[1].x - missile.x
|
|
local dy = missile.history[1].y - missile.y
|
|
local length = math.sqrt(dx * dx + dy * dy)
|
|
|
|
local from_x, from_y = toScreenCoordinates(missile.x, missile.y)
|
|
for _, point in ipairs(missile.history) do
|
|
local visibility = missile_trail_fade ^ length
|
|
if point.length ~= nil then
|
|
length = length + point.length
|
|
end
|
|
|
|
local x, y = toScreenCoordinates(point.x, point.y)
|
|
if missile.reflected then
|
|
love.graphics.setColor(1, 1, 0.5, visibility)
|
|
else
|
|
love.graphics.setColor(1, 0.4, 0, visibility)
|
|
end
|
|
love.graphics.line(from_x, from_y, x, y)
|
|
from_x = x
|
|
from_y = y
|
|
end
|
|
end
|
|
|
|
-- Missiles themselves. Drawn separately so that they're always over the trails
|
|
for _, missile in ipairs(missiles) do
|
|
local style = 'fill'
|
|
if missile.reflected then
|
|
love.graphics.setColor(1, 0.5, 0)
|
|
style = 'line'
|
|
else
|
|
love.graphics.setColor(1, 0.5, 0.5)
|
|
end
|
|
local x, y = toScreenCoordinates(missile.x, missile.y)
|
|
local radius = toScreenSize(missile_radius)
|
|
love.graphics.circle(style, x, y, radius)
|
|
end
|
|
end
|
|
|
|
function drawPaddle()
|
|
love.graphics.setColor(1, 1, 1)
|
|
local x, y = toScreenCoordinates(paddle_x - paddle_width / 2, paddle_y - paddle_height / 2)
|
|
local width = toScreenSize(paddle_width)
|
|
local height = toScreenSize(paddle_height)
|
|
love.graphics.rectangle('fill', x, y, width, height)
|
|
end
|
|
|
|
function drawCities()
|
|
love.graphics.setColor(1, 1, 1)
|
|
for _, city in ipairs(cities) do
|
|
if city.alive then
|
|
local x, y = toScreenCoordinates(city.x, city.y)
|
|
local radius = toScreenSize(city_radius)
|
|
love.graphics.circle('fill', x, y, radius)
|
|
end
|
|
end
|
|
end
|
|
|
|
function drawExplosions()
|
|
for _, explosion in ipairs(explosions) do
|
|
love.graphics.setColor(1, 0, 0)
|
|
local x, y = toScreenCoordinates(explosion.x, explosion.y)
|
|
local radius = toScreenSize(explosion.radius)
|
|
love.graphics.circle('fill', x, y, radius)
|
|
end
|
|
end
|
|
|
|
function drawEnemies()
|
|
for _, enemy in ipairs(enemies) do
|
|
love.graphics.setColor(0.7, 0.5, 1)
|
|
local x, y = toScreenCoordinates(enemy.x, enemy.y)
|
|
local radius = toScreenSize(enemy_radius)
|
|
love.graphics.circle('fill', x, y, radius)
|
|
end
|
|
end
|
|
|
|
function love.draw()
|
|
drawCities()
|
|
drawWalls()
|
|
drawMissiles()
|
|
drawPaddle()
|
|
drawEnemies()
|
|
drawExplosions()
|
|
end
|