reflect/bundle/main.lua

664 lines
17 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 = nil
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 missile_speed_min = 0.15
local missile_speed_max = 0.25
local unreflected_missiles = nil
local unreflected_missiles_allowed = nil
local unreflected_missiles_allowed_ramp_up = 10
local unreflected_missiles_max = nil
local cities = nil
local city_radius = 0.05
local explosions = nil
local explosion_radius = 0.08
local explosion_duration = 0.4
local enemies = nil
local enemy_radius = 0.025
local enemy_min_shoot = 2
local enemy_max_shoot = 10
local enemy_inaccuracy = 0.05
local intro_duration = 1
local outro_duration = 0.5
local stage = nil
local window_width = nil
local window_height = nil
local viewport_x_offset = nil
local viewport_y_offset = nil
local scale = nil
local time = nil
local states = {title = 0, intro = 1, outro = 2, gameplay = 3, game_over = 4}
local state = nil
font = require("font")
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 startStage()
missiles = {}
unreflected_missiles = 0
unreflected_missiles_allowed = 0
explosions = {}
enemies = {}
time = 0
-- First row
if stage >= 3 then
spawnEnemy(0.1, 0.1)
end
if stage >= 2 then
spawnEnemy(0.2, 0.1)
spawnEnemy(0.3, 0.1)
end
spawnEnemy(0.4, 0.1)
spawnEnemy(0.5, 0.1)
spawnEnemy(0.6, 0.1)
if stage >= 2 then
spawnEnemy(0.7, 0.1)
spawnEnemy(0.8, 0.1)
end
if stage >= 3 then
spawnEnemy(0.9, 0.1)
end
-- Second row
if stage >= 3 then
spawnEnemy(0.15, 0.2)
end
if stage >= 2 then
spawnEnemy(0.25, 0.2)
end
spawnEnemy(0.35, 0.2)
spawnEnemy(0.45, 0.2)
spawnEnemy(0.55, 0.2)
spawnEnemy(0.65, 0.2)
if stage >= 2 then
spawnEnemy(0.75, 0.2)
end
if stage >= 3 then
spawnEnemy(0.85, 0.2)
end
-- Third row
if stage >= 4 then
spawnEnemy(0.1, 0.3)
spawnEnemy(0.2, 0.3)
spawnEnemy(0.3, 0.3)
spawnEnemy(0.4, 0.3)
spawnEnemy(0.5, 0.3)
spawnEnemy(0.6, 0.3)
spawnEnemy(0.7, 0.3)
spawnEnemy(0.8, 0.3)
spawnEnemy(0.9, 0.3)
end
if stage == 3 or stage >= 5 then
unreflected_missiles_max = unreflected_missiles_max + 1
end
state = states.intro
end
function love.load()
math.randomseed(os.time())
local width, height = love.graphics.getDimensions()
setScreenDimensions(width, height)
movePaddle(window_width / 2)
unreflected_missiles_max = 1
cities = {}
spawnCities()
stage = 1
state = states.title
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 = enemy_min_shoot + math.random() * (enemy_max_shoot - enemy_min_shoot),
angle = math.random() * 2 * math.pi,
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
if not missiles[i].reflected then
unreflected_missiles = unreflected_missiles - 1
end
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 and state == states.gameplay 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_allowed then
local speed = missile_speed_min + math.random() * (missile_speed_max - missile_speed_min)
local inaccuracy = math.random() * 2 * enemy_inaccuracy - enemy_inaccuracy
local behaviour = math.random(0, 7)
if behaviour == 0 then
spawnMissile(enemy.x, enemy.y, enemy.x + inaccuracy, 1, speed)
elseif behaviour <= 5 then
local target = cities[math.random(1, #cities)]
for i = 1, 7 do
if target.alive then
break
end
target = cities[math.random(1, #cities)]
end
spawnMissile(enemy.x, enemy.y, target.x + inaccuracy, target.y, speed)
else
local angle = (math.random() * 140 - 70) * math.pi / 180
local x = enemy.x + math.sin(angle)
local y = enemy.y + math.cos(angle)
spawnMissile(enemy.x, enemy.y, x, y, speed)
end
end
end
local dangle = 10 / (2 + enemy.until_shoot) * dt
enemy.angle = enemy.angle + dangle
if not enemy.alive then
table.remove(enemies, i)
else
i = i + 1
end
end
end
function love.update(dt)
if state == states.gameplay then
updateMissiles(dt)
updateExplosions(dt)
updateEnemies(dt)
time = time + dt
if unreflected_missiles_allowed < unreflected_missiles_max and time >= unreflected_missiles_allowed * unreflected_missiles_allowed_ramp_up then
unreflected_missiles_allowed = unreflected_missiles_allowed + 1
end
local cities_remaining = 0
for _, city in ipairs(cities) do
if city.alive then
cities_remaining = cities_remaining + 1
end
end
if cities_remaining == 0 or #enemies == 0 then
state = states.outro
time = 0
explodeAllMissiles()
end
elseif state == states.outro then
updateMissiles(dt)
updateExplosions(dt)
if #explosions == 0 then
time = time + dt
if time >= outro_duration then
local cities_remaining = 0
for _, city in ipairs(cities) do
if city.alive then
cities_remaining = cities_remaining + 1
end
end
if cities_remaining == 0 then
love.mouse.setVisible(true)
love.mouse.setGrabbed(false)
state = states.game_over
else
time = 0
stage = stage + 1
startStage()
end
end
end
elseif state == states.intro then
time = time + dt
if time >= intro_duration then
time = 0
state = states.gameplay
end
end
end
function explodeAllMissiles()
for _, missile in ipairs(missiles) do
spawnExplosion(missile.x, missile.y)
-- Freeze the missile
missile.dx = 0
missile.dy = 0
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.8
end
love.mousemoved = movePaddle
function love.mousepressed(x, y, button, istouch, presses)
love.mouse.setVisible(false)
love.mouse.setGrabbed(true)
if state == states.gameplay then
explodeAllReflected()
elseif state == states.title then
startStage()
elseif state == states.game_over then
love.event.quit()
end
end
function love.keypressed(key, scancode, isrepeat)
-- Ungrab mouse if user tried to do any GUI commands
if scancode == 'lctrl' or scancode == 'lshift' or scancode == 'lalt' or scancode == 'lgui' or
scancode == 'rctrl' or scancode == 'rshift' or scancode == 'ralt' or scancode == 'rgui' then
love.mouse.setVisible(true)
love.mouse.setGrabbed(false)
end
end
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 fifth = 2 * math.pi / 5
local points = {}
for i = 0, 4 do
local x = enemy.x + math.cos(i * fifth + enemy.angle) * enemy_radius
local y = enemy.y + math.sin(i * fifth + enemy.angle) * enemy_radius
local x, y = toScreenCoordinates(x, y)
table.insert(points, x)
table.insert(points, y)
end
love.graphics.polygon('fill', points)
end
end
function drawText(x, y, text_scale, text)
love.graphics.setLineWidth(2 / 700 * scale)
love.graphics.setColor(1, 1, 1)
for i = 1, #text do
local char = string.sub(text, i, i)
local glyph = font[char]
for _, line in ipairs(glyph) do
local x0, y0 = toScreenCoordinates(x + (i - 1) * text_scale + line[1] * text_scale, y + line[2] * text_scale)
local x1, y1 = toScreenCoordinates(x + (i - 1) * text_scale + line[3] * text_scale, y + line[4] * text_scale)
love.graphics.line(x0, y0, x1, y1)
end
end
end
function textWidth(text_scale, text)
-- Each character is 0.7 wide with 0.3 character spacing before scaling
-- Width of a text is therefore math.max(#text - 0.3, 0) before scaling
-- and math.max(#text - 0.3, 0) * text_scale after
return math.max(#text - 0.3, 0) * text_scale
end
function drawTextCentered(y, text_scale, text)
local x = (1 - textWidth(text_scale, text)) / 2
drawText(x, y, text_scale, text)
end
function love.draw()
if state == states.gameplay or state == states.outro or state == states.intro or state == states.game_over then
drawCities()
drawWalls()
drawMissiles()
drawPaddle()
drawEnemies()
drawExplosions()
if state == states.intro then
drawTextCentered(0.4, 0.1, "wave")
local number = tostring(stage)
drawTextCentered(0.55, 0.07, number)
end
if state == states.game_over then
drawTextCentered(0.4, 0.1, "game over")
local number = tostring(stage)
drawTextCentered(0.55, 0.07, number)
drawTextCentered(0.7, 0.03, "click to quit")
end
elseif state == states.title then
drawTextCentered(0.3, 0.1, "reflect")
drawTextCentered(0.45, 0.03, "click to start")
end
end