0 <04m֙L0e< @UaY}eHy@m^ J!Be͛gE3# ?1PUefffof  2D43# YYYYYY♒YYYYKKKKK_Y_Y+[[KKiiiitgwiiiiiiiKKKKIKKKKKKdKIKyKkKKKKKikIKKKKKKKKݸ(؍ߍ݈ݍ؍UY!Q"UQU!RU!A!!ABADADADADDD!AA뻴뻻仛)/)ݑ)ݪҭ*"wy旙wgygvyvyfwwYIiY$Y$""I$ IDDD"BD""""" "Ј ЙID""" -BB B)"")"")""""!""!!"!!!!"!""!"ffafafffafffafffafffafqqqwqwqvqwwqwvqwvwDDD***!**B!DD!AAAؙ؉((DD!AAIaaggvv""ADAD---݁؁ݪ!!"""$"й$ $ع$" D"YEB E"ؙE"  Ќ" - "" BB B)"!)!!"""!"!"!"B"!!B"!B!D""!"fvbf""bfffbffwbw""!"ffbfwwrwqqqvawqvgqggqwvwqwgwDDDDDDKKKKKKDKDKDDDTUY))""))Y%Y"")))!"""-ID""""-"ѻID"""(""!HKD"!""!(!!HKD  @ Д I""I$"D"IDYDIU$- """""""$$"DDDB -B""ITII؍݈+ع؍ݙ""""DBDBDKKKKKDqvvvwvg$qfwgw$qvKw$qDqwgwaggwqvgwqggvqvvgqwgwqwvvqwvg***!**KDKK-!KD-!DADKDIB!DDAK!KDD!!ABADADADADR!YQ垙^YU.UЀ H  IЙD DЍ   ݽAAOKAO$DݸDDݸKB^qFD-(ݒ!+++++-++++-+-+++INNIN!UQ!AUY!Q"UQU!RU!AYQQUU!UUUUQU"UYUYUUUUUQU"YUUU%Q!!KK$DDKDKKDKDDDDIDDDD_DD*DDDKDI"D$DDDDDDKK鴻랻鞻KK龴NfiiiiiwwiyGfigivwywGwwwtwGwwwwfiiiyiyywiIIIK뻴뛻乛UQ"/!!"!!KKAKA$BA""!!A^!UUQUU!A^!UUQUU!A)")BDIDDIDDDDIIDDDDDUIDDEDDODODHDKDDIDDDDI䞙랻K龻黻fywwwGywwiwwytiwGKKKKGywtiwwyiitwGiwwGiADDKKDKKKDKDKKKDDDDDINRUN$"DD$IDDDDDIDDyQua陙a'ݝ YyuwWWyuWyIDDDDIAAAAA_Q_QfwywiiiiiwwtGGwwwtwywyiiiitgwiiiiiiiKDADDADAD!"""DDADDDADD("""DD!"aV".gfYUQU"A"AIY%UYUD!AAA!!ABADADAD@Dwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww33333333333333333333333333333333)()!2<뻴뻻任UQQURUU"!AAYU%NDDI̔\96nvflji9?",)""""""""""""""""""""""""""""""""̻)"*"NI鞙䛙D陙鞙fiiiiiii뾾wwwwwwww~ww~ww~ww~wwwwww}ww}}}ww}}}}}ݻݻۻۻۻ۽ۻݻff"~w'q'\1cl60-"!!!!)fn枙niin鞙iifn枙閙iiiiwywyi陖i鞙iiݍ("""!!؈("""!!DKDDDDDDKDKKKKIIwwwywwwwyyywywwywwyyywyffiffffiiiffifiifiififiifn鞙fnififiii""""iiiini잙枖i鞙iiΞni枙ifiinΞii ) """ """ """  "wpwgwwgxw gb` 3#33̼"Ȉ񏐙 """  " """  " /  "  " """ """ ppww """ """p'rpww """ """ /wwxg"bb Ѝ/򏐙  "  "aaaa`dfffiiiifififf~ffpffpgvww ww̓x8  ff~ffpfvpgw7ww '"ff~ffpgfswv}w w "r8ff~wq\ =̓8  ff~wq\3 #=8ff~wq\30-20ffpwwpqwws݃ȃ"ffpwwpqwww\1cl6=ff~wq\1cl60Ӎ( ff~ffpffsgv7|wwww x  ff~ffpfvsgw7ẁw '"ff~ffpgfswv7}ww "rff~wq\00=  ff~wq\00= ff~wq\00= ffpwwpqww3<Ӎ"!ffpwwpqww|..>""""""@ "@ BC "$B""&'(PP""""@ RS"34R""678PP@ @ @ ""@ "rsc'E@ !@A"""@ "BCD@'''E12""@ @ ""BCD"@ RSTdc  $'''''(@A""PQPPP@ "RSTQQQ""mnQd@ c34'579'(@ @ QBCP"BCDQQQQQQ@ }~cc @ 5869E""BCDPPPRSPQPPQ"@ RSTQrsefVW'c   (P&'E"@ RSTcPPQQQ@ QQcefVW'' BCDBCEF'''''''''EBCrs @ QefVW'''cRSTS''EF'''''''''''''ERS QQQQF''''''''''''''''''']]]]]]]]]]]]]]]]bc @AAAAAAAAA rsBCDF''''''579''''''''HI']]]]]]]@ cbc#"rs"##"""RSTF''577778&''''''''''']]]]M]]efVW"##"@ BCD""#"QF''58&''''''''''']M]]]]]!efVW""""RST"#$"###!""BCDF'''(####677779'''''']]]]]@A @ #12#efVW@ ####""@ BCD34###12@ "RSTF'''''(#####69''''']]M]]"##efVWKb"""&''("RST"#&'577778###&''''']]]]]"@A efVW@ @ @ "677"QcQQ####P"F'(######cc#&''''']]]]]"efVW"@ """"""#QQ####@ " &''E#####c#c#####69'''']]]M]"BCDrsefVW""@ "@ ""QQ@ &'''(#cc#####&'''']M]]]"@ RSTefVW""BCD@ """$@ mn 69''(###F'''']]]]]"efVW""RST&J("rs" 34"}~69'E#####&''''''''']]]]]")+,bdSgg""QQ@ F'(@ "@ ""rsefV&''E####&'''''5779]]]M]"*:;"""&''58"" #@ "efV&''''''J(F'''578&]M]]]">>rsrs@ rs"6778@ "@ BCD@ efVQrs69''''''(F''J'(#&]]]]]@ eppppW"@ @ RSTQQefV&5779''EF'''''''''EF]]]]]]]]]]]]@ eppppW@ QcefV&(&'''''''''''''''EF'BCRSCC&'(BC<<rs@ BCAA mnrs@ @ rsrsc S&'ERS*:;<"@ RS]]]]]]]]]]]]QQQQ}~@ @ Q@ @ @ @ c&''(BCD*::--::;BCBCD""QQ]]]]]]]]]]]]QQQQQ""rsQ"@ Q@ @ C&HIERST-------RScRST"P P "]]]]]]]]]]]]@ cBCDcQQ""Q@A @ Q"@ @ "c69'58rs".>....>."BC""rs]]]]]]]]]]@ "rsRSTQQ"@A "Q"!@ Q"@ "@ ac&'("##"RScQ&'(Q*:;QQ""]]]]]]]]]@ Q"P"@A 12$"Q"@ @ "@ C&58""QQQQ678Q>>QQ"P@ ]]]]]]]]]" QQQBCD""P"34"Q@ #"rs$@Sc&(@ @ ""]]]]]]]]]""@ QQQRST""P"Q@ ""34c&("##"@ @ ]]]]]]]]]""!"QQQQQBCQ@ "P ""Q #""@F("" @  @ ]]BCD]]]]]]]]]"12"QQQQRSQ"@A  ""Q!@ @ @ mnrs&'("##"rs]]]]]"]]RST]]]]]]]]]@ @ QQQQQ"@ Q12"""}~&'(@ @ ]]]]]@ @ @ ]QQ]]]]]]]]]]@ rsQ@ "QQ @ "69(""QQQQ]]]]]]]]]""]]]]]]]]]]]]]]]]]]]]QQ"@A QBCrs""BCD&E"##"QQQQQQ]]]]]]]]]@ ]]]]]]]]]]]]]]]]]]]]BCDQ@ @ QQQRSBCD"@ RSTBC69(@ QQQQQc]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]RSTc@A rsQQQQQQRST@ cRSc&EQc]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]QQQQQQQQcc&'EbcBCRS]]]]]]]RSBCBCBCDRSBCDQQ--Qcrs]]]]]]pppWccRSRST@ QQQcRSTQQQQ--<QQQQQBC]]]]]pppWrscccQcQcQQrsQQQcQQQc*::;QQRQcRS]]]]]pppWcrs@ mnQQQQQQQ>>cQQQQ]]]]]]]pppWcQ@ }~@ QcQQQcQ@ "QQQQQ@ @ ]]]]]]]pppW##QQ@ @ "QQQQQQQQQcQ@ @ QQQQQ"@ "]]]]rspppW###@ #!!@ QQBCDVWefVW" $QQQc""mnQQQBCD]]]]pppW#12 12"RSTVWefVW"34!KQQ""}~@ cRST""]]]]pppW"Q"VWefVW"12""eppc@"]]]]]]]pppWQ"rsVWefVWQQ@ @ eppcc@ ]]]]]]pppW@ BCDQ"@ mnVWefVW@ QQQQQ@ @ QeppcBCD@ ]rs]]]pppWrsRST"@ @}~VWefVWBCDQQQQBCD@ QeppRSTBC]]]]pppWBCrsBCcrs@ @ QgggQRSTcQQRSTrseppcRS]]]]pppWBCDRSRS@ QQQQ@ QQQQQQQQQQ@ rsc]]]]]]]pppWRSTccBCDQQQQQQQQQccc]]]]]]]pppWQQRST&'(cpppWQQQc678BCBCDppppppppppBCrsBCrsRSBCRST"ppppRSRCS@ BCcRSrsQQQQ"pp++pprsQQPcPS@ @ Sc@ "@A++<ppQQQPdScS""c@ @ @ ++< QBBC@ @  "@ rsS]"++<@ QRRSBCD@ @ """@A]"*::;"]QQPcRST""rsrs"@  !""###]"=-"]QBC$""@ $ 12"####$QdS]">>"]QRS34"""34 ""34rs]""]QQQQrs"@ @ SmnBCD""a###]######"]QQ@ ""}~RST@ @AQQQ@ @ cc@@ @ @ ""'''''(@ @@rsBCc""'''''(BC@ rsF'''''RS'HI''EdBCDmnRS&''''''c''''''JEPcRST}~BCDc&''''''&'''''''''''''''''''''('''''''''RST'bbdSbcBCrsRSBC HIKMNOOOOOOOOOOOOOOOOOOOOOOOOO/.-*+ * * ************-./////////++ *0* * *.///////////////////////    ((((((''(((((((((((((((((''''' ;<>;::???????>>??>>>???>>?????5:>>;<=>????????????????>>????=<<<;:::::::::::::::::::::::::   <;9988888889:;=>>>>>>>>???????  @@@@@@@@@@@@@@@@@@@ @ @ @ @ @ @ @ @ @ @ @              ! " # $ % & '  #p"P!0!              4`d` d`` d`` d``````$`` $`` $`` $````$`$``$````````````````````````````````AAAA!AAATTATTaATTTaTacccccccccccccc  cccccccCcccccccccccbb bbb$$$$$$$$$D$D$DDDbbBbbbb`cCCCCCCCCcCCCcCCCCCcccCCbCCCCCC#CCCCCCC#CC##coj|x8L8qcBBBBbbb ````````````````````@@@@````@@@@````````````````ATTATTATTTT FfffFfffFfffFfFFFFFFFFFFFFFFFFFF@@@@@@@@@@@@@@@@````````````````eeeeeeeeeeeeeeeeeeeeeFfffFfffFFFFFFFFFFFFFFFFFFFFF@@@@@@@@@@@@@@@@`````````````````` " @4 4 4 u 4 uuP$00?*"""B$DD"BDDD4$DDBD$$DDDBJDDB$$$D"BDD4DDDDDDDB$DCDJ4>KDDDDDDDDD4ή$CDDDDDBDDD4K>KDDDKDKKDDC3쾻$(K>CCD3CDCDDDDDBDϬ3;;3;3;:K+BCCD334CDKDDBDʪ3333K3CC43443BDDDD$D333;;:DK4D4CC43DDDDDD3̳33>3;333̯KD4443C4CDDDDDDD4̯̯:ʪ:̳3;3>ïKDDDDDCDDDDDDKDDD4Ϫ3;33<;3;3:DDDDDDDDDDDCDDDDDD44ʪ:3>;3<<;3>>KDDDDDDDDDD3DDDDDDD:ʪ:̯3;<33:DDDDKKDDDD4D3CBDD@D4Ϫ::̪:ϳ3<33<3CDD$BDDDDDDC34"$DBD4::̬3;̼33<3DDD@DDDDD433BB3DDDDD<<̪:Ϫ>3;<;3ïJDDDD+$DBDDD4D<33;<;3ì::CCDDDDBDBD34LD$"BDDD43:<<<<ʬ:<3333μ3;Ϫ:*DKD$ " D33.KK+"̯(ϯ(""("NDDD$",̯,"Š"4JCBDD2333#DD4DDD:::3:+KKD"(8Ϭ"("("Ϗ;DDDD"/ϯ2,":;CCCDDBDD33<3#DCDDD::>:KB;33#""""̯(""ς*""(":*283DDDD$"#,"¬*"Š"3CCCDKDDD3<>+">LCDDD+$"D4D$BDD$BDKDD$$B"("B("B88B. LBDDDD$@DHDD DDK$"DDDC"""""""("""뾾+"LBDKDDD4C"DDDBDDDDD<""("*""";$DK$."C@DDDDBBHKDDDDDDB@KKD<"""ʏ(ʬ""̊"BKDDBDD+ ,DDDDD$DD844DD$BDDDDD<$""(" "¯"+DDBD$.">,DDKDD$D4DHHDDD$+BDDDD""¯""(ʯ""(""DD43DK"DDDDDDDH34DDDDDBDDDDC"""̏H""̊ʯ""DD43D"BKHDK(D""4BK2D2+DCDD$N""""D4(̲뾻;>>KKBB>N"((""L̏"4㾻KKK뾾BBN""""C(("ʄ뾻K;4K>KK뾾+"+"""*"""̌"(3C;K뾻KKKKKKB뾾+(""̬(ϊ"";DKKKKKKK>3뾻#""Ϫ""""Ϭ("";;;;KKKKK>>:*(""(̌"";KKKKKKK;KKKDK뾻룪#(""""""ʪ(""뾻>4KKK볾뾾뾾뻾뾾뻻33("""("""""(""CKKKKKK뾻뾾("""((("(""K;KKKKKKDKKNDK뾻"(""""""""""""(""""KKDKKKKKK뻻뾻"""""("""""""""""""""""BKKKKKD뻴K뾻KKDDKK$"""""""((""""""""""""""(("""""BDKKDKKKKKKKKKKKNK뻻DKDDKK+"""""""""""""""""""""""""""""""""BDDDDKKKKDDDDDDDDDDKDDDDKDDK$""""""""""""""""""""""""""""""""""DKDDKKKKKKKKKDDKKKDDDKKKDKDKK+""""""""""""""" """""""""""""""""DDDKDDDDKDDDDKKKDDDKDDD+""""""""""""""""""""""""""""""""""BDDDKKKK$DDDDKDDKKDD뻻DDDKK$("""" """""""""""""""""""""""""""""BDDDKKKKDDDKKDKDKDDDKKDDKDKDD뻻+("""" """""""""""""""""""" """""BDDDKK+DKDDDDKDKDKKKDKKDKDKKD$""""""""""""""""""""" """"""""""BDDDKDKKDDDDDDDDKDDDKKDDDD뻻$""""B """""""""""""" "" """""""DDDKKKDDKKDDKKDDDDDDDDD""""""D""""" """""""""" """ """"""DDDKDDDDKDDDDDDDDKKKDKDDKDDDK""""""K""""""""""""""""""""""""""BDDDDKDDDKKKKKDDKDDKKDKDKKDD""""""""""""""""""""""""""""""""BDDDKKDKDDDKDKDDDDDKDDDDDDDDKKDK""""" $""""""""""""""""""""""""""""BDDDDKDDDDKKDKDKDDDDDKDDDKDD+""""""$"""""""""""""""""""  """"""BDDDKKDDDKKKDDDDDDDDDDDKKDKNKKDDD""""""+"""""""""""""""""""""""""""($DDKKDDKKDKKDDKKDDDDDKDKDDDDBD$""""""K$""""""""""""""""""B" """""D$D+KDKKDDDDDDDKKDDDDDDDDDKKKDDKKDB@DK"""""B+"""""""""""""""""""B$"""""""$DDBDKDDKDDDDDDDDDDDDKDDKDKKDDD$""""""""""""""""""""""""" DD"""""""DDBDDK+DD$$D"DDDKKKKDKDKDKDDDDDDDDDKDKDDDDD$"""" B+""""""""""""""""""""DD""""(HD$DDDBD$$DDDKKDDDDKDKDKDKDDKKDDDDKDDDD"("""""""""""""""""""""""""@DD$"""""CDHD@DDDD$DDKKDDDDDDDDDKDKKDDKDDDD(""""B""""""""""""""""""""""DD"""""JD"D$DBDDDB@BDDKDDKKDDDKKDDDDDDDDDDKDKDDDDDDB(""""" """"""""""""""""""""""$D$"""""(HDDDD$BDDD$DDDDDDDDDDKKKDDDDDDDDKKDDKKDDDDDDD$"""""" """""""""""""""""""""BD$"""""(HDDKDD$B@"DDDDDDDDDDDKKKDDDDDDDDKKKDDDDKDD$DD$"""""" """"""""""""""""""""""" DD """"(HDDDD$DD@DDDDDKKKKDDKKDDDDDDDDDDDDKKDDDKDDKDBD$"(""""""""""""""""""""""""""""DD$"""""(DDDDDB@DBDDDKKKDDDKKDDDDDD"$DDDDKDDKDDKDDDBB$"""""" """"""""""""""""""""""""BD"""""((DDKDKDDDDD@BDDDDKKDDDDDDDDDDBDDKKKDKDDDDDDD"$D (""""""""""""""""""""""""""""BD""""""(B$DKDDBB$$D$DD$DDDDDDDDDDDDDDDDDDKDKDDDDDDDDDDDB"""""" """"""""""""""""""""""""BD"""""(HDDD$DDBBBB$BBBDDKKDDDKDDD$D$DDKKDKDDDDDKDDDD$("""""*"""""""""""""""""""""""BD"""""(H$DDKDBDD$$DBDBDDDDDDDDDDDD$DDDDKDDDD$DDDDDDDDDD"""""Ï"""""""""""""""""""""""BD$"""""(HDDD$DBDBD@$$DDDDDDDDDDDDDDKDD$DBBBDDKDDDDD$"""""""̬"""""""""""""""""""""""BDD""""""H$DDDDBD$D$DDBBDDDDDDDDDDDDKD$$DBDDDD$DDD$"""""2"*"""""""""""""""""""""HDD""""""HBBDDDD$D$DBBDD$$DDDDDDDDDDKKDDDDB$BDDDD$BDD"""""B̯"""""""""""""""""""""D"""""""HDBD$$D@B"D$$BD@DDDDKKKKDDDBDDDDDKDD$DD$""""2<"̬("""""""""""""""""""DD""""""KB$DDDDBD$DDBDDD"DDKK䴻KDDDD$DDDDDKDDDBDD$"""B4#"""""""""""""""""""CBB$"""""HDDBDDDDD$D$B"BD$BDKDDKDDDDDDDKDKDDD$D$"""4L2("""""""""""""""""̃DD"""""CBBBDDDBDDD"D$D$DDDKKKKDDDDDDDDDDDDDDKDDDB""""B3<"̌ """""""""""""""$$$$"""""H$$DD$$D$$$$B$D$$BDKDKDDDDKDDDDDDDKDDD"D$"""3<"<<"""""""""""""""DB""""""DD$$BDBDB$DBD@BBBDDKDD$DDDDDDKDDK$D$""""43B3$ """""""""""""$DD"""""HBDD$$DD"$BDBD$$$DD$DDDDDBDDKDDDDDDDKDD$D"""<<," """""""""":"$$$"""""D$$"$$B@DB$D$$$DBDBD뻻KKDDDDB$BDDDDDDDDDDDDDDB"(""B3#B3L"""" """"""""B̃DBD$""""""DDBBBD@$DBB$$$D"$DKDD$"DBD"DDDDDDDDDDDD$DD"""B83L"<3""""" "" ""2 px do local l, r = textwid(string.sub(str, 1, pos)), textwid(string.sub(str, 1, pos + 1)) if l <= px then if r >= px then buf[#buf + 1] = string.sub(str, 1, pos) str = string.sub(str, pos + 1) pos = est else pos = pos + 1 end else pos = pos - 1 end end if #str > 0 then buf[#buf + 1] = str end return buf end -- prints centered text with a shadow local function title(t, y, fg, bg) local x = math.floor(120 - 0.5 * print(t, 0, -8)) return text(t, x, y, fg, bg) end -- linear transformation, takes 3 or 5 args local function lc(t, t1, t2, s1, s2) if s1 == nil then t1, t2, s1, s2 = 0, t1, 0, t2 end return s1 + (s2 - s1) * (t - t1) / (t2 - t1) end -- if function, call local function call(f, ...) if type(f) == "function" then return f(...) end end -- multi-table loop macro; preserves table order -- WARN key variable is the index in the acording table, **not the loop counter** local function _ialliter(tt, i) if not tt[1] then return nil end local v = tt[1][i + 1] if v == nil then table.remove(tt, 1) -- ok to destr, it's {...} return _ialliter(tt, 0) else return i + 1, v, tt[1] end end local function iall(...) return _ialliter, {...}, 0 end -- OOP helpers local function extend(t, Type) if Type then setmetatable(t, Type) t.Type = t t.Parent = Type end t.__index = t return t end local function new(Type, ...) local t = {} setmetatable(t, Type) call(t.constructor, t, ...) return t end -- sugar local Object = extend({}) function Object:constructor(...) call(self.Parent and self.Parent.constructor, self, ...) end function Object:with(vals) for k, v in pairs(vals) do self[k] = v end return self end function Object:is(T) return self.Type == T end Object.new = new function Object:extend(t) return extend(t, self) end local function _enumIter(e) return function(_, i) local l = e[i + 1] if l then return i + 1, l end end, nil, -1 end local function enum(vals) local t = {} for i, v in ipairs(vals) do t[i-1] = v t[v] = i - 1 end t.iter = _enumIter return t end -- memory layout local MEM_PALETTE = 0x03FC0 -- Button codes local K = enum{"UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y"} -- WARN this check uses enum order function K.isDir(d) return d >= K.UP and d <= K.RIGHT end -- Direction util local D = enum{"UP", "DOWN", "LEFT", "RIGHT", "STOP", "GO"} function D.fromKey(k) return k end -- Get direction from deltas function D.ir (dx, dy) if dx == 0 then if dy > 0 then return D.DOWN end if dy < 0 then return D.UP end elseif dy == 0 then if dx > 0 then return D.RIGHT end if dx < 0 then return D.LEFT end else return nil end end -- Get deltas from direction function D.eltas(dir, scale) scale = scale or 1 if dir == D.UP then return 0, -scale elseif dir == D.DOWN then return 0, scale elseif dir == D.LEFT then return -scale, 0 elseif dir == D.RIGHT then return scale, 0 else return 0, 0 end end -- Opposite direction -- WARN hacky. Uses enum order function D.reverse (dir) return dir + 1 - (dir % 2) * 2 end local function xyToMap(x, y) return x // 8, y // 8 end --local function rectsOverlap(r1, r2) -- rect = {x y w h} -- -- F.IXME temp fix -- if r1.h2 then r1.h, r1.w = 2 * r1.h2, 2 * r1.w2 end -- if r2.h2 then r2.h, r2.w = 2 * r2.h2, 2 * r2.w2 end -- return (r2.x >= r1.x + r1.w) ~= (r2.x + r2.w >= r1.x) -- and (r2.y >= r1.y + r1.h) ~= (r2.y + r2.h >= r1.y) --end --[[ CLASSES ]]-- local Game, Clock, GameState, MapState, MenuState, ContinueState, DialogState, TransitionState, Piece, Player, Guard, Bucket, Bed, Plant, Door, Wall local game, Levels -- instances do -- main class Game = Object:extend{ player=nil, t=0, conKeys={}, checkedKeys={}, lastdbtn=-1, palette={}, nowplaying=-1, showGuardSight=false, -- DEBUG -- musicOn=true, stepByStep=false, debugRects={} } -- save cart palette into a variable for i = 0, 47 do Game.palette[i + 1] = peek(MEM_PALETTE + i) end function Game:constructor(initState) self.state = self:create(initState or GameState) end function Game:pollDBtn() if not btn(self.lastdbtn) then self.lastdbtn = -1 end for b in K:iter() do if K.isDir(b) and (btnp(b) or (self.lastdbtn == -1 and btn(b))) then self.lastdbtn = b end end end function Game:tint(f) local tr = f if type(f) == "number" then f = {f, f, f} end if type(f) == "table" then tr = function(r, g, b) return r * f[1], g * f[2], b * f[3] end end for col = 0,15 do local addr, pal = MEM_PALETTE + col * 3, self.palette local r, g, b = pal[col * 3 + 1], pal[col * 3 + 2], pal[col * 3 + 3] r, g, b = tr(r, g, b, col) poke(addr, r) poke(addr + 1, g) poke(addr + 2, b) end end function Game:loop() -- if self.stepByStep and not (btnp(K.Y) or (btn(K.Y) and btn(K.X))) then return end self.t = self.t + 1 self:pollDBtn() self:updateKeys() self.state:update() if self.state.nextState then local prev = self.state self.state = prev.nextState prev.nextState = nil end call(self.state.draw, self.state) end function Game:saveProgress(n) if n > self.progress and n <= #self.levels then self.progress = n pmem(0, self.progress) end end function Game:music(track) if not self.musicOn then return end if track ~= self.nowplaying then music(track) self.nowplaying=track end end -- factory function Game:create(Type, ...) return Type:new(self, ...) end -- Keys are things for which the game can tell whether they were checked in on the last tick -- and for how many ticks they've been checked in. They represent the duration of ongoing ontinuous process -- such as frame-by-frame animation or tween. Any unique item can be a key, e.g. an empty table -- frames start from 1; it means unchecked animations are at 1st frame -- very convenient to scale them with lc() from above function Game:continuous(key) if not key then return 1 end return self.conKeys[key] or 1 end function Game:checkIn(key) if not key then return 1 end self.checkedKeys[key] = true return self:continuous(key) end function Game:checkOut(key) local was = self.conKeys[key] or 1 self.checkedKeys[key] = nil self.conKeys[key] = nil return was end function Game:updateKeys() local conKeys, checkedKeys = self.conKeys, self.checkedKeys for key in pairs(conKeys) do if checkedKeys[key] then conKeys[key] = conKeys[key] + 1 checkedKeys[key] = nil else conKeys[key] = nil end end for key in pairs(checkedKeys) do conKeys[key] = 2 checkedKeys[key] = nil end end end do -- game state base GameState = Object:extend{} function GameState:constructor(game) self.game = game self.ttable = {} self.t = 0 end function GameState:update(game) self.t = self.t + 1 self:updateTimers() end -- a function can only have one timer at time function GameState:setTimer(clicks, cb) self.ttable[cb] = self.t + clicks end function GameState:setRepeat(clicks, cb) local loop loop = function () self:setTimer(clicks, function () cb() loop() end) end loop() end function GameState:dropTimers() self.ttable = {} end function GameState:updateTimers() local calls = {} for cb, time in pairs(self.ttable) do if time == self.t then self.ttable[cb] = nil table.insert(calls, cb) end end for _, cb in ipairs(calls) do cb() end end end do -- map state MapState = GameState:extend{ mapX=0, mapY=0, level=-1, wallpiece=nil, cloudsCount=24, clouds={}, -- WARN reuse plants=nil, guards=nil, doors=nil, buckets=nil, beds=nil, remv=nil, player=nil } function MapState:constructor(game, level) self.Parent.constructor(self, game) local lv = game.levels[level] game:saveProgress(level) self.player = game:create(Player):with(lv.p) self.level = level self.beds = {} self.plants = {} self.buckets = {} self.guards = {} self.doors = {} self.remv = {} if #self.clouds ~= self.cloudsCount then self.clouds = {} end -- TODO reduce, reuse, recycle if #self.clouds == 0 then self:seedClouds() end -- reuse this wall for all pieceAt checks self.wallpiece = game:create(Wall) for x=0,30 do for y=0,17 do mset(x, y, mget(x + 30 * lv.mx, y + 17 * lv.my)) end end for _, d in ipairs(lv.bd) do table.insert(self.beds, game:create(Bed):with(d)) end for _, d in ipairs(lv.bu) do table.insert(self.buckets, game:create(Bucket):with(d)) end for _, d in ipairs(lv.gu) do table.insert(self.guards, game:create(Guard):with(d)) end for _, d in ipairs(lv.dr) do table.insert(self.doors, game:create(Door):with(d)) end self:setTimer(1, function() game:music(0) end) if #lv.int > 0 then self.nextState=game:create(DialogState):with{lines=lv.int, returnState=self} end end local _cloudsz, _cloudvx, _cloudvy = 16, -0.03, 0 function MapState:seedClouds() for i=1,self.cloudsCount do table.insert(self.clouds, { math.random(1 - _cloudsz, 240 + _cloudsz), math.random(1 - _cloudsz, 130 + _cloudsz)}) end end function MapState:updateClouds() for _, c in ipairs(self.clouds) do c[1] = c[1] + _cloudvx c[2] = c[2] + _cloudvy if c[1] < -_cloudsz then c[1] = 240 + _cloudsz end if c[1] > 240 + _cloudsz then c[1] = -_cloudsz end if c[2] < -_cloudsz then c[2] = 138 + _cloudsz end if c[2] > 138 + _cloudsz then c[2] = -_cloudsz end end end function MapState:reset() self.guards={} sfx(63, "G#4", 50, 3, 14) self.nextState = self.game:create(TransitionState):with{returnState=game:create(MapState, self.level)} end function MapState:pieceAt(x, y) if self:isWall(x, y) then return self.wallpiece:with{x=x,y=y} end -- i == table index, **~=** loop counter @see iall for i, p in iall({self.player}, self.buckets, self.guards, self.plants, self.doors) do if not p.seethrough and x == p.x and y == p.y then return p, i end end -- stops player from pushing a bucket onto guards -- TODO how bad will be just checking it for every piece in the loop above? for i, gu in ipairs(self.guards) do if gu.isWalking and not gu:isPastHalfTile() then local cx, cy = gu:sight() if x == cx and y == cy then return gu, i end end end end function MapState:isWall(x, y) local s = mget(x,y) return s >= 16 and s < 160 end local function victorysfx() sfx(62, "E-3", 100, 1, 10, -1) sfx(62, "E-5", 100, 2, 10, -1) sfx(62, "E-6", 100, 3, 10, -1) end function MapState:guardSees(gu) for s=0,29 do local pc = self:pieceAt(gu:sight(s)) if pc and not pc.seethrough and pc ~= gu then return pc end end end function MapState:update() self.Parent.update(self) local p = self.player if btnp(K.B) then self:reset() end if btnp(K.A) then self.game.showGuardSight = not self.game.showGuardSight end for _, pc in iall(self.buckets, self.doors, self.guards, self.plants, {p}) do pc:update(self) end self:updateClouds() for bdi = #self.beds, 1, -1 do local bd = self.beds[bdi] local pc, pci = self:pieceAt(bd:gridCoords()) if pc and pc:is(Bucket) and not pc.isWalking then table.insert(self.plants, game:create(Plant):with{x=bd.x, y=bd.y}) table.remove(self.buckets, pci) table.remove(self.beds, bdi) end end if #self.beds == 0 then -- check in key => player animation starts if self.player.celebAni:checkIn() == 1 then victorysfx() end if self:goalComplete() then self.nextState = self.game:create(DialogState):with{ lines={randpick(strings.levelComplete)}, returnState=game:create(TransitionState):with{ returnState=self.game:create(MapState, self.level + 1) } } end end for _, gu in ipairs(self.guards) do if gu.isWalking and not gu:isPastHalfTile() then local pc = self:pieceAt(gu:sight()) if pc and pc:is(Bucket) then local flew = self:kick(gu, pc) if not flew then gu:turnBack() end elseif pc and pc ~= gu and pc ~=self.player and not pc.tranparent then gu:turnBack() call(pc.turnBack, pc) end end if self:guardSees(gu) == p then p.isCaught = true gu.isChasing = true if not p.isWalking then p.dir = D.reverse(gu.dir) end if self:pieceAt(gu:gridCoords()) == p then gu.isWalking = false self:discovered(gu) end return end end if not p.isWalking then self:controlPlayer(p) end if self.level == #Levels then self.player.celebAni:checkIn() end end function MapState:controlPlayer(p) local press = self.game.lastdbtn p.isPushing = false if K.isDir(press) and not p.isCaught then p.dir = D.fromKey(press) -- change facing even when can't move local infront = self:pieceAt(p:sight()) if infront then if infront:is(Bucket) and self:kick(p, infront) then p:move(p.dir) elseif infront:is(Guard) then p.isCaught = true self:discovered(infront) end else p:move(p.dir) end end end function MapState:kick(who, bu) local dest = self:pieceAt(who:sight(2)) if dest and not dest:is(Bed) and not dest:is(Player)then return false end bu.v = who.v who.isPushing = true bu:move(who.dir) return true end function MapState:discovered(guard) self:setTimer(1, function () -- fires immediately upon return self:reset() end) self.nextState = self.game:create(DialogState):with{lines=guard.dialog, returnState=self} end function MapState:goalComplete() if #self.beds > 0 then return false end for _, pt in ipairs(self.plants) do if not pt.haveGrown then return false end end return true end local _water = 39 function MapState:draw() cls(13) for _, c in ipairs(self.clouds) do spr(480, c[1], c[2], 0, 1, 0, 0, 2, 2) end map(0, 0, 30, 17, 0, 0, 0, 1, function(tl, x, y) if tl == _water then local t = self.t / 10 local f = (x + y + t) % 10 > 5 local r = (x - y + t) % 10 > 5 return tl, f and 1 or 0, r and 3 or 0 end return tl end) if self.game.showGuardSight then for _, gu in ipairs(self.guards) do self:drawSight(gu) end end for _, pc in iall(self.beds, self.doors, {self.player}, self.buckets, self.plants, self.guards) do pc:draw() end end function MapState:drawSight(gu) for s=1,29 do local x, y = gu:sight(s) local pc = self:pieceAt(x, y) if pc and not pc.seethrough and pc ~= gu then return end spr(505, 8 * x, 8 * y, 0) end end end do -- menu state MenuState = GameState:extend{ spr=353, w2=8, h=10, bed = nil, seq = nil, current = nil, growSpeed = 10, scrollSpeed = 3, growAni = nil, pattern={}, patternSize=12, variance={12, 50} } function MenuState:constructor(game) self.Parent.constructor(self, game) local seq = {} self.bed = {} self.seq = {} self.growAni = game:create(Clock) -- generate for y = 1, self.h do self.bed[y] = {} for x = 1, 2 * self.w2 do self.bed[y][x] = {0,x,y} seq[#seq + 1] = self.bed[y][x] end end -- shuffle for i = 1, #seq do self.seq[i] = table.remove(seq, math.random(#seq)) end self.current = table.remove(self.seq) for i=1,self.patternSize do table.insert(self.pattern, { math.random(0,240), math.random(0,136), math.random(table.unpack(self.variance))}) end end function MenuState:update() local t = self.growAni:checkIn() self.current[1] = t // self.growSpeed if self.current[1] == 4 then -- empty table == screen not affected self.growAni:checkOut() self.current = table.remove(self.seq) or {} end for b in K:iter() do if btnp(b) then sfx(5, "E-5", 16, 2, 8) self.nextState = self.game:create(ContinueState):with{ skip=(self.game.progress == 1) } end end end local shadow = function(x) return x - 16 end local norm = math.sin(0.6 * math.pi) function MenuState:draw() local l, r local t, sv = self.game.t, self.scrollSpeed local slide = math.sin(0.6 * math.pi * math.min(30, t) / 30) / norm map(0,18,15,10,0,t//sv%16 - 16,0,2) for y=1,self.h do for x=1,self.w2 do l, r = self.bed[y][x][1], self.bed[y][self.w2 + x][1] if l > 0 then spr(self.spr + l, (x - 1) * 16 - 8, (t // sv + (y - 1) * 16) % 160 - 16, 0, 2) end if r > 0 then spr(self.spr + r, 240 - (x - 1) * 16 - 8, (t // sv + (y - 1) * 16) % 160 - 16, 0, 2) end end end --frame -- clip(16 * self.w2 - 3, 0, 240 - 32 * self.w2 + 6, 136) -- cls(5) for _, c in ipairs(self.pattern) do local x, y, r = table.unpack(c) circ(x, y, r, 7) end -- clip() -- rect(16 * self.w2 - 3, 0, 240 - 32 * self.w2 + 6, 136, 1) -- rect(16 * self.w2 - 2, 0, 240 - 32 * self.w2 + 4, 136, 4) -- title for d in D:iter() do local x, y = D.eltas(d) map(0,28,10,2,lc(slide,0,1,240,40)+x,22+y,0,2,shadow) map(0,30,5,2,lc(slide,0,1,-80,96)+x,42+y,0,2,shadow) end map(0,28,10,2,lc(slide,0,1,240,40),22,0,2) map(0,30,5,2,lc(slide,0,1,-80,96),42,0,2) title(strings.greeting, lc(slide, 0, 1, 154, 96) + t%60//30, 15 - t%60//30, 1) end end do -- level selector ContinueState = GameState:extend{ selected=1, skip=false, transAni=nil -- ok to reuse } function ContinueState:constructor(game) self.Parent.constructor(self, game) self.transAni = self.transAni or game:create(Clock) self.selected = game.progress end function ContinueState:update(game) -- DEBUG local sel = self.selected if btnp(K.UP) or btnp(K.B) then self.nextState = self.game.menu elseif btnp(K.A) then if sel <= self.game.progress then sfx(5, "E-5", 16, 2, 8) self.skip=true else sfx(5, "D#3", 16, 2, 8) end elseif btnp(K.LEFT) and sel > 1 then self.selected = sel - 1 elseif btnp(K.RIGHT) and sel < #self.game.levels then self.selected = sel + 1 end if self.skip then self.nextState = self.game:create(TransitionState):with{ returnState=self.game:create(MapState, sel) } end if self.transAni:checkIn() == 0 then sfx(2, "E-8", 4, 3, 5) end if sel ~= self.selected then self.transAni:checkOut() end end function ContinueState:draw(game) if self.skip then return end local tk = math.min(8, self.transAni:progress()) local locked = self.selected > self.game.progress if locked then rect(120 - tk * 16, 0, tk * 32, 136, 3 + self.selected % 2*2) spr(506, 112, 52 - self.game.t%60//30, 0, 2) title(strings.menuLocked, 72, 15, 1) else rect(120 - tk * 16 - 2, 0, tk * 32 + 4, 136, 5) rect(120 - tk * 16, 0, tk * 32, 136, 13) local lv = self.game.levels[self.selected] map(lv.mx * 30 + (15 - tk * 2), lv.my * 17, tk * 4, 17, (120 - tk * 16), 0, 0) end rect(90, 7, 60, 10, 1) rect(90, 6, 60, 10, 7) title(strings.menuContinue,8, 15, 1) rect(86, 119, 68, 10, 1) rect(86, 118, 68, 10, 7) title(strings.menuLevel .. " " .. self.selected,120, locked and 9 or 15, 1) title("< ",120, btn(K.LEFT) and 15 or 13, 1) title(" >",120, btn(K.RIGHT) and 15 or 13, 1) end end do -- dialog state DialogState = GameState:extend{ currentLine=1, currentChar=1, pad=10, lineheight=8, height=40, bg=8, lines={"Hey you forgot to add a message!"}, speed=3, returnState=nil, beepAni=nil -- OK to reuse btwn instances } function DialogState:constructor(game) self.Parent.constructor(self, game) self.beepAni = self.beepAni or game:create(Clock) self:setRepeat(self.speed, function() self.currentChar = math.min(self.currentChar + 1, string.len(self:getLine())) end) end function DialogState:getLine() return (self.lines or {})[self.currentLine] or "" end function DialogState:lineComplete() return string.len(self:getLine()) == self.currentChar end function DialogState:update() self.Parent.update(self) if not self:lineComplete() and self.beepAni:checkIn() % 4 == 0 then sfx(5, "E-3", 16, 1, 5) end if self.currentLine > #self.lines or btnp(K.X) then self.nextState = self.returnState end if btnp(K.A) or btnp(K.DOWN) then if self:lineComplete() then sfx(5, "E-5", 16, 2, 8) self.currentLine = self.currentLine + 1 self.currentChar = 1 else self.currentChar = string.len(self:getLine()) - 1 end end end function DialogState:draw() local msg = string.sub(self:getLine(), 1, self.currentChar) rect(0, 136 - self.height, 240, self.height, self.bg) rectb(0, 136 - self.height - 1, 240, self.height + 1, 2) rectb(0, 136 - self.height - 1, 240 + 2, self.height + 2, 13) pix(239, 136 - self.height - 1, self.bg) pix(0, 135, self.bg) line(0, 136, 240, 136, 7) for ln, txt in ipairs(textwrap(msg, 200 - 2 * self.pad)) do text(txt, self.pad, 136 - self.height + self.pad + (ln - 1) * self.lineheight, 15, 2) end if self:lineComplete() then spr(511, 227, 125 -(self.game.t % 80) // 40, 0) end --- local frame = (self.game.t // 15) % 2 if frame == 0 then --- spr(392, 206, 100) spr(393, 214, 100) spr(394, 222, 100) spr(395, 230, 100) --- spr(408, 206, 108) spr(409, 214, 108) spr(410, 222, 108) spr(411, 230, 108) ----- spr(424, 206, 116) spr(425, 214, 116) spr(426, 222, 116) spr(427, 230, 116) --- spr(440, 206, 124) spr(441, 214, 124) spr(442, 222, 124) spr(443, 230, 124) else spr(396, 206, 100) spr(397, 214, 100) spr(398, 222, 100) spr(399, 230, 100) --- spr(412, 206, 108) spr(413, 214, 108) spr(414, 222, 108) spr(415, 230, 108) --- spr(428, 206, 116) spr(429, 214, 116) spr(430, 222, 116) spr(431, 230, 116) --- spr(444, 206, 124) spr(445, 214, 124) spr(446, 222, 124) spr(447, 230, 124) end end end do -- transition state TransitionState = GameState:extend{ fadeout=30, fadein=30, easing=function(t) return t ^ 3 end, returnState=nil, fadeAni=nil } function TransitionState:constructor(game) self.Parent.constructor(self, game) self.fadeAni = game:create(Clock) end function TransitionState:update() local fout, fin = self.fadeout, self.fadein local t = self.fadeAni:checkIn() if t >= fout + fin then self.nextState = self.returnState t = 0 elseif t >= fout then t = (fout + fin - t) / fin else t = t / fout end self.game:tint(1 - self.easing(t)) end function TransitionState:draw() if self.fadeAni:progress() == self.fadein then self.returnState:draw() end end end do -- timer thing -- wraps game time keeping methods by using itself as the key Clock = Object:extend{game = nil} function Clock:constructor(game) self.game = game end -- avoid this function Clock:set(time) self.game.conKeys[self] = time end -- if the timer isn't checked in onto the current frame, it's reset to 1 function Clock:checkIn() return self.game:checkIn(self) end function Clock:checkOut() return self.game:checkOut(self) end function Clock:progress() return self.game:continuous(self) end end do -- basic tile-bound object Piece = Object:extend{ spr=-1, x=0, y=0, w=8, h=8, v=15, -- [1/speed] time it takes to move through 1 cell dir=D.DOWN, seethrough=false, isWalking=false, walkAni=nil } function Piece:constructor(game) self.game = game self.walkAni=game:create(Clock) end -- grids coordinates change when the piece moves half distance -- to emulate another behavior use Piece:isPastHalfTile() function Piece:gridCoords() return self.x, self.y end function Piece:screenCoords() local dx, dy = 0, 0 if self.isWalking then local t = self.walkAni:progress() % self.v local dist, half = lc(t, self.v, 8), t >= self.v // 2 dx, dy = D.eltas(self.dir, (half and - 8 + dist or dist)) end return self.x * 8 + dx, self.y * 8 + dy end function Piece:isPastHalfTile() return self.walkAni:progress() % self.v >= self.v // 2 end function Piece:isOnGrid() local t = self.walkAni:progress() return t == 1 or t % self.v == 0 end function Piece:sight(dist) local x, y = self:gridCoords() local dx, dy = D.eltas(self.dir, dist) return x + dx, y + dy end function Piece:move(dir) self.dir = dir self.isWalking = true end function Piece:update() if self.isWalking then local t = self.walkAni:checkIn() % self.v if t == self.v // 2 then self.x, self.y = self:sight() elseif t == 0 then self.isWalking = false end end end function Piece:draw() local x, y = self:screenCoords() spr(self.spr, x, y, 0) end end do -- player Player = Piece:extend{ spr=256, x=0, y=0, isPushing=false, isCaught=false, celebAni=nil } function Player:constructor(game) self.Parent.constructor(self, game) self.celebAni = game:create(Clock) end function Player:update() if not self.isCaught then self.Parent.update(self) end end function Player:draw() local celebt = self.celebAni:progress() - 1 local x, y = self:screenCoords() if celebt > 0 then spr(268 + lc(celebt % 20, 20, 2), x, y - math.sin(celebt / 2), 0) elseif self.isCaught then spr(284 + self.dir, x, y, 0) else local walkt = self.walkAni:progress() local psh = self.isPushing and 16 or 0 local frame = self.isWalking and (1 + walkt % 16 // 8) or 0 spr(self.spr + 3 * self.dir + frame + psh, x, y, 0) end -- DEBUG --rectb(8*self.x,8*self.y,8,8,14) end end do -- bucket Bucket = Piece:extend{spr=320} function Bucket:draw() local x, y = self:screenCoords() local dx, dy = D.eltas(self.dir, (self.walkAni:progress() > 1) and 1 or 0) x, y = x - dx, y - dy spr(self.spr, x, y, 0) end end do -- Plant Plant = Piece:extend{ spr=357, v=0, -- just in case? haveGrown=false, growAni=nil } function Plant:constructor(game) self.Parent.constructor(self, game) self.growAni = Game:create(Clock) end function Plant:update() if self.haveGrown then return end local t = self.growAni:checkIn() if t == 2 then sfx(61, "B-4", 48, 3, 12, 2) elseif t >= 120 then self.haveGrown = true end end function Plant:draw() local x, y = self:screenCoords() if self.haveGrown then spr(self.spr, x, y, 0) else spr(self.spr + lc(math.min(self.growAni:progress(), 60), 0, 60, -5, 0), x, y, 0) end end end do -- bed Bed = Piece:extend{ spr=352, seethrough=true, } end do Door = Piece:extend{ spr=384, opn=nil, -- check callback seethrough=false } function Door:update(state) self.seethrough = call(self.opn, state) end function Door:draw() local x, y = self:screenCoords() spr(self.seethrough and (self.spr + 1) or self.spr, x, y, 0) end end do -- wall -- this is a plug to make collision code more uniform -- the game does not use these objects for tiles Wall = Piece:extend{} end do -- Guards Guard = Piece:extend{ spr=288, bubbleSpr=507, isChasing=false, chaseSpeed=Piece.v // 2, dialog={randpick(strings.guard)}, alertAni=nil, reel=nil, reelDir=1, -- 1 or -1 frame=1, reelAni=nil, isBwd=false, -- for playing the partially completed frame backwards bwdt=0 } function Guard:constructor(game) self.Parent.constructor(self, game) self.alertAni = game:create(Clock) self.reelAni = game:create(Clock) end function Guard:update() if self.isChasing then self:chase() elseif self.reel then -- if self:isOnGrid() then trace(self.reelAni:progress()) end-- DEBUG self:patrol() end self.Parent.update(self) end local function alertsfx() sfx(5, "E-5", 16, 0) sfx(5, "A#6", 16, 1) sfx(5, "F-6", 16, 2) sfx(2, "E-4", 16, 3, 6) end function Guard:chase() local alert = self.alertAni:checkIn() if alert == 1 then alertsfx() end if alert < 30 then return end self.walkAni:checkIn() self.v = self.chaseSpeed self.isWalking = true if alert % 6 == 0 then sfx(2, "E-8", 4, 3, 3) end end -- Guard.reel contains a sequence of actions that a guard performs during -- patrol, back and forth. Guard.rewind=true makes sequence loop instead -- commands: -- {T, D.STOP} : wait T ticks -- {T, (D.UP .. D.RIGHT)} : face said dir and wait for T ticks -- {L, D.GO, (D.UP .. D.RIGHT)} : face said dir and walk L squares -- FIXME reel and walk are out of sync, hence glitchy movement. Adding a stop -- frame in the reel allows it to catch up. When fixing, mind turn back function Guard:patrol() local tm, frame = self.reelAni:checkIn(), self.reel[self.frame] local len, comm = frame[1], frame[2] if comm == D.GO then self.isWalking = true comm = self.reelDir > 0 and frame[3] or D.reverse(frame[3]) len = len * self.v --[[ - 1 ]] end if self.isBwd then -- partially completed frame len = self.bwdt end if comm ~= D.STOP then self.dir = comm end if tm >= len then self:nextFrame() end -- -- DEBUG -- if (btn(K.Y)) then -- local f = self.reel[self.frame] -- trace("| " .. (self.isBwd and "B" or " ") .. (self.reelDir > 0 and ">" or "<") .. " " .. self.reelAni:progress() .. " " .. self.walkAni:progress() .. " | " -- .. f[1] .. " " .. (D[f[2]] or "X") .. " " .. (D[f[3]] or "X")) -- end end -- TODO: nested turn back function Guard:turnBack() local passed, walked = self.reelAni:progress(), self.walkAni:progress() self.reelDir = -self.reelDir self.walkAni:set(self.v - walked % self.v) self.isBwd = not self.isBwd if self.isBwd then self.bwdt = passed self.reelAni:checkOut() else self.reelAni:set(self.bwdt - passed) end end function Guard:nextFrame() self.reelAni:checkOut() self.isBwd = false if self.rewind and self.frame == #self.reel then self.reelAni:checkOut() self.frame = 1 elseif self.reelDir < 0 and self.frame <= 1 or self.reelDir > 0 and self.frame == #self.reel then -- last frame plays twice: once forwards, once backwards self.reelDir = -self.reelDir else self.frame = self.frame + self.reelDir end -- DEBUG if not self:isOnGrid() then trace("WARN< Guard frame switch " .. (self.v - self.walkAni:progress() % self.v) .. " ticks earlier", 12) end -- local f = self.reel[self.frame] -- trace(f[1] .. " " .. (D[f[2]] or "X") .. " " .. (D[f[3]] or "X")) end function Guard:draw() local x, y = self:screenCoords() local alert = self.alertAni:progress() - 1 local walk = self.walkAni:progress() - 1 local frame = walk > 0 and (alert > 30 or not self.chasing) and (1 + walk % 16 // 8) or 0 spr(self.spr + 3 * self.dir + frame, x, y, 0) if alert > 0 and alert < 30 then -- spr expression makes the last frame hold longer spr(self.bubbleSpr + math.min(3, lc(alert, 30, 8) // 1), x, y - 8, 0) end end end --[[ RUN ]]-- function TIC() game:loop() end --[[ INIT ]]-- -- position shorthand local _r = function(a) local tt = {} for _, r in ipairs(a) do tt[#tt + 1] = {x=r[1],y=r[2]} end return tt end local _grown = function(n) -- door condition return function(state) return #state.plants >= n end end local _grownAt = function(x, y) -- door condition return function(state) for _, pt in ipairs(state.plants) do if pt.x == x and pt.y == y then return true end end return false end end local _waitAfter = function(r) -- guard reel transform local t = {} for i, f in ipairs(r) do t[2 * i - 1] = f t[2 * i] = {5, D.STOP} end return t end local UP,DOWN,LEFT,RIGHT,STOP,GO = D.UP,D.DOWN,D.LEFT,D.RIGHT,D.STOP,D.GO Levels = { [1]={mx=2,my=4, p={x=12,y=8,dir=DOWN}, bu=_r{{14,8}}, bd=_r{{17,8}}, gu={}, dr={}}, [2]={mx=3,my=4, p={x=18,y=8,dir=LEFT}, bu=_r{{11,8}}, bd=_r{{9,7}}, gu={}, dr={}}, [3]={mx=4,my=4, p={x=10,y=7,dir=RIGHT}, bu=_r{{12,8},{17,8}}, bd=_r{{12,7},{12,9}}, gu={}, dr={{x=14,y=7,opn=_grown(1)},{x=14,y=9,opn=_grown(1)}}}, [4]={mx=3,my=5, p={x=13,y=9,dir=DOWN}, bu=_r{{11,5},{16,6}}, bd=_r{{10,5},{17,6}}, gu={}, dr={}}, [5]={mx=5,my=4, p={y=5,x=12,dir=DOWN}, bu=_r{{11,11},{12,11},{13,11}}, bd=_r{{10,11},{12,12},{14,11}}, gu={{y=7,x=15,dir=LEFT}}, dr={}}, [6]={mx=2,my=5, p={y=7,dir=DOWN,x=13}, bu=_r{{12,6},{12,9},{15,6},{15,9}}, bd=_r{{13,4},{13,11},{14,4},{14,11}}, gu={{y=10,dir=RIGHT,x=11,rewind=true,reel={{5,GO,RIGHT},{5,GO,UP},{5,GO,LEFT},{5,GO,DOWN}}}}, dr={}}, [7]={mx=1,my=5, p={dir=LEFT,x=15,y=3}, bu=_r{{11,7},{12,6},{13,6},{14,7},{16,11}}, bd=_r{{14,13},{16,5}}, gu={{dir=RIGHT,x=9,y=9},{dir=RIGHT,x=10,y=7},{dir=RIGHT,x=10,y=11}}, dr={}}, [8]={mx=5,my=3, p={y=12,x=17,dir=LEFT}, bu=_r{{9,9},{19,9}}, bd=_r{{14,13}}, gu={{y=5,x=14,dir=DOWN}}, dr={}}, [9]={mx=2,my=3, p={x=14,y=5}, bu=_r{{12,6},{14,6},{17,10},{17,11}}, bd=_r{{15,6},{18,8},{18,11}}, gu={{x=13,y=10,dir=UP}}, dr={}}, [10]={mx=1, my=0, p={x=7,y=7,dir=DOWN}, bu=_r{{15,6},{14,9},{17,8}}, bd=_r{{9,13},{24,6},{24,9}}, gu={{x=18,y=10,dir=UP,reel={{90,UP},{90,LEFT}}}}, dr={}}, [11]={mx=2, my=1, p={x=24,y=8,dir=DOWN}, bu=_r{{9,6},{15,7},{15,10},{16,10}}, bd=_r{{2,7},{3,7},{4,7}}, gu={{x=21,y=8,dir=LEFT,reel={{60,DOWN},{120,LEFT},{60,UP}}}}, dr={}}, [12]={mx=5, my=0, p={x=9,y=5,dir=DOWN}, bu=_r{{6,6},{7,9},{14,8}}, bd=_r{{24,3},{24,12},{21,8}}, gu={ {x=5,y=12,dir=RIGHT,reel={{60,RIGHT},{60,UP}}}, {x=10,y=8,dir=RIGHT,reel={{30,STOP},{9,GO,RIGHT},{30,STOP}}} }, dr={}}, [13]={mx=1, my=1, p={x=11,y=11,dir=DOWN}, bu=_r{{7,4},{6,10},{21,12},{21,13},{23,13},{19,12}}, bd=_r{{20,9},{22,9},{23,8}}, gu={ {x=7,y=5,dir=RIGHT,reel={{30,RIGHT},{60,UP},{30,LEFT}}}, {x=14,y=7,dir=RIGHT,rewind=true,reel={{90,RIGHT},{90,UP},{90,LEFT},{90,DOWN}}}, {x=17,y=11,dir=UP,reel={{60,STOP},{2,GO,DOWN},{2,GO,RIGHT},{60,STOP}}} }, dr={}}, [14]={mx=3, my=0, p={x=19,y=12,dir=UP}, bu=_r{{12,8},{14,8},{16,8},{16,9},{17,9}}, bd=_r{{16,4},{19,7},{20,8},{21,10},{18,11}}, gu={{x=15,y=10,dir=UP,reel={{60,UP},{60,RIGHT}}}}, dr={}}, [15]={mx=1,my=2, p={y=2,x=5,dir=DOWN}, bu=_r{{4,4},{7,6},{9,6},{12,6},{13,6},{15,6},{18,6}}, bd=_r{{4,2},{9,2},{10,10},{15,9},{15,10},{19,2},{19,3}}, gu={ {y=6,x=5,dir=RIGHT,reel={{60,STOP},{1,GO,LEFT},{30,STOP}}}, {y=6,x=21,dir=LEFT,reel={{60,STOP},{1,GO,RIGHT},{30,STOP}}}, {y=12,x=20,dir=LEFT,reel={{5,STOP},{10,GO,LEFT},{10,STOP},{2,GO,DOWN},{10,STOP},{6,GO,LEFT},{5,STOP},{5,UP}}}}, dr={}}, [16]={mx=1,my=3, p={x=15,y=4}, bu=_r{{13,7},{14,6},{14,8},{15,7},{16,6},{16,8},{17,7}}, bd=_r{{8,2},{10,11},{15,6},{15,8},{17,13},{21,3},{21,13}}, gu={}, dr={{x=9,y=3,opn=_grown(4)}}}, [17]={mx=4,my=5, p={x=18,dir=RIGHT,y=9}, bu=_r{{10,8},{11,8},{14,8}}, bd=_r{{9,6},{13,7},{15,8}}, gu={{x=12,dir=UP,y=10,reel={{60,UP},{60,RIGHT}}},{x=15,dir=RIGHT,y=7,reel={{60,RIGHT},{60,DOWN}}}}, dr={}}, [18]={mx=3, my=1, p={x=8,y=5,dir=DOWN}, bu=_r{{11,2},{11,10},{15,5},{23,8}}, bd=_r{{11,1},{11,11},{23,7},{24,6}}, gu={{x=2,y=4,dir=RIGHT,reel={ {30,STOP},{6,GO,RIGHT},{2,GO,DOWN},{3,GO,RIGHT},{4,GO,UP}, {25,LEFT},{25,RIGHT},{25,DOWN},{25,UP}, -- look around {4,GO,DOWN},{12,GO,RIGHT}, {25,LEFT},{25,RIGHT},{25,DOWN},{25,UP}, {12,GO,LEFT}, {25,LEFT},{25,RIGHT},{25,DOWN},{25,UP}, {100000,RIGHT} }} }, dr={}}, [19]={mx=4, my=0, p={x=15,y=14,dir=UP}, bu=_r{{13,6},{21,5}}, bd=_r{{8,4},{16,6}}, gu={ {x=14,y=5,dir=RIGHT,reel={{120,RIGHT},{120,DOWN}}}, {x=19,y=8,dir=LEFT,reel={{150,STOP},{2,GO,RIGHT},{150,STOP}}} }, dr={}}, [20]={mx=2, my=0, p={x=7,y=10,dir=DOWN}, bu=_r{{15,6},{20,11}}, bd=_r{{20,2},{21,2}}, gu={ {x=14,y=5,dir=RIGHT,reel={{90,RIGHT},{90,DOWN}}}, {x=13,y=11,dir=LEFT,reel={{4,GO,LEFT},{4,GO,UP},{4,GO,RIGHT}}}, {x=21,y=12,dir=UP,reel={{90,UP},{90,LEFT}}} }, dr={}}, [21]={mx=5,my=5, p={dir=DOWN,x=10,y=6}, bu=_r{{11,8},{16,10},{17,7},{18,7}}, bd=_r{{12,8},{15,10},{16,7},{16,9}}, gu={{dir=UP,x=14,y=10,rewind=true,reel={{4,GO,UP},{4,GO,LEFT},{3,GO,DOWN},{1,GO,RIGHT},{1,GO,DOWN},{3,GO,RIGHT}}}}, dr={{y=6,x=13,opn=_grown(1)}}}, [22]={mx=4,my=3, p={x=12,y=14,dir=UP}, bu=_r{{5,5},{18,12},{20,4}}, bd=_r{{8,8}}, gu={ {x=4,y=6,dir=UP,reel={{50,UP},{50,RIGHT}}}, {x=10,y=10,dir=RIGHT}, {x=13,y=7,dir=LEFT}, {x=19,y=12,dir=UP,rewind=true,reel={{150,UP},{150,LEFT},{150,DOWN},{150,RIGHT}}}, {x=24,y=4,dir=LEFT,reel={{100,LEFT},{3,GO,LEFT},{1,GO,UP},{2,GO,RIGHT},{100,LEFT}}}}, dr={}}, [23]={mx=3,my=3, p={x=17,y=13,dir=UP}, bu=_r{{4,6},{4,11},{19,5}}, bd=_r{{10,11},{14,14},{21,8}}, gu={ {x=9,y=8,dir=RIGHT,reel={{12,GO,RIGHT}}}, {x=21,y=8,dir=LEFT,reel={{6,GO,LEFT},{5,GO,UP},{6,GO,RIGHT},{5,GO,DOWN}}}, {x=25,y=10,dir=LEFT,rewind=true,reel={{2,GO,LEFT},{2,GO,DOWN},{2,GO,RIGHT},{2,GO,UP}}}}, dr={}}, [24]={mx=1,my=4, p={y=4,x=12,dir=RIGHT}, bu=_r{{8,8},{8,9},{10,8},{11,8},{11,10},{12,8},{14,3},{14,13},{15,9}}, bd=_r{{11,13},{12,7},{13,6},{17,5},{18,13}}, gu={ {y=12,x=17,v=Guard.v*2,dir=UP,reel={{6,GO,UP}}}, {y=6,x=18,v=Guard.v*2,dir=DOWN,reel={{6,GO,DOWN}}}, {y=9,x=20,dir=LEFT}}, dr={}}, [25]={mx=5,my=2, p={x=24,y=3}, bu=_r{{6,7},{8,8},{8,10},{26,12}}, bd=_r{{10,1},{10,2},{10,5},{28,12}}, gu={ {x=5,y=12,dir=UP,reel={{80,UP},{80,RIGHT}}}, {x=10,y=8,dir=LEFT,reel={{80,LEFT},{80,DOWN}}}, {x=15,y=1,v=Guard.v*2,dir=LEFT,reel={{20,LEFT},{2,GO,DOWN},{40,LEFT},{2,GO,DOWN},{20,LEFT}}}, {x=21,y=7,dir=DOWN,rewind=true,reel={{1,GO,DOWN},{4,GO,RIGHT},{3,GO,UP},{2,GO,LEFT},{2,GO,UP},{5,GO,LEFT},{4,GO,DOWN},{3,GO,RIGHT}}}, {x=24,y=14,dir=UP,rewind=true,reel={{1,GO,UP},{1,GO,RIGHT},{5,GO,UP},{4,GO,LEFT},{2,GO,DOWN},{2,GO,LEFT},{4,GO,DOWN},{5,GO,RIGHT}}}}, dr={{x=18,y=10,opn=_grown(1)},{x=9,y=2,opn=_grownAt(10,1)},{x=9,y=5,opn=_grownAt(10,2)}}}, [26]={mx=3,my=2, p={x=14,y=2}, bu=_r{{8,6},{8,10},{15,7},{20,5}}, bd=_r{{12,10},{13,5},{17,5},{18,10}}, gu={ {x=8,y=13,dir=LEFT,rewind=true,reel=_waitAfter{{2,GO,LEFT},{6,GO,UP},{4,GO,RIGHT},{3,GO,DOWN},{3,GO,DOWN},{2,GO,LEFT}}}, {x=11,y=2,dir=DOWN,rewind=true,reel=_waitAfter{{3,GO,LEFT},{3,GO,DOWN},{2,GO,LEFT},{2,GO,DOWN},{5,GO,RIGHT},{5,GO,UP}}}, {x=13,y=10,v=math.floor(1.5*Guard.v),dir=DOWN,reel=_waitAfter{{20,RIGHT},{4,GO,DOWN},{4,GO,RIGHT},{4,GO,UP},{20,LEFT}}}, {x=26,y=7,dir=LEFT,rewind=true,reel=_waitAfter{{7,GO,LEFT},{4,GO,UP},{2,GO,RIGHT},{2,GO,DOWN},{5,GO,RIGHT},{2,GO,DOWN}}}}, dr={{x=13,y=9,opn=_grownAt(13,5)},{x=17,y=9,opn=_grownAt(17,5)}}}, [27]={mx=2, my=2, p={y=9,x=15,dir=DOWN}, bu=_r{{12,6},{12,10},{18,6},{18,10}}, bd=_r{{6,7},{13,14},{17,2},{24,9}}, gu={ {y=10,x=7,dir=RIGHT,rewind=true,reel=_waitAfter{{1,GO,RIGHT},{1,GO,DOWN},{4,GO,RIGHT},{4,GO,UP},{5,GO,LEFT},{3,GO,DOWN}}}, {y=3,x=12,dir=DOWN,rewind=true,reel=_waitAfter{{1,GO,DOWN},{1,GO,LEFT},{2,GO,DOWN},{6,GO,RIGHT},{3,GO,UP},{5,GO,LEFT}}}, {y=13,x=18,dir=UP,rewind=true,reel=_waitAfter{{1,GO,UP},{1,GO,RIGHT},{2,GO,UP},{6,GO,LEFT},{3,GO,DOWN},{5,GO,RIGHT}}}, {y=6,x=23,dir=LEFT,rewind=true,reel=_waitAfter{{1,GO,LEFT},{1,GO,UP},{4,GO,LEFT},{4,GO,DOWN},{5,GO,RIGHT},{3,GO,UP}}}}, dr={}}, [28]={mx=4, my=1, p={x=25,y=7,dir=DOWN}, bu=_r{ {17,5},{18,5},{20,5},{18,7}, {19,10},{21,10},{23,10}, {20,11},{22,11},{24,11}, {19,12},{21,12},{23,12}, {20,13},{22,13},{24,13} }, bd=_r{ {7,3},{8,3},{9,3},{10,3}, {7,8},{8,8},{9,8},{10,8}, {7,10},{8,10},{9,10},{10,10}, {7,13},{8,13},{9,13},{10,13}, }, gu={}, dr={}}, [29]={mx=5, my=1, p={x=24,y=14,dir=DOWN}, bu=_r{}, bd=_r{{-10,0}}, gu={}, dr={}}} for i, l in ipairs(Levels) do l.int = strings.intros[i] end local progress = pmem(0) if progress == 0 then progress = 1 end game = Game:new():with{ levels=Levels, progress=progress} game:music(2) game.menu = game:create(MenuState):with{returnState=game:create(ContinueState)} game.state = game.menu do -- an util that prints the level template based on the placeholder tiles in the level editor. local dev_tt dev_tt = function(t) local s = "" -- performance idc local map = not t[1] for k, v in (map and pairs or ipairs)(t) do s = s .. (#s > 0 and "," or "") .. (map and (k .. "=") or "") .. (type(v) == "string" and ('"' .. v .. '"') or (type(v) == "table" and dev_tt(v) or v)) end return "{" .. s .. "}" end -- extract position of placeholder tiles and print a level template local function dev_objgen(mx, my) local o = function () return Object:new() end local t = o():with{mx=mx, my=my, int={"INTRO", "GOES", "HERE"}, p=o(), bu={}, bd={}, gu=o(), dr=o()} local w, h = 30, 17 local i_pla = 165 local i_buc = 167 local i_bed = 168 local i_gua = 166 local i_dor = 169 -- extract for x=1,w-1 do for y=1,h-1 do local i, xy, a = mget(mx * w + x, my * h + y), o():with{x=x, y=y}, o():with{x, y} if i == i_pla then t.p = xy:with{dir=D.DOWN} elseif i == i_buc then table.insert(t.bu, a) elseif i == i_bed then table.insert(t.bd, a) elseif i == i_gua then table.insert(t.gu, xy:with{dir=D.DOWN}) elseif i == i_dor then table.insert(t.dr, xy) end end end -- run from terminal to copy trace(string.format("{mx=%d,my=%d,\n\tint=%s,\n\tp=%s,\n\tbu=_r%s,\n\tbd=_r%s,\n\tgu=%s,\n\tdr=%s}", mx, my, dev_tt(t.int), dev_tt(t.p), dev_tt(t.bu), dev_tt(t.bd), dev_tt(t.gu), dev_tt(t.dr))) t.bu = _r(t.bu) t.bd = _r(t.bd) return t end -- Levels[1] = Levels[1] or dev_objgen(5, 5) -- (mapx, mapy) in whole screens -- game.state = game:create(MapState,1) -- start immediately end