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