1132 lines
32 KiB
Lua
1132 lines
32 KiB
Lua
local easing = require('lib.easing')
|
|
local tau = math.pi * 2
|
|
local delta_time = 1/120
|
|
local game = {}
|
|
|
|
local function load_level(level, bubble_diameter, row_gap)
|
|
local slots = {}
|
|
local bubble_radius = bubble_diameter / 2
|
|
local num_rows = #level
|
|
local max_cols = math.max(#level[1], #level[2])
|
|
local min_cols = math.min(#level[1], #level[2])
|
|
|
|
assert(
|
|
max_cols == min_cols + 1,
|
|
'number of columns in alternating rows must differ by one'
|
|
)
|
|
|
|
local num_bubble_slots = 0
|
|
if num_rows % 2 == 0 then
|
|
num_bubble_slots = num_rows / 2 * #level[1] +
|
|
num_rows / 2 * #level[2]
|
|
else
|
|
num_bubble_slots = math.ceil(num_rows / 2) * #level[1] +
|
|
math.floor(num_rows / 2) * #level[2]
|
|
end
|
|
print('slots', num_bubble_slots)
|
|
|
|
for row = 1, #level do
|
|
local num_cols = #level[row]
|
|
|
|
assert(
|
|
num_cols == min_cols or num_cols == max_cols,
|
|
string.format("wrong number of columns (%d) in row %d", num_cols, row)
|
|
)
|
|
|
|
local x_offset = num_cols == min_cols and bubble_radius or 0
|
|
for col = 1, num_cols do
|
|
local index = #slots + 1
|
|
slots[index] = {
|
|
x = x_offset + bubble_radius + (col - 1) * bubble_diameter,
|
|
y = bubble_radius + (row - 1) * row_gap,
|
|
bubble_type = level[row][col],
|
|
neighbours = {}
|
|
}
|
|
if col > 1 then -- left
|
|
slots[index].neighbours[#slots[index].neighbours+1] = index - 1
|
|
end
|
|
if col < num_cols then -- right
|
|
slots[index].neighbours[#slots[index].neighbours+1] = index + 1
|
|
end
|
|
if row % 2 == 0 or (row > 1 and col > 1) then -- up left
|
|
slots[index].neighbours[#slots[index].neighbours+1] = index - max_cols
|
|
end
|
|
if row % 2 == 0 or (row > 1 and col < num_cols) then -- up right
|
|
slots[index].neighbours[#slots[index].neighbours+1] = index - min_cols
|
|
end
|
|
if row % 2 == 0 or col > 1 then -- down left
|
|
if index + min_cols <= num_bubble_slots then
|
|
slots[index].neighbours[#slots[index].neighbours+1] = index + min_cols
|
|
end
|
|
end
|
|
if row % 2 == 0 or col < num_cols then -- down right
|
|
if index + max_cols <= num_bubble_slots then
|
|
slots[index].neighbours[#slots[index].neighbours+1] = index + max_cols
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return slots
|
|
end
|
|
|
|
-- ex. remaining_bubble_types = {1,4,5,8} -- should only be regular, not special
|
|
local function get_next_bubble_type(index, remaining_bubble_types)
|
|
return remaining_bubble_types[love.math.random(#remaining_bubble_types)]
|
|
end
|
|
|
|
local function get_bubble_counts()
|
|
local total = 0
|
|
local count_by_type = {}
|
|
for i = 1, #game.bubble_slots do
|
|
local bubble_type = game.bubble_slots[i].bubble_type
|
|
if bubble_type ~= 0 then
|
|
total = total + 1
|
|
end
|
|
if count_by_type[bubble_type] == nil then
|
|
count_by_type[bubble_type] = 0
|
|
end
|
|
count_by_type[bubble_type] = count_by_type[bubble_type] + 1
|
|
end
|
|
return total, count_by_type
|
|
end
|
|
|
|
local function find_nearest_slot(x, y)
|
|
local nearest_slot_index = 0
|
|
local min_dist = math.huge
|
|
for i = 1, #game.bubble_slots do
|
|
local slot = game.bubble_slots[i]
|
|
if slot.bubble_type == 0 then
|
|
local diff_x = x - slot.x
|
|
local diff_y = y - slot.y
|
|
local dist = math.sqrt(diff_x * diff_x + diff_y * diff_y)
|
|
if dist < min_dist then
|
|
min_dist = dist
|
|
nearest_slot_index = i
|
|
end
|
|
end
|
|
end
|
|
assert(nearest_slot_index ~= 0, "no nearest slot found")
|
|
return nearest_slot_index
|
|
end
|
|
|
|
local function move_bubble(x, y, velocity_x, velocity_y)
|
|
local start_time = love.timer.getTime()
|
|
local new_x = x + velocity_x
|
|
local new_y = y + velocity_y
|
|
local new_velocity_x = velocity_x
|
|
local new_velocity_y = velocity_y
|
|
local should_stop = false
|
|
|
|
-- collision with ceiling
|
|
if new_y - game.bubble_radius <= game.ceiling_bottom then
|
|
should_stop = true
|
|
end
|
|
|
|
-- collision with walls
|
|
if not should_stop then
|
|
if new_x + game.bubble_radius >= game.level_right or
|
|
new_x - game.bubble_radius <= game.level_left then
|
|
local impact_x = game.level_left + game.bubble_radius
|
|
if new_x + game.bubble_radius >= game.level_right then
|
|
impact_x = game.level_right - game.bubble_radius
|
|
end
|
|
local dx = impact_x - x
|
|
local ratio = dx / new_velocity_x
|
|
local dy = new_velocity_y * ratio
|
|
local impact_y = y + dy
|
|
local ratio_after_bounce = 1.0 - ratio
|
|
new_velocity_x = -new_velocity_x
|
|
new_x = impact_x + new_velocity_x * ratio_after_bounce
|
|
new_y = impact_y + new_velocity_y * ratio_after_bounce
|
|
end
|
|
end
|
|
|
|
-- collision with another bubble
|
|
if not should_stop then
|
|
for i = 1, #game.bubble_slots do
|
|
local slot = game.bubble_slots[i]
|
|
if slot.bubble_type ~= 0 then
|
|
local diff_x = new_x - slot.x
|
|
local diff_y = new_y - slot.y
|
|
local dist = math.sqrt(diff_x * diff_x + diff_y * diff_y)
|
|
if dist <= game.bubble_diameter - 6 then -- -6 to allow squeezing through
|
|
should_stop = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local nearest_slot_index = 0
|
|
if should_stop then
|
|
nearest_slot_index = find_nearest_slot(new_x, new_y)
|
|
new_x = game.bubble_slots[nearest_slot_index].x
|
|
new_y = game.bubble_slots[nearest_slot_index].y
|
|
new_velocity_x = 0
|
|
new_velocity_y = 0
|
|
end
|
|
-- print(string.format("%.10f", love.timer.getTime() - start_time))
|
|
|
|
return {
|
|
x = new_x,
|
|
y = new_y,
|
|
velocity_x = new_velocity_x,
|
|
velocity_y = new_velocity_y,
|
|
should_stop = should_stop,
|
|
nearest_slot_index = nearest_slot_index
|
|
}
|
|
end
|
|
|
|
local function generate_aim_guide(angle)
|
|
local velocity_x = math.cos(angle) * game.bubble_speed
|
|
local velocity_y = math.sin(angle) * game.bubble_speed
|
|
local new_x = game.launcher_x
|
|
local new_y = game.launcher_y
|
|
local radius = 5
|
|
|
|
game.aim_guide = {}
|
|
for i = 1, 125 do
|
|
if i >= 15 and i % 5 == 0 then
|
|
game.aim_guide[#game.aim_guide+1] = {
|
|
x = new_x,
|
|
y = new_y,
|
|
radius = radius
|
|
}
|
|
end
|
|
|
|
local m = move_bubble(new_x, new_y, velocity_x, velocity_y)
|
|
|
|
new_x = m.x
|
|
new_y = m.y
|
|
velocity_x = m.velocity_x
|
|
velocity_y = m.velocity_y
|
|
|
|
if m.should_stop then
|
|
game.aim_guide[#game.aim_guide+1] = {
|
|
x = new_x,
|
|
y = new_y,
|
|
radius = radius
|
|
}
|
|
break
|
|
end
|
|
end
|
|
for i = 1, #game.aim_guide - 1 do
|
|
local curr_dot = game.aim_guide[i]
|
|
local next_dot = game.aim_guide[i+1]
|
|
curr_dot.tween = {
|
|
delay = 20,
|
|
t = 0,
|
|
d = 40,
|
|
b_x = curr_dot.x,
|
|
c_x = next_dot.x - curr_dot.x,
|
|
b_y = curr_dot.y,
|
|
c_y = next_dot.y - curr_dot.y
|
|
}
|
|
end
|
|
end
|
|
|
|
local function find_matches(start_index)
|
|
local bubble_type = game.bubble_slots[start_index].bubble_type
|
|
local to_check = {start_index}
|
|
local matches = {start_index}
|
|
local visited = {}
|
|
visited[start_index] = true
|
|
|
|
while #to_check > 0 do
|
|
local current_index = table.remove(to_check)
|
|
local slot = game.bubble_slots[current_index]
|
|
|
|
for i = 1, #slot.neighbours do
|
|
local neighbour_index = slot.neighbours[i]
|
|
local neighbour = game.bubble_slots[neighbour_index]
|
|
if not visited[neighbour_index] and neighbour.bubble_type == bubble_type then
|
|
matches[#matches+1] = neighbour_index
|
|
to_check[#to_check+1] = neighbour_index
|
|
end
|
|
visited[neighbour_index] = true
|
|
end
|
|
end
|
|
|
|
return matches
|
|
end
|
|
|
|
local function find_unattached_bubbles()
|
|
local to_check = {}
|
|
local visited = {}
|
|
local attached = {}
|
|
local unattached = {}
|
|
|
|
-- start with top row bubbles attached to the ceiling
|
|
for i = 1, #game.levels[game.current_level][1] do
|
|
if game.bubble_slots[i].bubble_type ~= 0 then
|
|
to_check[#to_check+1] = i
|
|
attached[i] = true
|
|
end
|
|
visited[i] = true
|
|
end
|
|
|
|
while #to_check > 0 do
|
|
local current_index = table.remove(to_check)
|
|
local slot = game.bubble_slots[current_index]
|
|
|
|
for i = 1, #slot.neighbours do
|
|
local neighbour_index = slot.neighbours[i]
|
|
local neighbour = game.bubble_slots[neighbour_index]
|
|
if not visited[neighbour_index] and neighbour.bubble_type ~= 0 then
|
|
attached[neighbour_index] = true
|
|
to_check[#to_check+1] = neighbour_index
|
|
end
|
|
visited[neighbour_index] = true
|
|
end
|
|
end
|
|
|
|
for i = 1, #game.bubble_slots do
|
|
if game.bubble_slots[i].bubble_type ~= 0 and not attached[i] then
|
|
unattached[#unattached+1] = i
|
|
end
|
|
end
|
|
|
|
return unattached
|
|
end
|
|
|
|
-- don't allow aiming backwards or straight sideways
|
|
local function clamp_launcher_angle(angle)
|
|
if (angle >= 0 and angle <= tau / 4) or angle > tau * 25 / 26 then
|
|
angle = tau * 25 / 26
|
|
elseif angle > tau / 4 and angle < tau * 14 / 26 then
|
|
angle = tau * 14 / 26
|
|
end
|
|
return angle
|
|
end
|
|
|
|
local function start_level()
|
|
game.game_over = false
|
|
game.paused = false
|
|
game.fade_to_grey = {time = 0, duration = 120, progress = 0.0}
|
|
game.frame_by_frame = false
|
|
game.frame_counter = 0
|
|
game.timer = 0
|
|
game.score = 0
|
|
game.points_display = {}
|
|
game.bubbles_launched = 0
|
|
game.ceiling_drops_after = 5
|
|
game.ceiling_drop_tween = nil
|
|
game.ceiling_should_drop = false
|
|
|
|
game.bubble_slots = load_level(
|
|
game.levels[game.current_level], game.bubble_diameter, game.row_gap
|
|
)
|
|
local max_cols = math.max(
|
|
#game.levels[game.current_level][1],
|
|
#game.levels[game.current_level][2]
|
|
)
|
|
local num_rows = #game.levels[game.current_level]
|
|
game.level_width = max_cols * game.bubble_diameter
|
|
game.level_height = (num_rows - 1) * game.row_gap + game.bubble_diameter
|
|
game.level_left = (game.window_width - game.level_width) / 2
|
|
game.level_top = (
|
|
game.window_height - game.level_height -
|
|
game.bubble_diameter - game.bubble_radius
|
|
) / 2
|
|
game.level_right = game.level_left + game.level_width
|
|
game.level_bottom = game.level_top + game.level_height
|
|
game.ceiling_bottom = game.level_top
|
|
|
|
for i = 1, #game.bubble_slots do
|
|
game.bubble_slots[i].x = game.bubble_slots[i].x + game.level_left
|
|
game.bubble_slots[i].y = game.bubble_slots[i].y + game.ceiling_bottom
|
|
end
|
|
|
|
game.launcher_x = game.window_width / 2
|
|
game.launcher_y = game.level_bottom + game.bubble_diameter
|
|
game.launcher_rotation = -tau / 4 -- up
|
|
|
|
local total_bubble_count, counts_by_type = get_bubble_counts()
|
|
local bubble_types = {}
|
|
for bubble_type, count in pairs(counts_by_type) do
|
|
if bubble_type >= 1 and bubble_type <= 8 then
|
|
bubble_types[#bubble_types+1] = bubble_type
|
|
end
|
|
end
|
|
game.current_bubble = {
|
|
x = game.launcher_x,
|
|
y = game.launcher_y,
|
|
bubble_type = get_next_bubble_type(1, bubble_types),
|
|
velocity_x = 0,
|
|
velocity_y = 0
|
|
}
|
|
game.next_bubble = {
|
|
x = game.launcher_x + game.bubble_diameter * 2,
|
|
y = game.launcher_y,
|
|
bubble_type = get_next_bubble_type(2, bubble_types),
|
|
scale = 1
|
|
}
|
|
|
|
game.show_aim_guide = true
|
|
game.aim_guide = {}
|
|
generate_aim_guide(game.launcher_rotation)
|
|
|
|
game.bursting_bubbles = {}
|
|
game.falling_bubbles = {}
|
|
end
|
|
|
|
function love.load(arg, unfiltered_arg)
|
|
if arg[#arg] == "debug" then require("lldebugger").start() end
|
|
|
|
game.window_width, game.window_height = love.graphics.getDimensions()
|
|
game.window_center_x = game.window_width / 2
|
|
game.window_center_y = game.window_height / 2
|
|
|
|
game.bm_font72 = love.graphics.newFont('fonts/font72.fnt', 'fonts/font72.png')
|
|
game.bm_font36 = love.graphics.newFont('fonts/font36.fnt', 'fonts/font36.png')
|
|
game.bm_font18 = love.graphics.newFont('fonts/font18.fnt', 'fonts/font18.png')
|
|
game.font36_widths = {}
|
|
|
|
game.game_over_image = love.graphics.newImage('images/game_over.png')
|
|
|
|
game.bubble_diameter = 60
|
|
game.bubble_radius = game.bubble_diameter / 2
|
|
game.bubble_speed = 960 * delta_time -- 960 px/s = 8 px/frame
|
|
|
|
-- vertical distance between bubble center points
|
|
game.row_gap = math.ceil(math.sqrt(
|
|
(game.bubble_diameter * game.bubble_diameter) -
|
|
(game.bubble_radius * game.bubble_radius)
|
|
))
|
|
|
|
game.background_image = love.graphics.newImage('images/background.png')
|
|
game.background_width = game.background_image:getWidth()
|
|
|
|
game.bubble_images = {
|
|
love.graphics.newImage('images/red.png'), -- 1
|
|
love.graphics.newImage('images/orange.png'), -- 2
|
|
love.graphics.newImage('images/yellow.png'), -- 3
|
|
love.graphics.newImage('images/green.png'), -- 4
|
|
love.graphics.newImage('images/blue.png'), -- 5
|
|
love.graphics.newImage('images/purple.png'), -- 6
|
|
love.graphics.newImage('images/pink.png'), -- 7
|
|
love.graphics.newImage('images/white.png') -- 8
|
|
}
|
|
|
|
game.levels = {}
|
|
game.levels[1] = {
|
|
{1,1,1,3,3,2,2,2},
|
|
{ 1,1,3,3,3,2,2 },
|
|
{4,4,7,8,8,6,5,5},
|
|
{ 4,7,7,8,6,6,5 },
|
|
{0,0,0,0,0,0,0,0},
|
|
{ 0,0,0,0,0,0,0 },
|
|
{0,0,0,0,0,0,0,0},
|
|
{ 0,0,0,0,0,0,0 },
|
|
{0,0,0,0,0,0,0,0},
|
|
{ 0,0,0,0,0,0,0 },
|
|
{0,0,0,0,0,0,0,0},
|
|
{ 0,0,0,0,0,0,0 },
|
|
{0,0,0,0,0,0,0,0},
|
|
{ 0,0,0,0,0,0,0 }
|
|
}
|
|
game.current_level = 1
|
|
|
|
game.launcher_image = love.graphics.newImage('images/launcher.png')
|
|
game.launcher_height = game.launcher_image:getHeight()
|
|
game.launcher_width = game.launcher_image:getWidth()
|
|
game.launcher_offset_x = 90 -- rotation point in launcher image
|
|
game.launcher_offset_y = game.launcher_height / 2
|
|
|
|
|
|
game.shader = love.graphics.newShader [[
|
|
uniform float progress;
|
|
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords)
|
|
{
|
|
vec4 pixel = Texel(texture, texture_coords);
|
|
number average = (pixel.r + pixel.b + pixel.g) / 3.0;
|
|
pixel.r = pixel.r + (average-pixel.r) * progress;
|
|
pixel.g = pixel.g + (average-pixel.g) * progress;
|
|
pixel.b = pixel.b + (average-pixel.b) * progress;
|
|
return pixel;
|
|
}
|
|
]]
|
|
|
|
start_level()
|
|
end
|
|
|
|
function love.update(dt)
|
|
if game.game_over then
|
|
if game.fade_to_grey.time < game.fade_to_grey.duration then
|
|
game.fade_to_grey.time = game.fade_to_grey.time + 1
|
|
game.fade_to_grey.progress =
|
|
game.fade_to_grey.time / game.fade_to_grey.duration
|
|
game.shader:send("progress", game.fade_to_grey.progress)
|
|
end
|
|
end
|
|
|
|
if game.current_bubble and game.current_bubble_tween then
|
|
game.current_bubble_tween.t = game.current_bubble_tween.t + 1
|
|
if game.current_bubble_tween.t < game.current_bubble_tween.d then
|
|
game.current_bubble.x = easing.outBack(
|
|
game.current_bubble_tween.t,
|
|
game.current_bubble_tween.b,
|
|
game.current_bubble_tween.c,
|
|
game.current_bubble_tween.d
|
|
)
|
|
else
|
|
game.current_bubble_tween = nil
|
|
game.current_bubble.x = game.launcher_x
|
|
end
|
|
end
|
|
|
|
if game.next_bubble_tween then
|
|
game.next_bubble_tween.t = game.next_bubble_tween.t + 1
|
|
if game.next_bubble_tween.t < game.next_bubble_tween.d then
|
|
game.next_bubble.scale = easing.outBack(
|
|
game.next_bubble_tween.t,
|
|
game.next_bubble_tween.b,
|
|
game.next_bubble_tween.c,
|
|
game.next_bubble_tween.d
|
|
)
|
|
else
|
|
game.next_bubble_tween = nil
|
|
game.next_bubble.scale = 1
|
|
end
|
|
end
|
|
|
|
if game.bubble_swap_tween then
|
|
game.bubble_swap_tween.t = game.bubble_swap_tween.t + 1
|
|
if game.bubble_swap_tween.t < game.bubble_swap_tween.d then
|
|
game.current_bubble.x = easing.outBack(
|
|
game.bubble_swap_tween.t,
|
|
game.bubble_swap_tween.b1,
|
|
game.bubble_swap_tween.c1,
|
|
game.bubble_swap_tween.d
|
|
)
|
|
game.next_bubble.x = easing.outBack(
|
|
game.bubble_swap_tween.t,
|
|
game.bubble_swap_tween.b2,
|
|
game.bubble_swap_tween.c2,
|
|
game.bubble_swap_tween.d
|
|
)
|
|
else
|
|
game.bubble_swap_tween = nil
|
|
game.next_bubble.bubble_type, game.current_bubble.bubble_type =
|
|
game.current_bubble.bubble_type, game.next_bubble.bubble_type
|
|
game.current_bubble.x = game.launcher_x
|
|
game.next_bubble.x = game.launcher_x + game.bubble_diameter * 2
|
|
end
|
|
end
|
|
|
|
if game.paused or game.game_over then
|
|
return
|
|
end
|
|
|
|
if game.show_aim_guide then
|
|
for i = 1, #game.aim_guide do
|
|
local dot = game.aim_guide[i]
|
|
if dot.tween then
|
|
if dot.tween.delay == 0 then
|
|
dot.tween.t = dot.tween.t + 1
|
|
if dot.tween.t == dot.tween.d then
|
|
dot.tween.t = 0
|
|
else
|
|
dot.x = easing.linear(dot.tween.t, dot.tween.b_x, dot.tween.c_x, dot.tween.d)
|
|
dot.y = easing.linear(dot.tween.t, dot.tween.b_y, dot.tween.c_y, dot.tween.d)
|
|
end
|
|
else
|
|
dot.tween.delay = dot.tween.delay - 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if game.ceiling_drop_tween then
|
|
local old_y = game.ceiling_bottom
|
|
game.ceiling_drop_tween.t = game.ceiling_drop_tween.t + 1
|
|
game.ceiling_bottom = easing.outBack(
|
|
game.ceiling_drop_tween.t,
|
|
game.ceiling_drop_tween.b,
|
|
game.ceiling_drop_tween.c,
|
|
game.ceiling_drop_tween.d
|
|
)
|
|
local diff_y = game.ceiling_bottom - old_y
|
|
for i = 1, #game.bubble_slots do
|
|
game.bubble_slots[i].y = game.bubble_slots[i].y + diff_y
|
|
if game.bubble_slots[i].bubble_type ~= 0 and
|
|
game.bubble_slots[i].y > game.level_bottom then
|
|
game.game_over = true
|
|
game.fade_to_grey.time = 0
|
|
game.fade_to_grey.progress = 0.0
|
|
end
|
|
end
|
|
if game.game_over then
|
|
return
|
|
end
|
|
if game.ceiling_drop_tween.t == game.ceiling_drop_tween.d then
|
|
game.ceiling_drop_tween = nil
|
|
end
|
|
if game.show_aim_guide then
|
|
generate_aim_guide(game.launcher_rotation)
|
|
end
|
|
end
|
|
|
|
if game.current_bubble and
|
|
(game.current_bubble.velocity_x ~= 0 or game.current_bubble.velocity_y ~= 0) then
|
|
|
|
local movement_info
|
|
|
|
movement_info = move_bubble(
|
|
game.current_bubble.x,
|
|
game.current_bubble.y,
|
|
game.current_bubble.velocity_x,
|
|
game.current_bubble.velocity_y
|
|
)
|
|
game.current_bubble.x = movement_info.x
|
|
game.current_bubble.y = movement_info.y
|
|
game.current_bubble.velocity_x = movement_info.velocity_x
|
|
game.current_bubble.velocity_y = movement_info.velocity_y
|
|
|
|
if movement_info.should_stop then
|
|
game.bubble_slots[movement_info.nearest_slot_index].bubble_type =
|
|
game.current_bubble.bubble_type
|
|
|
|
if game.ceiling_should_drop then
|
|
game.ceiling_drop_tween = {
|
|
t = 0,
|
|
d = 30,
|
|
b = game.ceiling_bottom,
|
|
c = game.row_gap
|
|
}
|
|
end
|
|
game.ceiling_should_drop = false
|
|
|
|
local matches = find_matches(movement_info.nearest_slot_index)
|
|
|
|
if #matches < 3 and game.current_bubble.y > game.level_bottom then
|
|
game.game_over = true
|
|
game.fade_to_grey.time = 0
|
|
game.fade_to_grey.progress = 0.0
|
|
return
|
|
end
|
|
|
|
if #matches >= 3 then
|
|
game.current_bubble = nil
|
|
|
|
-- remove matches
|
|
for i = 1, #matches do
|
|
local index = matches[i]
|
|
game.bursting_bubbles[#game.bursting_bubbles+1] = {
|
|
delay = (i - 1) * 5,
|
|
x = game.bubble_slots[index].x,
|
|
y = game.bubble_slots[index].y,
|
|
bubble_type = game.bubble_slots[index].bubble_type,
|
|
scale = 1.0,
|
|
alpha = 1.0
|
|
}
|
|
game.bubble_slots[index].bubble_type = 0
|
|
local points = (i + 1) ^ 2
|
|
game.score = game.score + points
|
|
game.points_display[#game.points_display+1] = {
|
|
points = points,
|
|
x = game.bubble_slots[index].x,
|
|
y = game.bubble_slots[index].y,
|
|
tween = {
|
|
delay = (i - 1) * 5,
|
|
t = 0,
|
|
d = (i - 1) * 10 + 40,
|
|
b = game.bubble_slots[index].y - 10,
|
|
c = -game.bubble_radius
|
|
}
|
|
}
|
|
end
|
|
|
|
-- remove unattached bubbles
|
|
local unattached = find_unattached_bubbles()
|
|
for i = 1, #unattached do
|
|
local index = unattached[i]
|
|
game.falling_bubbles[#game.falling_bubbles+1] = {
|
|
x = game.bubble_slots[index].x,
|
|
y = game.bubble_slots[index].y,
|
|
velocity_x = (i % 2 == 0 and -200 or 200) * delta_time,
|
|
velocity_y = love.math.random(-200.0, -500.0) * delta_time,
|
|
bubble_type = game.bubble_slots[index].bubble_type
|
|
}
|
|
game.bubble_slots[index].bubble_type = 0
|
|
local points = (i + 1) ^ 3
|
|
game.score = game.score + points
|
|
game.points_display[#game.points_display+1] = {
|
|
points = points,
|
|
x = game.bubble_slots[index].x,
|
|
y = game.bubble_slots[index].y,
|
|
tween = {
|
|
delay = (i - 1) * 5,
|
|
t = 0,
|
|
d = (i - 1) * 10 + 40,
|
|
b = game.bubble_slots[index].y - 10,
|
|
c = -game.bubble_radius
|
|
}
|
|
}
|
|
end
|
|
end
|
|
|
|
if game.show_aim_guide then
|
|
generate_aim_guide(game.launcher_rotation)
|
|
end
|
|
|
|
-- calculate remaining bubble types
|
|
local total_bubble_count, counts_by_type = get_bubble_counts()
|
|
if total_bubble_count == 0 then
|
|
print('level cleared!')
|
|
else
|
|
local bubble_types = {}
|
|
for bubble_type, count in pairs(counts_by_type) do
|
|
if bubble_type >= 1 and bubble_type <= 8 then
|
|
bubble_types[#bubble_types+1] = bubble_type
|
|
end
|
|
end
|
|
-- get next bubble
|
|
game.current_bubble = {
|
|
x = game.launcher_x + game.bubble_diameter * 2,
|
|
y = game.launcher_y,
|
|
bubble_type = game.next_bubble.bubble_type,
|
|
velocity_x = 0,
|
|
velocity_y = 0
|
|
}
|
|
game.current_bubble_tween = {
|
|
t = 0,
|
|
d = 30,
|
|
b = game.current_bubble.x,
|
|
c = game.launcher_x - game.current_bubble.x
|
|
}
|
|
game.next_bubble.bubble_type = get_next_bubble_type(game.bubbles_launched+1, bubble_types)
|
|
game.next_bubble.scale = 0
|
|
game.next_bubble_tween = {t = 0, d = 30, b = 0, c = 1}
|
|
end
|
|
end
|
|
end
|
|
|
|
if #game.bursting_bubbles > 0 then
|
|
for i = #game.bursting_bubbles, 1, -1 do
|
|
local bubble = game.bursting_bubbles[i]
|
|
if bubble.delay == 0 then
|
|
bubble.scale = bubble.scale * 1.03
|
|
bubble.alpha = bubble.alpha - 0.02
|
|
if bubble.scale >= 1.5 then
|
|
table.remove(game.bursting_bubbles, i)
|
|
end
|
|
else
|
|
bubble.delay = bubble.delay - 1
|
|
end
|
|
end
|
|
end
|
|
|
|
if #game.falling_bubbles > 0 then
|
|
for i = #game.falling_bubbles, 1, -1 do
|
|
local bubble = game.falling_bubbles[i]
|
|
bubble.x = bubble.x + bubble.velocity_x
|
|
bubble.y = bubble.y + bubble.velocity_y
|
|
bubble.velocity_x = bubble.velocity_x * 0.99
|
|
bubble.velocity_y = bubble.velocity_y + 36 * delta_time -- gravity
|
|
if bubble.x - game.bubble_radius <= game.level_left or
|
|
bubble.x + game.bubble_radius >= game.level_right then
|
|
bubble.velocity_x = -bubble.velocity_x
|
|
end
|
|
if bubble.y - game.bubble_radius > game.window_height then
|
|
table.remove(game.falling_bubbles, i)
|
|
end
|
|
end
|
|
end
|
|
|
|
for i = #game.points_display, 1, -1 do
|
|
local p = game.points_display[i]
|
|
if p.tween then
|
|
if p.tween.delay == 0 then
|
|
p.tween.t = p.tween.t + 1
|
|
if p.tween.t == p.tween.d then
|
|
table.remove(game.points_display, i)
|
|
else
|
|
p.y = easing.outBack(p.tween.t, p.tween.b, p.tween.c, p.tween.d)
|
|
end
|
|
else
|
|
p.tween.delay = p.tween.delay - 1
|
|
end
|
|
end
|
|
end
|
|
|
|
if game.frame_by_frame then
|
|
game.paused = true
|
|
game.frame_by_frame = false
|
|
end
|
|
end
|
|
|
|
function love.draw(alpha)
|
|
if game.paused then
|
|
alpha = 0
|
|
end
|
|
|
|
-- draw background
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.draw(game.background_image, 0, 0)
|
|
love.graphics.draw(game.background_image, game.background_width, 0)
|
|
|
|
-- draw walls
|
|
love.graphics.setColor(0, 0, 0, 1)
|
|
love.graphics.rectangle(
|
|
'line',
|
|
game.level_left,
|
|
game.level_top,
|
|
game.level_width,
|
|
game.level_height,
|
|
game.bubble_radius,
|
|
game.bubble_radius
|
|
)
|
|
if game.ceiling_bottom > game.level_top then
|
|
love.graphics.line(
|
|
game.level_left,
|
|
game.ceiling_bottom,
|
|
game.level_right,
|
|
game.ceiling_bottom
|
|
)
|
|
end
|
|
|
|
-- draw ceiling drop indicator
|
|
love.graphics.setColor(0, 0, 0, 1)
|
|
for i = 1, game.ceiling_drops_after do
|
|
love.graphics.circle(
|
|
'line',
|
|
game.level_left + (i - 1) * 15 + 15,
|
|
game.level_bottom + 15,
|
|
6
|
|
)
|
|
end
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
local ceiling_progress = game.bubbles_launched % game.ceiling_drops_after
|
|
for i = 1, ceiling_progress do
|
|
love.graphics.circle(
|
|
'fill',
|
|
game.level_left + (i - 1) * 15 + 15,
|
|
game.level_bottom + 15,
|
|
5
|
|
)
|
|
end
|
|
|
|
if game.game_over then
|
|
love.graphics.setShader(game.shader)
|
|
end
|
|
|
|
-- draw stationary bubbles
|
|
local ceiling_drops_in = game.ceiling_drops_after - ceiling_progress
|
|
if not game.game_over and (ceiling_drops_in == 1 or game.ceiling_should_drop) then
|
|
local dx = love.math.random(-2, 2)
|
|
local dy = love.math.random(-2, 2)
|
|
love.graphics.translate(dx, dy)
|
|
end
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
for i = 1, #game.bubble_slots do
|
|
local slot = game.bubble_slots[i]
|
|
if slot.bubble_type >= 1 and slot.bubble_type <= #game.bubble_images then
|
|
love.graphics.draw(
|
|
game.bubble_images[slot.bubble_type],
|
|
slot.x,
|
|
slot.y,
|
|
0, 1, 1,
|
|
game.bubble_radius,
|
|
game.bubble_radius
|
|
)
|
|
end
|
|
end
|
|
love.graphics.origin()
|
|
|
|
-- draw launcher
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.draw(
|
|
game.launcher_image,
|
|
game.launcher_x,
|
|
game.launcher_y,
|
|
game.launcher_rotation,
|
|
1,
|
|
1,
|
|
game.launcher_offset_x,
|
|
game.launcher_offset_y
|
|
)
|
|
|
|
-- draw bursting bubbles
|
|
if #game.bursting_bubbles > 0 then
|
|
for i = 1, #game.bursting_bubbles do
|
|
local bubble = game.bursting_bubbles[i]
|
|
love.graphics.setColor(1, 1, 1, bubble.alpha)
|
|
love.graphics.draw(
|
|
game.bubble_images[bubble.bubble_type],
|
|
bubble.x,
|
|
bubble.y,
|
|
0,
|
|
bubble.scale,
|
|
bubble.scale,
|
|
game.bubble_radius,
|
|
game.bubble_radius
|
|
)
|
|
end
|
|
end
|
|
|
|
-- draw falling bubbles
|
|
if #game.falling_bubbles > 0 then
|
|
for i = 1, #game.falling_bubbles do
|
|
local bubble = game.falling_bubbles[i]
|
|
love.graphics.setColor(1, 1, 1, bubble.alpha)
|
|
love.graphics.draw(
|
|
game.bubble_images[bubble.bubble_type],
|
|
bubble.x + bubble.velocity_x * alpha,
|
|
bubble.y + bubble.velocity_y * alpha,
|
|
0,
|
|
bubble.scale,
|
|
bubble.scale,
|
|
game.bubble_radius,
|
|
game.bubble_radius
|
|
)
|
|
end
|
|
end
|
|
|
|
-- draw aim guide
|
|
if game.show_aim_guide then
|
|
love.graphics.setColor(0, 0, 0, 1)
|
|
for i = 1, #game.aim_guide - 1 do
|
|
love.graphics.circle(
|
|
'line',
|
|
game.aim_guide[i].x,
|
|
game.aim_guide[i].y,
|
|
game.aim_guide[i].radius
|
|
)
|
|
end
|
|
end
|
|
|
|
-- draw upcoming bubble
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.draw(
|
|
game.bubble_images[game.next_bubble.bubble_type],
|
|
game.next_bubble.x,
|
|
game.next_bubble.y,
|
|
0,
|
|
game.next_bubble.scale,
|
|
game.next_bubble.scale,
|
|
game.bubble_radius,
|
|
game.bubble_radius
|
|
)
|
|
|
|
-- draw current bubble
|
|
-- if moving, extrapolate position based on alpha value from love.run
|
|
if game.current_bubble then
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.draw(
|
|
game.bubble_images[game.current_bubble.bubble_type],
|
|
game.current_bubble.x + game.current_bubble.velocity_x * alpha,
|
|
game.current_bubble.y + game.current_bubble.velocity_y * alpha,
|
|
0,
|
|
1,
|
|
1,
|
|
game.bubble_radius,
|
|
game.bubble_radius
|
|
)
|
|
end
|
|
|
|
|
|
if game.game_over then
|
|
love.graphics.setShader()
|
|
end
|
|
|
|
-- draw help text
|
|
love.graphics.setFont(game.bm_font18)
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.printf(
|
|
"S to swap",
|
|
game.launcher_x + game.bubble_diameter,
|
|
game.launcher_y + game.bubble_radius + 10,
|
|
game.bubble_diameter * 2,
|
|
'center'
|
|
)
|
|
|
|
-- draw score
|
|
love.graphics.setFont(game.bm_font72)
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.print(
|
|
string.format("%08d", game.score),
|
|
game.bubble_radius,
|
|
game.bubble_radius
|
|
)
|
|
love.graphics.setFont(game.bm_font36)
|
|
for i = 1, #game.points_display do
|
|
local p = game.points_display[i]
|
|
if not game.font36_widths[p.points] then
|
|
game.font36_widths[p.points] = game.bm_font36:getWidth(p.points)
|
|
end
|
|
if p.tween.delay == 0 then
|
|
love.graphics.printf(
|
|
p.points,
|
|
p.x - game.bubble_radius,
|
|
p.y,
|
|
game.font36_widths[p.points],
|
|
'center'
|
|
)
|
|
end
|
|
end
|
|
|
|
-- draw game over
|
|
if game.game_over and game.fade_to_grey.progress == 1.0 then
|
|
love.graphics.setColor(0, 0, 0, 0.8)
|
|
love.graphics.rectangle(
|
|
'fill',
|
|
game.level_left + 10,
|
|
game.window_height / 2 - 80,
|
|
game.level_width - 20,
|
|
160,
|
|
20,
|
|
20
|
|
)
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.draw(
|
|
game.game_over_image,
|
|
game.window_center_x,
|
|
game.window_center_y,
|
|
0,
|
|
1,
|
|
1,
|
|
game.game_over_image:getWidth() / 2,
|
|
game.game_over_image:getHeight() / 2
|
|
)
|
|
end
|
|
|
|
end
|
|
|
|
function love.mousepressed(x, y, button, is_touch, presses)
|
|
if game.game_over then
|
|
start_level()
|
|
return
|
|
end
|
|
if button == 1 and game.current_bubble and
|
|
game.current_bubble.x == game.launcher_x and
|
|
game.current_bubble.y == game.launcher_y and
|
|
game.current_bubble.velocity_x == 0 and
|
|
game.current_bubble.velocity_y == 0 then
|
|
|
|
-- use the angle from mousemoved to calculate the velocity so that shooting
|
|
-- backwards is not possible
|
|
game.current_bubble.velocity_x =
|
|
math.cos(game.launcher_rotation) * game.bubble_speed
|
|
game.current_bubble.velocity_y =
|
|
math.sin(game.launcher_rotation) * game.bubble_speed
|
|
|
|
game.bubbles_launched = game.bubbles_launched + 1
|
|
|
|
-- if ceiling_should_drop is true, it will create a tween to drop the
|
|
-- ceiling (and bubbles) after the bubble that was just launched comes to a
|
|
-- stop in love.update()
|
|
game.ceiling_should_drop = game.bubbles_launched % game.ceiling_drops_after == 0
|
|
end
|
|
end
|
|
|
|
function love.wheelmoved(x, y)
|
|
if y ~= 0 then
|
|
local angle = game.launcher_rotation
|
|
angle = angle + (y < 0 and 0.01 or -0.01)
|
|
game.launcher_rotation = clamp_launcher_angle(angle)
|
|
if game.show_aim_guide then
|
|
generate_aim_guide(game.launcher_rotation)
|
|
end
|
|
end
|
|
end
|
|
|
|
function love.mousemoved(x, y, dx, dy, is_touch)
|
|
if not game.game_over then
|
|
local diff_x = x - game.launcher_x
|
|
local diff_y = y - game.launcher_y
|
|
local angle = math.atan2(diff_y, diff_x)
|
|
|
|
-- force angle to be positive
|
|
if y < game.launcher_y then
|
|
angle = angle + tau
|
|
end
|
|
|
|
game.launcher_rotation = clamp_launcher_angle(angle)
|
|
|
|
if game.show_aim_guide then
|
|
generate_aim_guide(game.launcher_rotation)
|
|
end
|
|
end
|
|
end
|
|
|
|
function love.keypressed(key, scan_code, is_repeat)
|
|
if key == 'escape' then
|
|
love.event.quit()
|
|
elseif game.game_over or key == 'r' then
|
|
start_level()
|
|
elseif key == 's' then
|
|
if game.current_bubble and
|
|
game.current_bubble.x == game.launcher_x and
|
|
game.current_bubble.y == game.launcher_y and
|
|
game.current_bubble.velocity_x == 0 and
|
|
game.current_bubble.velocity_y == 0 then
|
|
game.bubble_swap_tween = {
|
|
t = 0,
|
|
d = 30,
|
|
b1 = game.launcher_x,
|
|
b2 = game.launcher_x + game.bubble_diameter * 2,
|
|
c1 = game.bubble_diameter * 2,
|
|
c2 = -game.bubble_diameter * 2
|
|
}
|
|
end
|
|
elseif key == 'space' then
|
|
game.paused = not game.paused
|
|
elseif key == 'n' and game.paused then
|
|
game.frame_by_frame = true
|
|
game.paused = false
|
|
end
|
|
end
|
|
|
|
function love.run()
|
|
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
|
|
|
|
if love.timer then love.timer.step() end
|
|
|
|
local dt = delta_time -- update 120 times per second
|
|
local accumulator = 0.0
|
|
|
|
return function()
|
|
if love.event then
|
|
love.event.pump()
|
|
for name, a,b,c,d,e,f in love.event.poll() do
|
|
if name == "quit" then
|
|
if not love.quit or not love.quit() then
|
|
return a or 0
|
|
end
|
|
end
|
|
love.handlers[name](a,b,c,d,e,f)
|
|
end
|
|
end
|
|
|
|
local frame_time = dt -- default in case love.timer is disabled
|
|
|
|
if love.timer then
|
|
frame_time = love.timer.step()
|
|
if frame_time > 0.25 then
|
|
frame_time = 0.25
|
|
end
|
|
end
|
|
|
|
accumulator = accumulator + frame_time
|
|
|
|
while (accumulator >= dt) do
|
|
if love.update then love.update(dt) end
|
|
accumulator = accumulator - dt
|
|
end
|
|
|
|
local alpha = accumulator / dt
|
|
|
|
if love.graphics and love.graphics.isActive() then
|
|
love.graphics.origin()
|
|
love.graphics.clear(love.graphics.getBackgroundColor())
|
|
|
|
if love.draw then love.draw(alpha) end
|
|
|
|
love.graphics.present()
|
|
end
|
|
|
|
if love.timer then love.timer.sleep(0.001) end
|
|
end
|
|
end
|