pico-8 cartridge // http://www.pico-8.com version 38 __lua__ modules={} frame=0 function _init() _doall("init") end function _update() frame+=1 if (frame%1==0) _doall("update") end function _draw() _doall("draw") end mnames={} function names(root) local n=mnames[root] if(n)return all(n) n={root.."0", root, root.."2", root.."3"} mnames[root]=n return all(n) end function _doall(x) for n in names(x) do for mod in all(modules) do local f=mod[n] if (f) f(mod) end end end -- source: https://www.lexaloffle.com/bbs/?pid=78990 gaps=split"57,23,10,4,1" --{701,301,132,57,23,10,4,1} function shellsort(a) for gap in all(gaps) do for i=gap+1,#a do local x,j=a[i],i-gap while j>=1 and a[j].key>x.key do a[j+gap]=a[j] j-=gap end a[j+gap]=x end end end function linefill(ax,ay,bx,by,r,c) if(c) color(c) local dx,dy=bx-ax,by-ay -- avoid overflow -- credits: https://www.lexaloffle.com/bbs/?tid=28999 local d=max(abs(dx),abs(dy)) local n=min(abs(dx),abs(dy))/d d*=sqrt(n*n+1) if(d<0.001) return local ca,sa=dx/d,-dy/d -- polygon points local s={ {0,-r},{d,-r},{d,r},{0,r} } local u,v,spans=s[4][1],s[4][2],{} local x0,y0=ax+u*ca+v*sa,ay-u*sa+v*ca for i=1,4 do local u,v=s[i][1],s[i][2] local x1,y1=ax+u*ca+v*sa,ay-u*sa+v*ca local _x1,_y1=x1,y1 if(y0>y1) x0,y0,x1,y1=x1,y1,x0,y0 local dx=(x1-x0)/(y1-y0) if(y0<0) x0-=y0*dx y0=-1 local cy0=y0\1+1 -- sub-pix shift x0+=(cy0-y0)*dx for y=y0\1+1,min(y1\1,127) do -- open span? local span=spans[y] if span then rectfill(x0,y,span,y) else spans[y]=x0 end x0+=dx end x0,y0=_x1,_y1 end end function _apply(x,ts,a) local t=deli(ts,1) for k,v in pairs(t) do if k=="update" then if not v(x,a) then add(ts,t,1) end else x[k]=v end end end function sgn0(x) if (x==0) return x return sgn(x) end function _mnmx(x,y) return min(x,y),max(x,y) end -->8 bg={} add(modules,bg) function bg:draw0() cls(0) end -->8 level={} add(modules,level) function level:init() level:reinit(0) end function level:reinit(n) self.ix=n self.todo={} self.bigx=(n%16) self.bigy=(n\16) self:load_dynobjs() self:recollide() self:reanchor(true) self:spawn_exit() end function level:restart() self:reinit(self.ix) end function level:advance() self:reinit(self.ix+1) end function level:draw() cls(1) pal(1,0) map( self.bigx*16,self.bigy*16, 0,0,16,16, 64 -- flag 6: visible ) for _,pit in pairs(self._pits) do spr(pit.s,pit.px,pit.py) if pit.contents then pal(7,0) pal(0,1) palt(0,false) spr(pit.contents,pit.px,pit.py) pal() end for _,crate in pairs(self._crates) do spr(crate.s,crate.px,crate.py) end end pal() end function level:busy() for _,crate in pairs(self.crates) do if (#crate.todo>0) return true end return false end function level:update() _apply(self, self.todo) local remove={} for cix,crate in pairs(self._crates) do _apply(crate, crate.todo) if #crate.todo==0 then local pit=self._pits[_mix(crate.mx,crate.my)] if pit!=nil and pit.contents==nil then add(remove,cix) crate.dead=true pit.contents=crate.s end end end for cix in all(remove) do self._crates[cix]=nil end if #remove>0 then self:recollide() self:reanchor(true) end end function level:load_dynobjs() self._crates={} self._pits={} for mx=0,15,1 do for my=0,15,1 do local mxy=_mix(mx,my) local px,py=mx*8,my*8 local s=self:_mget(mx,my) local def=self:_get_cratedef(s) if def!=nil then self._crates[mxy]={ s=s,def=def, mx=mx,my=my, px=px,py=py, todo={} } end if s==28 then -- pit self._pits[mxy]={ s=s, mx=mx,my=my, px=px,py=py, contents=nil } end end end end function level:recollide() self._coll={} for mx=0,15,1 do for my=0,15,1 do local mxy=_mix(mx,my) self._coll[mxy]= fget(self:_mget(mx,my),7) or self._crates[mxy]!=nil end end end function level:reanchor(remove) if remove or self._anch==nil then self._anch={} end for ax0=0,31,1 do for ay0=0,31,1 do local ax1,ay1=ax0-1+2*(ax0%2),ay0-1+2*(ay0%2) local mx0,my0=ax0\2,ay0\2 local mx1,my1=ax1\2,ay1\2 if ( not self:mcoll(mx0,my0) and not self:mcoll(mx0,my1) and not self:mcoll(mx1,my0) and self:mcoll(mx1,my1) ) then local px0,py0=level:a2p(ax0,ay0) self._anch[_amix(ax0,ay0)]={ax=ax0,ay=ay0,x=px0,y=py0} end end end end function level:win_at(mx,my) return self._wins[_mix(mx,my)] end function level:anchor_points() return pairs(self._anch) end function level:anchors_in(px0,py0,px1,py1) ancs={} for ax=px0\4,(px1+3)\4 do for ay=py0\4,(py1+3)\4 do local anc=self._anch[_amix(ax,ay)] if (anc!=nil) add(ancs, anc) end end return ancs end function level:get_open_pit(mx,my) local pit=self._pits[_mix(mx,my)] if (pit and pit.contents==nil) return pit end function level:point_anchor(px,py) local ax,ay=self:p2a(px,py) local anc=self._anch[_amix(ax,ay)] return anc end function level:spawn_exit() self._wins={} local spawned=false local spawn_at=function(x,y) if (self:_mget(x,y)!=1) return assert(not spawned,x..","..y) spawned=true player:reinit(x,y) player.orientx=-1 if (x<8) player.orientx=1 end local win_at=function(x,y) if (self:_mget(x,y)!=4) return for n in all(neighbors{x=x,y=y}) do if n.x<0 or n.y<0 or n.x>15 or n.y>15 then self._wins[_mix(n.x,n.y)]=true end end end for f in all{spawn_at,win_at} do for x=1,14 do f(x,0) f(x,15) end for y=0,15 do f(0,y) f(15,y) end end assert(spawned) end function level:p2a(px,py) return px\4,py\4 end function level:a2p(ax,ay) local px=ax*4+3*(ax%2) local py=ay*4+3*(ay%2) return px,py end function level:mcoll(mx,my) return self._coll[_mix(mx,my)]!=false end function level:pcoll(px,py) return self:mcoll(px\8,py\8) end function level:get_crate(mx,my) return self._crates[_mix(mx,my)] end function level:_mget(mx,my) return mget( self.bigx*16+mx, self.bigy*16+my ) end function _amix(ax,ay) return ax..","..ay --if (ax<0 or ay<0 or ax>31 or ay>31) return nil --return ay*32+ax end function _mix(mx,my) return mx..","..my --if (mx<0 or my<0 or mx>15 or my>15) return nil --return my*16+mx end function level:_get_cratedef(s) if (s<64 or s>=80) return nil local s2=s-64 return { up=s2&1!=0, right=s2&2!=0, down=s2&4!=0, left=s2&8!=0 } end function level:get_latch(dx,dy,px,py) local mx,my=px\8,py\8 local mxy=_mix(mx,my) local crate=self._crates[mxy] if crate then if (crate.def.up and dy>0) or (crate.def.down and dy<0) or (crate.def.left and dx>0) or (crate.def.right and dx<0) then return { el="crate", dx=-dx,dy=-dy, px_offset=px-crate.px-sgn0(dx), py_offset=py-crate.py-sgn0(dy), rec=crate } end end local m=self:_mget(mx,my) if (m==60 and dy>0) or (m==61 and dx<0) or (m==62 and dy<0) or (m==63 and dx>0) then return { el="eyehook", dx=-dx,dy=-dy, mx=mx,my=my } end end function level:can_move( is_player, mx0,my0,dmx,dmy,exclude_src,exclude_dst ) if is_player and self:win_at(mx0+dmx,my0+dmy) then return true end if self:mcoll(mx0+dmx,my0+dmy) then return false end if player.x==mx0+dmx and player.y==my0+dmy then return false end -- todo: check tongue collision if player.rope then local px,py=mx0*8,my0*8 local chk=false if dmx==0 and dmy==-1 then chk=player.rope:collide_rect(px+3,py-5,px+4,py+5,exclude_src,exclude_dst) elseif dmx==0 and dmy==1 then chk=player.rope:collide_rect(px+3,py+3,px+4,py+13,exclude_src,exclude_dst) elseif dmx==-1 and dmy==0 then chk=player.rope:collide_rect(px-5,py+3,px+5,py+4,exclude_src,exclude_dst) elseif dmx==1 and dmy==0 then chk=player.rope:collide_rect(px+3,py+3,px+13,py+4,exclude_src,exclude_dst) end if (chk) return false end return true end function level:tug_crate(mx0,my0,dmx,dmy) local mxy0=_mix(mx0,my0) local existing=self._crates[mxy0] if (existing==nil) return self._crates[mxy0]=nil local mx1,my1=mx0+dmx,my0+dmy local mxy1=_mix(mx1,my1) existing.mx=mx1 existing.my=my1 existing.todo={ {px=mx0*8+dmx*2,py=my0*8+dmy*2}, {px=mx0*8+dmx*7,py=my0*8+dmy*7}, {px=mx1*8,py=my1*8,update=function() self:reanchor(true) return true end} } self._crates[mxy1]=existing self:recollide() self:reanchor(false) end -->8 player={} add(modules,player) function player:init() --self:reinit(8,14) -- don't change this on reinit: -- it stays the same when the level is changed or reloaded self.vanish_frame=0 end function player:reinit(x,y) self.x=x self.y=y self.px=0 self.py=0 self.todo={} self.fall_frame=0 self.reset_frame=0 self.orientx=-1 self.orienty=0 self.rope=nil end function player:any_busy() if (#self.todo>0) return true if (level:busy()) return true if (self.rope!=nil and self.rope:busy()) return true return false end function player:update() local _addall=function(t,xs) for i in all(xs) do add(t,i) end end local f4 = function(xs) -- todo: other anim stuff xs[#xs].px=0 xs[#xs].py=0 return xs end -- this is a non-gameplay action that takes precedence over -- all gameplay actions self:_vanish_if_requested() if not self:any_busy() then if level:win_at(self.x,self.y) then level:advance() return end if level:get_open_pit(self.x,self.y) then self.todo={{update=self._fall}} return end if btn(⬅️) then if level:can_move(true,self.x,self.y,-1,0,0,2) then self.todo=f4({{orientx=-1,orienty=0,px=-2},{px=-7},{x=self.x-1}}) else self.orientx=-1 self.orienty=0 end elseif btn(➡️) then if level:can_move(true,self.x,self.y,1,0,0,2) then self.todo=f4({{orientx=1,orienty=0,px=2},{px=7},{x=self.x+1}}) else self.orientx=1 self.orienty=0 end elseif btn(⬆️) then if level:can_move(true,self.x,self.y,0,-1,0,2) then self.todo=f4({{orienty=-1,py=-2},{py=-7},{y=self.y-1}}) else self.orienty=-1 end elseif btn(⬇️) then if level:can_move(true,self.x,self.y,0,1,0,2) then self.todo=f4({{orienty=1,py=2},{py=7},{y=self.y+1}}) else self.orienty=1 end elseif btn(🅾️) then if self.rope==nil then local rx,ry,rx2,ry2=self:_rope_pos() local dx,dy=12*self.orientx,12*self.orienty if (dy!=0) dx=0 self.rope=rope:new(rx,ry,rx2,ry2,dx,dy) self.todo={{ update=function() return self.rope==nil or not self.rope:casting() end },{},{}} else self.rope:tug() self.todo={{},{},{}} end elseif btn(❎) then if self.rope!=nil then self.rope=nil end end end _apply(self,self.todo) if self.rope then self.rope:update() local rx,ry=self:_rope_pos() self.rope:drag_dst(rx,ry) local tdx,tdy=self.rope:tug_orientxy() if (tdx!=0) self.orientx=tdx if (tdy!=0) self.orienty=tdy if self.rope:done() then self.rope=nil add(self.todo,{}) add(self.todo,{}) add(self.todo,{}) end end end function player:_vanish_if_requested() local bvan=btn(❎) if self.bvan and not bvan then self.vanishing=false elseif not self.bvan and bvan then self.vanishing=true end self.bvan=bvan if self.vanishing then self.vanish_frame+=1 if (self.fall_frame>0 or self.vanish_frame>20) then level:restart() self.vanish_frame=20 self.vanishing=false end else self.vanish_frame-=1 end self.vanish_frame=max(self.vanish_frame,0) end function player:_fall() if (self.fall_frame<10) self.fall_frame+=1 end function player:_rope_pos() local px=self.x*8+self.px local px2=px+4 if self.orientx==-1 then px+=2 else px+=6 end local py=self.y*8+self.py+2 local py2=py+1 return px,py,px2,py2 end function player:draw() local px=self.x*8+self.px local py=self.y*8+self.py local head=2-self.orienty local vanish_level=self.vanish_frame/20 local invis_level=max(self.fall_frame/10,4*(vanish_level-0.75)) if (invis_level>=1.0) return --px+=sin(vanish_level*16)*max(vanish_level-0.1,0)*1 local HEAD=14--3 local BODY=12--12 local TAIL=14--14 local IRIS=7--9 local PUPIL=0--0 local setpal=function() -- base colors pal(13,TAIL) pal(14,TAIL) pal(15,TAIL) pal(4,BODY) pal(5,BODY) pal(12,BODY) pal(2,HEAD) pal(3,HEAD) pal(9,IRIS) pal(10,PUPIL) -- vanish colors local vanish=split"13,15,14,5,4,12,2,3,9,10" for i,ilc in ipairs(vanish) do if (vanish_level>i/#vanish) pal(ilc,1) end if self.fall_frame>3 then for i=0,15 do pal(i,1) end end end if self.orientx==-1 then setpal() spr(16,px+6,py-2,1,1) spr(17,px+1,py,1,1) if (self.rope and invis_level<=0.25) pal() self.rope:draw() setpal() spr(head,px-3,py-3,1,1) else setpal() spr(16,px-6,py-2,1,1,true) spr(17,px-1,py,1,1,true) if (self.rope and invis_level<=0.25) pal() self.rope:draw() setpal() spr(head,px+3,py-3,1,1,true) end pal() end -->8 rope={} rope.__index=rope function rope:new( x,y,src_x,src_y,dx,dy ) local r={ id=0, src={x=src_x,y=src_y,todo={}}, ancs={}, dst={x=x,y=y,todo={}}, cast={dx=dx,dy=dy}, latch=nil, latch_frame=0, under_destruction=false, } setmetatable(r,rope) return r end function rope:casting() return self.cast!=nil end function rope:done() return self.latch_frame>=2 and ( self.latch==nil or self.under_destruction ) end function rope:busy() for i=0,#self.ancs+1 do if (#(self:_anc(i).todo)>0) return true end return false end function rope:update() if self.cast!=nil then self:continue_cast() return end self.latch_frame+=1 if self.latch_frame>=10 then self.latch_frame=10 end self:_make_consistent() end function rope:_make_consistent() for i=0,#self.ancs+1 do local anc=self:_anc(i) _apply(anc,anc.todo,i) end if not self.under_destruction and self.latch!=nil and self.latch.rec!=nil then self:drag_src( self.latch.rec.px+self.latch.px_offset, self.latch.rec.py+self.latch.py_offset ) if #self.latch.rec.todo==0 then if self.latch.rec.dead==true then self.under_destruction=true return end for i=0,#self.ancs do local a0=self:_anc(i) local a1=self:_anc(i+1) if not self:_can_stretch(a0, a1) then self.under_destruction=true return end end end end end function rope:continue_cast() local dx,dy=self.cast.dx,self.cast.dy local x0=self.src.x local y0=self.src.y local x1=x0+dx local y1=y0+dy for x,y in self:_rast( x0,y0,x1,y1 ) do local latch= level:get_latch(dx,dy,x,y) if latch!=nil or level:pcoll(x,y) then self.latch=latch self.cast=nil break end self.src={x=x,y=y,todo={}} end end function rope:_reindex() self.src.ix=0 self.dst.ix=#self.ancs for i,anc in ipairs(self.ancs) do anc.ix=i end end function rope:draw() local points=self:_anchors_simplified() for i=1,(#points-1) do local src=points[i] local dst=points[i+1] local x,y=src.x,src.y local dx,dy=dst.x-x,dst.y-y linefill(x,y,x+0.25*dx,y+0.25*dy,1.0,8) linefill(x+0.25*dx,y+0.25*dy,x+1*dx,y+1*dy,0.5,8) linefill(x+0.9*dx,y+0.9*dy,x+dx,y+dy,1.0,8) circfill(x+0.5,y+0.5,1.0,8) end for i,p in ipairs(self.ancs) do rectfill(p.x-1,p.y-1,p.x+1,p.y+1,12) print(p.id..":"..p.x..","..p.y..","..#p.todo,0,-8+i*8,9) end for _,p in pairs(level._anch) do pset(p.x,p.y,11) end if self.all_ops!=nil then for i,o in ipairs(self.all_ops) do rect(o.mx*8,o.my*8,o.mx*8+7,o.my*8+7,4) --print(o.mx..","..o.my,0,i*8,3) end end end function rope:_anc(i) if (i==0) return self.src if (i==#self.ancs+1) return self.dst return self.ancs[i] end function rope:drag_dst(x,y) self:drag(function() return #self.ancs+1 end,x,y) end function rope:drag_src(x,y) self:drag(function() return 0 end,x,y) end function rope:drag( i,x,y ) local anc=self:_anc(i()) local busy=self:busy() for x,y in self:_rast( anc.x,anc.y,x,y ) do local a=self:_anc(i()) if not (_point_eq(a,{x=x,y=y})) then a.x=x a.y=y a.dirty=true self.dirty=true if (not busy) self:_tidy_up_gen() end end end function rope:_tidy_up_gen() if (self.under_destruction) return if (not self.dirty) return local settled=true local touched={} local loop=function(f) local a=0 while a<=#self.ancs+1 do local anc=self:_anc(a) if anc.dirty then anc.seen=true if self[f](self,a) then settled=false anc.changed=true end end a+=1 end end local mark_unseen=function() touched={} for a=0,#self.ancs+1,1 do local anc=self:_anc(a) anc.seen=false anc.changed=false end end local propagate_dirty=function(f) for a=0,#self.ancs+1,1 do local a1=self:_anc(a) if a1.dirty then local a0=self:_anc(a-1) if (a0!=nil) a0.dirty=true local a2=self:_anc(a+1) if (a2!=nil) a2.dirty=true end end end while true do settled=true mark_unseen() propagate_dirty() loop("_find_needed_anchors") loop("_find_touched_anchors") loop("_elide_point") for a=0,#self.ancs+1,1 do local anc=self:_anc(a) if (anc.seen) anc.dirty=anc.changed end if (settled) break end self.dirty=false end function rope:_find_needed_anchors(i) if (i<=0) return false if (#self.ancs+11) return false local ub= ((x2-x1)*(y1-y3)-(y2-y1)*(x1-x3))/denom if (ub<0 or ub>1) return false return true end function rope:_can_stretch( p1,p2 ) -- faster implementation for straight lines if p1.y\8==p2.y\8 then local my=p2.y\8 for mx=p1.x\8,p2.x\8 do if (level:mcoll(mx,my)) return false end end if p1.x\8==p2.x\8 then local mx=p2.x\8 for my=p1.y\8,p2.y\8 do if (level:mcoll(mx,my)) return false end end if (level:pcoll(p1.x,p1.y)) return false if (level:pcoll(p2.x,p2.y)) return false local res=true for x,y in self:_rastn(p1.x,p1.y,p2.x,p2.y,8,8) do if level:pcoll(x,y) then res=false break end end return res end function rope:_rastn( x0,y0,x1,y1,dx,dy ) -- todo: more optimized implementation? local iter=self:_rast(x0,y0,x1,y1) local prevx,prevy=nil,nil local done=false return function() while not done do local x,y=iter() if (x==nil) done=true return x1, y1 local x8 = x\dx local y8 = y\dy if not (x8==prevx and y8==prevy) then prevx,prevy=x8,y8 return x,y end end end end function rope:_rast( x0,y0,x1,y1 ) local dx=abs(x1-x0) local dy=abs(y1-y0) local x=x0 local y=y0 local sx=-1 local sy=-1 if (x0dy then err=dx/2.0 return function() if (done) return if (x==x1) done=true return x1,y1 local oldx,oldy=x,y err-=dy if (err<0) y+=sy err+=dx x+=sx return oldx,oldy end else err=dy/2.0 return function() if (done) return if (y==y1) done=true return x1,y1 local oldx,oldy=x,y err-=dx if (err<0) x+=sx err+=dy y+=sy return oldx,oldy end end end function _point_eq(p1,p2) return p1.x==p2.x and p1.y==p2.y end function neighbors(p) local r={} for dx=-1,1,1 do for dy=-1,1,1 do if dx!=0 or dy!=0 then add(r,{x=p.x+dx,y=p.y+dy}) end end end return r end -->8 -- moved here because it's complicated function rope:tug_orientxy() local a1=self:_anc(#self.ancs+1) local a0=self:_anc(#self.ancs) local dx=a0.x-a1.x local tdx=0 if (dx>3) tdx=1 if (dx<-3) tdx=-1 local dy=a0.y-a1.y local tdy=0 if abs(dy)>abs(dx)/2 then if (dy>3) tdy=1 if (dy<-3) tdy=-1 end return tdx,tdy end function rope:tug() self:_make_consistent() if (self.under_destruction) return self:_tug() self:_make_consistent() end function rope:_tug() local ancs=self:_anchors_simplified() local touched={} for i=#ancs-1,2,-1 do local ops= self:_calc_push(ancs[i+1],ancs[i],ancs[i-1]) for o in all(ops) do add(self.all_ops,o) end local can_do=true for o in all(ops) do if not level:mcoll(o.mx,o.my) then -- great! else local crate=level:get_crate(o.mx,o.my) if crate==nil or touched[_mix(o.mx,o.my)] then can_do=false else if not level:can_move(false,o.mx,o.my,o.dmx,o.dmy,0,0) then can_do=false end end end if (not can_do) break end if can_do and #ops>=1 then local dmx,dmy=ops[1].dmx,ops[1].dmy for o in all(ops) do touched[_mix(o.mx,o.my)]=true touched[_mix(o.mx+dmx,o.my+dmy)]=true level:tug_crate( o.mx,o.my,dmx,dmy ) end for node=ancs[i-1].ix,ancs[i].ix do local anc=self:_anc(node) local x0,y0=anc.x,anc.y local upd=function(x,y,force) return {update=function(s,i) if force or not level:pcoll(x,y) then s.x=x s.y=y self.dirty=true end return true end} end anc.todo={ {}, upd(x0+dmx*2,y0+dmy*2), upd(x0+dmx*7,y0+dmy*7), upd(x0+dmx*8,y0+dmy*8), } end for node=ancs[i-1].ix-1,ancs[i].ix+1 do local anc=self:_anc(node) end return end end local latch=self.latch if (latch==nil) return if latch.el=="crate" then local dmx,dmy= sgn0(latch.dx), sgn0(latch.dy) local lanc=ancs[2] local mx0=latch.rec.mx local my0=latch.rec.my local mxa=(lanc.x+dmx)\8 local mya=(lanc.y+dmy)\8 local too_far=false if sgn0(mx0-mxa)!= sgn0(mx0+dmx-mxa) or sgn0(my0-mya)!= sgn0(my0+dmy-mya) then too_far=true end if not too_far and not touched[_mix(mx0,my0)] and level:can_move(false,mx0,my0,dmx,dmy,1,0) then level:tug_crate( mx0,my0, dmx,dmy ) -- be busy for 4 ticks while the crate moves self:_anc(0).todo={{},{},{},{}} end end end function rope:_calc_push( an,a0,a1 ) local ops={} if a0.x==a1.x then local y0,y1=_mnmx(a0.y,a1.y) local my0,my1=(y0+1)\8,(y1-1)\8 local mx,dmx if a0.x%8==0 and a0.x>an.x+7 then -- push left mx=(a0.x-1)\8 dmx=-1 elseif a0.x%8==7 and a0.xan.y+6 then -- push up my=(a0.y-1)\8 dmy=-1 elseif a0.y%8==7 and a0.y