diff --git a/chameleonic.p8 b/chameleonic.p8 new file mode 100644 index 0000000..c03b108 --- /dev/null +++ b/chameleonic.p8 @@ -0,0 +1,1363 @@ +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 + +function _doall(x) + for x2 in all(split"0,,2,3") do + for mod in all(modules) do + local f=mod[x..x2] + if (f != nil) f(mod) + end + end +end + +-- source: https://www.lexaloffle.com/bbs/?pid=78990 +gaps={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=a[i] + local j=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 + + -- collision map + self:load_dynobjs() + self:recollide() + self:reanchor() + player:reinit(6,14) +end + +function level:draw() + map( + self.bigx,self.bigy, + 0,0,16,16, + 64 -- flag 6: visible + ) + for _,crate in pairs(self._crates) do + spr(crate.s,crate.px,crate.py) + end +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) + + for _,crate in pairs(self._crates) do + _apply(crate, crate.todo) + end +end + +function level:load_dynobjs() + self._crates={} + for mx=0,15,1 do + for my=0,15,1 do + local mxy=_mix(mx,my) + 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=mx*8,py=my*8, + todo={} + } + 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:anchor_points() + return pairs(self._anch) +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: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)]==true +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( + mx0,my0,dmx,dmy,exclude_src,exclude_dst +) + if self:mcoll(mx0+dmx,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) +end + +function player:reinit(x,y) + self.x=x + self.y=y + self.px=0 + self.py=0 + self.todo={} + + 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 + + if not self:any_busy() then + if btn(⬅️) then + if level:can_move(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(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(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(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) + + if self.rope:done() then + self.rope=nil + add(self.todo,{}) + add(self.todo,{}) + add(self.todo,{}) + end + end +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 + + if self.orientx==-1 then + spr(16,px+6,py-2,1,1) + if (self.rope) self.rope:draw() + spr(17,px+1,py,1,1) + spr(head,px-3,py-3,1,1) + else + spr(16,px-6,py-2,1,1,true) + if (self.rope) self.rope:draw() + spr(17,px-1,py,1,1,true) + spr(head,px+3,py-3,1,1,true) + end + --spr(17,px+3,py) + --spr(17,px+6,py) + -- if spr(2,self.x*8+self.px) +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 + self:_tidy_up_gen() + 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 + break + 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()) + for x,y in self:_rast( + anc.x,anc.y,x,y + ) do + self:_drag1(i(),x,y) + self:_tidy_up_gen() + end +end + +function rope:_tidy_up_gen() + if (self:busy()) return + + for i=0,#self.ancs+1 do + local a=self:_anc(i) + a.dirty=true + end + + local a=0 + while a<=#self.ancs+1 do + local anc=self:_anc(a) + if anc.dirty and #anc.todo==0 then + while not self.under_destruction and ( + self:_find_needed_anchors(a) or + self:_find_touched_anchors(a) or + self:_elide_point(a) + ) do end + + anc.dirty=false + a=0 + else + a+=1 + end + end +end + +function rope:_drag1( + i,x,y +) + local a_old=self:_anc(i) + local a_new={x=x,y=y} + if (_point_eq(a_old, a_new)) return + + a_old.x=x + a_old.y=y +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 +) + 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:_rastm(p1.x,p1.y,p2.x,p2.y) do + if level:pcoll(x,y) then + res=false + break + end + end + + return res +end + +function rope:_rastm( + x0,y0,x1,y1 +) + -- 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\8 + local y8 = y\8 + 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 (queue==nil) return + if (x==x1) queue=nil return x1,y1 + if #queue==0 then + add(queue,{x,y}) + err-=dy + if (err<0) y+=sy add(queue,{x,y}) err+=dx + x+=sx + end + return unpack(deli(queue,1)) + end + else + err=dy/2.0 + return function() + if (queue==nil) return + if (y==y1) queue=nil return x1,y1 + if #queue==0 then + add(queue,{x,y}) + + local oldx,oldy=x,y + err-=dx + if (err<0) x+=sx add(queue,{x,y}) err+=dy + y+=sy + end + return unpack(deli(queue,1)) + 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() + 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(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 + s.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) + if (anc!=nil) anc.dirty=true + 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(mx0,my0,dmx,dmy,1,0) + then + level:tug_crate( + mx0,my0, + dmx,dmy + ) + 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