Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1941 lines (1672 sloc)
54.3 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- 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 | |
| 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 |