bubble_shooter_love2d/main.lua

466 lines
14 KiB
Lua

local tau = math.pi * 2
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 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
function love.load(arg)
if arg[#arg] == "debug" then require("lldebugger").start() end
game.window_width, game.window_height = love.graphics.getDimensions()
game.paused = false
game.frame_by_frame = false
game.bubble_diameter = 60
game.bubble_radius = game.bubble_diameter / 2
game.bubble_speed = 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.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.bubble_radius
game.level_right = game.level_left + game.level_width
game.level_bottom = game.level_top + game.level_height
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.level_top
end
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_x = game.window_width / 2
game.launcher_y = game.level_top + game.level_height + game.bubble_diameter
game.launcher_offset_x = 90 -- rotation point in launcher image
game.launcher_offset_y = game.launcher_height / 2
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.next_bubble_index = 1
game.next_bubble = {
x = game.launcher_x,
y = game.launcher_y,
bubble_type = get_next_bubble_type(game.next_bubble_index, bubble_types),
velocity_x = 0,
velocity_y = 0
}
end
function love.update(dt)
if game.paused then
return
end
if game.next_bubble.velocity_x ~= 0 or game.next_bubble.velocity_y ~= 0 then
local next_x = game.next_bubble.x + game.next_bubble.velocity_x
local next_y = game.next_bubble.y + game.next_bubble.velocity_y
local bubble_should_stop = false
-- collision with ceiling
if next_y - game.bubble_radius <= game.level_top then
bubble_should_stop = true
end
-- collision with walls
if not bubble_should_stop then
if next_x + game.bubble_radius >= game.level_right or
next_x - game.bubble_radius <= game.level_left then
local impact_x = game.level_left + game.bubble_radius
if next_x + game.bubble_radius >= game.level_right then
impact_x = game.level_right - game.bubble_radius
end
local dx = impact_x - game.next_bubble.x
local ratio = dx / game.next_bubble.velocity_x
local dy = game.next_bubble.velocity_y * ratio
local impact_y = game.next_bubble.y + dy
local ratio_after_bounce = 1.0 - ratio
game.next_bubble.velocity_x = -game.next_bubble.velocity_x
next_x = impact_x + game.next_bubble.velocity_x * ratio_after_bounce
next_y = impact_y + game.next_bubble.velocity_y * ratio_after_bounce
end
end
-- collision with another bubble
if not bubble_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 = next_x - slot.x
local diff_y = next_y - slot.y
local dist = math.sqrt(diff_x * diff_x + diff_y * diff_y)
if dist <= game.bubble_diameter then
local depth = game.bubble_diameter - dist
local direction_x = diff_x / dist
local direction_y = diff_y / dist
next_x = next_x + direction_x * depth
next_y = next_y + direction_y * depth
bubble_should_stop = true
break
end
end
end
end
if bubble_should_stop then
local nearest_slot_index = find_nearest_slot(next_x, next_y)
game.bubble_slots[nearest_slot_index].bubble_type = game.next_bubble.bubble_type
game.next_bubble.x = game.bubble_slots[nearest_slot_index].x
game.next_bubble.y = game.bubble_slots[nearest_slot_index].y
game.next_bubble.velocity_x = 0
game.next_bubble.velocity_y = 0
local matches = find_matches(nearest_slot_index)
else
game.next_bubble.x = next_x
game.next_bubble.y = next_y
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 stationary bubbles
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
)
else
love.graphics.circle(
'line',
slot.x,
slot.y,
game.bubble_radius
)
end
end
-- 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
)
-- 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 next bubble
-- if moving, extrapolate position based on alpha value from love.run
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.velocity_x * alpha,
game.next_bubble.y + game.next_bubble.velocity_y * alpha,
0,
1,
1,
game.bubble_radius,
game.bubble_radius
)
end
function love.mousepressed(x, y, button, is_touch, presses)
if button == 1 then
local diff_x = x - game.launcher_x
local diff_y = y - game.launcher_y
local dist = math.sqrt(diff_x * diff_x + diff_y * diff_y)
game.next_bubble.velocity_x = diff_x / dist * game.bubble_speed
game.next_bubble.velocity_y = diff_y / dist * game.bubble_speed
end
end
function love.mousemoved(x, y, dx, dy, is_touch)
local diff_x = x - game.launcher_x
local diff_y = y - game.launcher_y
game.launcher_rotation = math.atan2(diff_y, diff_x)
end
function love.keypressed(key, scan_code, is_repeat)
if key == 'escape' then
love.event.quit()
elseif key == 'r' then
game.next_bubble.x = game.launcher_x
game.next_bubble.y = game.launcher_y
game.next_bubble.velocity_x = 0
game.next_bubble.velocity_y = 0
game.next_bubble.bubble_type = get_next_bubble_type(
game.next_bubble_index, {1,2,3,4,5,6,7,8}
)
game.paused = false
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 = 1/120 -- 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