Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
-- portal golf
-- by @maxbize
-------------------
-- global vars
-------------------
-- gameobject management / main loops
local _to_start = {} -- all gameobjects that still haven't had start() called
local gameobjects = {} -- global list of all objects
local actions = {} -- coroutines
local time_scale = 1
for i=1,5 do
add(gameobjects, {}) -- 5 layers: background, default, foreground, UI, mouse
end
local gradients = {0, 1, 1, 2, 1, 13, 6, 2, 4, 9, 3, 1, 5, 13, 14}
-- game data
--local walls = { -- indexed by sprite number
-- [1]={up=true, right=true, down=true, left=true},
-- [2]={up=false, right=false, down=false, left=false},
-- [3]={up=false, right=true, down=false, left=true},
-- [4]={up=true, right=false, down=true, left=false},
-- [5]={up=true, right=false, down=false, left=false},
-- [6]={up=false, right=true, down=false, left=false},
-- [7]={up=false, right=false, down=true, left=false},
-- [8]={up=false, right=false, down=false, left=true},
-- [9]={up=false, right=true, down=true, left=true},
-- [10]={up=true, right=false, down=true, left=true},
-- [11]={up=true, right=true, down=false, left=true},
-- [12]={up=true, right=true, down=true, left=false},
-- [13]={up=true, right=false, down=false, left=true},
-- [14]={up=true, right=true, down=false, left=false},
-- [15]={up=false, right=true, down=true, left=false},
-- [16]={up=false, right=false, down=true, left=true},
--}
function tobool(s) -- very simple since we only have one case
return s == "true"
end
local walls_str = "true,true,true,true,false,false,false,false,false,true,false,true,true,false,true,false,true,false,false,false,false,true,false,false,false,false,true,false,false,false,false,true,false,true,true,true,true,false,true,true,true,true,false,true,true,true,true,false,true,false,false,true,true,true,false,false,false,true,true,false,false,false,true,true"
local walls = {}
local walls_split = split(walls_str, ",")
for i=1,#walls_split,4 do
add(walls, {up=tobool(walls_split[i]), right=tobool(walls_split[i+1]), down=tobool(walls_split[i+2]), left=tobool(walls_split[i+3])})
end
local level = 1
--local levels = {
-- {start_x=60, start_y=78, start_vx=0, start_vy=1, gold=4, silver=8, bronze=12}, -- easy
-- {start_x=22, start_y=30, start_vx=-1, start_vy=0, gold=6, silver=10, bronze=16}, -- medium
-- {start_x=30, start_y=95, start_vx=3, start_vy=-2, gold=6, silver=10, bronze=16}, -- easy
-- {start_x=60, start_y=97, start_vx=1, start_vy=0, gold=5, silver=8, bronze=15}, -- hard
-- {start_x=66, start_y=95, start_vx=0, start_vy=0, gold=4, silver=6, bronze=10}, -- hard
-- {start_x=114, start_y=20, start_vx=0, start_vy=-3, gold=12, silver=14, bronze=18}, -- medium-hard
-- {start_x=100, start_y=40, start_vx=2, start_vy=0, gold=7, silver=10, bronze=15}, -- very hard
-- {start_x=42, start_y=100, start_vx=1, start_vy=1, gold=8, silver=12, bronze=16}, -- medium-hard
-- {start_x=66, start_y=30, start_vx=0, start_vy=0, gold=8, silver=10, bronze=14}, -- very hard
-- {start_x=66, start_y=64, start_vx=-3, start_vy=-3, gold=15, silver=20, bronze=30} -- hard (bonus)
--}
local levels_str = "60,78,0,1,4,8,12,22,30,-1,0,6,10,16,30,95,3,-2,6,10,16,60,97,1,0,5,8,15,66,95,0,0,4,6,10,114,20,0,3,12,14,18,100,40,2,0,7,10,15,42,100,1,1,8,12,16,66,30,0,0,8,10,14,66,64,-3,-3,15,20,30"
local levels = {}
local levels_split = split(levels_str, ",")
for i=1,#levels_split,7 do
add(levels, {start_x=levels_split[i], start_y=levels_split[i+1], start_vx=levels_split[i+2], start_vy=levels_split[i+3], gold=levels_split[i+4], silver=levels_split[i+5], bronze=levels_split[i+6]})
end
-- singletons (_m == manager). Now defined globally in _init because I ran out of tokens!
--local portal_m = nil -- type portal_manager_t
--local cash = nil -- type gameobject
--local level_m = nil -- type level_manager_t
--local back_particle_m = nil -- type particle_manager_t
--local front_particle_m = nil -- type particle_manager_t
--local menu_m = nil -- type menu_manager_t
--local end_menu_m = nil -- type end_level_menu_t
--local level_ui_m = nil -- type level_ui_t
--local mouse_m = nil -- type mouse_t
--local help_m = nil -- type help_menu_t
--local api_m = nil -- type api_manager_t
local current_track = -1 -- music
-------------------
-- main methods
-------------------
function _init()
printh('')
printh('--------------')
printh('')
cartdata('maxbize_portalgolf_1')
poke(0x5f2d, 1) -- enable mouse
local portal = gameobject:new()
portal_m = portal:add_component(portal_manager_t:new())
cash = gameobject:new{x=60, y=91, layer=3}
cash:add_component(rigidbody_t:new{width=3, height=3})
cash:add_component(cash_t:new())
local level_manager = gameobject:new()
level_m = level_manager:add_component(level_manager_t:new())
local api_manager = gameobject:new()
api_m = api_manager:add_component(api_manager_t:new())
local menu_manager = gameobject:new{layer=4}
menu_m = menu_manager:add_component(menu_manager_t:new())
local end_menu = gameobject:new{layer=4}
end_menu_m = end_menu:add_component(end_level_menu_t:new())
local level_ui = gameobject:new{layer=4}
level_ui_m = level_ui:add_component(level_ui_t:new())
local help_menu = gameobject:new{layer=4}
help_m = help_menu:add_component(help_menu_t:new())
local back_particle_manager = gameobject:new{layer=1}
back_particle_m = back_particle_manager:add_component(particle_manager_t:new())
local front_particle_manager = gameobject:new{layer=5}
front_particle_m = front_particle_manager:add_component(particle_manager_t:new())
local mouse = gameobject:new{layer=5}
mouse_m = mouse:add_component(mouse_t:new())
end
function _update60()
--if (not btnp(5) and time_scale == 1) then
-- return
--end
for c in all(actions) do
if costatus(c) ~= "dead" then
coresume(c)
else
del(actions, c)
end
end
for go in all(_to_start) do
go:start_components()
add(gameobjects[go.layer], go)
end
_to_start = {}
for layer in all(gameobjects) do
for go in all(layer) do
go:update_components()
end
end
end
function _draw()
cls(5)
for i=1,count(gameobjects) do
for go in all(gameobjects[i]) do
go:draw_components()
end
if (i == 1) then
draw_map()
end
end
--print('cpu: '..(stat(1) < 0.1 and '0' or '')..flr(stat(1) * 100), 1, 1, 0)
--print('obj: '..#gameobjects[1]..' '..#gameobjects[2]..' '..#gameobjects[3]..' '..#gameobjects[4], 1, 7, 0)
--print('mem: '..stat(0), 1, 13, 0)
end
-------------------
-- component system
-------------------
-- base gameobject class
gameobject = {
components = nil,
x = 0,
y = 0,
rb = nil, -- cached rigidbody
layer = 2 -- 1:background, 2:default, 3:foreground, 4:ui
}
function gameobject:new(o)
local o = o or {}
o.components = {}
setmetatable(o, self)
self.__index = self
-- instantiate
add(_to_start, o)
return o
end
function gameobject:start_components()
for comp in all(self.components) do
if (comp.start ~= nil) then
comp:start()
end
end
end
function gameobject:update_components()
for comp in all(self.components) do
if (comp.update ~= nil) then
comp:update()
end
end
end
function gameobject:draw_components()
for comp in all(self.components) do
if (comp.draw ~= nil) then
comp:draw()
end
end
end
function gameobject:add_component(comp)
add(self.components, comp)
comp.go = self
if (instanceof(comp, rigidbody_t)) then
self.rb = comp
end
return comp
end
function gameobject:get_component(prototype)
for comp in all(self.components) do
if (instanceof(comp, prototype)) then
return comp
end
end
return nil
end
-- base component class
component = {
go = nil -- parent gameobject
}
function component:new(o)
local o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function instanceof(obj, typ)
while obj do
obj = getmetatable(obj)
if typ == obj then
return obj
end
end
return false
end
function destroy(gameobject)
del(gameobjects[gameobject.layer], gameobject)
end
-------------------
-- generic helper methods
-------------------
function print_shadowed(text, x, y, color)
if color == 9 then -- for this game, lots of background UI uses the normal gradient for 9
print(text, x, y-1, 5)
else
print(text, x, y-1, gradients[color])
end
print(text, x, y, color)
end
-- print with auto text wrapping and marker support (e.g. "word is %marked")
-- % = shadowed
function print_formatted(text, start_x, start_y, start_color)
local x = start_x
local y = start_y
local c = start_color
for word in all(split(text, " ", false)) do
-- check for markers
local shadowed = false
if sub(word, 1, 1) == '%' then
shadowed = true
word = sub(word, 2)
end
if sub(word, 1, 1) == '#' then
c = tonum(sub(word, 2, 3))
if c == nil then
c = tonum(sub(word, 2, 2))
word = sub(word, 3)
else
word = sub(word, 4)
end
end
-- check for word wrap
local l = #word * 4
if x + l >= 128 then
y += 8
x = start_x
end
-- print
if shadowed then
print_shadowed(word, x, y, c)
else
print(word, x, y, c)
end
-- advanced print pointer
x += l + 4
c = start_color
end
-- return next suggested y
return y + 8
end
-- returns the cell index at x, y
function cell_at_point(x, y)
return flr(x / 8), flr(y / 8)
end
-- returns the top-left corner of the cell at index
function cell_location(cell_x, cell_y)
return cell_x * 8, cell_y * 8
end
function solid_at_point(x, y)
return fget(mget2(cell_at_point(x, y)), 0)
end
function round(n)
return n%1 < 0.5 and flr(n) or -flr(-n)
end
function dist(x1, y1, x2, y2)
return sqrt((x2 - x1)^2 + (y2 - y1)^2)
end
--function printh_nums(prefix, n1, n2, n3, n4)
-- local s = (prefix ~= nil and prefix or '') .. ' '
-- s = n1 ~= nil and s..tostr(n1)..' ' or s
-- s = n2 ~= nil and s..tostr(n2)..' ' or s
-- s = n3 ~= nil and s..tostr(n3)..' ' or s
-- s = n4 ~= nil and s..tostr(n4)..' ' or s
-- printh(s)
--end
-- draws a dotted line, animated by tweaking phase (0-1)
function draw_dotted_line(x1, y1, x2, y2, color)
local length = dist(x1, y1, x2, y2)
local dx = x2 - x1
local dy = y2 - y1
local phase = time() * 2 % 1
local num_dots = 10 -- not really ;)
local skip = 4
for i = -1, num_dots, skip do
local t1 = (i + phase*skip) / (num_dots)
local t2 = (i + phase*skip + 1) / (num_dots)
t2 = max(0, min(1, t2))
t1 = max(0, min(1, t1))
line(x1 + dx * t1, y1 + dy * t1, x1 + dx * t2, y1 + dy * t2, color)
end
end
function _yield(frames)
frames = frames or 1
for i=1,frames do
yield()
end
end
-------------------
-- game-specific helper methods
-------------------
-- level aware mget
function mget2(cell_x, cell_y)
cell_x += 16 * (level % 8)
cell_y += 16 * flr(level / 8)
return mget(cell_x, cell_y)
end
function draw_map()
local cell_x = 16 * (level % 8)
local cell_y = 16 * flr(level / 8)
map(cell_x, cell_y, 0, 0, 16, 16, 1)
end
-- sorting network, ascending
function sort_dirs(a, b, c, d)
if (a.dist > b.dist) then a, b = b, a end
if (c.dist > d.dist) then c, d = d, c end
if (a.dist > c.dist) then a, c = c, a end
if (b.dist > d.dist) then b, d = d, b end
if (b.dist > c.dist) then b, c = c, b end
return {a, b, c, d}
end
-- checks if aabb overlaps any solid geometry at the given position
-- returns:
-- 0 - no overlaps
-- 1 - overlaps portals
-- 2 - overlaps walls
function overlaps_solids(x, y, w, h)
-- need to keep track of individual corners separately to not conflate
-- an overlap from one brick with a non-overlap from another brick that has
-- a portal
local any_in_portal = false
local portal_ref = nil
-- run check for each corner
for i= 0, 3 do
-- check against the map (-1 since that's the edge of the collider)
local x_map = x + (w - 1) * (i%2)
local y_map = y + (h - 1) * flr(i/2)
local cell_x, cell_y = cell_at_point(x_map, y_map)
if (fget(mget2(cell_x, cell_y), 0)) then
-- collides with a wall. Let's see if it's a portal
local in_portal = false
if (#portal_m.chain > 1) then
for portal in all(portal_m.chain) do
if (portal.cell_x == cell_x and portal.cell_y == cell_y) then
local overlaps = overlaps_portal(portal, x, y, w, h)
if overlaps then
portal_ref = portal
in_portal = true
end
end
end
end
any_in_portal = any_in_portal or in_portal
-- hit a solid wall. no need to check anything else
if (not in_portal) then
return 2, nil
end
end
end
return any_in_portal and 1, portal_ref or 0, nil
end
-- checks if the collider overlaps with the given portal
function overlaps_portal(portal, x, y, w, h)
local px, py, pw, ph = portal_positions(portal)
return not (x > px + pw-1
or y > py + ph-1
or x + w-1 < px
or y + h-1 < py)
end
function portal_positions(portal)
local x, y = cell_location(portal.cell_x, portal.cell_y)
local w, h
if (portal.dir_x ~= 0) then
x += (portal.dir_x == 1 and 7 or 0)
w = 1
h = 8
else
y += (portal.dir_y == 1 and 7 or 0)
w = 8
h = 1
end
return x, y, w, h
end
function portals_equal(p1, p2)
return p1.cell_x == p2.cell_x
and p1.cell_y == p2.cell_y
and p1.dir_x == p2.dir_x
and p1.dir_y == p2.dir_y
end
-------------------
-- game types
-------------------
particle_manager_t = component:new{
particles = {},
index = 1,
max_particles = 1000
}
function particle_manager_t:start()
self.particles = {}
for i=1,self.max_particles do
self.particles[i] = {
x = -1,
y = -1,
vx = 0,
vy = 0,
ax = 0,
ay = 0,
frames = 0,
color = 0
}
end
end
function particle_manager_t:update()
for p in all(self.particles) do
if (p.frames > 0) then
p.vx += p.ax
p.vy += p.ay
p.x += p.vx
p.y += p.vy
p.frames -= 1
end
end
end
function particle_manager_t:draw()
for p in all(self.particles) do
if (p.frames > 0) then
pset(p.x, p.y, p.color)
end
end
end
-- Adds a second particle underneath with the gradient color
function particle_manager_t:add_particle_shadowed(x, y, vx, vy, ax, ay, color, frames)
self:add_particle(x, y, vx, vy, ax, ay, color, frames)
self:add_particle(x, y+1, vx, vy, ax, ay, gradients[color], frames)
end
-- "shifts" to the gradient color at the end
function particle_manager_t:add_particle_faded(x, y, vx, vy, ax, ay, color, frames)
self:add_particle(x, y, vx, vy, ax, ay, gradients[color], frames)
self:add_particle(x, y, vx, vy, ax, ay, color, frames * 0.9)
end
function particle_manager_t:add_particle(x, y, vx, vy, ax, ay, color, frames)
local seek = 10
while (seek > 0) do
seek -= 1
self.index += 1
if (self.index > self.max_particles) then
self.index = 1
end
if (self.particles[self.index].frames < 5) then
seek = 0
end
end
local p = self.particles[self.index]
p.x = x
p.y = y
p.vx = vx
p.vy = vy
p.ax = ax
p.ay = ay
p.color = color
p.frames = frames
end
portal_manager_t = component:new{
candidate = nil, -- cell_x, cell_y, dir_x, dir_x
chain = nil, -- [{cell_x, cell_y, dir_x, dir_x}]
move_index = 0, -- if we're moving a portal, this is the index of that portal
highlighted_portal = nil
}
function portal_manager_t:start()
self.chain = {}
end
function portal_manager_t:update()
if (menu_m.active or end_menu_m.active or help_m.active) then
return
end
-- find candidate wall for portal
local d_up = mouse_m.go.y % 8
local d_lt = mouse_m.go.x % 8
local d_dn = 7 - d_up
local d_rt = 7 - d_lt
local dirs = sort_dirs(
{dist=d_up, dir_x= 0, dir_y=-1},
{dist=d_dn, dir_x= 0, dir_y= 1},
{dist=d_rt, dir_x= 1, dir_y= 0},
{dist=d_lt, dir_x=-1, dir_y= 0}
)
local cell_x, cell_y = cell_at_point(mouse_m.go.x, mouse_m.go.y)
self.candidate = nil
for dir in all(dirs) do
-- check interior walls of current cell
local wall = walls[mget2(cell_x, cell_y)]
if (wall ~= nil) then
if ((dir.dir_x == 1 and wall.right) or (dir.dir_x == -1 and wall.left)) then
self.candidate = {cell_x=cell_x, cell_y=cell_y, dir_x = dir.dir_x, dir_y = dir.dir_y}
break
elseif ((dir.dir_y == 1 and wall.down) or (dir.dir_y == -1 and wall.up)) then
self.candidate = {cell_x=cell_x, cell_y=cell_y, dir_x = dir.dir_x, dir_y = dir.dir_y}
break
end
end
-- check exterior walls of neighboring cell. not exact copy/paste from above
wall = walls[mget2(cell_x + dir.dir_x, cell_y + dir.dir_y)]
if (wall ~= nil) then
if ((dir.dir_x == 1 and wall.left) or (dir.dir_x == -1 and wall.right)) then
self.candidate = {cell_x=cell_x + dir.dir_x, cell_y=cell_y + dir.dir_y, dir_x = -dir.dir_x, dir_y = -dir.dir_y}
break
elseif ((dir.dir_y == 1 and wall.up) or (dir.dir_y == -1 and wall.down)) then
self.candidate = {cell_x=cell_x + dir.dir_x, cell_y=cell_y + dir.dir_y, dir_x = -dir.dir_x, dir_y = -dir.dir_y}
break
end
end
end
-- handle user input
if (time_scale == 1) then
return
end
if (self.move_index ~= 0 and not mouse_m.left_mouse) then
self.move_index = 0
end
if (mouse_m.left_mouse_down) then
local existing, index = self:find_in_chain(self.candidate)
if (existing == nil and self.candidate ~= nil) then
sfx(12)
add(self.chain, self.candidate)
self.move_index = #self.chain
else
self.move_index = index
end
elseif (self.move_index ~= 0) then
self:move_portal(self.candidate, self.move_index)
elseif (mouse_m.right_mouse_down) then
self:remove_portal(self.candidate)
end
end
function portal_manager_t:find_in_chain(candidate)
if (candidate == nil) then
return
end
for i = 1, #self.chain do
if (portals_equal(self.chain[i], self.candidate)) then
return self.chain[i], i
end
end
end
function portal_manager_t:remove_portal(candidate)
if del(self.chain, self:find_in_chain(candidate)) ~= nil then
sfx(13)
end
end
function portal_manager_t:move_portal(candidate, index)
if (candidate ~= nil) then
-- Make sure we're not overwriting an existing portal
for portal in all(self.chain) do
if portals_equal(candidate, portal) then
return
end
end
self.chain[index] = candidate
end
end
function portal_manager_t:draw()
if (self.candidate ~= nil and time_scale == 0) then
self:draw_portal(self.candidate, 11)
end
self.highlighted_portal = nil
for i = 1, #self.chain do
local color = 9 -- default orange
if self.chain[i].flash_frames ~= nil and self.chain[i].flash_frames > 0 then
color = self.chain[i].flash_frames % 2 == 0 and 10 or 9
self.chain[i].flash_frames -= 1
elseif i == #self.chain then
color = 12
end
self:draw_portal(self.chain[i], color)
if (time_scale == 0 and not end_menu_m.active) then
self:draw_portal_number(i)
if (self.candidate ~= nil and portals_equal(self.chain[i], self.candidate)) then
self.highlighted_portal = i
end
end
end
if (self.highlighted_portal ~= nil and #self.chain > 1 and time_scale == 0 and not end_menu_m.active) then
local p0x, p0y, p0w, p0h = portal_positions(self.chain[self.highlighted_portal == 1 and #self.chain or self.highlighted_portal-1])
local p1x, p1y, p1w, p1h = portal_positions(self.chain[self.highlighted_portal])
local p2x, p2y, p2w, p2h = portal_positions(self.chain[(self.highlighted_portal%#self.chain)+1])
draw_dotted_line(p0x + flr(p0w/2), p0y + flr(p0h/2), p1x + flr(p1w/2), p1y + flr(p1h/2), 14, time()*5%1, 5)
draw_dotted_line(p1x + flr(p1w/2), p1y + flr(p1h/2), p2x + flr(p2w/2), p2y + flr(p2h/2), 15, time()*5%1, 5)
end
end
function portal_manager_t:draw_portal(portal, color)
local x, y, w, h = portal_positions(portal)
line(x, y, x + w - 1, y + h - 1, color)
end
function portal_manager_t:draw_portal_number(num)
portal = self.chain[num]
local l = #tostr(num) - 1
local cell_x, cell_y = cell_location(portal.cell_x, portal.cell_y)
if portal.dir_x == 1 then
print(tostr(num), cell_x + 3 - l * 4, cell_y + 2, 0)
elseif portal.dir_x == -1 then
print(tostr(num), cell_x + 2, cell_y + 2, 0)
elseif portal.dir_y == 1 then
print(tostr(num), cell_x + 3 - l * 2, cell_y + 1, 0)
else
print(tostr(num), cell_x + 3 - l * 2, cell_y + 2, 0)
end
end
-- rigidbody is any freefalling object in the world.
-- handles dynamic pixel-perfect movement
rigidbody_t = component:new{
x_remainder = 0, -- fractional movement. gameobject x, y only allowed ints
y_remainder = 0,
vx = 0, -- velocity
vy = 0,
width = 1, -- collider size
height = 1,
ay = 0.1, -- acceleration
friction = 0.9,
bounciness = 0.4,
bounce_friction = 0.9, -- bounciness on the tangent
max_vx = 3, -- max velocity
max_vy = 3,
angle = 0, -- angle and angular velocity for animation purposes only!
angular_vel = 0,
particle_trail = {}, -- ring buffer of historical positions
trail_index = 1, -- index into ring buffer
trail_size = 50, -- size of ring buffer
trail_on = true, -- whether or not to render trail
portal_offset = 0, -- offset from the center of the last portal we went through
}
function rigidbody_t:update()
-- apply ground friction and gravity
local grounded = self:is_grounded()
if (grounded) then
self.angular_vel = self.vx * 40; -- * 60 (speed per sec instead of frame) / 1.5 (radius)
self.vx *= self.friction
-- if we're stopped and grounded but are mostly over open space, add a little movement to "fall off" the ledge
if abs(self.vx) < 0.001 and self.vy == 0 then
local left_overlap = overlaps_solids(self.go.x, self.go.y + 1, 1, self.height)
local middle_overlap = overlaps_solids(self.go.x+1, self.go.y + 1, 1, self.height)
local right_overlap = overlaps_solids(self.go.x+2, self.go.y + 1, 1, self.height)
if middle_overlap ~= 2 and right_overlap ~= 2 then
self.vx += 0.1
elseif middle_overlap ~= 2 and left_overlap ~= 2 then
self.vx -= 0.1
end
end
end
if (not grounded or self.vy < 0) then
self.vy += self.ay * time_scale
end
-- acceleration and velocity cap
if (abs(self.vy) > self.max_vy) then
self.vy = self.max_vy * sgn(self.vy)
end
if (abs(self.vx) > self.max_vx) then
self.vx = self.max_vx * sgn(self.vx)
end
-- angular velocity
self.angle = (self.angle + self.angular_vel) % 360
-- movement
local vx = self.vx * time_scale
local vy = self.vy * time_scale
-- DEBUGGING!
--vx = 0
--vy = 0
--if btn(0) then vx = -1.0 end
--if btn(1) then vx = 1.0 end
--if btn(2) then vy = -1.0 end
--if btn(3) then vy = 1.0 end
local x = flr(abs(vx)) + 1
local y = flr(abs(vy)) + 1
for i=1, max(x, y) do
if (x > 0) then
x -= 1
self:move_x(sgn(vx) * (x > 0 and 1 or (abs(vx) % 1)),
function()
if (self:handle_portal()) then
x = 0
y = 0
end
end,
function()
sfx(3)
for i=1, flr(abs(self.vx)*5) do
back_particle_m:add_particle_faded(self.go.x+1.5+sgn(self.vx), self.go.y+1.5, rnd(0.1)*sgn(-self.vx), rnd(0.5)-0.25, 0, 0, 7, 10+rnd(20))
end
self.angular_vel = -vy * 40; -- * 60 (speed per sec instead of frame) / 1.5 (radius)
self.vx *= -self.bounciness
self.vy *= self.bounce_friction
x = 0
end
)
end
if (y > 0) then
y -= 1
self:move_y(sgn(vy) * (y > 0 and 1 or (abs(vy) % 1)),
function()
if (self:handle_portal()) then
x = 0
y = 0
end
end,
function()
sfx(3)
for i=1, flr(abs(self.vy)*5) do
back_particle_m:add_particle_faded(self.go.x+1.5, self.go.y+1.5+sgn(self.vy), rnd(0.5)-0.25, rnd(0.1)*sgn(-self.vy), 0, 0, 7, 10+rnd(20))
end
self.angular_vel = vx * 40; -- * 60 (speed per sec instead of frame) / 1.5 (radius)
self.vy *= -self.bounciness
if (abs(self.vy) < 0.5) then
self.vy = 0
end
self.vx *= self.bounce_friction
y = 0
end
)
end
end
end
function rigidbody_t:is_grounded()
local overlap_state = overlaps_solids(self.go.x, self.go.y + 1, self.width, self.height)
return overlap_state == 2
end
function rigidbody_t:move_x(amount, portal_callback, wall_callback)
self.x_remainder += amount
local move = round(self.x_remainder)
if (move ~= 0) then
self.x_remainder -= move
local sign = sgn(move)
while (move ~= 0) do
overlap_state = overlaps_solids(self.go.x + sign, self.go.y, self.width, self.height)
if (overlap_state == 1) then
self:record_trail(sign, 0)
self.go.x += sign
portal_callback()
break
elseif (overlap_state == 2) then
wall_callback()
break
else
self:record_trail(sign, 0)
self.go.x += sign
move -= sign
end
end
end
end
function rigidbody_t:move_y(amount, portal_callback, wall_callback)
self.y_remainder += amount
local move = round(self.y_remainder)
if (move ~= 0) then
self.y_remainder -= move
local sign = sgn(move)
while (move ~= 0) do
overlap_state = overlaps_solids(self.go.x, self.go.y + sign, self.width, self.height)
if (overlap_state == 1) then
self:record_trail(0, sign)
self.go.y += sign
portal_callback()
break
elseif (overlap_state == 2) then
wall_callback()
break
else
self:record_trail(0, sign)
self.go.y += sign
move -= sign
end
end
end
end
-- Record history for the particle trail. move_x/y state what our next move will be
function rigidbody_t:record_trail(move_x, move_y)
if (#self.particle_trail == 0) then
for i=1,self.trail_size do
add(self.particle_trail, {x=self.go.x, y=self.go.y})
end
end
local next_index = self.trail_index == self.trail_size and 1 or self.trail_index + 1
local dx = abs(self.go.x + move_x - self.particle_trail[self.trail_index].x)
local dy = abs(self.go.y + move_y - self.particle_trail[self.trail_index].y)
if dx + dy >= 4 or dx >= 3 or dy >= 3 then
self.particle_trail[next_index] = {x = self.go.x, y = self.go.y}
self.trail_index = next_index
end
end
function rigidbody_t:handle_portal()
local num_portals = #portal_m.chain
for i = 1, num_portals do
if (self:edge_in_portal(portal_m.chain[i])) then
p1 = portal_m.chain[i]
p2 = portal_m.chain[(i%num_portals)+1]
local p1x, p1y, p1w, p1h = portal_positions(p1)
local p2x, p2y, p2w, p2h = portal_positions(p2)
if (p1.dir_x == 0) then
self.portal_offset = self.go.x - p1x - 2
else
self.portal_offset = self.go.y - p1y - 2
end
local last_x = self.go.x
local last_y = self.go.y
self.go.x = flr(p2x + p2w/2 - self.width/2)
self.go.y = flr(p2y + p2h/2 - self.height/2)
self.angle = (self.angle + portal_angle_delta(p1, p2)) % 360
local v_normal = p1.dir_x == 0 and self.vy or self.vx
local v_tangent = p1.dir_x == 0 and self.vx or self.vy
if (p2.dir_x ~= 0) then
self.vx = abs(v_normal) * p2.dir_x
self.vy = p1.dir_x == 0 and -abs(v_tangent) or v_tangent
else
self.vx = v_tangent
self.vy = abs(v_normal) * p2.dir_y
end
local dx = self.go.x - last_x
local dy = self.go.y - last_y
for i = 1, 40 do
back_particle_m:add_particle_faded(self.go.x + 1, self.go.y + 1, rnd(1)-0.5 + p2.dir_x*0.5, rnd(1)-0.5 + p2.dir_y*0.5, 0, 0, i <= 20 and 12 or 1, rnd(10)+10)
local t = i / 41
front_particle_m:add_particle_faded(last_x + dx * t, last_y + dy * t, rnd(0.5)-0.25, rnd(0.5)-0.25, 0, 0, 12, i)
end
sfx(1, -1, 0, 1)
p1.flash_frames = 6
p2.flash_frames = 6
-- local speed = dist(0, 0, self.vx, self.vy)
-- self.vx = speed * p2.dir_x
-- -- needed to preserve momentum if the teleport threshold is in the center
-- --self.vy = (speed + self.ay) * p2.dir_y
-- self.vy = speed * p2.dir_y
return true
end
end
return false
end
-- checks to see if the outer middle pixel overlaps the portal
function rigidbody_t:edge_in_portal(p)
if (p.dir_x ~= 0) then
local x = self.go.x + (p.dir_x == 1 and self.width - 1 or 0)
for i=0, self.height-1 do
if (overlaps_portal(p, x, self.go.y + i, 1, 1)) then
return true
end
end
else
local y = self.go.y + (p.dir_y == 1 and self.height - 1 or 0)
for i=0, self.width-1 do
if (overlaps_portal(p, self.go.x + i, y, 1, 1)) then
return true
end
end
end
return false
end
-- the main object the player has to get to the end
cash_t = component:new{
draw_index = 1, -- index into the particle trail for the highlight
draw_pause = 0, -- how many frames to pause the highlight
}
function cash_t:draw()
-- Draw particle trail
local trail_length = #self.go.rb.particle_trail
if trail_length == 0 then
self.draw_index = 1
end
if self.go.rb.trail_on then
for particle in all(self.go.rb.particle_trail) do
pset(particle.x + 1, particle.y + 1, 13)
end
if time_scale == 0 and trail_length > 0 and not end_menu_m.active then
for i=1,10 do
--local index = (self.go.rb.trail_index + self.draw_index + i) % trail_length + 1
local index = self.draw_index + i
if index >= 0 and index < self.go.rb.trail_size then
local particle = self.go.rb.particle_trail[(self.go.rb.trail_index + self.draw_index + i) % trail_length + 1]
pset(particle.x + 1, particle.y + 1, 7)
end
end
if self.draw_pause > 0 then
self.draw_pause -= 1
else
self.draw_index = self.draw_index < trail_length and self.draw_index + 1 or -20
self.draw_pause = 2
end
end
end
-- Check for partial portal overlaps
local state, p1 = overlaps_solids(self.go.x, self.go.y, self.go.rb.width, self.go.rb.height)
if (state == 1) then
-- Check if we're entering or exiting this portal
local p1x, p1y, p1w, p1h = portal_positions(p1)
local dir = 0
local delta = 0 -- how many pixels from centerline of portal to center of collector
local sx = 72 + flr(self.go.rb.angle / 45) * 4
local sy = 10
local sw = 3
local sh = 3
if (p1.dir_x == 0) then
dir = sgn(self.go.rb.vy) == sgn(p1.dir_y) and -1 or 1
delta = abs((self.go.y + 1) - p1y)
local cutoff = 1 - delta -- how much to cut off for the front of the sprite
if p1.dir_y == 1 then
sspr(sx, sy + cutoff, sw, sh - cutoff, self.go.x, self.go.y + cutoff)
else
sspr(sx, sy, sw, sh - cutoff, self.go.x, self.go.y)
end
else
dir = sgn(self.go.rb.vx) == sgn(p1.dir_x) and -1 or 1
delta = abs((self.go.x + 1) - p1x)
local cutoff = 1 - delta -- how much to cut off for the front of the sprite
if p1.dir_x == 1 then
sspr(sx + cutoff, sy, sw - cutoff, sh, self.go.x + cutoff, self.go.y)
else
sspr(sx, sy, sw - cutoff, sh, self.go.x, self.go.y)
end
end
-- Find the portal we're supposed to do a preview on
local p2 = nil
for i=1,#portal_m.chain do
if (portals_equal(p1, portal_m.chain[i])) then
local idx = i + dir
if idx == 0 then
idx = #portal_m.chain
end
if idx > #portal_m.chain then
idx = 1
end
p2 = portal_m.chain[idx]
break
end
end
-- Draw the preview at that portal
local p2x, p2y, p2w, p2h = portal_positions(p2)
local cutoff = delta + 1 -- how much to cut off for the back of the sprite
sx = 72 + flr((self.go.rb.angle + portal_angle_delta(p1, p2)) % 360 / 45) * 4
if (p2.dir_x == 0) then
local x = flr(p2x + p2w/2 - self.go.rb.width/2)
local y = (p2y - 1) - delta * p2.dir_y
if dir == -1 then
x += self.go.rb.portal_offset
end
if p2.dir_y == 1 then
sspr(sx, sy + cutoff, sw, sh - cutoff, x, y + cutoff)
else
sspr(sx, sy, sw, sh - cutoff, x, y)
end
else
local x = (p2x - 1) - delta * p2.dir_x
local y = flr(p2y + p2h/2 - self.go.rb.height/2)
if dir == -1 then
y += self.go.rb.portal_offset
end
if p2.dir_x == 1 then
sspr(sx + cutoff, sy, sw - cutoff, sh, x + cutoff, y)
else
sspr(sx, sy, sw - cutoff, sh, x, y)
end
end
else
-- No overlaps - simple cash draw
sspr(72 + flr(self.go.rb.angle / 45) * 4, 10, 3, 3, self.go.x, self.go.y)
end
-- Draw arrow at start of level
if (time_scale == 0 and not end_menu_m.active) then
local end_x = (self.go.x + self.go.rb.vx * 5) + 1
local end_y = (self.go.y + self.go.rb.vy * 5) + 1
line(self.go.x + 1, self.go.y + 1, end_x, end_y, 12)
circ(end_x, end_y, 1, 1)
pset(self.go.x + 1, self.go.y + 1, 7)
end
end
-- 0-3 for N, E, S, W
function portal_dir(p)
if p.dir_y == -1 then return 0
elseif p.dir_x == 1 then return 1
elseif p.dir_y == 1 then return 2
elseif p.dir_x == -1 then return 3
end
end
function portal_angle_delta(p1, p2)
return (portal_dir(p1) - portal_dir(p2)) * 90 + 180
end
level_manager_t = component:new{
num_pickups = 0,
last_loaded_level = 0,
}
function level_manager_t:start()
time_scale = 0
--play_music(33)
music(32)
end
function level_manager_t:update()
if (menu_m.active or end_menu_m.active or help_m.active) then
return
end
-- handle input
if (btnp(4) and time_scale == 0) then
self:play_sim()
elseif (btnp(4) and time_scale == 1) then
self:stop_sim()
end
-- check/advance to next level
if (self.num_pickups == 0) then
sfx(2, -1, 0, 2)
local num_portals = #portal_m.chain
if (dget(level) == 0 or dget(level) > num_portals) then
dset(level, num_portals)
end
api_m:level_complete(level, num_portals, true)
end_menu_m:activate()
end
end
function level_manager_t:play_sim()
--play_music(0)
sfx(4, -1, 0, 2)
time_scale = 1
level_ui_m.btn_play.text = "stop"
cash.rb.particle_trail = {}
end
function level_manager_t:stop_sim()
--sfx(3)
level_ui_m.btn_play.text = "play"
self:restart_level()
end
function level_manager_t:notify_pickup()
self.num_pickups -= 1
if self.num_pickups > 0 then
sfx(0, -1, 31 - self.num_pickups, 1)
end
end
function level_manager_t:restart_level()
-- reset time
time_scale = 0
--play_music(33)
-- reset cash
l = levels[level]
cash.x = l.start_x
cash.y = l.start_y
cash.rb.vx = l.start_vx
cash.rb.vy = l.start_vy
cash.rb.x_remainder = 0
cash.rb.y_remainder = 0
cash.rb.angle = 0
cash.rb.angular_vel = 0
-- clear remaining pickups
local remaining = {}
for layer in all(gameobjects) do
for go in all(layer) do
if (go:get_component(pickup_t) ~= nil) then
remaining[go.x + go.y*16] = true
destroy(go)
end
end
end
-- create new pickups
self.num_pickups = 0
for x = 0, 15 do
for y = 0, 15 do
-- 34 == pickup sprite
if (mget2(x, y) == 34) then
self.num_pickups += 1
local pickup = gameobject:new{x=x*8, y=y*8}
pickup:add_component(pickup_t:new{collected_last_run=(remaining[x*8 + y*8*16] == nil and self.last_loaded_level == level) and true or false})
end
end
end
self.last_loaded_level = level
end
pickup_t = component:new{
chase_time = 0,
collected_last_run = false,
spawn_x = 0,
spawn_y = 0,
draw_x = 0,
draw_y = 0,
}
function pickup_t:start()
self.spawn_x = self.go.x
self.spawn_y = self.go.y
self.draw_x = self.spawn_x
self.draw_y = self.spawn_y
end
function pickup_t:update()
-- 4 == half sprite width/height
local distance = dist(self.go.x + 4, self.go.y + 3, cash.x + cash.rb.width/2, cash.y + cash.rb.height/2)
if (distance < 7.5) then
for i = 1, 10 do
back_particle_m:add_particle_shadowed(self.draw_x + 4, self.draw_y + 3, rnd(2)-1, rnd(2)-1, 0, 0, i <= 3 and 9 or 10, rnd(5)+5)
end
level_m:notify_pickup()
destroy(self.go)
end
end
function pickup_t:draw()
local distance = dist(self.go.x + 4, self.go.y + 3, cash.x + cash.rb.width/2, cash.y + cash.rb.height/2)
local target_x = self.spawn_x + (self.collected_last_run and 0 or sin(time() - self.spawn_x/128))
local target_y = self.spawn_y + (self.collected_last_run and 0 or cos(time() - self.spawn_y/128))
if distance < 10 then
local to_cash_x = ((cash.x + 1.5) - (self.go.x + 4))
local to_cash_y = ((cash.y + 1.5) - (self.go.y + 3))
target_x = self.go.x + to_cash_x * (2 - 2*distance/10)
target_y = self.go.y + to_cash_y * (2 - 2*distance/10)
end
self.draw_x += (target_x - self.draw_x) * 0.2
self.draw_y += (target_y - self.draw_y) * 0.2
if self.collected_last_run then
spr(50, self.draw_x, self.draw_y)
else
line(self.spawn_x+4, self.spawn_y+3, self.draw_x+4, self.draw_y+3, 1)
spr(34, self.draw_x, self.draw_y)
end
end
menu_manager_t = component:new{
active = true,
selected_level = -1,
}
function menu_manager_t:update()
if (not self.active) then
return
end
if (mouse_m.go.y >= 25 and mouse_m.go.y < 116) then
self.selected_level = flr((mouse_m.go.y - 26)/9) + 1
else
self.selected_level = -1
end
if (mouse_m.left_mouse_down and self.selected_level > -1) then
local unlocked = self.selected_level == 1 or dget(self.selected_level - 1) > 0
if (unlocked) then
sfx(12)
self.active = false
level = self.selected_level
level_m:restart_level()
else
sfx(13)
end
end
--particle_m:add_particle(12, 6+rnd(14), -1, 0, 0, 0, 9, 15)
--if rnd(1) < 0.25 then
-- particle_m:add_particle(112, 6+rnd(2), 1, 0, 0, 0, 12, 16)
--end
--if rnd(1) < 0.25 then
-- particle_m:add_particle(110, 12+rnd(2), 1, 0, 0, 0, 12, 18)
--end
end
function menu_manager_t:draw()
if (not self.active) then
return
end
rectfill(0, 0, 128, 128, 15)
rectfill(0, 25, 128, 116, 4)
palt(0, false)
sspr(0, 97, 128, 23, 0, 1)
palt(0, true)
print_shadowed('@maxbize', 48, 120, 7)
print_shadowed('v 1.1', 103, 120, 7)
if (self.selected_level > 0) then
local i = self.selected_level - 1
rectfill(0, 26 + i * 9, 128, 34 + i * 9, 2)
end
for i = 1, 10 do
local best = dget(i)
local unlocked = i == 1 or dget(i-1) > 0
local x = 19
local y = 19
print_shadowed('level '..(i < 10 and ' ' or '')..i, x, y + i * 9, unlocked and 7 or 1)
if (best ~= 0) then
if (best < levels[i].gold) then
spr(35, x+44, (y-1) + i * 9)
else
spr(best == levels[i].gold and 36 or 39, x+44, (y-1) + i * 9)
end
spr(best <= levels[i].silver and 37 or 39, x+52, (y-1) + i * 9)
spr(best <= levels[i].bronze and 38 or 39, x+60, (y-1) + i * 9)
else
spr(39, x+44, (y-1) + i * 9)
spr(39, x+52, (y-1) + i * 9)
spr(39, x+60, (y-1) + i * 9)
end
print_shadowed(best, best < 10 and x+84 or x+80, y + i * 9, unlocked and 7 or 1)
end
--particle_m:draw()
end
end_level_menu_t = component:new{
active = false,
draw_medals = 0, -- 0-3 for none, bronze, etc
flash_medal = 0, -- 0-3 for none, bronze, etc
offsets = {0, 0, 0}, -- offsets to draw medals/backgrounds for a little shake. Bronze, silver, gold
menu_offset = 0, -- offset for the entire menu so that we can slide it in
activate_action = nil, -- reference to the reveal coroutine
buttons = {}, -- list of buttons we've created for our UI
}
function end_level_menu_t:start()
add(self.buttons, make_button(34, 84, 0, -128, "retry", function(btn)
self:hide()
end))
add(self.buttons, make_button(72, 84, 0, -128, "next", function(btn)
portal_m.chain = {}
if (level < #levels) then
cash.rb.particle_trail = {}
level += 1
else
menu_m.active = true
end
self:hide()
end))
end
function end_level_menu_t:shake_medal_background(num)
sfx(5)
x = 75 - 18 * (num - 1)
for i=1,10 do
self.offsets[num] = (self.offsets[num] + 1) % 2
yield()
end
for i=1,6 do
self.offsets[num] = (self.offsets[num] + 1) % 2
_yield(2)
end
_yield(20)
end
function end_level_menu_t:reveal_medal(color, num, star)
x = 75 - 18 * (num - 1)
self:shake_medal_background(num)
self.draw_medals = num
self.flash_medal = num
sfx(3)
_yield(4)
self.flash_medal = 0
for i=1,75 do
front_particle_m:add_particle_shadowed(x + rnd(13), 32 + rnd(15), rnd(1.5)-0.75, rnd(1)-1.75, 0, 0.06, color, 100)
end
for i=1,25 do
front_particle_m:add_particle_shadowed(x + rnd(13), 32 + rnd(15), rnd(1.5)-0.75, rnd(1)-1.75, 0, 0.06, gradients[color], 100)
end
for i=0,num-1 do
if star then
sfx(6 + i, -1, 8, 11)
else
sfx(6 + i, -1, 0, 6)
end
end
_yield(15)
end
function end_level_menu_t:activate()
-- Reset state
self.active = true
self.draw_medals = 0
self.flash_medal = 0
self.offsets = {0, 0, 0}
self.menu_offset = 128
self.activate_action = cocreate(function()
-- Wait up to 3 sec for the collector to mostly stop
for i=1,180 do
if (abs(cash.rb.vx) < 0.01 and abs(cash.rb.vy) == 0) then
break
end
yield()
end
-- Stop the sim
time_scale = 0
-- Slide in menu
for i=1,16 do
self.menu_offset *= 0.70
for btn in all(self.buttons) do
btn.offset = -self.menu_offset
end
yield()
end
self.menu_offset = 0
for btn in all(self.buttons) do
btn.offset = 0
end
-- Perform medal animations
local num_portals = #portal_m.chain
if num_portals <= levels[level].bronze then
self:reveal_medal(9 , 1)
if num_portals > levels[level].silver then
self:shake_medal_background(2)
self.draw_medals = 2
end
end
if num_portals <= levels[level].silver then
self:reveal_medal(7 , 2)
if num_portals > levels[level].gold then
self:shake_medal_background(3)
self.draw_medals = 3
end
end
if num_portals <= levels[level].gold then
self:reveal_medal(10, 3, num_portals < levels[level].gold)
end
end)
add(actions, self.activate_action)
end
function end_level_menu_t:hide()
del(actions, self.activate_action)
self.active = false
back_particle_m:start()
front_particle_m:start()
level_m:restart_level()
for btn in all(self.buttons) do
btn.offset = -128
end
level_ui_m.btn_play.text = "play"
end
-- Draws background, medal, and requirement text
function end_level_menu_t:draw_medal(num, req)
local draw_x = 75 - 18 * (num - 1)
if self.flash_medal == num then
-- Draw medal flash
sspr(110, 53, 15, 17, draw_x - 1, 31)
-- Print medal requirements
print_shadowed("?", draw_x+5, 50, 7)
elseif self.draw_medals >= num then
-- Print medal requirements
print_shadowed(tostr(req), req < 10 and draw_x+5 or draw_x+3, 50 - self.menu_offset, 7)
if #portal_m.chain <= req then
-- Draw medal
if num == 3 and #portal_m.chain < req then
-- Star
sspr(109, 71, 17, 16, draw_x + self.offsets[num]-2, 31)
else
-- Regular medal
sspr(96 - 15 * (num - 1), 15, 13, 15, draw_x + self.offsets[num], 32)
end
else
-- Draw medal background
sspr(111, 15, 13, 15, draw_x + self.offsets[num], 32 - self.menu_offset)
end
else
-- Draw medal background
sspr(111, 15, 13, 15, draw_x + self.offsets[num], 32 - self.menu_offset)
-- Print medal requirements
print_shadowed("?", draw_x+5, 50 - self.menu_offset, 7)
end
end
function end_level_menu_t:draw()
if (not self.active) then
return
end
-- Draw background
rectfill(20, 20 - self.menu_offset, 128-21, 128-21 - self.menu_offset, 15)
rectfill(22, 22 - self.menu_offset, 128-23, 128-23 - self.menu_offset, 4)
-- Draw medals
self:draw_medal(1, levels[level].bronze)
self:draw_medal(2, levels[level].silver)
self:draw_medal(3, levels[level].gold)
-- Print level clear/stats
print_shadowed("level " .. level .. " cleared!", 33, 63 - self.menu_offset, 7)
print_shadowed("portals: " .. tostr(#portal_m.chain), 33, 72 - self.menu_offset, 7)
-- HACK! Draw particles again so they draw over the UI
--particle_m:draw()
end
-- UI at the bottom of the screen
level_ui_t = component:new{
btn_play = nil, -- the play button
buttons = {}, -- list of all buttons we've created
}
function level_ui_t:start()
self.btn_play = make_button(5, 119, 1, 0, "play", function(btn)
if btn.text == "play" then
btn.text = "stop"
level_m:play_sim()
else
level_m:stop_sim()
end
end)
add(self.buttons, self.btn_play)
add(self.buttons, make_button(31, 119, 1, 0, "reset", function(btn)
level_m:stop_sim()
portal_m.chain = {}
cash.rb.particle_trail = {}
end))
--add(btns_bottom, make_button( 49, 119, 1, 0, "trail", function(btn)
-- cash.rb.trail_on = not cash.rb.trail_on
--end))
add(self.buttons, make_button(76, 119, 1, 0, "help", function(btn)
help_m.page = 1
help_m.active = true
level_m:stop_sim()
end))
add(self.buttons, make_button(102, 119, 1, 0, "exit", function(btn)
level_m:stop_sim()
portal_m.chain = {}
cash.rb.particle_trail = {}
menu_m.active = true
end))
end
function make_button(x, y, top_cut, offset, text, cb)
local button_obj = gameobject:new{x=x, y=y, layer=4}
return button_obj:add_component(button_t:new{top_cut=top_cut, offset=offset, text=text, click_cb=cb})
end
function level_ui_t:draw()
if menu_m.active then
return
end
rectfill(0, 120, 128, 128, 4)
line(0, 120, 128, 120, 15)
end
function level_ui_t:update()
for btn in all(self.buttons) do
if end_menu_m.active and btn.offset < 10 then
btn.offset += 1
elseif not end_menu_m.active and btn.offset > 0 then
btn.offset -= 1
end
end
end
-- Note: x,y is of upper-left corner
button_t = component:new{
text = nil, -- displayed text on button
click_cb = nil, -- On click callback
offset = 0, -- y offset for slide-in
top_cut = 0 -- how much border to cut off the top
}
function button_t:update()
if menu_m.active or help_m.active then
return
end
if mouse_m.left_mouse_down and self:mouse_over() then
sfx(12)
self.click_cb(self)
end
end
function button_t:draw()
if menu_m.active or help_m.active then
return
end
local x = self.go.x
local y = self.go.y
local len = #self.text * 4 + 4
local is_mouse_over = self:mouse_over()
if is_mouse_over then
mouse_m.on_button = true
end
rect (x, y + self.top_cut + self.offset, x + len, y + 10 + self.offset, 15)
rectfill (x + 1, y + self.top_cut + 1 + self.offset, x + len - 1, y + 9 + self.offset, is_mouse_over and 6 or 5)
print (self.text, x + 3, y + 3 + self.offset, 7)
end
function button_t:mouse_over()
local len = #self.text * 4 + 4
return mouse_m.go.x >= self.go.x
and mouse_m.go.x <= self.go.x + len
and mouse_m.go.y >= self.go.y + self.offset + self.top_cut
and mouse_m.go.y <= self.go.y + 10 + self.offset
end
mouse_t = component:new{
last_mouse = 0, -- Mouse button stat from last frame
left_mouse = false, -- Left mouse button is being held down this frame
left_mouse_down = false, -- left/right mouse buttons are being clicked this frame
right_mouse_down = false,
on_button = false, -- Buttons will tell the mouse if it's over one of them
}
function mouse_t:update()
-- Mouse position
self.go.x = stat(32)
self.go.y = stat(33)
-- Mouse buttons
local this_mouse = stat(34)
self.left_mouse = this_mouse & 0x1 == 1
self.left_mouse_down = self.last_mouse & 0x1 == 0 and this_mouse & 0x1 == 1
self.right_mouse_down = self.last_mouse & 0x2 == 0 and this_mouse & 0x2 == 2
self.last_mouse = this_mouse
self.on_button = false
end
function mouse_t:draw()
if (menu_m.active or end_menu_m.active or help_m.active) then
sspr(2, 18, 3, 3, self.go.x - 1, self.go.y - 1)
else
if self.on_button then
sspr(2, 18, 3, 3, self.go.x - 1, self.go.y - 1)
elseif portal_m.candidate == nil then
sspr(24, 24, 7, 7, self.go.x - 3, self.go.y - 3)
elseif portal_m.highlighted_portal == nil then
sspr(2, 18, 3, 3, self.go.x - 1, self.go.y - 1)
elseif portal_m.move_index == 0 then
sspr(0, 24, 7, 7, self.go.x - 3, self.go.y - 3)
else
sspr(8, 24, 7, 7, self.go.x - 3, self.go.y - 3)
end
end
end
-- reset state for this frame. Useful for not using click on multiple items
function mouse_t:reset()
self.left_mouse = false
self.left_mouse_down = false
self.right_mouse_down = false
end
help_menu_t = component:new{
active = false,
page = 1, -- which help page number are we on
max_page = 3, -- which help page number are we on
}
function help_menu_t:draw()
if not self.active then
return
end
-- background
rectfill(0, 0, 127, 127, 15)
rectfill(2, 2, 125, 125, 4)
-- body
palt(0, false) -- draw black pixels
local title = "title"
if self.page == 1 then
title = "overview"
local y = print_formatted("your %objective is to collect all the %#10gold with the %#11ball using as few %#9portals as possible", 5, 20, 7)
y = print_formatted("you cannot control the %#11ball - %chain %#9portals around the level to get it where you need it", 5, y+4, 7)
rect(5, y+3, 15, y+37, 15)
sspr(8, 58, 9, 33, 6, y + 4)
y = print_formatted(" - %wall (place %#9portal here)", 16, y+6, 7)
y = print_formatted(" - %#11ball", 16, y, 7)
y = print_formatted(" - %#10gold", 16, y, 7)
y = print_formatted(" - %#9portal (number indicates order in %chain)", 16, y, 7)
elseif self.page == 2 then
title = "managing portals"
local y = print_formatted("%create %#9portals on any %white wall by %left %clicking it", 5, 20, 7)
y = print_formatted("%move %#9portals by %left %clicking one and %dragging it to a new %white wall", 5, y + 4, 7)
y = print_formatted("%delete %#9portals by %right %clicking them", 5, y + 4, 7)
y = print_formatted("%#9portals form %chains. %#9portal #001 will lead to %#9portal #002, etc.", 5, y + 4, 7)
for i=0,3 do
sspr(32 + i*4, 26, 3, 5, 65 + i*12, y + 4)
end
rect(5, y+1, 54, y+18, 15)
sspr(28, 66, 48, 16, 6, y+2)
y = print_formatted("1 2 3 4 1", 59, y + 4, 0)
y = print_formatted("this would work!", 59, y, 7)
elseif self.page == 3 then
title = "tips"
local y = print_formatted("the physics are %deterministic. every play with the %same %setup will have the %same %result", 5, 20, 7)
y = print_formatted("press %c to %start/stop", 5, y+4, 7)
y = print_formatted("use %gravity to your advantage", 5, y+4, 7)
y = print_formatted("if you missed any %#10gold - %stop, %tweak, and %try %again", 5, y+4, 7)
y = print_formatted("your progress is %saved %automatically. take a %break and come back %later", 5, y+4, 7)
y = print_formatted("- %#12have %#12fun! -", 38, y+3, 7)
end
palt(0, true)
-- header
local l = #title*4
print_shadowed(title, 64 - l/2, 6, 7)
line(63 - l/2, 12, 63 + l/2, 12, 7)
print_shadowed(self.page .. "/" .. self.max_page, 111, 6, 7)
end
function help_menu_t:update()
if not self.active then
return
end
if mouse_m.left_mouse_down then
self.page += 1
sfx(12)
end
if mouse_m.right_mouse_down then
self.page -= 1
sfx(13)
end
if self.page == 0 or self.page > self.max_page then
self.active = false
mouse_m:reset()
end
end
-- leaderboards and achievements
api_manager_t = component:new{
queue = {}, -- portal queue
records = {},
}
function api_manager_t:start()
-- re-trigger all earned achievements
for i=1,10 do
self.records[i] = 100
end
for i=1,10 do
if dget(i) > 0 then
self:level_complete(i, dget(i), false)
end
end
end
function api_manager_t:update()
if #self.queue > 0 and get_pin(0) == 0 then
for achievement in all(self.queue) do
set_pin(0, achievement)
--printh("Requested achievement unlock: " .. achievement)
del(self.queue, achievement)
break -- is there a better way to do a queue than for all / break?
end
end
end
function api_manager_t:level_complete(level, portals, report_leaderboard)
if report_leaderboard then
set_pin(level, portals)
end
-- achievements: 1/5/10x bronze/silver/gold, 1/2/3x stars
if self.records[level] > portals then
local medals_before = self:num_medals()
self.records[level] = portals
local medals_after = self:num_medals()
if medals_after.bronze == 1 and medals_before.bronze == 0 then add(self.queue, 77)
elseif medals_after.bronze == 5 and medals_before.bronze == 4 then add(self.queue, 80)
elseif medals_after.bronze == 10 and medals_before.bronze == 9 then add(self.queue, 83)
end
if medals_after.silver == 1 and medals_before.silver == 0 then add(self.queue, 78)
elseif medals_after.silver == 5 and medals_before.silver == 4 then add(self.queue, 81)
elseif medals_after.silver == 10 and medals_before.silver == 9 then add(self.queue, 84)
end
if medals_after.gold == 1 and medals_before.gold == 0 then add(self.queue, 79)
elseif medals_after.gold == 5 and medals_before.gold == 4 then add(self.queue, 82)
elseif medals_after.gold == 10 and medals_before.gold == 9 then add(self.queue, 85)
end
if medals_after.star == 1 and medals_before.star == 0 then add(self.queue, 86)
elseif medals_after.star == 2 and medals_before.star == 1 then add(self.queue, 87)
elseif medals_after.star == 3 and medals_before.star == 2 then add(self.queue, 88)
end
end
end
function api_manager_t:num_medals()
local medals = {bronze=0, silver=0, gold=0, star=0}
for i=1,10 do
if self.records[i] < levels[i].gold then
medals.star += 1
end
if self.records[i] <= levels[i].gold then
medals.gold += 1
end
if self.records[i] <= levels[i].silver then
medals.silver += 1
end
if self.records[i] <= levels[i].bronze then
medals.bronze += 1
end
end
return medals
end
-- pin 0 == medal to unlock. Only last two digits are sent
-- pins 1-10 == leaderboard scores
function get_pin(pin, value)
return peek(0x5f80+pin)
end
function set_pin(pin, value)
poke(0x5f80+pin, value)
end
--local music_offsets = {[0]=0, [33]=-1} -- start from -1 because the loop point is on the second measure
--local music_lengths = {[0]=4, [33]=20}
--function play_music(track)
-- if current_track ~= track then
-- if current_track ~= -1 then
-- if music_offsets[current_track] >= 0 or stat(25) > 0 then
-- music_offsets[current_track] = (music_offsets[current_track] + stat(25)) % music_lengths[current_track]
-- end
-- end
-- music(track + music_offsets[track])
-- current_track = track
-- end
--end