0,]']M}W̪p8d%qy)6oR6AsVl3# wG?ffffvwwgvwwgvwwgvwwgvwwg` ffff3333C3C34343 ""3333 !3333` !3C3C` "" 3434 pww  3333pp̪pp̪wrwrʪʪʪʪwwww̪pr'pr'̪pwwpww̬ ̪ʪʪʪ ̪pp̪pp̪wrwr ʪʪwwww ̪pr'pr' ̪pwwpww ̬ ̪ʪ  ̪pp̪ppp̪wrwrʪʪʪʪʪʪwwww ̪pr'pr' ̪pwwpww ̬ " ̪ʪʪʪʪʪ ̪pp̪pp˻˻˻˻˻˻˻˻̪wrwr""ʪʪʪʪʪʪwwww!̪pr'pr'!̪pwwpww""˻˻˻˻˻˻˻̬  ̪ʪʪʪʪʪ ̪̻pp˻˻pp""˻˻˻˻˻˻wrwrʪʪ Jwwww DDpr'pr'˻pwwpww ˻ˋˋ˻˻ ʪ ˻˻!!˻˻˻˻˻˻˻!!!!!˻!!˻˻ˈȻ˻˻˻  !! pp˻˻˻˻˫ʻ˻˻˻˻pwwpwwpp˻˻˻˻˻˻˻˻pffpffwrwr pvggpvgwwww pvggpvgpr'pr' ˻˻˻˻˻˻˻pffpffpwwpww˻˻˻˻˻˻˻ pww pww ˻˻˻˻˻˻˻˻˫˻ 9933 99 9 99 9 9 ˻˻˻˻˻˻˻33 99 99 pww˫ʻ˻˻˻˻˻˻gvgvgvgv gvgv gv ˻˻˻˻˻˻gvgv gvgv wwwwppp˻˻˻˻˻ʻ˻˻ppp wrwrwr wwwwww pr'pr'pr'˻˻˻˻˪˻˻ pwwpwwpww ˻˻˻˻˻˻˻˻˻˻˻˻˻˻˻ 33333333C3C3C3C3˻˻˫˻˻˻˻43434343 33333333 333333333C3C3C3C˻˫˻ʻ˻˻˻ 34343434  3333333333333333C3C3C3C3˻˻˻˻˫ʻ˻˻4343434333333333333333333C3C3C3C˻˻˻˫˻ʻ˻ 34343434  33333333˻˻˻˻˻˻˻˻˻˻˻˻˻˻˻ I-- title: sookoban -- author: soolar, soolar121@gmail.com -- desc: A sokoban game by soolar -- site: https://github.com/soolar/sookoban -- license: MIT License -- version: 0.1 -- script: lua -- NO DIAGONAL MOVEMENT, MKAY? local W,H=240,136--screen dimensions local MW,MH=W/8,H/8--map tiles on screen local edge=48--number of pixels to keep between edge of screen and player local cam={ x=0,y=0, minx=edge,miny=edge, maxx=W-edge-8,maxy=H-edge-8,--player is 8x8 pixels adjx=0,adjy=0,--amount of movement the camera has to do } local t=0 local tdeath local level--assigned in startlevel local levelnum=0 local function clamp(n,min,max) if nmax then return max end return n end function ttget(x,y) if xlevel.x2 or ylevel.y2 then return 1 end return mget(x,y) end function cam:move(dx,dy) self.x=clamp(self.x+dx,level.x1*8,(level.x2-MW+1)*8) self.y=clamp(self.y+dy,level.y1*8,(level.y2-MH+1)*8) end local Object={} Object.__index=Object local Player=setmetatable({},Object) Player.__index=Player local Box=setmetatable({},Object) Box.__index=Box local Splash=setmetatable({},Object) Splash.__index=Splash local Fireball=setmetatable({},Object) Fireball.__index=Fireball local Ash=setmetatable({},Object) Ash.__index=Ash local IceBlock=setmetatable({},Box) IceBlock.__index=IceBlock local function objectsat(mx,my,cond) return coroutine.wrap(function() for _,obj in ipairs(level.objects) do local omx,omy=obj:currtile() if omx==mx and omy==my and (not cond or cond(obj)) then coroutine.yield(obj) end end end) end local function collidingobjectsat(mx,my,obj,ignorespecial) return objectsat(mx,my,function(obj2) return obj2:hascollision(obj,ignorespecial) end) end local function getdir(dx,dy) if dy>0 then return 0 end if dy<0 then return 1 end if dx>0 then return 2 end if dx<0 then return 3 end end local function getdelta(dir,scale) scale=scale or 1 if dir==0 then return 0,scale end if dir==1 then return 0,-scale end if dir==2 then return scale,0 end if dir==3 then return -scale,0 end trace(string.format("invalid dir [%i]",dir)) return 0,0 end local function leveltiles() return coroutine.wrap(function() for y=level.y1,level.y2 do for x=level.x1,level.x2 do local tt=ttget(x,y) coroutine.yield(x,y,tt) end end end) end local function changeall(old,new,callback) for x,y,tt in leveltiles() do if tt==old then mset(x,y,new) if callback then callback(x,y) end end end end local function changeif(x,y,ttcurr,ttcond,ttnew) if ttcurr==ttcond then mset(x,y,ttnew) end end local function activatecolor(color,obj) level.colorsactive[color]=true if obj and getmetatable(obj)==Player then obj.pause=10 end for x,y,tt in leveltiles() do --open doors changeif(x,y,tt,color+16,color+20) --lower walls changeif(x,y,tt,color+24,color+28) --launch ze fireballs for dir=0,3 do local launchertt=color+60+16*dir if tt==launchertt then Fireball.new(x*8,y*8,dir) end end end end local function deactivatecolor(color,obj) level.colorsactive[color]=false for x,y,tt in leveltiles() do --close doors changeif(x,y,tt,color+20,color+16) --raise walls changeif(x,y,tt,color+28,color+24) end end local function modifybutton(button,delta,obj) if not obj:onground() then return end level.buttonpresses[button]=level.buttonpresses[button]+delta if delta>0 and level.buttonpresses[button]==1 then activatecolor(button-32,obj) elseif delta<0 and level.buttonpresses[button]==0 then deactivatecolor(button-32,obj) end end local buttoncallbacks={ onenter=function(tt,x,y,obj) modifybutton(tt,1,obj) return false end, onleave=function(tt,x,y,obj) modifybutton(tt,-1,obj) end, } local levercallbacks={ ondraw=function(tt,x,y) if level.colorsactive[tt-36] then return tt+4 end return tt end, oninteract=function(tt,x,y,obj) if level.colorsactive[tt-36] then deactivatecolor(tt-36,obj) else activatecolor(tt-36,obj) end end, onbump=function(tt,x,y,obj) if getmetatable(obj)==Fireball then if level.colorsactive[tt-36] then deactivatecolor(tt-36,obj) else activatecolor(tt-36,obj) end end end } local arrowcallbacks={ onenter=function(tt,x,y,obj) if obj:onground() then obj.dir=tt-3 obj.speed=obj.pspeed return true end end, } local function spinnercallbacks(xmult,ymult,reqground) return { onenter=function(tt,x,y,obj) if not reqground or obj:onground() then local dx,dy=getdelta(obj.dir) local ndx,ndy=dy*xmult,dx*ymult obj.dir=getdir(ndx,ndy) obj.speed=obj.pspeed return true end end, } end local function spawncallbacks(tospawn,...) return { onstart=function(tt,x,y) tospawn.new(x*8,y*8) end, ondraw=function(tt,x,y) return 0 end, } end local function powerupcallbacks(powerupname) return { onenter=function(tt,x,y,obj) if getmetatable(obj)==Player then level.powerup=powerupname end return false end, ondraw=function(tt,x,y) return level.powerup==powerupname and 0 or tt end, } end local tilecallbacks={ [3]=arrowcallbacks, [4]=arrowcallbacks, [5]=arrowcallbacks, [6]=arrowcallbacks, [7]=spinnercallbacks(-1,-1), [8]=spinnercallbacks(1,1), [9]=spinnercallbacks(-1,1,true), [10]=spinnercallbacks(1,-1,true), [11]={ ondraw=function(tt,x,y) local offset=t%240//60 if offset>=3 then offset=1 end return tt+offset end, onenter=function(tt,x,y,obj) if not obj:onground() then return end if not obj:canswim() then obj.dying=true Splash.new(x*8,y*8) else obj:startswimming() end return false end, onleave=function(tt,x,y,obj) local tx,ty=obj:nexttile() local ttt=ttget(tx,ty) if tt~=ttt then obj:stopswimming() end end, }, [15]={onenter=function(tt,x,y,obj) if getmetatable(obj)==Player then level.wint=t+60--the man, the myth, the legend --TODO:fireworks or a jingle or w/e return false end end}, [32]=buttoncallbacks, [33]=buttoncallbacks, [34]=buttoncallbacks, [35]=buttoncallbacks, [36]=levercallbacks, [37]=levercallbacks, [38]=levercallbacks, [39]=levercallbacks, [115]={ ondraw=function(tt,x,y) local offset=t%480//120 if offset>=3 then offset=1 end return tt+offset end, onenter=function(tt,x,y,obj) if not obj:onground() then return end obj.dying=true Splash.new(x*8,y*8) return false end, }, [118]={onbump=function(tt,x,y,obj) if getmetatable(obj)==Fireball then mset(x,y,119) Ash.new(x*8,y*8) end end}, [119]={onstart=function(tt,x,y) mset(x,y,118) end}, [176]=spawncallbacks(Player), [177]=spawncallbacks(Box), [178]=powerupcallbacks("flippers"), [179]=powerupcallbacks("pepper"), [180]=powerupcallbacks("feather"), [193]=spawncallbacks(IceBlock), } local lastpush=0 local function setuppushercallbacks(topleft,color) for i=0,3 do local tnum=topleft+i*16 local dx,dy=getdelta(i) tilecallbacks[tnum]={ onenter=function(tt,x,y,obj) if obj:onground() and (not color or not level.colorsactive[color]) then obj.dir=i mset(x,y,tt+2) lastpush=t return true end return false end, } tilecallbacks[tnum+1]={ onupdate=function(tt,x,y) if t-lastpush>4 then mset(x,y,tt-1) lastpush=t end end } tilecallbacks[tnum+2]=tilecallbacks[tnum+1] end end setuppushercallbacks(112) setuppushercallbacks(48,0) setuppushercallbacks(51,1) setuppushercallbacks(54,2) setuppushercallbacks(57,3) local function callbacktiles(whichcallback) return coroutine.wrap(function() for x,y,tt in leveltiles() do local cbs=tilecallbacks[tt] if cbs and cbs[whichcallback] then coroutine.yield(x,y,tt,cbs[whichcallback]) end end end) end local function runallcallbacks(whichcallback) for x,y,tt,cb in callbacktiles(whichcallback) do cb(tt,x,y) end end function trypushmany(objs,dx,dy) local success=true for _,obj in ipairs(objs) do success=obj:trypush(dx,dy) and success end return success end function Object.new(x,y,sprite) local res={x=x,y=y,sprite=sprite,dir=0,pdir=0,speed=0,pspeed=0,born=t} setmetatable(res,Object) table.insert(level.objects,res) return res end function Object:getdelta() return getdelta(self.dir,self.speed) end function Object:getpdelta() return getdelta(self.pdir,self.pspeed) end function Object:timealive() return t-self.born end function Object:moving() return self.speed~=0 end function Object:simpledelta() if not self:moving() then return 0,0 end return getdelta(self.dir) end function Object:currtile() return self.x//8,self.y//8 end function Object:nexttile(dx,dy) local mx,my=self:currtile() local sdx,sdy=self:simpledelta() dx,dy=dx or sdx,dy or sdy return mx+dx,my+dy end function Object:move(dx,dy) if self:moving() then return false end self.dir=getdir(dx,dy) local sdx,sdy=getdelta(self.dir) if self:canmove(sdx,sdy) then self.speed=math.max(math.abs(dx),math.abs(dy)) return true else local tx,ty=self:nexttile(sdx,sdy) local tt=ttget(tx,ty) if fget(tt,0) then self:hitwall(tx,ty) end local cbs=tilecallbacks[tt] if cbs and cbs.onbump then cbs.onbump(tt,tx,ty,self) end end return false end function Object:animates() return true end function Object:draw(ox,oy) ox,oy=ox or 0,oy or 0 spr(self.sprite +self.dir*16 +(self:animates() and self:moving() and (t%16//8+1) or 0), self.x-cam.x+ox,self.y-cam.y+oy,0) end function Object:canmove(dx,dy) if self:moving() or self.pause or level.wint then return false end local tx,ty=self:nexttile(dx,dy) local tt=ttget(tx,ty) local twall=fget(tt,0) if twall then self:hitwall(tx,ty) return false end if self:onground() then local mx,my=self:currtile() local mt=ttget(mx,my) local mhalfwall=fget(mt,2) local thalfwall=fget(tt,2) if thalfwall and not mhalfwall then return false end end local cobjs=self:wouldcollide(dx,dy) if cobjs then -- return trypushmany(cobjs,dx,dy) if you want to push and move again self:trycollide(cobjs,dx,dy) return false end return true end function Object:hitwall(wx,wy) --does nothing by default end function Object:trycollide(objs,dx,dy) trypushmany(objs,dx,dy) end function Object:canswim() return false end function Object:startswimming() --change to a swimming sprite end function Object:stopswimming()--maybe it's time... --change back to normal sprite end function Object:flammable() return false end function Object:frozen() return false end function Object:onground() return true end function Object:hascollision(obj,ignorespecial) return true end function Object:wouldcollide(dx,dy) if not self:hascollision() then return false end local tx,ty=self:nexttile(dx,dy) local res=false for obj in collidingobjectsat(tx,ty,self) do res=res or {} table.insert(res,obj) end return res end function Object:trypush(dx,dy) return false end function Object:betweentiles() return self.x%8~=0 or self.y%8~=0 end function Object:canstop() if self:betweentiles() then return false end local tx,ty=self:nexttile() local twall=fget(ttget(tx,ty),0) if twall then return true end local mx,my=self:currtile() local mtt=ttget(mx,my) local cice=fget(mtt,1) or self:frozen() if cice then local cobjs=self:wouldcollide(self:simpledelta()) if cobjs then return not trypushmany(cobjs,self:simpledelta()) end return false end return true end function Object:die() local mx,my=self:currtile() local tt=ttget(mx,my) local cbs=tilecallbacks[tt] if cbs and cbs.onleave then cbs.onleave(tt,mx,my,self) end if self.onice then --stop sliding sfx sfx(-1,-1,-1,3) end end function Object:stop() local tx,ty=self:nexttile() self.speed=0 local tt=ttget(tx,ty) if fget(tt,0) then self:hitwall(tx,ty) end local cbs=tilecallbacks[tt] if cbs and cbs.onbump then cbs.onbump(tt,tx,ty,self) end end function Object:update() self.pdir,self.pspeed=self.dir,self.speed -- leaving a tile if self.x%8==0 and self.y%8==0 and self:moving() then local mx,my=self:currtile() local mtt=ttget(mx,my) local cbs=tilecallbacks[mtt] if cbs and cbs.onleave then cbs.onleave(mtt,mx,my,self) end end local wasmoving=self:moving() local dx,dy=getdelta(self.dir,self.speed) self.x=self.x+dx self.y=self.y+dy local movedbytile=false if self.x%8==0 and self.y%8==0 and wasmoving then local mx,my=self:currtile() local mtt=ttget(mx,my) local cbs=tilecallbacks[mtt] if cbs and cbs.onenter then movedbytile=cbs.onenter(mtt,mx,my,self) end end if self:moving() and self:canstop() and not movedbytile then self:stop() end if self:onground() and self.x%8==0 and self.y%8==0 then if (fget(ttget(self:currtile()),1) or self:frozen()) and self:moving() then if not self.onice then -- not happy with this sfx yet, but ugh sfx(0,-1,-1,3) self.onice=true end elseif self.onice then self.onice=nil sfx(-1,-1,-1,3) end end if self.pause then self.pause=self.pause-1 if self.pause<=0 then self.pause=nil end end end function Player.new(x,y) local res=Object.new(x,y,256) setmetatable(res,Player) return res end function Player:die() Object.die(self) tdeath=t end function Player:update() Object.update(self) if btn(5) then self.dying=true return end if self.jump then self.jump=self.jump+1 if self.jump>=15 then self.jump=nil end end local canmove=true for _,obj in ipairs(level.objects) do if obj:moving() then canmove=false break end end if canmove then if btn(0) then self:move(0,-1) end if btn(1) then self:move(0,1) end if btn(2) then self:move(-1,0) end if btn(3) then self:move(1,0) end if btnp(4,30,30) then local nx,ny=self:nexttile(getdelta(self.dir)) local ntt=ttget(nx,ny) local cbs=tilecallbacks[ntt] if cbs and cbs.oninteract then cbs.oninteract(ntt,nx,ny,self) elseif level.powerup=="pepper" and self:canmove(getdelta(self.dir)) then Fireball.new(nx*8,ny*8,self.dir) elseif level.powerup=="feather" then self.jump=0 self:move(getdelta(self.dir)) end end end local sx,sy=self.x-cam.x,self.y-cam.y if sxcam.maxx then cam:move(1,0) end if sycam.maxy then cam:move(0,1) end end function Player:draw() local oy=0 if self.jump then oy=self.jump>8 and self.jump-15 or -self.jump end Object.draw(self,0,oy) end function Player:canstop() if self.jump then if not self:betweentiles() then local tx,ty=self:nexttile() local tt=ttget(tx,ty) local twall=fget(tt,0) return twall end return false end return Object.canstop(self) end function Player:onground() return not self.jump end function Player:hascollision(obj,ignorespecial) if not ignorespecial and obj and getmetatable(obj)==Fireball then return false end return true end function Player:canswim() return level.powerup=="flippers" end function Player:startswimming() self.sprite=259 end function Player:stopswimming() self.sprite=256 end function Player:flammable() return level.powerup~="pepper" end function Box.new(x,y) local res=Object.new(x,y,320) setmetatable(res,Box) return res end function Box:trypush(dx,dy) if self:canmove(dx,dy) then self:move(dx,dy) return true end return false end function Splash.new(x,y) local res=Object.new(x,y,384) setmetatable(res,Splash) sfx(1,"B-5",30) return res end function Splash:hascollision() return false end function Splash:update() if self:timealive()>=30 then self.dying=true end end function Splash:draw() spr(self.sprite+self:timealive()//10, self.x-cam.x,self.y-cam.y,0) end function Fireball.new(x,y,dir) local res=Object.new(x,y,400) setmetatable(res,Fireball) res.dir=dir return res end function Fireball:trycollide(objs,dx,dy) Object.trycollide(self,objs,dx,dy) self.willdie=true end function Fireball:hitwall(wx,wy) self.willdie=true end function Fireball:animates() return false end function Fireball:onground() return false end function Fireball:update() if self.willdie then self.dying=true end Object.update(self) --this way fireballs render once before they move if not self.willdie then self:move(getdelta(self.dir,2)) end local mx,my=self:currtile() for cobj in collidingobjectsat(mx,my,self,true) do if cobj:flammable() then cobj.dying=true Ash.new(cobj.x,cobj.y) end end if t-self.born>40 then self.dying=true end end function Ash.new(x,y) local res=Object.new(x,y,387) setmetatable(res,Ash) sfx(1,"C-3",20) return res end Ash.update=Splash.update Ash.draw=Splash.draw Ash.hascollision=Splash.hascollision function IceBlock.new(x,y) local res=Box.new(x,y) res.sprite=323 setmetatable(res,IceBlock) return res end function IceBlock:frozen() return true end local levels={ {x1=0,y1=0,x2=29,y2=16}, {x1=30,y1=0,x2=59,y2=16}, } local defaultzero={__index=function() return 0 end} local function startlevel(num) --cleanup existing level if level then --this should implicitly free up any pressed buttons, --since dying onleaves your current tile for _,obj in ipairs(level.objects) do obj:die() end end levelnum=num level=levels[num] level.objects={} level.buttonpresses=setmetatable({},defaultzero) level.colorsactive={} level.powerup=nil tdeath=nil level.wint=nil cam.x=level.x1*8 cam.y=level.y1*8 runallcallbacks("onstart") end startlevel(1) local function update() if tdeath and t-tdeath>120 then startlevel(levelnum) end local anydying=false for _,obj in ipairs(level.objects) do obj:update() if obj.dying then anydying=true end end if anydying then local newobjects={} for _,obj in ipairs(level.objects) do if obj.dying then obj:die() else table.insert(newobjects,obj) end end level.objects=newobjects end runallcallbacks("onupdate") if level.wint and t>=level.wint and levels[levelnum+1] then startlevel(levelnum+1) end end local function remap(tt,x,y) local cbs=tilecallbacks[tt] if cbs and cbs.ondraw then return cbs.ondraw(tt,x,y) end return tt end local function draw() cls(0) map( cam.x//8,--8 pixels per tile cam.y//8, MW+1,--one extra tile relative to the amount that fit on MH+1,--the map to handle the fact that we scroll by pixels -(cam.x%8),--most of the offsetting is done by the first -(cam.y%8),--two arguments, these two do the leftover bit -1,--transparent color 1,--scale remap ) for _,obj in ipairs(level.objects) do obj:draw() end end function TIC() update() draw() t=t+1 end