_>=LZZcμ:S9lO@έsZ!&?g3G9@qk{9B@cFlHlRYc:S}{$,!&?Hk{9B SܽYͪX%S&SE"C$Dkz;TvV$2dͼw "CT5"3D4#2Tv2Tv2TvͫgE# DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDADDDDDDAAAAAADDDDDD3CDDDTT C D3 0I0I#@@I@@@341 <33,ù̑ rwLDL  !""!33!""!""!33!""wwwwfffffffffwwwwffffffffwwwwf6ff6f#2"3#"'"#"PYř̙U̙̙Y̙ UV\\ \]AAAAB TT T U I I@ @B B E CCIDđD     3333DDDDffffffff833838738733hffffffffxwwx""x""x""x""x33333333""""CDD40AD4Ajooffffffff,",R"Q-R"%R"QU\]՝\\YYY+++++AADDDDA@/AAAADD@DDDD ADD DD AD D A  bhhhcccrrrLw|||"|w̜̜|̜,ǜ"ǜ,ǜD""D$0BB$ B$"0""""B"""ffffffffffffffffffffffffffffffffffffffffffff`hhh`hhhCDCDCDCDCDCDDDDDDDDDDDDD"b""wgrwwgrwffff&"""&www&wwwffffb"w`rpw`f`&"&ppwwwff""w"'ww"'ww'www'wwwwwwww8ff`" p"p" pw pw wpwwpwfADDA@AA@DAAA        fffdhhhfffdhhh||wDLD,ǜ|̜̜w̔LLL/DDDD"/DDDD"hffffffhffffhffh""""wwwwwffwwwwwwwwwwwwwwwwwwwww""""wwwwwwwwwwwwwwwwwwwwwwwwwwwwDDDDDDDDDDDDCDCDCDCDCDCDCDCDffffffffffffff`ff`fk` fififffif ik DDDDDDDDDDDDADADDDADD fffh`#"28"23"22"22"2b2"22"2xxwvgwvgfwvgwvgwvgwvg33d33343dD4D3A34ih h xwwgxwwgi vgfhxhhhh3333i`i`f`3333i`i`f`DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD23332333233323332333233323332333""""333333333333333333333333333333CD33Dw3CtD3DGDCtDDDGDDtDDDtDDDDDDDwwwwDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDAADDDDADDDDDDDDDDDDDDDDDDDADDDD433wD33DG43DtD3DDG4DDtDDDDGDDDG2"2233B"BC"B3"22"22"22"2wvgwvgwhwvgvgwvgwvgwvg88H383dd33dFn  h`ffffiiffffffffiiffff3333i`i`p`fxvxfv ``3333````a233323332333""""""""233323332333""""3333#""23333#""23333#""2333323332222222222222222222222222333tttDtDtDDtDDU@ ]՝\\YYYT]@Y ܝ]\\UU̙YYUU \ UŜ PUU @DDG@DDG@DDG@DDG@DDG@DDG@DDG@DDGDDDDDDDDDD#DD2DD"#2B42I2IBIIFdDiwvgwvgwvgwvgwffw`fi `(/h"ri/ h ` ffffffff A"""""""""A23232##32##3223222322##32##32323333333"233"B33"33"33333333@DD@DD@DD@DD@DD@DD@DD@DDtt>tt3t>tt3tIDDDDDiDDfDDDDDDDDDDDI33#)2#iB#f#""CDDCDD       DDG ADG DG AG G H  #2"""DD DD0#0324343DD43DD""""DDDDDDDD4343DDDDDDDD#"""C4DDC4DC4DC1DI1D9@""""DDDDDDDDDDDADDADI@""""DDDDDDDDDDDDDDDDDDDD""""DDDD433CDDDDDDDD@DDDD DDDDDDDO333333333""33""43130333333333#!13#!1""!1B!!13#!13#!13#!13#!1DD AD D A   tDDtDDtDDtDDtDDtDDtDDtDDDDDD3333DDDDDDDDDDDD "1DD$D4"3D2#DD"1DD$D4"3D2#DDDDDDDDD33DDDDDDDDDDDD33DDDDDCAIAI@CAIAI@DDDDDDI@DDDDDDI@DDDDDDDDDDDDDDDD    UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU333333333333333333333333333333333#"B3#3#A3#"3#A3#3#"3C   tDDDtDDDDGDDCtDD3DGD3CtD33Dw33CDDDDDDDDDDDDDDDDDDDDDDDDDwwwwDDDDDDDDDDwwwDDDA"1DD$D4"3D2#DD$1DD@D$3D0DDDDDDDDDD33333333DDDD DDDDDDDDUUUU  DDDDDDDD  3333CDDD3DDD3CDD33DD33CD333D333C3333DDDDDDDDDDDDDDDDDDDDDDDDDDDD Y PU PU Jwwwww333333333#""3 33333333333"""" 33333333""23039303I03DDDD3333DDDDDDDDDD3333999DDD9DDD9DDD9D433wD33 G43 pD3 G4 pD G H   3#233/333c6"ri/ c3 `@@CCCDDDDDHDDDDDDDDDDNN ADADADADADDADDD  3333333 3UUU\\\UYUYYYfI13f43f)43"43)"43$"13$03$03#"""9"""2P܍U݈ň܍Ō݈DDDGDDDGDDtDDDG4DtD3DG43wD33D433wwwwyfffyfffyfyfyfyȌȌ̈fyfyfyfyfyfyfyfy  `DDDDDDDADD 3 3333333 ` i```fhffh63i0303I03fI13fI13fI13fI13fI1313332222222222222222223333334333333333333333333333433CD443333999DD9DD9DD9Ȉ̈̈ȈȈ̈̈ȈȌȌ̌̈̈̈̈#""2#""2""""DD 33CC3333CC33AAAAAADDDD 뾻뾾I1343)43"43)"43$"13$03$03333333333333333333333333AADDDDADDDDDDDDDDDDDDDDDDDADDDAD9DD9DD9DD9DD9DD9DD9DD9ODD   D/DQIDJ IJJDȌȌ3333DDDDDD3333 00003333DDDDDDDDADDD3333D@D3333 ;;f;f3i3뛙뻻0303I03I13I13I13I13I13""""3#233#233#233#233#233#233#2333334333D333D433DD33DD43DDD3DDD4̙ AIDIJ IJJD̪  I   DDDDDDDDDDDDDDDDDDDDDDDDADIDIDIDDDD"$"B"!"BIIBAADDAADDAADDAABDD!0049D ` f f` n9v9h8cfhh`ff`f`3938hhvcfhffh`f`fI13f43f)43"$")"$"$$333" UYUYYYU\\lUPV\\ \]      D@N@O 'r D`c63f3DADADADDD@30)@3)r@r3 ;3;@30@3A@3 D;3;if9yi ffIDI3 Ic c ic jif9yi ffIDI3 Ic c ic 9 9cc鞪iff D : : 6 D@@ "@3)r)93I3;3 @3A93I3D;3 ii993939ic 9c f3y D ii993939ic 9c f3y D 933933933 333 D33 D3 D3  DIDI IID II  III9A6IA9AA 3j3 9969996999 99 9j939j1393jf99ciAj 3 jf6I9i393jf9iij9C 3 9f33AAy9C69A6 AA43j3j 9FDy9C69A6 4HDAA43j3j 6:i9c9i6i:9ccj6iic9 6jf96fc996i9cci9ii3c:3 III6A9IA6AA j3j 9999699969 99 939j931j93jf99ciAj 3 jf6I9i393jf9iij9C 3 9f33AAy6C96A9 AA4j3j3 9FDy6C96A9 4HDAA4j3j3 9:i6c6i9i:6cjc6ic9c 9jf66fc699i6cf9f3:3f  Ej̥3̥j\3 T Vj V3 Vj 3 {wyj3I y w j 3 j 3Oj3j 3 j 3 ៩93jff6cf3fAj 3 fjfcf6J:f36 9 6A j3jKj 3Oj3閐ij 3 j i3 i 3ajY͐̐̐Y͐ՙ͐Ŝ͐6 ̕j \\ \\ ̕\ ̙\ \ Y6 j)3j3)j39j j 3 j 3 j 3 j 9:cc69)6 9:i6339)񙙙6 6:f96c9i6ij:9cfcjj6jcjc6 IItIIF69A6 KGDD4:j3j 9:9c66 91:9j919)񙙙6 IDIININN E3̥j̥3\j T V3 Vj V3 j {wy3jI y w 3 j 3 jO3j3 j 3 j ៩93jff6cf3fAj 3 fjfcf6J:f39 6 9A 3j3K3 jO3j閐i3 j 3 ij i 3a3Y͐̐̐Y͐ՙ͐Ŝ͐3 ̕3 \\ \\ ̕\ ̙\ \ Y3 3)j3j)3j93 3 j 3 j 3 j 3 9:cc69)3 9:i6339)񙙙3 9:f69c6i9ij:6cfjjc6jjc6c IItIIF96A9 KGDD4:3j3 9:9c63 91:9j919)񙙙3   { p {w{ p pw {w{ pp wpwpwwwpwwwwwwwwwwwwwwwww{w{ pp w {w{ p pw { p ID IDD IDD DI { p {w{ w pp {w{wp p pwwwwpwwpwwwwwwwww{w{wp p {w{ w pp { p //gRRaRRaRRRR#Ï ؐa##########鐐a""a#################################kkk-k[kkkkkkkk!!!-KkkKkkkVVVVVV"/////g"VVVVV##kkkkkkkkk[kkkkkkk[#####e}ueuѢVVQQQxyyJKL}}wQQkkkkKk-kkkkk[-kKk????wSWÎMMMMMMؐaR#p##""##TTTTTWRR""RRaR############# #### ## ##########kkk,kkkkkkk[kk!!!,kkkkkkkVV!"?????g"1!'VVV##!kkkk[kkkkkkkkk[kkk##!##uɑuɑ#'QQpZ[\pQQk[kkkK,kKkkkk,kkK bKLwbe]]]]##p#""###⩥waa## ########### ##### ### ####k[kh-kkk""""-kkk-kKkV11'11!!'117V##!!-kk!!!!!////!!!!!-k####{{{{|{|{|}}}#7QQQjkl``-kkk,,,kkKk,,,,kkkćQ[\LJQummmm##Q+#""cĥƇRRaRRR""bbb##!!##!!##!!##!!##!!##!!##!!##[kkxyyy,[kk,kkk,kKkVV117!1117117111!!##!!,kk!!""PPPP""!!,kk!###7`QQ##bbb,kk[,kkkkkk[kkkk[,kk[ԇQklQ""wIJKL;Ʒb֗RRaRRR""``Q#!!##!!'#!!'!!##!!#!!##!!'#kkLLNNNNNNNNNNNNNNLLkK!!FFG!!!!!!11!'!17!111!##!,kK!!????!!,k!!##FGQQQRRR#֨-kOOOOOOO.,kִ##QkkLJQe"hYZ[\+;淐bb###QSg!!7!!!!'!!7..##^^^^^^^^^^^^^^""11###ƶ11111!117!17!1111##!!kk!!STTW!!,!k!##˖ְ֭``HXHXXX渹,k_______.,k##bbusz{|ijkl;##w##p###pg!!7!!!!7!!7..####""k""-kkk..######ƶ11111111111##!,!k!!!""w!!!!kk!##bbkk,,,k33333##### -kKk3333,k""-Khַ"RRRRRR""##bbp""s##g!! !!7!! !!7!! !!7######""KL"",k[[[k######ƶֶƶ1###""##!!,k!!!V""!!,kk!##gV[kkkkkCCCCC#111#,kkKCCCC,k"",kx""WRRR""Q""##""s##TTEFGEFGEFGEFGEFGEFG######""..[\[[kkKkk"""############<,,kk!""!,kk!##g.V..-k#111#,kkk ,K "w䐐MM""=>""Qe#####wQ##!!wSTTTTW!!'!!'!!'STTTTW!!.######""..lKkkkkKkk33####33dddd####33###kKkkkV""VVKkk!##'#'g#..,k#####,kkk[kkkkkk[,[f#""#}}}}}}##RRhٳbb]]""MM""Qִbb##!!!!!!w!!7!!7!!7!!!!w!!kJJ-kK-kLLCC####CC"""tt3333tt"""####CC##!!-Kk""Vkkk!!##7TTTT#7g ,k-kkkkkk[kkkɀe#""#peåٳYQmSć""""!!!!""7""7""7!!!!""kZZ,[k汱,k###33333333333'CCCC733333333333##!,kk#!#'""#!#-kkkK####'#7g#,,,k,k-e}uWRRaRRRaRRaRRuõhiQ˴sWԇ"""V!!EFGEFGEFG"""kjj,kk,k#CCCCCCCCCCC7FFFFGCCCCCCCCCCC##!,kk!!!7!!!,kKkk###)*#ԢkkKk,,, ,,,,,,,k,uɑwRRaSTTTTWexyyyQ#####bbbw///##hSTTW!!STTTTTV!!wSTTW.,kKkKkkkkkk[kk#FdddFFFFFFFGSTTTTWFFFFFFFdddF##!,kk#!#7#!#,kk!!##9:'dd²kkkkkkKkkkkkkkkkkkkKk,,,,,,,}}}}}}bwbbu׈##""pQQVVV}}""xyyys!!w!!STTW!!!!!!!!!!w.,kkKkk-k[k###tttSTTWTTTTwTTTTSTTWttt##!!-kVV'Vkk!!##7htt'[k-kkkkkk[k-kkkkkk[,kkQQe瘉## QQpV~bbzz!!!!VVg!!)*!!!!!!k[k.,kkkk[kܷ,kkkk[-k###TTTTgڇgTTTT##!!!-kK7Vkkk!!!##h#'z{|}}{|7#(#((-kkk%&,kkk-kQQuõו##=>QQQU??Q!!VVg!!9:!!kkk,kKkkkkk,kkkkk,[###wڇw##!!!!!-kkV7kkK!!!!##y#x#yyyyy#77TTTT##888,kkk56,kk[,kkkK##ֺkKkKKֺkKkkKkkkkkkk#####kk#k#kg""""'""'g""""'QSWSWQK####K#1###1####11#11#1##[####kKkKk""-Kkkk####""-kkKkkkKkKk##]]]]]QQS##H####!!!!!!!!!'!##!'!!!!!!!!!##kKk##kkkkk,kKkkkkkkkKkkk###kkk##g""""7V""7VVg""""7bbbַg//////ַgbbbK#############################kkKkk"",kkKk##K#"",kkkkkkkKkk##mmmmmbbb##X####!!!!!!!!!7!##!7!!!!!!!!!##kkk11 kkkkKkkkkkk11-kk11kK111111#[kkk[[kk#######'VEF!!!!'EF""7QQgVPVPPVgQ######1#######################kKkKkKk""-########11-kwQpse"q !!'##!!!!!!!!!!!!!!##'!!11-11,kK""-k11-k11-k""-11,kk11kk333333##kkkk[kk#######7VVV!!!!7""7QQ##??????###Q###[kk[###kkKkkkk"",###-####11,PPQQhuɑkk!!!!7!!'!!!!!!!!!!7!!7!!!!K1,kJKkk##,k11,k11,k##,KJJKkKkCCCCCC####kkk[kgV""""FGV####!!!!'!!'EFGQQ##p"HHHHH"p##Q##[kkkk[##kkk[k""##11##,K###K#""1111111LJQQx}}}}}}MM!!'!!7EFFFFFFFFFG!!7!!lkZJKkkk""-k11-K""-kkkZZkkkkkkKkgV!!!!""'!!######!!7!!7VVbb######XhX######bb#########11#####kkkkk""##11#"""'####""1!1!!!1LJQQS]]!!7!!!!7!!!!'!!ljZk##,11,##,kjjkk333333#-kk1133331-g!!!!FFG!!######!!7!!7VVQ#####RRRpxyyyypRR#####Q####""###11"""#####kk11#########"7###'""!!1!!pQʲć+!!7!!##7!!!!!!!!!!'##!!7!!Kjk7lKKlkCCCCCC#,kk11CCCC1,g!!!'!!'""'####7######!!Qa# "" #aQQK###""###############"""K#K###kK11#########"7##7""!1111bbs²ʇ;+##7!!##7!########!7##!!7##"3333333"'""G"3333333"'kkkkKkkkkkkkKkk#kg"""7!!7""7EFFFG######!!Qa"aQK##########################K#K""###########"7#711'VV!1!TTTTTTWQQhijmh?ڗq;;!!7!!!!7!!!!!!!!!!7!!!!7!!"CCCCCCC"7""'"CCCCCCC"7kKkkkkkkkkkEFFG""GEFG"""####""!!eea"===="aee#############33333333#########""#######'##'"""'#7117!!1###gQQxy|{|}}z{||||;!!7!!7EFFFFFFFFFG!!'!!EFFFFFFFFG##7EFFFFFFFFGk11111111-K33kkkKkTW"""##""""!!uu⸹MMMMMܷ}}uu#############CCCCCCCC#####################'#7"7##'""""'1!egQQ((((!!!!'!!7!!7!!!!TTTTTTWEFGSTTTTTTkk111111111,kCCk#kkkg!!!!!##!!!!!!F!!""""7d]]]]]ddd##########333333###1,,,,,#########'"7##7""""71!ugbb8888!!!!7!!7!!!!!!!!!!'!!7!!!!"g''s"!!!11111-kkKkk#k#kkg!!!!!##!!!!!!V!!FFFFGtmnnmmttt##333333CCCPPPPPPCCC1kkkKk<,,,"'VVVgQQHHHH#FF!!7##7!!!!!!!!!!7##7!!'"w77"##3333333-kkkkkkkkkk#k#k!!!!!!!!!!###'aa萐gaagaa11CCCCCCSTTTTWPPPPkkkkKkkk"'VVVVegQQSVUUVVUVUVUVUVdXXXXV!!7!!7EFFFFFFFG!!7!!7##########7'####7########CCCCCCC,kkK-kkk###!!!!!'V""!!!!!!####'RaRaRRgRSTTTTWRgRRaRRaPPP##wP##PPPPPPP""-kk[k""7'"""'eugQQUUUVwTTTTt////VEFG!!7!!'!!7EFGEFFFG)*7EFFFGkkKkkkkkkkkkk,KKk###!!!!7V!)*!!!!!####7aᩐgSWVVVVgeg⩐aPPdddPPPPP###)*ڇP##PdddPPP[k[kkkk""7)*7"'"""eegpQUUUUUTTTTd????V##!!!!!!!'!%&!7!!!!!!!'##9:7kkkkkkkKkkkkk,kKKkkkkk!!!FGVV!9:!FFFFF###7agVPPVgugaTTtttTTTTT##T9:ڇPTTTtttTTTkkkkKkk""7V9:7VV"""eegQQVUUUVeeeVteeVV###!!!!!!!7!56!7!!!!!!!7## { !  * ! """"#$%&&''((()))))))) ) ) ) ) )) 7701247;??????????????????????? cc G @B@@@@@@@@@@@@@@@@@@@@@@@@@@@RSSUUVVVPPP PPPPPPPPPPPPPPPPPPPtIfp@OB@@@@@@@@@@@@@@@@@@@@@@@@@@@ 0 R?$,4$ | pP{$ !""$$(.//////*))))**+,...../T{$A`t           ``ĢĢ*LĢ*Ln| P{ &F  %kA Aa A Aa A Aa A Aa A AA A AiA Aa A Aa A A a A Aa A Aa A A d K d K d K d K d K d K d K d d K d k d K d K d K d K d K d K d d K d k d K d K d K d K d K d K d d K d k d K d K d K d K d K d k  kaAAaAAaAAaAAaAAaAA A  !! A A A A A A ! A ! AAA ! ! ! !A ! ! ! !AA ! ! ! ! ! A Adkkdkkdkkdkkdkkdkk c dD d d Ddd D d D d dlllllllllLLLLLLLMMMM----lLLLLllllllllllllllDDDCCCCDDCCCCCBBBBCCBBBBBAAAABBAAAAABBBBAABBJJJJ````````''z~jnoj^_Z^OJNO:>?:E''z~jnoj^_Z^OJNO''NJJJJ c d d d d d d d d c r r r r r r r r r r r r r r r rcC C R R R R R R R R R R RQRRRRR R R RRSRSSR!!83 ''''''''''''''''''''603030U33333 0330\3 333Y[U03]՝SUUS YccSUUI:ʻ̖\͐ccC I̓Y˕\̐řU Yɕ3U\ YYݕŜU\ 33U̼ YUU 333ikfͼ Y̪ iff 0033髪̼ fff 骪 03Uܽ UU 566UU5 3̕] 0Y̕U̕] 4::< Y˕030řU Yɕ̜U\ I6ƻ͓03330̣C Icc͓303YUY̕Y[U0330333SUUccS YSUU3033YU̜ 03330 033fff̜00030033̜033U5U0YUݝ00\\00\Yݕ]՝]՝3̜U\ 0\\3466< 0\\35::UU5 03YY030333 YY033030YY0303 UU030ET \\93 3̥V3 ]՝]՝\ j̥Vj \\ܙ] 3\V3 \\ܕ] jj YY]\ 3Y̕3 YY]\ \\ YYܕ3 \\ ɦ Y̕\ 3\eZ3 ՙ̙\ \eZ̦ Ŝ\ 3\eZ3 3Y3 EiT i0i0 UU3\\3303]՝]՝303\\3333\\0333YY03YY300YY0UU\\0]՝]՝\\\\YYYYYYUUUU\\\\]՝]՝]՝]՝\\\\\\\\YYYYYYYYYYYYUU\\]՝]՝\\\\YYYYYY 6 j9 36AjKIK ItGDffIIDifIDF49I3 6:jIc 9A3yc 6 jiic iic i9c 9 939f3y 39D ,` 8=C?MG,0'"ffDxHQkC}U|wzNqH 8=C?MG,((>=>=:=<=====<>???01247;???????????????????????77/ L L L L LLLLLLLL                  mmmmMmmmmmmmmmmmmmmmmmm$$$$$$$$$$$$$$$$HhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHHhhHhhFFFFFF$BCBBCBCBBBBCCCCBCBBCBCBBbbCCfFHHHHHHhHhHhHHhHhHhHHhHhHhHHhHh(h(HH(HH(HH(HHH(HH(HH(HHHHHH&&&&&&$BCBBCBCBBBBCCCCBCBBCBCBBbbCCHhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHhhHHhhHhh.1QQ(QL*a%2=JZ$J70F7j99LGG36BZJ` dwwwwߛh$`ͫxwwwݬ2TvͫgE$p<@ͼicGA """"""""""""""""""""w""xxx(""Bw""""""""""""""""""""""""""""w43""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""$"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""f"""""""""""""""""""""ffffkh"""""""fff(f"""""hkkf""""k"""fhfkxxxx"""#fffhkkkxxxxqwwkfxxxxwwwfxxxxwwwxxxxwww"""q"""B""""ffffffffwwWUxxxx4D4""""fffffffff("2w""""ffffffffffff(2"84ffffffffffxw"""""""""("(ffffffffwfff""""""""""""""""""""&"""f&""fff""""""""""""""""""""""""""""""""""""""""""""""""""""""""""ffkW8"2"""""""""""""""""""""ff"""a(""c""""""##"2223#3#ffffffffffffb#kk2A3CfffffffffffffkkkkfiTfffffffffffffkkkkfffffffffffffkkkkfffffffffffffkkkkfffffffffffffkkkkwwwwwwwwwwwwwwww xww);ww CwwDwwswwwwwwww47w sGwftwgsGwwwpwwwpwwpwwvwwwvwwwwwwwwwwwwwwwwwwwwwffwwgfwwwwwwwwwwwwwwxwWUuUUffffffffwGwwwwwWWWWUuUUffffffffwwwUuwUfffffffUuUffkkk̙Ak̪CCCwtUuUWWfGAAwEDtDVWWfwwWufVW̬k   (   BAQ D DAD{wWWWWWwWWWWWWItݙݙ    "   @$BD@D @zZJFCJ4DDCwtwAswwwwwwwtwwC4wgwgwgfwewGwiffkZzUuUZWWWZuUUiffffffffk̙ݪݩݩݪݪ @ @@ @  0 " "D ""  BDAJDJ"L>>>>>>>>>>>>>>>>>>>?CDKLLMMMMNNNNNNNNNNNNNNNNNNNNOST[\\]]]]^^^^^^^^^^^^^^^^^^^^_mmlllllkkkkkjjjjjiiiihhhhggggf{ apa p0 @P```ppppppppppppppppppp ``aabbflmmmmmmmmmmmmmmmmmmmmnosppppqqqqrrrsstx}}}}}}}}}}}}}~0ppppqqqqrrrsstx}}}}}}}}}}}}}~0`pb0filnnoooooooooooooooooooooo  pP0А |Аz`aeikkkkkkkkkkkklllllllmmmmnnor 0000@@@@ 000Pqd`abbddhnoooooojiiiijjklnnnnno T 0000@@@@ 000PAo Ppp  p  Ppp  p A!JJ&&FFFFFF @``  `  @``  `  N . N . N . . N . . N N N N N N n N N N N nn  nJ&&FFFFF N N N L M N l N N N N n m N N L Nn3@P` _wOqP@g3G|R8@<8x{xtZ:`JlHX hx8lHlRYc8@}{$,!&?HHX hx8 UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUzwwwwwgygygfw ww ww gy gy fy  fi3 676cff 6ݍݻݽݽݽݽȻ]]ݽ]]]]" ""Rqp!""" p'   " ٛxy**"Ҫ"r"%Rٙ*wUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUggygywwiy gy gy ww f iI6i I ݽؽݻ݈ݍ݌UUU]ݽ]]XݍXݍXXUUUU!! '"""""""w'! ) "  """"""y+***򯙪ٚUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUww'{//{/"'{w'"w/+"UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUDG@D DGHiHt@w@t Hwy @3UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUwzzwwwzzwzqqUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUwwwwzzwwwwwwwwwzw?wzwzwwwwwzzzwzwwwwzwwwwwwwwzw,{ww("w--"ww'/'"w߮/"UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU iG9GDGDG7 fIw HIwwHtHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUwwzzwzwwqqzwwzqqqqqqqqUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUzwzwwwwwwwzwwwzwzwwzwzwwwwzwzwwzwwzwzwwwzzwwwwwwwww-]"w%(*-""""}/'"ׯw/w**""""UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUGGGGGGGGHtHDDDttttDDDDDDDDDDDDwwDOwxttHGwttHHtHxwzwwwzwwwzzwwwwww fiffifffffff||\\\Ūڬ̬ܪ]UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU DDDDkkkkDDDDUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUGHGHGGGHGHGGHDHtHDDDDtxwHDHDGHtHHHHtHwHtHwUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUDODDOD DDDDDD __DOETT_DU__EEDPDTDPUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUDDDDPUDDDD, ,\kkkkkkUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUDDDDHtHwHtHwHtHHtHwDHGHDGDGDHHtHDDttDDDDDDwwwwwwzzwwzwzwwzwzzwzwwwwyy wwywwwywwzwwwywwywww  UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*ww{w'ww'"{/w/}:"}**ww*?*'w7#"{7#({7*w*""-wwwwwzwwzwzpppzwwwwHHDHDHGHHwHtHwHHGHtHDGtGGxwOGDtwDDDDttxwDDDDttwwzywzwzw ffi6`s iif ccwyyywwyyUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU}'}z'}{'{{_w{:S7:ZUUUU"x]zSWxZW}ZZ*]ZUUUUU7333?3f37cf6ywyyzz333wfc3w333wwwywyzwzDHGHDGDHGHDGHHwHtHwHHwHtHw  wzw zwi3i y`3 i3 i p y ywwyzwwwpwwUUUUUUUUUU\UU\UUUUUUUUUUU\\UUUUUUUU\UU\UU\UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUwzGDDDGfdDJDDDwywywwzzwwwwwwwzwwwzw3zwzzQUwwwUUUwwUQww7UQ:c233j233jbfffbfff"""")"""YYuuu33uuuwzwUUUUEDDDDU\\\UUUU  UUUUwzwwwzwwzzwwwwwwwwwwzw wpzw y y wwyzppzpwwwp zww zzwwzwwwzwUU\UUUUUU\\UU\UU\U\U\UU wwwwwwwwwwwzwwwzwwwwwwwwwwwwzwwwwwwwwwzwzzwzwzwzwzwwwwwwzwwzwwzwwzwwwwwwwHD΄DȌDȌDDDDNNHDLHDHHDHHDHDDDHDDDHDDDHDDDD̄DDDĎDDDDDDDDHDDDLD\D\DD\DDDDDDDDDDDDDDDDDDDDDLDDDHDDDHDHDHDHLHJNNDDADD ) )   )UUUU\UUU\UUUUUUUU\\UUUUUUUUUU\UU\U\UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUwwwzwwzzwwwzwwwwwwwzwwwwwzwzwzwwwwzwwwzwwwwwwwwwwwzzwwwwwL̄DDDUUXDDDHDDDHHUȈ̄DDDDDDĎDDDDDDDDDDHDDDLDD\DD\DD\DDDDDDDDDDDDUDUD̎LHLHLHLHLHLHLHLHŪŪʪŪŪꮪUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUwwwwwwwwwzwzwwwzwwwwzwwwwwwwww  wwwwwwwwwwwzwzwwwwwwwwwwwwwwwwwzwzwwwwwwwwwwwUXLȞUXLHUȈDDĈÜDDĈȄDDDDDĎDDDDDDDDDDDDDLDD\DD\D\DDDDDDDDDDDDDDDDDDDDDDDDDDHDDDHDDHDHDDHLHLHLDH wwwwwwwwUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUwwwwwwwwwwwwffffffffffffffffUUUU""%Rwwwwwwwwzwwwwwwwwwww wzwwzwwzwwwwwzzwwzwwwwzzwwwwwwwwwwwwwzzzw̌UXLȞUXLHÜDDĈÜDDĈȄDDĎDDDDDDDDDDLDD\D\D\DDDDDDDDDDDDDDDDDDDDDDDDDDDHLHLDHLHDDDHDDDHHDDDDUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUʪUUUU"""R"""""""""""%R""%R"%%UUUUUUUUwwwwwwzzwwzwzwwzwzzwzwwwwwwwzwwzwwwww̌̈XUU\HUUUUUUUUU\\ň\LUUEUUU\UUU\UUUUUUUUU^\UXLX\U\U\UX\UUUUUUXǓU\UUUUUUUUUUUUUUUU\U\UUU\EẌXUTUXUUXUU\U\\\U\UUUU\UUǓNNNNHNHNDUUUȌȌĈ̌\̄DNDNDLD̈ȈHHHDHĄDNDDNDD̈̈UTUŎDN\HĈHHHĈHĈDĈNĪeUQUUZQU6asUUUUUUSwzwwwzwwwzzwwwwwwwzzwwwwwwwzwwwzwUUUUUUNUEXȈHX\XXUTUDňĈDDȌHDHU\XUUU\UUU\UUUUUUUUUUUUUNTHLXXUUUUUUUUUUUUUUEUUXUELȅX\UUXEȌ\\UUUUUHUΌĈXXUUUUUUňDȌ̄DH\\_ΎTTĎTXXXXX\\\\NNNώNĎ΄XXX\\\ȈNDČNDTĄH^΄TTĄTXXX\\\^X̏DD^XńHX\U\\UHDDȌDD\\U3eVZUQUUQVQUUVUD@N@O 'r D`c63f3DADADADDD@30)@3)r@r3 ;3;@30@3A@3 D;3;if9yi ffIDI3 Ic c ic jif9yi ffIDI3 Ic c ic 9 9cc鞪iff D : : 6 D@@ "@3)r)93I3;3 @3A93I3D;3 ii993939ic 9c f3y D ii993939ic 9c f3y D 933933933 333 D33 D3 D3  DIDI IID II  III9A6IA9AA 3j3 9969996999 99 9j939j1393jf99ciAj 3 jf6I9i393jf9iij9C 3 9f33AAy9C69A6 AA43j3j 9FDy9C69A6 4HDAA43j3j 6:i9c9i6i:9ccj6iic9 6jf96fc996i9cci9ii3c:3 III6A9IA6AA j3j 9999699969 99 939j931j93jf99ciAj 3 jf6I9i393jf9iij9C 3 9f33AAy6C96A9 AA4j3j3 9FDy6C96A9 4HDAA4j3j3 9:i6c6i9i:6cjc6ic9c 9jf66fc699i6cf9f3:3f  Ej̥3̥j\3 T Vj V3 Vj 3 {wyj3I y w j 3 j 3Oj3j 3 j 3 ៩93jff6cf3fAj 3 fjfcf6J:f36 9 6A j3jKj 3Oj3閐ij 3 j i3 i 3a@ODjY͐̐̐Y͐ՙ͐Ŝ͐6 ̕j \\ \\ ̕\ ̙\ \ Y6 j)3j3)j39j j 3 j 3 j 3 j 9:cc69)6 9:i6339)񙙙6 6:f96c9i6ij:9cfcjj6jcjc6 IItIIF69A6 KGDD4:j3j 9:9c66 91:9j919)񙙙6 IDIININN E3̥j̥3\j T V3 Vj V3 j {wy3jI y w 3 j 3 jO3j3 j 3 j ៩93jff6cf3fAj 3 fjfcf6J:f39 6 9A 3j3K3 jO3j閐i3 j 3 ij i 3a3Y͐̐̐Y͐ՙ͐Ŝ͐3 ̕3 \\ \\ ̕\ ̙\ \ Y3 3)j3j)3j93 3 j 3 j 3 j 3 9:cc69)3 9:i6339)񙙙3 9:f69c6i9ij:6cfjjc6jjc6c IItIIF96A9 KGDD4:3j3 9:9c63 91:9j919)񙙙3 OO@D@ DD@@OODODO { p {w{ p pw {w{ pp wpwpwwwpwwwwwwwwwwwwwwwww{w{ pp w {w{ p pw { p ID IDD IDD DI { p {w{ w pp {w{wp p pwwwwpwwpwwwwwwwww{w{wp p {w{ w pp { p JJJJJJJJJJJJJ,-no*no*--,-ႃ,-,-,-,,-,-,-,-,-,-,-,,-JJJJJJJJJJJOOOOOOJJJJOOOOOOJJJJ<=~:~:==-=HH*HH*<=<=<=<<=pq,-<=<=<-,-NNNNNNNNNNNNNNNNNNNNϜJJJJ______JJJJ______JJJJ,HH*,--HH*ᖖHH*,--HH**HH:HH:ႃ,--,-,-<=<=,,-<=<=^^^^^^^^^^^^^^^^^^ߜJJJJnonoJJJJnoJJJJ:<=*<=<=<=<:<=<==:==<=:=<=X:Ӕ:ѰX:43 3j3j3j3j0jjCj3j3f6cf3333U5SUU5S553355S3U53S5ffff3333UUUUeUUeefffUUUU333S^BUU"UU'"YyUUUUUUUUUrDDDAD$DUUU%""UET"vBUBUV)U DW)D"Gv'BGnBGG"GG"G%"G"BG$"DEDDD""D$DBD$v"D$n)DG)BG)B%)B)DGDDDGD'W')' @"%% $$V D$[ DDDD IT )""PD`%afff;3;f;fffffff33f33fffffffffff33f33ffffffff111131f1f1$'y"$'iU$'Y"$%P"%P%@%@ "yEr"y"Y P P P @ ;f;f݁c݁c݁c݁c;f;ffffffaaaffaffaffafffffffffffaffaffaffaffafffff1f161616161f1f1f03`33c3f0663630c06366063I@FDBGDBEDDEttRDDDpDpDBDBDDDDDDDDDDD DDD ;f;f݁c݁c݁c݁c;f;fffffaffaffaffaffafaffffffffffaffaffaffaaffffff1f161616161f1f1c000333606f3c3f063633f33pD@DBDGDGDFDDDFTgTqDDD DDD DDDD$IDDIDTBuDDvU%;f;f;3fffff33f33fffff33f33f1f1311111a$"" @ T@T@t@t@T@.""RN@@AT@NtP.tTtDDDDEDDDEDUUEEDDEDTUEDDEDDDDDDDDUUUUDDDDUUUUDDDDDDDDDDDDUUUUDDDDUUUUDDDDDDDDDDDDUUEDPDDDTDEUTDDDTDPDD"""DDDDE"DBD%TBEDDBDDTBEDDBDDTB""""DDDDDBDBTBTBDBDBTBTBDBDBTBTB""""DDDDDBDBTBTBDBDBTBTBDBDBTBTB""""DDD$DB"$TBE$DBD$TBD$DBD$TBD$@B@B$ $ B@B@ $ $@B@B$ $ B@B@ $ @B@B$ $ B@B@ $ $@B@B$ $ B@B@ $ D""""DDDDDBDBTBTBDBDBTBTBDBDBTBTB""""DDDDDBDBTBTBDBDBTBTBDBDBTBTBt@t@ t@ d@T@Vn&UdtP-t@Nd@ATPAN-&^U&"EDDEDTUEDDEEDDEDTUEDDEDDDDUUUUDDDDDDDDUUUUDDDDDDDDUUUUDDDDDDDDUUUUDDDDDDTDUUTDDDTDPDDDTDEUTDDDTDPDEDDBDDTBEDTBDDTBEDTBDDTDEDDDDDDDDBDBTBTBTBTBTBTBTBTBTDTDDDDDDDDDDBDBTBTBTBTBTBTBTBTBTDTDDDDDDDDDDBD$TBD$TBD$TBD$TBD$TDD$DDD$DDD$@B@B$ $ B@B@ $ $@B@B$ $ B@B@ $ $@B@B$ $ B@B@ $ $@B@B$ $ B@B@ $ $DBDBTBTBTBTBTBTBTBTBTDTDDDDDUUUUDBDBTBTBTBTBTBTBTBTBTDTDDDDDUUUUm&^~WU&!&A^AWAAAAUAEDDEDTUEDDEEDDEDTUEDDEDDDDUUUUDDDDDDDDUUUUDDDDDDDDUUUUDDDDDDDDUUUUDDDDDDTDUUTDDDTDPDDDTDEUTDDDTDPDEDDDDDDBEDDBDDTBEDDBDDTBEDDBDDTBDDDDDBDBDBDBTBTBDBDBTBTBDBDBTBTBDDDDDBDBDBDBTBTBDBDBTBTBDBDBTBTBDDD$DBD$DBD$TBD$DBD$TBD$DBD$TBD$"233R""R##"U%"RU%"UU%RU%%UU"UU%"33#"U%"%U"2"%"R5""U5"RU5"UU2RU%2) FDBGDBEDDEtDRDDDpDDDpDDD""D$ D$ D$IDGIDDDDDD DDD :"3&12:#:3""j#!"1&"*:2c"b#"1#""2"26UBD^BUUAUQAUQAUQAAA-D^UUU%EDDEDTUEDDEEEDDDEDDDUUUUDDDDUUUUDDDDDDDDDDDDUUUUDDDDUUUUDDDDDDDDDDDDUUUUDDDDUUDDDDDD@D@DDDDDDDDDUUUEEDDBDDTBEDDBEDTBE"TBEETDEDDDVTTTDBDBTBTBDBDBTBTBTBTBTDTDDDDDTTTTDBDBTBTBDBDBTBTBTBTBTDTDDDDDTTTTDBD$TBD$DBD$TBD$TB"$TD%$DDD$TTT$UU""V%"RV""U%"RU&"UU#SU%RRU""RVUUU"2U%"2U""5%"R5""U5"R5""U%%UU%"pDDD@DDDBDDDGDDDGDDDFDDDFTgTqDDD DDD DDDDDDDDDDDDuDDvU%:3"3"j#"*:3b:c:232"b3"2#"f22*D@N@O 'r D`c63f3DADADADDD@30)@3)r@r3 ;3;@30@3A@3 D;3;if9yi ffIDI3 Ic c ic jif9yi ffIDI3 Ic c ic 9 9cc鞪iff D : : 6 D@@ "@3)r)93I3;3 @3A93I3D;3 ii993939ic 9c f3y D ii993939ic 9c f3y D 933933933 333 D33 D3 D3  DIDI IID II  DADADDOOOADDADDAAADADAONOAAAAc00000c0`00r'"www""W"""""" )5U")"ww%w'%)())()U)""""wwww""""RR""RRw""r"""""wwww""""""%%w%%%"%"" )3U""wywyRrwRrYU "wU """"""""AAADDNAAAOADOAOODOAO@A@@@@@@00030`000""""W""w""RUx)98)ii)#Y94 ""r"RRRURR""""""UUUU"%""U%%%""%%""""UUUUww299))9CI """""""U U 993i399i9 3 3 ? 9i;III9A6IA9AA 3j3 9969996999 99 9j939j1393jf99ciAj 3 jf6I9i393jf9iij9C 3 9f3399F39j93 969d99a93 f 03 AAy9C69A6 AA43j3j 9FDy9C69A6 4HDAA43j3j 6:i9c9i6i:9ccj6iic9 6jf96fc996i9cci9ii3c:3 i939399i9 3 3 ? 9i;III6A9IA6AA j3j 9999699969 99 939j931j93jf99ciAj 3 jf6I9i393jf9iij9C 3 9f339F9j939j 699d9a99j 93 f AAy6C96A9 AA4j3j3 9FDy6C96A9 4HDAA4j3j3 9:i6c6i9i:6cjc6ic9c 9jf66fc699i6cf9f3:3f 9Ej53]]Y < T S j Z 3 Z Z {wyj3I y w j 3 j 3Oj3j 3 j 3 ៩93jff6cf3fAj 3 fjfcf6J:f36 9 6A j3jKj 3Oj3閐ij 3 j i3 i 3a 999i999i9 3 3 ? 9i;P\YP\YY]̕P\\IP\YjY\ZZZY YYŕj j)3j3)j39j j 3 j 3 j 3 j 9:cc69)6 9:i6339)񙙙6 6:f96c9i6ij:9cfcjj6jcjc6 IItIIF69A6 KGDD4:j3j 9:9c66 91:9j919)񙙙6 9j93 99d9993 f 03 IDIININN9E35j]]Y < T S 3 Z j Z Z {wy3jI y w 3 j 3 jO3j3 j 3 j ៩93jff6cf3fAj 3 fjfcf6J:f39 6 9A 3j3K3 jO3j閐i3 j 3 ij i 3a i999999i9 3 3 ? 9i;P\YP\YY]̕P\\IP\Y3Y\ZZZY YYŕ3 3)j3j)3j93 3 j 3 j 3 j 3 9:cc69)3 9:i6339)񙙙3 9:f69c6i9ij:6cfjjc6jjc6c IItIIF96A9 KGDD4:3j3 9:9c63 91:9j919)񙙙3 99939j 99d999j 3 f ` `` { p {w{ p pw {w{ pp wpwpwwwpwwwwwwwwwwwwwwwww{w{ pp w {w{ p pw { p ID IDD IDD DI { p {w{ w pp {w{wp p pwwwwpwwpwwwwwwwww{w{wp p {w{ w pp { p Zʆ ʆ ˖ʖNONONONONONONONONONONONONONONO ۦڦ^_^_^_^_^_^_^_^_^_^_^_^_^_^_^_ 0 ˶˶ 000 0ʐ`a@A @A`aچ 00000 pqPQPQpq ۖ00ېʆˆʦ@A@A@A@A@A@A@A 񆇈 ږۖ PQPQPQPQPQPQPQ 񀁖 ː@A릧릧@A ʀ&'ڐPQPQņɆڐᶷ `aʀ@A@Aʀ`aնٶ a` pqڐPQPQʐpq͆@A@A ۀqpʐ00000ݶPQPQ0000뀁džɆ׶BCCDٶ @ART@A %&'(PQRTPQʐ0000000''''''''_G6]&9B)ZQ 46CJ9Z9RJUNF&&B last_level then saved_level = last_level end return saved_level end -- Fader Class for screen transitions local Fader = {} Fader.__index = Fader local _orig_vbank = vbank _G.current_vbank = 0 vbank = function(b) if b ~= nil then _G.current_vbank = b end return _orig_vbank(b) end function Fader.new() local self = setmetatable({ fading = false, dir = 0, percent = 0, spd = 0.05 }, Fader) return self end function Fader:fade_in(spd) self.percent, self.fading, self.dir, self.spd = 1, true, -1, (spd or 0.05) end function Fader:fade_out(spd) self.percent, self.fading, self.dir, self.spd = 0, true, 1, (spd or 0.05) end function Fader:update() if not self.fading then return end self.percent = self.percent + self.dir * self.spd if self.dir == 1 and self.percent >= 1 then self.percent = 1 self.fading = false elseif self.dir == -1 and self.percent <= 0 then self.percent = 0 self.fading = false end end function Fader:apply(banks) local target_banks = banks or {vbank()} -- Use current bank if none specified local old_bank = vbank() -- Only apply transformation if we are actively fading, or if we are fully black (percent = 1) if not self.fading and self.percent <= 0 then -- Ensure mapping is clean 1:1 on normal state for all target banks for _, b in ipairs(target_banks) do vbank(b) for i=0,15 do poke4(0x3FF0*2 + i, i) end end vbank(old_bank) return end local p = self.percent for _, b in ipairs(target_banks) do vbank(b) -- Ensure palette mapping is 1:1 so our RGB edits affect logical indices correctly for i=0,15 do poke4(0x3FF0*2 + i, i) end -- Real-time RGB Scaling: Read from RAM (set by loadPalette) and scale down -- IMPORTANT: This works only if the bank's palette has been reset to "original" -- this frame (e.g. by loadPalette) BEFORE calling apply. for i=0,47 do local original_c = peek(0x3FC0 + i) local dimmed_c = math.floor(original_c * (1 - p)) poke(0x3FC0 + i, dimmed_c) end end vbank(old_bank) -- Restore original bank end -- Initialize Fader Game.fader = Fader.new() Stage={ player={}, enemy_container={}, player_created=false, enemy_total=0, finishing_timestamp=0, tank_coordinates={},--1 for player --do not record bullets for now destructed_tank_count=0, created_enemy_quantity=0, current_wave_index=1, wave_created_count=0, wave_destroyed_count=0, -- Track destroyed enemies in current wave is_spawning_wave=true, -- Starts by spawning the first wave start_time=0, last_spawn_time=0, -- Track last enemy spawn time } function Stage:new(obj) local stage=obj or {} -- ensure each level instance has its own table to prevent data leakage stage.player = stage.player or {} stage.enemy_container = stage.enemy_container or {} stage.tank_coordinates = stage.tank_coordinates or {} stage.top_layer_tiles = stage.top_layer_tiles or {} -- Explicitly initialize wave-related states stage.player_created = stage.player_created or false stage.current_wave_index = stage.current_wave_index or 1 stage.wave_created_count = stage.wave_created_count or 0 stage.wave_destroyed_count = stage.wave_destroyed_count or 0 stage.destructed_tank_count = stage.destructed_tank_count or 0 stage.created_enemy_quantity = stage.created_enemy_quantity or 0 stage.item_timer = stage.item_timer or math.random(2*60, 5*60) stage.active_item = stage.active_item or nil stage.player_shield_timer = stage.player_shield_timer or 0 stage.base_shield_timer = stage.base_shield_timer or 0 stage.player_bullet_timer = stage.player_bullet_timer or 0 stage.player_sniper_timer = stage.player_sniper_timer or 0 stage.player_shotgun_timer = stage.player_shotgun_timer or 0 setmetatable(stage,self) self.__index=self return stage end local function reset_game_session() Game.title_started = false Game.title_sync = false Game.session_started = false Game.mode_5_sync = false Game.mode_6_sync = false Game.mode_4_sync = false Game.current_stage = 1 Game.ingame = false Game.is_game_over = false Game.gameover_enter_time = 0 Game.stage = Stage:new() Game.next_mode = nil Game.shake = 0 vbank(1) cls(0) vbank(0) cls(0) Game.mode = 0 music() -- Stop all music when returning to title end local Movable={ x=0, y=0, vx=0, vy=0, direction=0, rotate=0, size=0,-- as w always equals h explosion_timestamp=0, } function Movable:new(obj) local movable_object=obj or {} setmetatable(movable_object,self) self.__index=self return movable_object end function Movable:dir_to_rotate() if self.direction==1 then self.rotate=2 elseif self.direction==2 then self.rotate=3 elseif self.direction==3 then self.rotate=1 elseif self.direction==0 then self.rotate=0 end end -- Helper to check flags local function hasFlag(x, y, flag) local tile = mget(x // 8, y // 8) return fget(tile, flag) end local function isSolid(x, y) return hasFlag(x, y, Game.CONFIG.FLAGS.SOLID) end -- Used for determining if bullet explodes local function isDestructible(tile) return fget(tile, Game.CONFIG.FLAGS.DESTRUCTIBLE) end local function isBulletBlocker(tile) return fget(tile, Game.CONFIG.FLAGS.BLOCK_BULLET) end local function isBase(tile) return fget(tile, Game.CONFIG.FLAGS.BASE) end local function isStrong(tile) return fget(tile, Game.CONFIG.FLAGS.STRONG) end local function stage_builder(current_stage) local stage_coordinate=Game.LEVELS[current_stage].map_pos local stagex=stage_coordinate.x local stagey=stage_coordinate.y for x=0,Game.screen_columns-1 do for y=0,Game.screen_rows-1 do local mirror_x=x+stagex local mirror_y=y+stagey local tile=mget(mirror_x,mirror_y) if tile==255 then tile=0 end mset(x,y,tile) --draw the map for the current stage end end end -- classes local Bullet=Movable:new({ size=8, speed=1.5, exploding=false, explodable_coordinates={}, type="bullet", fired_by=0, silent_removal=false, -- If true, bullet disappears without explosion animation }) function Bullet:new(obj) local bullet=obj or {} bullet.type = "bullet" -- Crucial for collision logic branching bullet.explodable_coordinates = {} -- Ensure instance-level list bullet.lifetime_counter = 0 bullet.is_shotgun = bullet.is_shotgun or false setmetatable(bullet,self) self.__index=self return bullet end function Movable:dir_to_speed() self.vx=Game.movement_patterns[self.direction+1].x*self.speed self.vy=Game.movement_patterns[self.direction+1].y*self.speed end function Bullet:explode() if Game.stage.player and self.fired_by == Game.stage.player.tank_id then sfx(62,'E-1',-1,2,15,0) end -- explode an adjacent tile of the same type for i = 1, #self.explodable_coordinates do local coord = self.explodable_coordinates[i] local tx, ty = coord.x//8, coord.y//8 -- Prevent bullet explosion from modifying tiles outside the visible screen (0-29) -- which would corrupt map data stored for other levels. if tx >= 0 and tx < 30 and ty >= 0 and ty < 17 then local tile = mget(tx, ty) if isStrong(tile) then -- For Wall B (2 hits): Change to a "damaged" state (first hit) mset(tx, ty, tile + 1) else -- Fully destroyed: place ruins sprite (171 or 159) for DESTRUCTIBLE tiles, -- or clear to empty (0) for non-DESTRUCTIBLE tiles (e.g. base) if isDestructible(tile) then local ruins = math.random() < 0.5 and 171 or 159 mset(tx, ty, ruins) else mset(tx, ty, 0) end end end end end -- Check for bullet-to-bullet collision function Movable:bullet_ahead() if self.type ~= "bullet" then return false end local s = self.size local x1, y1 = self.x, self.y local x2, y2 = x1 + s, y1 + s for _, other_bullet in ipairs(Game.bullets) do -- Don't check against self, and don't collide with sibling bullets if other_bullet ~= self and other_bullet.explosion_timestamp == 0 and other_bullet.fired_by ~= self.fired_by then local ox1, oy1 = other_bullet.x, other_bullet.y local ox2, oy2 = ox1 + other_bullet.size, oy1 + other_bullet.size -- AABB collision check if not (x2 < ox1 or x1 > ox2 or y2 < oy1 or y1 > oy2) then -- Collision detected! Both bullets disappear immediately (no explosion) sfx(53,"C-4",50,3,15,-1) -- Bullet collision sound other_bullet.silent_removal = true other_bullet.explosion_timestamp = Game.time + 999 -- Force immediate removal self.silent_removal = true return true end end end return false end function Bullet:update(id) self.lifetime_counter = self.lifetime_counter + 1 if self.is_shotgun and self.lifetime_counter > 30 then self.silent_removal = true self.explosion_timestamp = Game.time end -- Check if bullet is out of bounds if self.x < -16 or self.y < -16 or self.x > Game.screen_width + 16 or self.y > Game.screen_height + 16 then self:on_remove() table.remove(Game.bullets, id) return end if self.explosion_timestamp == 0 then if self:collision_ahead() or self:bullet_ahead() or self:tank_ahead() then self.vx=0 self.vy=0 self.explosion_timestamp=Game.time else spr(self.sprite_id or Game.CONFIG.SPRITES.BULLET, self.x, self.y, 0, 1, 0, self.rotate, 1, 1) end end if self.exploding==true then self:explode();self.exploding=false end if self.silent_removal and self.explosion_timestamp~=0 then -- Silent removal: immediately remove without animation self:on_remove() table.remove(Game.bullets,id) return elseif Game.time-self.explosion_timestamp>=8 and self.explosion_timestamp~=0 then self:on_remove() table.remove(Game.bullets,id) return end self.x=self.x+self.vx self.y=self.y+self.vy end function Bullet:draw_explosion() if not self.silent_removal and self.explosion_timestamp~=0 and Game.time-self.explosion_timestamp<8 then local offset1=self.size local offset2=4 -- to align different sprites local elapsed = Game.time - self.explosion_timestamp local anim_offset = (elapsed // 2) * 2 local sprite_id = Game.CONFIG.SPRITES.EXPLOSION_SMALL + anim_offset if self.direction==1 then spr(sprite_id,self.x-offset1+offset2,self.y,0,1,0,self.rotate,2,2) elseif self.direction==3 then spr(sprite_id,self.x,self.y-offset1+offset2,0,1,0,self.rotate,2,2) elseif self.direction==0 then spr(sprite_id,self.x-offset1+offset2,self.y-offset1,0,1,0,self.rotate,2,2) else spr(sprite_id,self.x-offset1,self.y-offset1+offset2,0,1,0,self.rotate,2,2) end end end function Bullet:on_remove() local shooter = nil if Game.stage.player and Game.stage.player.tank_id == self.fired_by then shooter = Game.stage.player else for i = 1, #Game.stage.enemy_container do local enemy = Game.stage.enemy_container[i] if enemy.tank_id == self.fired_by then shooter = enemy break end end end if shooter and shooter.flying_bullets > 0 then shooter.flying_bullets = shooter.flying_bullets - 1 end end -- ================================================================ -- Particle system (sand dust trailing effect, ported from tank_warface.lua) -- ================================================================ PTCEVENTS = {} PTC_TOTAL_CNT = 0 function ptcInit() PTCEVENTS = {} PTC_TOTAL_CNT = 0 end function newPtcEvent(scheme, x, y) local evnt = { update = scheme.update, dead = false, pts = scheme.init(x, y), } evnt.cnt = #evnt.pts table.insert(PTCEVENTS, evnt) end function updatePtc() if #PTCEVENTS == 0 then return end PTC_TOTAL_CNT = 0 local i = 1 local n = #PTCEVENTS while i <= n do local evnt = PTCEVENTS[i] evnt.dead = true for j = 1, evnt.cnt do local ptc = evnt.pts[j] if ptc then PTC_TOTAL_CNT = PTC_TOTAL_CNT + 1 evnt.dead = false if ptc.t > 0 then evnt.update(ptc) ptc.x = ptc.x + ptc.vx ptc.y = ptc.y + ptc.vy local sz = math.max(1, math.floor(ptc.sz)) rect(math.floor(ptc.x), math.floor(ptc.y), sz, sz, ptc.color) end ptc.t = ptc.t + 1 if ptc.t > ptc.life then PTCEVENTS[i].pts[j] = nil end end end if evnt.dead then table.remove(PTCEVENTS, i) n = n - 1 else i = i + 1 end end end function sandmaker(angle_min) local pi = math.pi return { init = function(x, y) local cnt = 4 local v = 1 local pts = {} for i = 1, cnt do local d = math.random() * pi / 4 + angle_min local ptc = { x = x + math.random(-2, 2), y = y + math.random(-2, 2), color = 3, -- palette lookup index vx = v * math.cos(d), vy = v * math.sin(d), life = math.random() * 6 + 12, sz = 3, t = math.random(-6, 0), } table.insert(pts, ptc) end return pts end, update = function(ptc) ptc.vx = ptc.vx * 0.8 ptc.vy = ptc.vy * 0.8 ptc.sz = 3 - ptc.t / ptc.life * 2 end } end -- Four directions of sand dust schemes (angle points in the opposite direction of tank movement) local _pi = math.pi -- These need to be accessed during the initialization of PTCEVENTS and cannot be declared globally but are placed in the same file local ptc_sand_up = sandmaker(_pi / 3) -- Moving up -> dust scatters downwards local ptc_sand_down = sandmaker(4 * _pi / 3) -- Moving down -> dust scatters upwards local ptc_sand_left = sandmaker(11 * _pi / 6) -- Moving left -> dust scatters right local ptc_sand_right = sandmaker(5 * _pi / 6) -- Moving right -> dust scatters left local Tank=Movable:new({ id=0, lifetime=0, destruction_timestamp=0, animation_time=71, -- Star animation duration (approx 1.2s) shoot_interval=0.6*60, --normally you are allowed to shoot every 0.6 sec last_shoot=0, speed=0.5, hp=1, max_bullets=1, bullet_speed=1.5, hit_timer=0, flying_bullets=0, cd_mode=false, created_at=0, can_move=false, direction=0, -- rotate parameter for spr movement=Game.movement_patterns[math.random(1,4)], -- shooting_range=10, size=16, --both length or width tank_id=0, destructed=false, gone=false, last_direction=0, -- For anti-backtrack (matches initial direction) decision_timer=0, -- For periodic direction change decision_threshold=180, -- Random threshold for next decision possible_directions={0,0,0,0}, -- Track blocked directions (0=open, 1=blocked) stuck_counter=0, -- Track how long tank has been stuck decision_cooldown=0, -- Cooldown between wall-hit decisions }) function Tank:new(obj) local tank=obj or {} setmetatable(tank,self) self.__index=self return tank end local PlayerTank=Tank:new({x=Game.player_generation_location_x, y=Game.player_generation_location_y, id=Game.player_model, control_sequence={},--to store key sequence moving_v=false, moving_h=false, tank_id=1, is_moving=false, shoot_interval = 10, --player can fire much faster to close targets max_bullets = 1, speed=0.5, bullet_speed = 1.5, type="player"}) function Tank:timer() if self.created_at==0 then self.created_at=Game.time else self.lifetime=Game.time-self.created_at end end function Movable:tank_ahead(check_dir) local dir = check_dir or self.direction local vx = Game.movement_patterns[dir+1].x * 2 local vy = Game.movement_patterns[dir+1].y * 2 local s = self.size - 1 -- My prospective bounding box local x1, y1 = self.x + vx, self.y + vy local x2, y2 = x1 + s, y1 + s local is_bullet = (self.type == "bullet") local tank_size = 16 local ts1 = tank_size - 1 for i = 1, #Game.stage.tank_coordinates do local entry = Game.stage.tank_coordinates[i] local other_id, pos = entry[1], entry[2] -- Don't collide with self or the shooter if other_id == self.tank_id or (is_bullet and other_id == self.fired_by) then goto next_tank end -- AABB Overlap check -- AABB Overlap check (true if entities overlap) if not (x2 < pos.x or x1 > pos.x + ts1 or y2 < pos.y or y1 > pos.y + ts1) then if is_bullet then -- Define local variables for collision analysis local hit_player = (other_id == 1) local fired_by_player = (self.fired_by == 1) -- Bullet Damage Logic: Only hits if target has no shield local target_shielded = false if other_id == 1 then target_shielded = (Game.stage.player.lifetime < Game.CONFIG.SHIELD_DURATION) or (Game.stage.player_shield_timer and Game.time < Game.stage.player_shield_timer) else for i = 1, #Game.stage.enemy_container do local enemy = Game.stage.enemy_container[i] if enemy.tank_id == other_id then target_shielded = (enemy.lifetime < Game.CONFIG.SHIELD_DURATION) break end end end -- Friendly fire check: enemy bullets don't damage other enemies if not fired_by_player and not hit_player then -- Enemy bullet hitting another enemy: bullet disappears immediately (no explosion) self.silent_removal = true return true end if not target_shielded and fired_by_player ~= hit_player then self.exploding = true local target_tank = nil if hit_player then target_tank = Game.stage.player else for i = 1, #Game.stage.enemy_container do local enemy = Game.stage.enemy_container[i] if enemy.tank_id == other_id then target_tank = enemy break end end end if target_tank and not target_tank.destructed then target_tank.hp = target_tank.hp - 1 if target_tank.hp <= 0 then target_tank.destruction_timestamp, target_tank.destructed = Game.time, true Game.shake = 15 -- Set screen shake duration on destruction if target_tank.type ~= "player" then sfx(60,"G-2",-1,3,15,0) -- Heavier enemy explosion sound else sfx(49,"C-5",50,3,15,-1) -- Unique player explosion sound end make_explosion_ps(target_tank.x + 8, target_tank.y + 8) make_explosparks_ps(target_tank.x + 8, target_tank.y + 8) else -- Hit but not destroyed (for MEDIUM/HEAVY/ELITE tanks) if target_tank.type ~= "player" then sfx(61, 'C#8', -1, 3, 15, 1) -- Bullet hit indicator sound end end end return true end -- If bullet hits a shielded tank, the bullet should still explode but not kill the tank if target_shielded and fired_by_player ~= hit_player then self.exploding = true return true end else -- Tank collision (only after star animation) if self.lifetime > self.animation_time then return true end end end ::next_tank:: end return false end function Movable:register_coordinate() -- If tank is destructed, remove it from collision map immediately if self.destructed then -- Only remove coordinate once, prevent multiple deletions during death animation if not self.coordinate_removed then if self.type == "player" then Game.stage.tank_coordinates[1] = {-1, {x=-99, y=-99}} -- Sentinel for no player else for i = 1, #Game.stage.tank_coordinates do local tank = Game.stage.tank_coordinates[i] if tank[1] == self.tank_id then table.remove(Game.stage.tank_coordinates, i) break end end end self.coordinate_removed = true end return end local coordinates={x=self.x,y=self.y} if self.type=="player" then Game.stage.tank_coordinates[1] = {self.tank_id, coordinates} elseif self.type=="enemy" then for i = 1, #Game.stage.tank_coordinates do local tank = Game.stage.tank_coordinates[i] if tank[1]==self.tank_id then Game.stage.tank_coordinates[i] = {self.tank_id, coordinates} return -- Found and updated end end -- If not found (e.g. just spawned), optionally add it here or rely on spawn insertion end end function Movable:collision_ahead(check_dir, distance) local dir = check_dir or self.direction local dist = distance or 1 local vx = Game.movement_patterns[dir+1].x * dist local vy = Game.movement_patterns[dir+1].y * dist local is_bullet = (self.type == "bullet") local s = self.size - 1 local mid = s // 2 -- Leading edge points based on direction local pts if dir == 0 then pts = {{x=0, y=0}, {x=s, y=0}, {x=mid, y=0}} -- Up elseif dir == 1 then pts = {{x=0, y=s}, {x=s, y=s}, {x=mid, y=s}} -- Down elseif dir == 2 then pts = {{x=0, y=0}, {x=0, y=s}, {x=0, y=mid}} -- Left elseif dir == 3 then pts = {{x=s, y=0}, {x=s, y=s}, {x=s, y=mid}} -- Right end for _, p in ipairs(pts) do local nx, ny = self.x + p.x + vx, self.y + p.y + vy -- Out of bounds if nx < 0 or ny < 0 or nx >= Game.screen_width or ny >= Game.screen_height then return true end local tx, ty = nx // 8, ny // 8 local tile = mget(tx, ty) if is_bullet then if self.is_sniper then -- Sniper bullets ignore base and destructible/solid blocks else if isBase(tile) then if Game.stage.base_shield_timer and Game.time < Game.stage.base_shield_timer then self.exploding = true return true end if not Game.stage.base_destroyed then Game.stage.base_destroyed = true sfx(59,"C-4",60,3,15,-1) -- Base destruction sound Game.stage.base_destruction_timestamp = Game.time Game.shake = 45 -- stronger screen shake -- Trigger tank explosion effect at the center of the base local bp = Game.stage.base_pos make_explosion_ps(bp.x + 8, bp.y + 8) make_explosparks_ps(bp.x + 8, bp.y + 8) -- Replace all base tiles with ruins sprite 2x2 size for bx = 0, Game.screen_columns - 1 do for by = 0, Game.screen_rows - 1 do if isBase(mget(bx, by)) then mset(bx, by, 3) mset(bx+1, by, 4) mset(bx, by+1, 19) mset(bx+1, by+1, 20) end end end end return true end if isBulletBlocker(tile) then if isDestructible(tile) then self.exploding = true table.insert(self.explodable_coordinates, {x=nx, y=ny}) return true else if Game.stage.player and self.fired_by == Game.stage.player.tank_id then sfx(61, 'C#8', -1, 3, 15,1) -- Steel hit sound end return true -- Indestructible block (Steel) or non-destructible blocker end end end else -- Tank -- Blocked by SOLID objects if isSolid(nx, ny) then return true end end end return false end function Tank:animate() -- 1. Birth animation phase (Early exit) if self.lifetime <= 70 then spr(Game.CONFIG.SPRITES.SPAWN_BASE+self.lifetime//10*2,self.x,self.y,0,1,0,self.rotate,2,2) return end -- 2. Active Tank Logic (Sound & Graphics) if not self.destructed then --Engine sound for player (triggers regardless of shield/normal visuals)(disable for now) if self.type == "player" and self.is_moving then if Game.time % 8 == 0 then sfx(56, "C-1", 10, 0, -1) end end -- Visual branches if self.lifetime < Game.CONFIG.SHIELD_DURATION or (self.type == "player" and Game.stage.player_shield_timer and Game.time < Game.stage.player_shield_timer) then -- Shield visuals local draw_id = self.id if self.id == 389 and self.hp == 1 then draw_id = 395 elseif self.id == 391 then if self.hp == 2 then draw_id = 327 elseif self.hp == 1 then draw_id = 329 end elseif self.id == 321 and self.hp == 1 then draw_id = 397 end spr(draw_id,self.x,self.y,0,1,0,self.rotate,2,2) spr((Game.time//2)%4*2+Game.CONFIG.SPRITES.SHIELD,self.x,self.y,0,1,0,self.rotate,2,2) else -- Normal/Damaged visuals local draw_id = self.id if self.id == 389 and self.hp == 1 then draw_id = 395 elseif self.id == 391 then if self.hp == 2 then draw_id = 327 elseif self.hp == 1 then draw_id = 329 end elseif self.id == 321 and self.hp == 1 then draw_id = 397 end if self.type=="player" and self.is_moving==false then spr(draw_id,self.x,self.y,0,1,0,self.rotate,2,2) else spr(draw_id+Game.time%6//3*32,self.x,self.y,0,1,0,self.rotate,2,2) end end -- muzzle flash (player tank only) -- Display 384 for the first 3 frames and 400 for the last 3 frames within 6 frames after shooting if self.type == "player" and self.last_shoot > 0 then local elapsed = Game.time - self.last_shoot if elapsed < 6 then local flame_spr = (elapsed < 3) and 384 or 400 local dir = self.direction local fx, fy, frot if dir == 0 then fx, fy, frot = self.x + 4, self.y - 8, 0 elseif dir == 1 then fx, fy, frot = self.x + 4, self.y + 16, 2 elseif dir == 2 then fx, fy, frot = self.x - 8, self.y + 4, 3 elseif dir == 3 then fx, fy, frot = self.x + 16, self.y + 4, 1 end spr(flame_spr, fx, fy, 0, 1, 0, frot, 1, 1) end end end end function Tank:draw_explosion() -- Tank explosions are now handled by particle systems created at the moment of destruction end function Tank:emit_sand() if Game.time % 4 == 0 then local dir = self.direction if dir == 0 then newPtcEvent(ptc_sand_up, self.x + 2, self.y + 16) newPtcEvent(ptc_sand_up, self.x + 14, self.y + 16) elseif dir == 1 then newPtcEvent(ptc_sand_down, self.x + 2, self.y) newPtcEvent(ptc_sand_down, self.x + 14, self.y) elseif dir == 2 then newPtcEvent(ptc_sand_left, self.x + 16, self.y) newPtcEvent(ptc_sand_left, self.x + 16, self.y + 14) elseif dir == 3 then newPtcEvent(ptc_sand_right, self.x, self.y) newPtcEvent(ptc_sand_right, self.x, self.y + 14) end end end function PlayerTank:update() self:timer() self:animate() self:register_coordinate() if self.lifetime>self.animation_time and (not self.destructed) then local temp_dir=0 if btnp(0) then temp_dir=0 table.insert(self.control_sequence,#self.control_sequence+1,temp_dir) elseif btnp(1) then temp_dir=1 table.insert(self.control_sequence,#self.control_sequence+1,temp_dir) elseif btnp(2) then temp_dir=2 table.insert(self.control_sequence,#self.control_sequence+1,temp_dir) elseif btnp(3) then temp_dir=3 table.insert(self.control_sequence,#self.control_sequence+1,temp_dir) end if #self.control_sequence==0 then self.vx=0 self.vy=0 self.is_moving=false else self.direction=self.control_sequence[#self.control_sequence] self:dir_to_rotate() self.is_moving=true -- Enhanced Auto-align: Snap to 8-pixel grid BEFORE collision check if self.direction == 0 or self.direction == 1 then -- Moving vertically: align X to nearest 8-pixel grid self.x = math.floor(self.x / 8 + 0.5) * 8 elseif self.direction == 2 or self.direction == 3 then -- Moving horizontally: align Y to nearest 8-pixel grid self.y = math.floor(self.y / 8 + 0.5) * 8 end if (not self:collision_ahead()) and (not self:tank_ahead()) then self:dir_to_speed() self.x=self.x+self.vx self.y=self.y+self.vy self:emit_sand() end end for i=0,3 do if not btn(i) then for id = #self.control_sequence, 1, -1 do if self.control_sequence[id]==i then table.remove(self.control_sequence,id) end end end end if btnp(4) then self:shoot() end elseif self.destructed then self.vx=0 self.vy=0 self:cleanup() end end local EnemyTank=Tank:new({type="enemy"}) -- ================================================================ -- Boss Type A: Large 6x2-tile boss with dual downward-firing turrets -- Moves horizontally, bounces off screen edges. 15 HP. -- ================================================================ local BossTypeA = {} BossTypeA.__index = BossTypeA function BossTypeA.new(cfg) local self = setmetatable({}, BossTypeA) self.x = cfg.x or 80 self.y = cfg.y or 12 self.hp = 5 self.max_hp = 15 self.width = 48 -- 6 tiles * 8px self.height = 16 -- 2 tiles * 8px self.vx = 0.4 -- initial velocity (moving right) self.shoot_interval = 30 self.last_shoot = 0 self.destructed = false self.gone = false self.destruction_timestamp = 0 -- Turret pixel offsets from boss top-left corner self.turret_offsets = {{x=11, y=13}, {x=36, y=13}} return self end function BossTypeA:update() if self.destructed then -- Chained reaction: Phase 2 (T+10) and Phase 3 (T+20) local elapsed = Game.time - self.destruction_timestamp local bx = math.floor(self.x) local by = math.floor(self.y) + 8 if elapsed == 10 then make_explosion_ps(bx + 24, by) ; make_explosparks_ps(bx + 24, by) sfx(60,"G-2",-1,3,15,0) elseif elapsed == 20 then make_explosion_ps(bx + 40, by) ; make_explosparks_ps(bx + 40, by) sfx(60,"G-2",-1,3,15,0) end -- After death flash, mark as fully gone if elapsed > 90 then self.gone = true end return end -- Horizontal movement with screen-edge bounce self.x = self.x + self.vx if self.x <= 0 then self.x = 0 self.vx = 0.4 elseif self.x + self.width >= Game.screen_width then self.x = Game.screen_width - self.width self.vx = -0.4 end -- Dual-turret simultaneous downward fire if Game.time - self.last_shoot >= self.shoot_interval then for _, t in ipairs(self.turret_offsets) do local bullet = Bullet:new({ x = math.floor(self.x + t.x), y = math.floor(self.y + t.y), direction = 1, -- always fire downward fired_by = 999, -- special boss ID, not in tank_coordinates speed = 1, sprite_id = 259, }) bullet:dir_to_rotate() bullet:dir_to_speed() table.insert(Game.bullets, bullet) end self.last_shoot = Game.time end -- Contact damage: boss body overlapping player if Game.stage.player and not Game.stage.player.destructed then local p = Game.stage.player local shielded = (p.lifetime < Game.CONFIG.SHIELD_DURATION) or (Game.stage.player_shield_timer and Game.time < Game.stage.player_shield_timer) if not shielded then if not (self.x + self.width <= p.x or self.x >= p.x + 16 or self.y + self.height <= p.y or self.y >= p.y + 16) then p.hp = p.hp - 1 if p.hp <= 0 then p.destructed = true p.destruction_timestamp = Game.time Game.shake = 15 sfx(49,"C-5",50,3,15,-1) -- Player explosion sound from Boss contact make_explosion_ps(p.x + 8, p.y + 8) make_explosparks_ps(p.x + 8, p.y + 8) end end end end end function BossTypeA:draw() if self.gone then return end -- Death flash animation (alternates every 4 frames) if self.destructed then if (Game.time // 4) % 2 == 0 then spr(298, math.floor(self.x), math.floor(self.y), 0, 1, 0, 0, 6, 2) end return end spr(298, math.floor(self.x), math.floor(self.y), 0, 1, 0, 0, 6, 2) end function BossTypeA:take_hit() if self.destructed then return end self.hp = self.hp - 1 sfx(61,"C#8",50,3,15,1) Game.shake = 6 if self.hp <= 0 then self.destructed = true self.destruction_timestamp = Game.time Game.shake = 45 sfx(60,"G-2",-1,3,15,0) -- Primary explosion -- Secondary and Tertiary explosions moved to update() for sequential effect local bx = math.floor(self.x) local by = math.floor(self.y) + 8 make_explosion_ps(bx + 8, by) ; make_explosparks_ps(bx + 8, by) end end function BossTypeA:check_bullet_hit(bullet) local bx1, by1 = bullet.x, bullet.y local bx2, by2 = bx1 + 8, by1 + 8 if not (bx2 <= self.x or bx1 >= self.x + self.width or by2 <= self.y or by1 >= self.y + self.height) then bullet.vx, bullet.vy = 0, 0 bullet.explosion_timestamp = Game.time self:take_hit() return true end return false end -- ================================================================ -- Boss Type B: Three-component boss (fixed body + 2 sliding turrets) -- Body: 9x6 tiles (72x48px), sprite #295, fixed at (85, 0) -- Turret Left: 2x4 tiles (16x32px), sprite #396, X in [0, 111] -- Turret Right: 2x4 tiles (16x32px), sprite #398, X in [127, 224] -- Shared HP: 10. Both turrets fire downward every 30 frames. -- ================================================================ local BossTypeB = {} BossTypeB.__index = BossTypeB function BossTypeB.new(cfg) local self = setmetatable({}, BossTypeB) self.hp = 10 self.max_hp = 10 self.destructed = false self.gone = false self.destruction_timestamp = 0 self.shoot_interval = 30 self.last_shoot = 0 -- Body: fixed, 9x6 tiles = 72x48px self.body = {x=85, y=0, width=72, height=48, sprite=295} -- Left turret: 2x4 tiles = 16x32px, X range [0, 111] self.tl = { x=70, y=0, target_x=70, speed=1, width=16, height=32, sprite=396, range_min=0, range_max=111, move_timer=0, move_interval=30, } -- Right turret: 2x4 tiles = 16x32px, X range [127, 224] self.tr = { x=158, y=0, target_x=158, speed=1, width=16, height=32, sprite=398, range_min=127, range_max=224, move_timer=0, move_interval=30, } return self end local function move_turret(t) -- Independent random movement decision every move_interval frames t.move_timer = t.move_timer + 1 if t.move_timer >= t.move_interval then t.move_timer = 0 t.target_x = math.random(t.range_min, t.range_max - t.width) end -- Slide toward target at fixed speed local dx = t.target_x - t.x if math.abs(dx) <= t.speed then t.x = t.target_x else t.x = t.x + (dx > 0 and t.speed or -t.speed) end end function BossTypeB:update() if self.destructed then -- Chained reaction: Phase 2 (T+10) and Phase 3 (T+20) local elapsed = Game.time - self.destruction_timestamp local bx = self.body.x local by = self.body.y + 24 -- Barrage of explosions: every 10 frames from T+10 to T+90 if elapsed >= 10 and elapsed <= 90 and elapsed % 10 == 0 then local rx = bx + math.random(4, 68) -- Randomize across body width (9 tiles = 72px) local ry = self.body.y + math.random(4, 44) -- Randomize across body height (6 tiles = 48px) make_explosion_ps(rx, ry) make_explosparks_ps(rx, ry) sfx(60, "F-2", -1, 3, 15, 0) Game.shake = math.max(Game.shake, 20) -- Keep the screen shaking during barrage end if elapsed > 130 then self.gone = true end return end -- Move turrets independently move_turret(self.tl) move_turret(self.tr) -- Both turrets fire downward simultaneously if Game.time - self.last_shoot >= self.shoot_interval then for _, t in ipairs({self.tl, self.tr}) do local bullet = Bullet:new({ x = math.floor(t.x + t.width / 2), y = math.floor(t.y + t.height), direction = 1, fired_by = 999, speed = 1, sprite_id = 273, }) bullet:dir_to_rotate() bullet:dir_to_speed() table.insert(Game.bullets, bullet) end self.last_shoot = Game.time end -- Contact damage to player from any component if Game.stage.player and not Game.stage.player.destructed then local p = Game.stage.player local shielded = (p.lifetime < Game.CONFIG.SHIELD_DURATION) or (Game.stage.player_shield_timer and Game.time < Game.stage.player_shield_timer) if not shielded then local function overlaps(cx, cy, cw, ch) return not (cx + cw <= p.x or cx >= p.x + 16 or cy + ch <= p.y or cy >= p.y + 16) end local hit = overlaps(self.body.x, self.body.y, self.body.width, self.body.height) or overlaps(self.tl.x, self.tl.y, self.tl.width, self.tl.height) or overlaps(self.tr.x, self.tr.y, self.tr.width, self.tr.height) if hit then p.hp = p.hp - 1 if p.hp <= 0 then p.destructed = true p.destruction_timestamp = Game.time Game.shake = 15 sfx(49,"C-5",50,3,15,-1) -- Player explosion sound from Boss contact make_explosion_ps(p.x + 8, p.y + 8) make_explosparks_ps(p.x + 8, p.y + 8) end end end end end function BossTypeB:draw() if self.gone then return end -- Death flash: alternate visibility every 4 frames if self.destructed and (Game.time // 4) % 2 ~= 0 then return end -- Body drawn first (background) spr(self.body.sprite, self.body.x, self.body.y, 0, 1, 0, 0, 9, 6) -- Turrets drawn on top of body spr(self.tl.sprite, math.floor(self.tl.x), self.tl.y, 0, 1, 0, 0, 2, 4) spr(self.tr.sprite, math.floor(self.tr.x), self.tr.y, 0, 1, 0, 0, 2, 4) end function BossTypeB:check_bullet_hit(bullet) if self.destructed then return false end local bx1, by1 = bullet.x, bullet.y local bx2, by2 = bx1 + 8, by1 + 8 local function hits(cx, cy, cw, ch) return not (bx2 <= cx or bx1 >= cx + cw or by2 <= cy or by1 >= cy + ch) end if hits(self.body.x, self.body.y, self.body.width, self.body.height) or hits(self.tl.x, self.tl.y, self.tl.width, self.tl.height) or hits(self.tr.x, self.tr.y, self.tr.width, self.tr.height) then bullet.vx, bullet.vy = 0, 0 bullet.explosion_timestamp = Game.time self:take_hit() return true end return false end function BossTypeB:take_hit() if self.destructed then return end self.hp = self.hp - 1 Game.shake = 6 sfx(61,"C#8",50,3,15,1) if self.hp <= 0 then self.destructed = true self.destruction_timestamp = Game.time Game.shake = 45 sfx(60,"G-2",-1,3,15,0) -- Primary explosion -- Secondary and Tertiary explosions moved to update() for sequential effect local bx = self.body.x local by = self.body.y + 24 make_explosion_ps(bx + 8, by) ; make_explosparks_ps(bx + 8, by) end end function Tank:shoot() if self.flying_bullets < self.max_bullets and Game.time - self.last_shoot > self.shoot_interval then local offset = Game.CONFIG.BULLET_OFFSETS[self.direction] local is_sniper = false local is_shotgun = false local b_sprite = self.bullet_sprite or Game.CONFIG.SPRITES.BULLET local b_speed = 1.5 * self.bullet_speed if self.type == "player" and Game.stage.player_sniper_timer and Game.time < Game.stage.player_sniper_timer then is_sniper = true b_sprite = 273 b_speed = 4 elseif self.type == "player" and Game.stage.player_shotgun_timer and Game.time < Game.stage.player_shotgun_timer then is_shotgun = true b_sprite = 256 b_speed = 2.0 self.shoot_interval = 6 end local bullet_configs = {} if is_shotgun then bullet_configs = { {v=1, u=0}, {v=0.96, u=0.25}, {v=0.96, u=-0.25}, {v=0.86, u=0.5}, {v=0.86, u=-0.5} } else table.insert(bullet_configs, {v=1, u=0}) end for _, ang in ipairs(bullet_configs) do local bullet = Bullet:new({ x = self.x + offset.x, y = self.y + offset.y, direction = self.direction, fired_by = self.tank_id, speed = b_speed, sprite_id = b_sprite, is_sniper = is_sniper, is_shotgun = is_shotgun }) bullet:dir_to_rotate() bullet:dir_to_speed() if is_shotgun then if self.direction == 0 then bullet.vx = bullet.speed * ang.u bullet.vy = -bullet.speed * ang.v elseif self.direction == 1 then bullet.vx = bullet.speed * ang.u bullet.vy = bullet.speed * ang.v elseif self.direction == 2 then bullet.vx = -bullet.speed * ang.v bullet.vy = bullet.speed * ang.u elseif self.direction == 3 then bullet.vx = bullet.speed * ang.v bullet.vy = bullet.speed * ang.u end end table.insert(Game.bullets, bullet) end self.flying_bullets = self.flying_bullets + 1 self.last_shoot = Game.time if self.type == "player" then sfx(63, 'C-3', -1, 3, 15,0) -- Player fire sound end end end function EnemyTank:new(obj) local enemy=obj or {} setmetatable(enemy,self) self.__index=self return enemy end -- Weighted FSM AI System -- Weighted random selection (roulette wheel) local function weighted_random(weights) local total = 0 for _, w in ipairs(weights) do total = total + w end if total == 0 then return math.random(0, 3) end -- Fallback local rand = math.random() * total local cumulative = 0 for i, w in ipairs(weights) do cumulative = cumulative + w if rand <= cumulative then return i - 1 -- Return direction (0-3) end end return 0 -- Fallback end function EnemyTank:calculate_direction_weights() local weights = {10, 10, 10, 10} -- Base score for all directions (Up, Down, Left, Right) -- 1. Detect impassable directions (record flags, do NOT zero weights yet) local blocked = {false, false, false, false} for i = 1, 4 do local dir = i - 1 -- Check if direction is already marked blocked OR is effectively blocked on map -- Use lookahead of 4 pixels to avoid turning into walls that are very close (alignment jitter) if self.possible_directions[i] == 1 or self:collision_ahead(dir, 4) or self:tank_ahead(dir) then blocked[i] = true end end -- 1.5. Reduce current direction weight to encourage turning at intersections weights[self.direction + 1] = weights[self.direction + 1] * 0.7 -- 30% penalty to current direction -- 2. Base bias (Eagle) - Applied only after 5 seconds of level time, and NOT in hunt_player mode if self.ai_mode ~= "hunt_player" and Game.time - Game.stage.start_time > 600 then local base_x, base_y = 14 * 8, 15 * 8 if Game.stage.base_pos then base_x, base_y = Game.stage.base_pos.x, Game.stage.base_pos.y end local dx = base_x - self.x local dy = base_y - self.y if dy < -8 then weights[1] = weights[1] + 50 end -- Base is above: prefer UP if dy > 8 then weights[2] = weights[2] + 50 end -- Base is below: prefer DOWN if dx < -8 then weights[3] = weights[3] + 50 end -- Base is left: prefer LEFT if dx > 8 then weights[4] = weights[4] + 50 end -- Base is right: prefer RIGHT end -- 3. Player bias (Chase player) - Applied immediately (no delay) if Game.stage.player and not Game.stage.player.destructed then local p = Game.stage.player local pdx = p.x - self.x local pdy = p.y - self.y local chase_weight = (self.ai_mode == "hunt_player") and 300 or 30 -- Add weight to directions towards player if pdy < -8 then weights[1] = weights[1] + chase_weight end if pdy > 8 then weights[2] = weights[2] + chase_weight end if pdx < -8 then weights[3] = weights[3] + chase_weight end if pdx > 8 then weights[4] = weights[4] + chase_weight end end -- 4. Anti-backtrack: heavily penalize opposite direction local opposite = (self.last_direction + 2) % 4 weights[opposite + 1] = weights[opposite + 1] * 0.1 -- Reduce to 10% -- 5. Enforce blocked directions: zero out weights for impassable directions -- This must happen AFTER all bias additions to prevent blocked directions from gaining weight for i = 1, 4 do if blocked[i] then weights[i] = 0 end end -- 6. Deadlock prevention: if all weights are 0, give small weights to all directions local total_weight = weights[1] + weights[2] + weights[3] + weights[4] if total_weight == 0 then -- Tank is stuck, allow movement in any direction (will be blocked but triggers re-decision) weights = {1, 1, 1, 1} end return weights end function EnemyTank:should_make_decision() -- Trigger 1: Hit obstacle local hit_wall = self:collision_ahead() local hit_tank = self:tank_ahead() if hit_wall or hit_tank then if self.decision_cooldown > 0 then return false end self.decision_cooldown = 30 -- Prevent rapid direction changes when hitting walls -- If we hit a tank but NOT a wall, skip hard snapping to prevent jumping local skip_snap = hit_tank and not hit_wall return true, skip_snap end -- Trigger 2: At intersection (check if side paths are clear) -- Use tolerance for alignment check since enemy speeds are fractional (0.4, 0.5, 0.8, etc.) local tx, ty = (self.x + 4) // 8, (self.y + 4) // 8 local x_mod, y_mod = self.x % 8, self.y % 8 local tolerance = 1.0 local is_aligned = (x_mod < tolerance or x_mod > 8 - tolerance) and (y_mod < tolerance or y_mod > 8 - tolerance) if is_aligned then -- Prevent multiple decisions at the same intersection tile cluster if self.last_decision_tile and self.last_decision_tile.x == tx and self.last_decision_tile.y == ty then return false end -- Check perpendicular directions WITHOUT modifying self.direction if self.direction == 0 or self.direction == 1 then -- Moving vertically local left_clear = not self:collision_ahead(2) local right_clear = not self:collision_ahead(3) if left_clear or right_clear then if math.random(1, 10) <= 6 then self.last_decision_tile = {x=tx, y=ty} return true end end else -- Moving horizontally local up_clear = not self:collision_ahead(0) local down_clear = not self:collision_ahead(1) if up_clear or down_clear then if math.random(1, 10) <= 6 then self.last_decision_tile = {x=tx, y=ty} return true end end end end -- Reset last_decision_tile once we've moved sufficiently away from the center of that tile if self.last_decision_tile and (math.abs(self.x - tx*8) > 2 or math.abs(self.y - ty*8) > 2) then self.last_decision_tile = nil end -- Trigger 3: Random timer (every 1-3 seconds) - only when aligned to grid self.decision_timer = self.decision_timer + 1 if self.decision_timer >= self.decision_threshold then if is_aligned then self.decision_timer = 0 self.decision_threshold = 60 + math.random(0, 120) self.last_decision_tile = {x=tx, y=ty} return true end end return false end function EnemyTank:selectpath(skip_snap) local old_dir = self.direction -- Calculate weights for all 4 directions local weights = self:calculate_direction_weights() -- Select direction using weighted random local new_direction = weighted_random(weights) -- Snap to grid if direction changes to ensure alignment and prevent twitching -- Skip hard snap if collision was with a tank (skip_snap flag) if new_direction ~= old_dir and not skip_snap then self.x = (self.x + 4) // 8 * 8 self.y = (self.y + 4) // 8 * 8 end -- Update direction and remember it self.last_direction = self.direction self.direction = new_direction end -- Raycast detection: check what's ahead in the firing line (16px width beam) function EnemyTank:raycast_ahead() local dir = self.direction local step_x = Game.movement_patterns[dir+1].x * 8 -- Step by tile (8 pixels) local step_y = Game.movement_patterns[dir+1].y * 8 -- Set cross-section offsets based on direction local ox1, oy1, ox2, oy2 = 0, 0, 0, 0 if dir == 0 or dir == 1 then -- Moving Up/Down: scan horizontal width ox1, ox2 = 0, 15 else -- Moving Left/Right: scan vertical height oy1, oy2 = 0, 15 end local cur_x = self.x local cur_y = self.y local max_steps = 25 -- Reduced slightly for performance for i = 1, max_steps do cur_x = cur_x + step_x cur_y = cur_y + step_y -- Out of bounds check (using center) if cur_x < -8 or cur_y < -8 or cur_x > Game.screen_width or cur_y > Game.screen_height then return "none" end -- Sample tiles at both edges of the tank's width local tile1 = mget((cur_x + ox1) // 8, (cur_y + oy1) // 8) local tile2 = mget((cur_x + ox2) // 8, (cur_y + oy2) // 8) -- 1. Check for player (AABB overlap with the 16px beam segment) if Game.stage.player and not Game.stage.player.destructed then local p = Game.stage.player local bw, bh = 8, 8 -- Beam segment size if dir == 0 or dir == 1 then bw = 16 else bh = 16 end if not (cur_x + bw-1 < p.x or cur_x > p.x + 15 or cur_y + bh-1 < p.y or cur_y > p.y + 15) then return "player" end end -- 2. Check for Base/Eagle if isBase(tile1) or isBase(tile2) then return "eagle" -- 3. Check for destructible tiles elseif isDestructible(tile1) or isDestructible(tile2) then return "brick" -- 4. Check for indestructible blockers (Steel) elseif fget(tile1, Game.CONFIG.FLAGS.SOLID) or fget(tile2, Game.CONFIG.FLAGS.SOLID) then return "none" -- Blocks potential shot behind it end end return "none" end function EnemyTank:update() self:timer() if self.decision_cooldown > 0 then self.decision_cooldown = self.decision_cooldown - 1 end self:animate() self:register_coordinate() if self.lifetime>self.animation_time and (not self.destructed) then -- Weighted FSM Decision System local should_decide, skip_snap = self:should_make_decision() if should_decide then -- Re-check for direction blocking if self:collision_ahead() or self:tank_ahead() then self.possible_directions[self.direction+1]=1 end self:selectpath(skip_snap) self:dir_to_speed() self:dir_to_rotate() self.possible_directions={0,0,0,0} -- Reset after decision end -- Execute movement if not (self:collision_ahead() or self:tank_ahead()) then self:dir_to_speed() self.x=self.x+self.vx self.y=self.y+self.vy self.stuck_counter = 0 -- Reset stuck counter when moving self:emit_sand() else self.vx=0 self.vy=0 self.stuck_counter = self.stuck_counter + 1 -- If stuck for too long (1 second), force a new decision if self.stuck_counter >= 30 then self.stuck_counter = 0 self.decision_timer = self.decision_threshold -- Force decision on next frame end end -- Smart shooting system with raycast detection if Game.time - self.last_shoot > self.shoot_interval then local shoot_chance = 0.3 -- Default: 30% for suppressive fire -- Raycast detection in current direction local raycast_result = self:raycast_ahead() if raycast_result == "player" then shoot_chance = self.player_shoot_chance or 0.7 -- use wave-specific chance or default 70% elseif raycast_result == "eagle" then shoot_chance = self.eagle_shoot_chance or 0.6 -- use wave-specific chance or default 60% elseif raycast_result == "brick" then shoot_chance = self.brick_shoot_chance or 0.7 -- use wave-specific chance or default end -- Roll the dice if math.random() < shoot_chance then self:shoot() end self.last_shoot = Game.time -- Reset timerRegardless of whether it fired or not end elseif self.destructed then self.vx=0;self.vy=0 self:cleanup() end end function Tank:cleanup() if Game.time-self.destruction_timestamp>Game.destruction_animation_time then -- Record wreckage position for SPR drawing at 2x2 scale (#238) table.insert(Game.ground_effects, {x=self.x, y=self.y}) self.gone=true end end local function is_spawn_clear(x, y) local padding = 2 local s = 16 - padding * 2 local x1, y1 = x + padding, y + padding local x2, y2 = x1 + s, y1 + s for i = 1, #Game.stage.tank_coordinates do local entry = Game.stage.tank_coordinates[i] local pos = entry[2] -- Regular tank size is 16 if not (x2 < pos.x or x1 > pos.x + 15 or y2 < pos.y or y1 > pos.y + 15) then return false end end return true end local function create_enemy() local level = Game.LEVELS[Game.current_stage] -- Count only active (non-destructed) enemies local current_enemies = 0 for i = 1, #Game.stage.enemy_container do local enemy = Game.stage.enemy_container[i] if not enemy.destructed then current_enemies = current_enemies + 1 end end local min_on_screen = level.min_enemies or 1 -- Wave progression check if Game.stage.current_wave_index <= #level.waves then local current_wave = level.waves[Game.stage.current_wave_index] -- Check if current wave is complete (all enemies spawned AND destroyed) -- OR if we should advance early due to low enemy count local wave_fully_complete = Game.stage.wave_created_count >= current_wave.count and Game.stage.wave_destroyed_count >= current_wave.count local should_advance_early = Game.stage.wave_created_count >= current_wave.count and current_enemies < min_on_screen and Game.stage.current_wave_index < #level.waves if wave_fully_complete or should_advance_early then -- Current wave finished (or advancing early), move to next wave Game.stage.current_wave_index = Game.stage.current_wave_index + 1 Game.stage.wave_created_count = 0 Game.stage.wave_destroyed_count = 0 Game.stage.last_spawn_time = Game.time - Game.CONFIG.SPAWN_INTERVAL_WAVE -- Allow immediate spawning -- Update local reference to the new wave if Game.stage.current_wave_index <= #level.waves then current_wave = level.waves[Game.stage.current_wave_index] else current_wave = nil end end -- Spawn enemy if: current wave exists, haven't spawned all for this wave, and enough time has passed if current_wave and Game.stage.wave_created_count < current_wave.count and Game.time - Game.stage.last_spawn_time >= Game.CONFIG.SPAWN_INTERVAL_WAVE then -- Use level-specific spawn points if available, otherwise use default local spawn_points = level.spawn_points or Game.CONFIG.ENEMY_SPAWNS local spawn_indices = {} for i=1, #spawn_points do table.insert(spawn_indices, i) end for i=1, #spawn_points do local idx = math.random(1, #spawn_indices) local pick = table.remove(spawn_indices, idx) local pos = spawn_points[pick] if is_spawn_clear(pos.x, pos.y) then -- Select type from current wave's pool local pool = current_wave.pool or {SMALL=1.0} local roll = math.random() local chosen_type_name = "SMALL" local cumulative = 0 for type_name, probability in pairs(pool) do cumulative = cumulative + probability if roll <= cumulative then chosen_type_name = type_name break end end local type_data = Game.CONFIG.ENEMY_TYPES[chosen_type_name] local enemy = EnemyTank:new({ id = type_data.sprite_id, x = pos.x, y = pos.y, direction = 1, possible_directions = {0, 0, 0, 0}, created_at = Game.time, tank_id = Game.stage.created_enemy_quantity + 2, speed = type_data.speed, hp = type_data.hp, max_bullets = type_data.max_bullets, bullet_speed = type_data.bullet_speed, shoot_interval = type_data.shoot_interval, bullet_sprite = type_data.bullet_sprite, -- nil means use default bullet sprite brick_shoot_chance = current_wave.brick_shoot_chance, player_shoot_chance = current_wave.player_shoot_chance, eagle_shoot_chance = current_wave.eagle_shoot_chance, ai_mode = current_wave.ai_mode, wave_index = Game.stage.current_wave_index, }) enemy:dir_to_rotate() enemy:dir_to_speed() table.insert(Game.stage.enemy_container, enemy) Game.stage.created_enemy_quantity = Game.stage.created_enemy_quantity + 1 Game.stage.wave_created_count = Game.stage.wave_created_count + 1  table.insert(Game.stage.tank_coordinates, {enemy.tank_id, {x=enemy.x, y=enemy.y}}) Game.stage.last_spawn_time = Game.time -- Update spawn timer return end end end end end local function field_cleanup() for id = #Game.stage.enemy_container, 1, -1 do local tank = Game.stage.enemy_container[id] if tank.gone then table.remove(Game.stage.enemy_container,id) if tank.wave_index == Game.stage.current_wave_index then Game.stage.wave_destroyed_count = Game.stage.wave_destroyed_count + 1 -- Track wave progress end for coor_id = #Game.stage.tank_coordinates, 1, -1 do local tank_coor = Game.stage.tank_coordinates[coor_id] if tank.tank_id==tank_coor[1] then table.remove(Game.stage.tank_coordinates,coor_id) end end end end end local function respawn_player() local p = Game.stage.player p.x = Game.player_generation_location_x p.y = Game.player_generation_location_y p.hp = 1 p.destructed = false p.gone = false p.created_at = Game.time -- Triggers respawn animation and shield p.lifetime = 0 p.direction = 0 p.id = Game.player_model p.bullet_speed = 1.5 p.max_bullets = 1 p.shoot_interval = 10 Game.stage.player_bullet_timer = 0 Game.stage.player_sniper_timer = 0 Game.stage.player_shotgun_timer = 0 Game.stage.player_shield_timer = 0 p.speed = 0.5 p:dir_to_rotate() p.control_sequence = {} end local function game_status_checker() if not Game.ingame then return end if Game.stage.player_created then -- base destruction check if Game.stage.base_destroyed then if not Game.is_game_over then music() -- Stop music immediately Game.is_game_over = true Game.fail_timestamp = Game.stage.base_destruction_timestamp end end -- player death check if Game.stage.player.gone == true then if Game.player_lives > 0 then Game.player_lives = Game.player_lives - 1 Game.ui_lives_timer = Game.time respawn_player() else if not Game.is_game_over then music() -- Stop music immediately Game.is_game_over = true Game.fail_timestamp = Game.time end end end end end local function scan_for_base() for x=0,Game.screen_columns-1 do for y=0,Game.screen_rows-1 do -- hasFlag expects x,y in pixels? No, hasFlag implementation: -- local tile = mget(x // 8, y // 8) -> It expects pixels if I look at previous `isSolid` usage. -- Wait, let me check definition of hasFlag I added earlier. -- `local function hasFlag(x, y, flag) local tile = mget(x // 8, y // 8) ...` -- So `hasFlag` takes PIXEL coordinates. -- Here I am iterating TILES (0..29). So I should pass x*8, y*8. if hasFlag(x*8, y*8, Game.CONFIG.FLAGS.BASE) then return {x=x*8, y=y*8} end end end return {x=14*8, y=15*8} -- Default to classic position if not found end local function scan_for_top_layer() local tiles = {} for x=0,Game.screen_columns-1 do for y=0,Game.screen_rows-1 do local tile = mget(x, y) if fget(tile, Game.CONFIG.FLAGS.TOP_LAYER) then table.insert(tiles, {id=tile, x=x*8, y=y*8}) end end end return tiles end local function generate_item_position() if not Game.stage.player then return {x=0, y=0} end local px = Game.stage.player.x // 8 local py = Game.stage.player.y // 8 local pool = {} local fallback_pool = {} local maxR = math.max(px, 29 - px, py, 15 - py) - 1 local minR = 7 for y = 0, 15 do for x = 0, 28 do -- check 2x2 area if it is free of walls to avoid spawning inside walls partially if not isSolid(x*8, y*8) and not isSolid(x*8+8, y*8) and not isSolid(x*8, y*8+8) and not isSolid(x*8+8, y*8+8) then table.insert(fallback_pool, {x=x*8, y=y*8}) local dist = math.abs(x - px) + math.abs(y - py) if dist > minR and dist < maxR then table.insert(pool, {x=x*8, y=y*8}) end end end end if #pool > 0 then return pool[math.random(#pool)] elseif #fallback_pool > 0 then return fallback_pool[math.random(#fallback_pool)] else return {x=0, y=0} end end local function update_items(freeze) if Game.stage.player_bullet_timer and Game.stage.player_bullet_timer > 0 then if Game.time >= Game.stage.player_bullet_timer then Game.stage.player_bullet_timer = 0 if Game.stage.player and not Game.stage.player.destructed and (not Game.stage.player_sniper_timer or Game.stage.player_sniper_timer == 0) and (not Game.stage.player_shotgun_timer or Game.stage.player_shotgun_timer == 0) then Game.stage.player.id = Game.player_model Game.stage.player.bullet_speed = 1.5 Game.stage.player.max_bullets = 1 Game.stage.player.shoot_interval = 10 end end end if Game.stage.player_sniper_timer and Game.stage.player_sniper_timer > 0 then if Game.time >= Game.stage.player_sniper_timer then Game.stage.player_sniper_timer = 0 if Game.stage.player and not Game.stage.player.destructed and (not Game.stage.player_bullet_timer or Game.stage.player_bullet_timer == 0) and (not Game.stage.player_shotgun_timer or Game.stage.player_shotgun_timer == 0) then Game.stage.player.id = Game.player_model end end end if Game.stage.player_shotgun_timer and Game.stage.player_shotgun_timer > 0 then if Game.time >= Game.stage.player_shotgun_timer then Game.stage.player_shotgun_timer = 0 if Game.stage.player and not Game.stage.player.destructed and (not Game.stage.player_bullet_timer or Game.stage.player_bullet_timer == 0) and (not Game.stage.player_sniper_timer or Game.stage.player_sniper_timer == 0) then Game.stage.player.id = Game.player_model Game.stage.player.shoot_interval = 10 end end end -- Restore speed when shield expires if Game.stage.player_shield_timer and Game.stage.player_shield_timer > 0 then if Game.time >= Game.stage.player_shield_timer then Game.stage.player_shield_timer = 0 if Game.stage.player and not Game.stage.player.destructed then Game.stage.player.speed = 0.5 end end end -- Check if we should stop spawning items (Boss Type B finale condition) local level = Game.LEVELS[Game.current_stage] local is_finishing = false if level and level.boss and level.boss.type == "boss_type_b" then local all_waves_done = Game.stage.current_wave_index > #level.waves local active_enemies = 0 for i = 1, #Game.stage.enemy_container do if not Game.stage.enemy_container[i].destructed then active_enemies = active_enemies + 1 end end if all_waves_done and active_enemies == 0 then is_finishing = true end end if not freeze and Game.stage.active_item == nil and not is_finishing then if Game.stage.item_timer > 0 then Game.stage.item_timer = Game.stage.item_timer - 1 if Game.stage.item_timer <= 0 then local pos = generate_item_position() local rd = math.random() local itype, sid if rd < 0.15 then itype = "item_1up"; sid = 270 elseif rd < 0.3 then itype = "item_bomb"; sid = 268 elseif rd < 0.5 then itype = "item_player_shield"; sid = 264 elseif rd < 0.7 then itype = "item_base_shield"; sid = 266 elseif rd < 0.8 then itype = "item_bullet"; sid = 262 elseif rd < 0.9 then itype = "item_shotgun"; sid = 262 else itype = "item_sniper"; sid = 260 end Game.stage.active_item = { x = pos.x, y = pos.y, type = itype, sprite_id = sid, creation_time = Game.time } sfx(58,'F-5',60,2,15,-1) -- Item spawn sound end end elseif Game.stage.active_item ~= nil then local age = Game.time - Game.stage.active_item.creation_time if not freeze then if age >= 8 * 60 then Game.stage.active_item = nil Game.stage.item_timer = math.random(2 * 60, 5 * 60) else if Game.stage.player_created and not Game.stage.player.destructed then local px, py = Game.stage.player.x, Game.stage.player.y local ix, iy = Game.stage.active_item.x, Game.stage.active_item.y local ps, is = 15, 15 if not (px+ps < ix or px > ix+is or py+ps < iy or py > iy+is) then local itype = Game.stage.active_item.type if itype == "item_bomb" then sfx(50,"C-4",60,2,15,-1) sfx(50,"C-4",60,3,15,-1) else sfx(57,'F#5',60,2,15,0) end make_explosparks_item(ix + 8, iy + 8) if itype == "item_1up" then Game.player_lives = Game.player_lives + 1 Game.ui_lives_timer = Game.time elseif itype == "item_bomb" then for i = 1, #Game.stage.enemy_container do local enemy = Game.stage.enemy_container[i] if not enemy.destructed then enemy.hp = 0 enemy.destructed = true enemy.destruction_timestamp = Game.time Game.shake = 30 make_explosion_ps(enemy.x + 8, enemy.y + 8) make_explosparks_ps(enemy.x + 8, enemy.y + 8) end end elseif itype == "item_player_shield" then Game.stage.player_shield_timer = Game.time + 7 * 60 local p = Game.stage.player if p then p.speed = 0.7 end elseif itype == "item_base_shield" then Game.stage.base_shield_timer = Game.time + 10 * 60 elseif itype == "item_bullet" then Game.stage.player_bullet_timer = Game.time + 10 * 60 local p = Game.stage.player if p then p.id = 325 p.bullet_speed = 2 p.max_bullets = 3 p.shoot_interval = 5 end elseif itype == "item_sniper" then Game.stage.player_sniper_timer = Game.time + 10 * 60 local p = Game.stage.player if p then p.id = 323 end elseif itype == "item_shotgun" then Game.stage.player_shotgun_timer = Game.time + 10 * 60 local p = Game.stage.player if p then p.id = 325 end end Game.stage.active_item = nil Game.stage.item_timer = math.random(2 * 60, 5 * 60) end end end end if Game.stage.active_item then local age_draw = Game.time - Game.stage.active_item.creation_time local is_visible = true if age_draw < 40 or age_draw >= (8 * 60 - 60) then is_visible = (age_draw % 20 < 10) end if is_visible then local item = Game.stage.active_item spr(item.sprite_id, item.x, item.y, 0, 1, 0, 0, 2, 2) if item.type == "item_shotgun" then spr(416, item.x + 4, item.y + 3, 0, 1, 0, 0, 1, 1) end end end end end -- change the border color -- poke(0x03FF8, 0) function TIC() poke(0x3FFB, 0) -- hide the mouse cursor -- Calculate Screen Shake Offset local sx, sy = 0, 0 if Game.shake > 0 then local d = Game.shake_dist sx = math.random(-d, d) sy = math.random(-d, d) Game.shake = Game.shake - 1 end local function apply_shake() poke(0x3FF9, sx) poke(0x3FF9+1, sy) end apply_shake() -- Update Screen Fader if Game.fader then Game.fader:update() if not Game.fader.fading and Game.next_mode then Game.mode = Game.next_mode Game.next_mode = nil Game.fader:fade_in() end end if Game.mode==0 then -- 1. Sync assets from Bank 3 once (including Music from Bank 3) if not Game.title_sync then sync(0x3F, 3, false) -- Sync Tiles, Sprites, Map, SFX, Music, Palette (0x3F = 63) for i=0,3 do sfx(-1, -1, -1, i) end -- stop all 4 channels of sound effects Game.title_sync = true music(0, 0, 0, true) -- Play Track 0 looping end -- Prepare fresh palettes for both vbanks before applying fader loadPalette(CACHED_PALETTES.BANK3_V0, 0) loadPalette(CACHED_PALETTES.BANK3_V1, 1) if Game.fader then Game.fader:apply({0, 1}) end vbank(0) -- Background Layer cls(0) -- Background: Looping scroll on X-axis local scroll_x = (Game.time // 3) % 240 map(0, 0, 30, 17, scroll_x, 0, 0) map(0, 0, 30, 17, scroll_x - 240, 0, 0) -- Middle and Foreground layers (static) map(30, 0, 30, 17, 0, 0, 0) map(60, 0, 30, 17, 0, 0, 0) -- Animation sequence: (0, 4, 8, 12, 48, 52, 56, 60), 4X3 tiles, 7 frames speed local title_anim_frames = {0, 4, 8, 12, 48, 52, 56, 60} local f_idx = (Game.time // 7) % #title_anim_frames + 1 local title_anim_frames2 = {8, 12, 48, 52, 56, 60, 0, 4} local f_idx2 = (Game.time // 7) % #title_anim_frames2 + 1 spr(title_anim_frames[f_idx], 215, 56, 13, 1, 0, 0, 4, 3) spr(title_anim_frames2[f_idx2], -7, 56, 13, 1, 0, 0, 4, 3) vbank(1) -- Foreground Layer (LOGO, Tank, Menu) cls(0) -- Game LOGO: Sprite #352, W=12 H=5 Tiles spr(352, 40, 3, 0, 1, 0, 0, 12, 5) --part1 spr(432, 136, 3, 0, 1, 0, 0, 7, 5) --part2 -- Tank Image: Sprite #256, W=11 H=6 Tiles spr(256, 112, 87, 0, 1, 0, 0, 11, 6) if not Game.title_started then -- Phase 1: Press Z to start if Game.time % 60 < 35 then print("PRESS Z TO START", 71, 103, 6, true, 1) print("PRESS Z TO START", 71, 102, 15, true, 1) end if btnp(4) then Game.title_started = true sfx(51) -- Confirm sound end else -- Phase 2: Menu: New Game / Continue local menu_y = 98 local has_save = pmem(0) > 0 if not Game.next_mode then -- Handle Menu Input if btnp(0) then Game.title_cursor = 0 end if btnp(1) and has_save then Game.title_cursor = 1 end -- NEW GAME local col_new = (Game.title_cursor == 0) and 14 or 6 print("NEW GAME", 96, menu_y, 3, false, 1) print("NEW GAME", 96, menu_y - 1, col_new, false, 1) -- CONTINUE local col_cont = (Game.title_cursor == 1) and 14 or 6 if not has_save then col_cont = 6 end -- Dim if no save print("CONTINUE", 96, menu_y + 12, 3, false, 1) print("CONTINUE", 96, menu_y + 11, col_cont, false, 1) -- Handle Selection if btnp(4) then sfx(51) -- Confirm selection if Game.title_cursor == 0 then Game.current_stage = level_select or 1 else Game.current_stage = load_game() end -- Trigger Fade Out instead of immediate mode switch Game.player_lives = 5 Game.session_started = true Game.title_sync = true if Game.fader then Game.fader:fade_out(0.03) -- Smooth fade speed Game.next_mode = 6 else Game.mode = 6 end end end -- Allow returning to "Press Z" if needed (Optional, but usually not) -- if btnp(5) then Game.title_started = false end end elseif Game.mode==1 then vbank(0) -- ensure we clear on vbank 0 cls() -- wipe out previous map -- Sync tiles(1)+sprites(2)+map(4)+palette(32)+flags(64)=103 from the -- bank configured in the current level's LEVELS table entry. -- Falls back to bank 0 if the field is absent. -- Reset to Bank 0 Music + SFX pattern before loading level specific visuals local level_bank = (Game.LEVELS[Game.current_stage] or {}).bank or 0 sync(103, level_bank, false) -- Sync visuals from level bank sync(24, 0, false) -- Force SFX/Music from Bank 0 music(0, 0, 0, true) -- Play Track 0 stage_builder(Game.current_stage) -- draw new map for each stage particle_systems = {} -- clear residual particle effects ptcInit() -- reset particle pool Game.intro_timer = 80 -- initialize intro timer (20+40+20=80 frames) Game.ingame = false -- Ensure fresh stage init when entering mode 2 Game.mode=2 elseif Game.mode==2 then --game -- 1. Restore fresh stage palettes and apply fade to both banks loadStagePalettes(Game.current_stage) if Game.fader then Game.fader:apply({0, 1}) end -- ========================================== -- [vbank 0] background layer: map -- ========================================== vbank(0) apply_shake() -- 1. Draw Background Map map() -- Draw ground wreckages (2x2 sprite #238) using vbank(0) palette for _, wreckage in ipairs(Game.ground_effects) do spr(238, wreckage.x, wreckage.y, 10, 1, 0, 0, 2, 2) end -- game_status_checker() moved to after stage initialization -- update intro timer if Game.intro_timer > 0 then Game.intro_timer = Game.intro_timer - 1 end --reset game/stage whenever we enter a new stage/game if Game.ingame==false and not Game.stage.finale_triggered then local current_level = Game.LEVELS[Game.current_stage] local total_enemies = 0 for _, wave in ipairs(current_level.waves) do total_enemies = total_enemies + wave.count end Game.stage=Stage:new({ enemy_total=total_enemies, start_time=Game.time, last_spawn_time=Game.time - Game.CONFIG.SPAWN_INTERVAL_WAVE, -- Ensure immediate spawn for first wave base_pos=scan_for_base(), top_layer_tiles=scan_for_top_layer(), base_destroyed = false, base_destruction_timestamp = 0, player_shield_timer = 0, base_shield_timer = 0, player_bullet_timer = 0, player_sniper_timer = 0, player_shotgun_timer = 0, }) Game.bullets={} -- Clear any leftover bullets from previous level Game.ground_effects={} -- Reset ground wreckage for new stage Game.is_game_over=false Game.ingame=true ptcInit() -- reset particle pool -- Spawn boss if this level defines one Game.stage.boss = nil local boss_cfg = Game.LEVELS[Game.current_stage].boss if boss_cfg then if boss_cfg.type == "boss_type_b" then Game.stage.boss = BossTypeB.new(boss_cfg) else Game.stage.boss = BossTypeA.new(boss_cfg) end Game.stage.boss.last_shoot = Game.time + 90 -- Delay first shot by 1.5s end end game_status_checker() -- Ensure this runs after stage reset to avoid stale state -- ========================================== -- [vbank 1] sprite layer: tanks / bullets / explosions -- ========================================== vbank(1) apply_shake() cls() -- clear sprite layer every frame (color 0 transparent, background shows through) -- freeze gameplay if game over and shake <= 0 local freeze_gameplay = Game.is_game_over and Game.shake <= 0 if not freeze_gameplay then -- 1. create or update player if Game.stage.player_created==false then Game.stage.player=PlayerTank:new() if Game.session_started then Game.player_lives = Game.player_lives - 1 Game.ui_lives_timer = Game.time Game.session_started = false end local coordinate={Game.stage.player.tank_id,{x=Game.stage.player.x,y=Game.stage.player.y}} table.insert(Game.stage.tank_coordinates,#Game.stage.tank_coordinates+1,coordinate) Game.stage.player_created=true else Game.stage.player:update() end -- 2. bullet update for id = #Game.bullets, 1, -1 do local bullet = Game.bullets[id] -- Check player bullet vs boss collision (dispatched via polymorphic method) if Game.stage.boss and not Game.stage.boss.destructed and bullet.fired_by == 1 and bullet.explosion_timestamp == 0 then Game.stage.boss:check_bullet_hit(bullet) end bullet:update(id) end -- 3. enemy generation and update field_cleanup() create_enemy() for id = #Game.stage.enemy_container, 1, -1 do local enemy = Game.stage.enemy_container[id] enemy:update(id) end -- 4. Boss update and draw if Game.stage.boss then Game.stage.boss:update() Game.stage.boss:draw() if Game.stage.boss.gone then Game.stage.boss = nil end end else -- clear bullets Game.bullets = {} end -- render sand particles (before grass layer, covering tanks) updatePtc() -- ========================================== -- [vbank 1] top layer: grass drawn on sprite layer, later drawing covers earlier drawing -- vbank(1) internal: tank first draw -> grass later draw -> grass covers tank -- ========================================== -- vbank remains 1, no need to switch -- 2. Draw Top Layer (Grass/Trees) for i = 1, #Game.stage.top_layer_tiles do local t = Game.stage.top_layer_tiles[i] spr(t.id, t.x, t.y, 0, 1, 0, 0, 1, 1) end -- draw items update_items(freeze_gameplay) -- draw base shield if Game.stage.base_shield_timer and Game.time < Game.stage.base_shield_timer then local bp = Game.stage.base_pos if bp then spr((Game.time//2)%4*2+Game.CONFIG.SPRITES.SHIELD, bp.x, bp.y, 0, 1, 0, 0, 2, 2) end end -- ========================================== -- draw explosion effects -- ========================================== for id = 1, #Game.bullets do local bullet = Game.bullets[id] bullet:draw_explosion() -- Bullet still uses sprite sequence explosion end -- Particle Systems integration update_psystems() draw_psystems() --print(#Game.stage.enemy_container.." enemies left") -- Level Completion Check local level = Game.LEVELS[Game.current_stage] local all_waves_done = Game.stage.current_wave_index > #level.waves -- Check active enemies instead of container size to avoid waiting for explosions local active_enemies = 0 for i = 1, #Game.stage.enemy_container do local enemy = Game.stage.enemy_container[i] if not enemy.destructed then active_enemies = active_enemies + 1 end end local no_enemies_left = (active_enemies == 0) -- Boss must also be dead (nil = never existed or fully gone; destructed = killed) local boss_dead = (Game.stage.boss == nil) or Game.stage.boss.destructed -- Special finale flow: boss_type_b cleared -> 1s stillness -> slow fade to ending local is_boss_type_b = level.boss and level.boss.type == "boss_type_b" if is_boss_type_b and all_waves_done and no_enemies_left and not Game.stage.finale_triggered then -- Phase 1: wait for boss death-flash to complete (boss becomes nil) if Game.stage.boss == nil then music(-1) -- stop music -- Phase 2: 1 second of stillness before fading if not Game.stage.finale_timer then Game.stage.finale_timer = Game.time end if Game.time - Game.stage.finale_timer >= 120 then -- Stillness done: trigger slow fade to ending screen Game.stage.finale_triggered = true if Game.fader then Game.fader:fade_out(0.01) Game.next_mode = 4 else Game.ingame = false Game.mode = 4 end end end elseif all_waves_done and no_enemies_left and boss_dead then -- Normal level completion if Game.stage.finishing_timestamp == 0 then Game.stage.finishing_timestamp = Game.time end if Game.time - Game.stage.finishing_timestamp > 90 then -- 1.5 seconds delay Game.ingame=false if Game.current_stage < last_level then Game.current_stage = Game.current_stage + 1 Game.mode = 5 else Game.mode = 4 -- Special Win Page end end elseif Game.is_game_over and Game.shake <= 0 then -- screen stable (explosion end) immediately turn to black and white ruins Game.ingame=false Game.mode=3 end -- UI: Display Player Lives (Animated) local elapsed = Game.time - Game.ui_lives_timer if elapsed >= 0 and elapsed < 180 then local y = 0 if elapsed < 30 then y = -10 + (10 * elapsed / 30) elseif elapsed >= 150 then y = 0 - (10 * (elapsed - 150) / 30) end spr(496, 112, y, 0, 1, 0, 0, 1, 1) print(":" .. Game.player_lives, 121, y + 2, 9, false, 1) print(":" .. Game.player_lives, 121, y + 1, 4, false, 1) end -- Level Intro Animation ("FIGHT") if Game.intro_timer > 0 then local it = Game.intro_timer local bx = 0 if it > 60 then -- enter (20 frames): from right x=240 to x=0 local t = (it - 60) / 20 -- 1.0 -> 0.0 bx = 240 * (t * t) -- smooth acceleration enter elseif it > 20 then -- stay (40 frames) bx = 0 else -- move out (20 frames): from x=0 to left x=-240 local t = (20 - it) / 20 -- 0.0 -> 1.0 bx = -240 * (t * t) -- smooth acceleration move out end local h = 24 local y = (136 - h) // 2 -- draw red background (use color 2 or 9, here choose 2) rect(bx, y, 240, h, 4) -- draw "FIGHT" text (light/white 15) local text = "FIGHT!" -- roughly calculate center position: Scale 3 text width is about 15*6 = 90 local tx = bx + (240 - 60) // 2 local ty = y + (h - 12) // 2 --print(text, tx + 2, ty + 2, 1, false, 3) -- shadow print(text, tx, ty, 14, false, 2) -- main text end --content_generator() -- Deprecated in favor of map overlay elseif Game.mode==3 then --Game Over -- record timestamp when entering for the first time if Game.gameover_enter_time == 0 then Game.gameover_enter_time = Game.time Game.stage_cursor = 0 Game.gameover_music_played = false end -- 1. background layer (gray ruins) -- vbank 0 stays gray, vbank 1 uses standard sprite palette for UI/Text loadPalette(CACHED_PALETTES.GRAY, 0) loadPalette(CACHED_PALETTES.BANK0_V1, 1) if Game.fader then Game.fader:apply({0, 1}) end vbank(0) -- 2. UI vbank(1) cls(0) -- clear dynamic tanks/bullets/explosions, they were originally in vbank 1 local elapsed = Game.time - Game.gameover_enter_time local RISE_FRAMES = 120 local TEXT_Y_END = 54 local TEXT_Y_START = 150 local text_y if elapsed < RISE_FRAMES then local t = elapsed / RISE_FRAMES t = 1 - (1 - t) * (1 - t) -- ease-out quad text_y = math.floor(TEXT_Y_START + (TEXT_Y_END - TEXT_Y_START) * t) else text_y = TEXT_Y_END end -- draw GAME OVER text local text = "GAME OVER" local tx = 112 - 9*4 print(text, tx+1, text_y+1, 1, false, 2) -- shadow print(text, tx, text_y, 4, false, 2) -- color4 -- UI if elapsed >= RISE_FRAMES then local show_menu = (elapsed - RISE_FRAMES >= 30) -- direction keys to select if btnp(0) then Game.stage_cursor = 0 end if btnp(1) then Game.stage_cursor = 1 end if show_menu then if not Game.gameover_music_played then music(1, 0, 0,false,true) Game.gameover_music_played = true end local menu_x = 112 local menu_y1 = text_y + 28 local menu_y2 = text_y + 44 --draw text background rect(105, 76, 40, 32, 6) -- RETRY local col_retry = (Game.stage_cursor == 0) and 14 or 10 print("RETRY", menu_x, menu_y1+1, 3, false, 1) print("RETRY", menu_x, menu_y1, col_retry, false, 1) -- EXIT local col_exit = (Game.stage_cursor == 1) and 14 or 10 print("EXIT", menu_x, menu_y2+1, 3, false, 1) print("EXIT", menu_x, menu_y2, col_exit, false, 1) -- Z confirm if btnp(4) then music() -- stop music sfx(51) -- Confirm sound sync(0x3F, 0, false) -- Sync everything from Bank 0 to restore state Game.bullets = {} Game.ground_effects = {} particle_systems = {} ptcInit() if Game.stage_cursor == 0 then Game.player_lives = 5 Game.session_started = true Game.ingame = false Game.is_game_over = false Game.gameover_enter_time = 0 Game.mode = 1 else -- Exit & Save save_game() reset_game_session() end end end end vbank(0) elseif Game.mode==4 then -- special win page (Ending) for i=0,3 do sfx(-1, -1, -1, i) end -- stop all sounds vbank(1) cls(0) vbank(0) cls(0) -- 1. Sync Asset from Bank 2 only once (including Music + SFX from Bank 2) if not Game.mode_4_sync then sync(0x3F, 2, false) -- Sync Tiles, Sprites, Map, Palette, Music, SFX (0x3F = 63) Game.mode_4_sync = true Game.mode_4_start_time = Game.time music(0, 0, 0, true) -- Play Track 0 (Ending BGM) looping end -- Prepare fresh palette for vbank 0 (ending image) loadPalette(CACHED_PALETTES.BANK2_V0, 0) if Game.fader then Game.fader:apply({0}) end -- Only vbank 0 used here -- 2. Draw Background Graphics from Bank 2 -- Left side: Sprite 0 (16x15 tiles) spr(0, 0, 0, 0, 1, 0, 0, 16, 15) -- Right side: Sprite 256 (14x15 tiles) spr(256, 128, 0, 0, 1, 0, 0, 14, 15) -- Hair Animation Sequence (#240-#252, change every 7 frames) local hair_frames = {240, 242, 244, 246, 248, 250,252} local h_idx = (Game.time // 7) % #hair_frames + 1 spr(hair_frames[h_idx], 112, 32, 0, 1, 0, 0, 1, 1) local elapsed = Game.time - Game.mode_4_start_time local full_txt = "The world is broken, but hope still survives. The journey is over... for now." local char_speed = 5 local char_count = elapsed // char_speed -- Split into two lines as requested local full_txt = "The world is broken, but hope still survives. The journey is over... for now." local char_speed = 5 local char_count = elapsed // char_speed -- Split into three lines for better layout local lines = { "The world is broken,", "but hope still survives.", "The journey is over...for now." } -- Calculate centered X base on the longest line local char_w = 6 local max_w = 0 for _, l in ipairs(lines) do max_w = math.max(max_w, #l * char_w) end local tx = (240 - max_w) // 2 -- Draw lines sequentially with typewriter effect local current_total = 0 local typing_finished = false for i, line in ipairs(lines) do local line_y = 110 + (i-1) * 10 if char_count > current_total then local disp = string.sub(line, 1, char_count - current_total) print(disp, tx+1, line_y+1, 7, false, 1) -- Shadow print(disp, tx, line_y, 11, false, 1) -- Text end current_total = current_total + #line end typing_finished = char_count >= current_total -- 3. Handle ending input after text and 2sec delay if typing_finished then -- Delay check: 120 frames = 2 seconds if elapsed > (current_total * char_speed) + 120 then if btnp(4) then pmem(0, 0) -- Reset saved progress after ending reset_game_session() end end end vbank(0) elseif Game.mode==5 then -- MAP Interface for i=0,3 do sfx(-1, -1, -1, i) end -- stop all sounds -- 1. Background Layer (vbank 0) vbank(0) loadPalette(CACHED_PALETTES.MAP, 0) -- Palette for vbank 0 loadPalette(CACHED_PALETTES.BANK1_V0, 1) -- Palette for vbank 1 (UI) vbank(0) apply_shake() -- vbank 0: static map backdrop only — cls + draw_image once per visit (see mode_5_sync) if not Game.mode_5_sync then cls(8) draw_image(0, 0, 240, 88, Map_bg_pixel_data) end -- 2. UI Overlay Layer (vbank 1) vbank(1) apply_shake() cls(0) -- Transparent clear -- Only sync asset bank data once if not Game.mode_5_sync then sync(63, 1, false) music(0, 0, 0, true) -- Play Track 0 (Map BGM) Game.mode_5_sync = true Game.mode_5_start_time = Game.time particle_systems = {} -- Clear previous particles (like the 3D warp) -- Select briefing text for the current level if Game.current_stage == 1 then Game.map_text = "No pressure. It's just|your first mission." else local texts = Game.CONFIG.MAP_TEXTS local raw_text = texts[math.random(#texts)] -- Replace 'X' with actual level number Game.map_text = string.gsub(raw_text, "X", Game.current_stage) end if Game.current_stage > 1 then particle_systems = {} local prev_pos = Game.MAP_LOCATIONS[Game.current_stage - 1] if prev_pos then -- Pass center of the 16x16 pin make_explosion_map(prev_pos[1] + 8, prev_pos[2] + 8) make_explosparks_ps(prev_pos[1] + 8, prev_pos[2] + 8) end end end -- Draw UI background from Bank 1 Map data onto vbank 1 map(0, 11, 24, 6, 0, 88) -- Draw player portrait with glitch effect local is_glitch = (Game.time % 120 < 4) if is_glitch then for i=0,4 do local ox = math.random(-2, 2) spr(48 + i * 16, 6 + ox, 95 + i * 8, 15, 1, 0, 0, 5, 1) end else spr(48,6,95,15,1,0,0,5,5) end spr(10,192,80,14,1,0,0,6,7) -- Draw player life print(string.format("%02d", Game.player_lives), 222, 122, 3, false, 1, true) -- Update and Draw particles for the explosion (vbank 1) update_psystems() draw_psystems() local elapsed_total = Game.time - Game.mode_5_start_time local EXPLOSION_DELAY = (Game.current_stage > 1) and 60 or 0 local elapsed = elapsed_total - EXPLOSION_DELAY if elapsed >= 0 then local PIN_INTERVAL = 15 -- Frames between pins local total_pins = math.min(5, last_level - Game.current_stage + 1) local pins_finished_time = total_pins * PIN_INTERVAL -- 1. Draw Map Markers (Pins) for i = 0, total_pins - 1 do local pin_level = Game.current_stage + i local pin_delay = i * PIN_INTERVAL if elapsed > pin_delay then local pos = Game.MAP_LOCATIONS[pin_level] if pos then -- Only animate the current stage pin AFTER all pins have spawned if pin_level == Game.current_stage and elapsed >= pins_finished_time + 10 then -- Animated Marker for Current Level (#128, #130, #132) local anim_frames = {128, 130, 132} local sid = anim_frames[1 + (Game.time // 8) % 3] spr(160, pos[1], pos[2], 0, 1, 0, 0, 2, 2) spr(sid, pos[1], pos[2], 0, 1, 0, 0, 2, 2) else -- Static red marker (#160) for all pins initially, and future pins later spr(160, pos[1], pos[2], 0, 1, 0, 0, 2, 2) end -- Pop-up frame animation (#162, #164, #166, #168, change every 5 frames) local pop_elapsed = elapsed - pin_delay if pop_elapsed >= 0 and pop_elapsed < 20 then local pop_anim_frames = {162, 164, 166, 168} local anim_idx = (pop_elapsed // 5) + 1 spr(pop_anim_frames[anim_idx], pos[1], pos[2], 0, 1, 0, 0, 2, 2) end end end end -- 2. Draw Typewriter text (Randomized Briefing or Mission 1 special text) -- Start text typing after a short delay following the animation start local text_start_time = pins_finished_time + 30 local full_txt = Game.map_text local text_elapsed = elapsed - text_start_time if text_elapsed > 0 then local char_speed = 3 -- Faster typing local char_count = text_elapsed // char_speed local lines = {} -- Remove the literal '|' to calculate true text length local actual_text = string.gsub(full_txt, "|", "") -- Split the string by '|' for line in string.gmatch(full_txt, "[^|]+") do table.insert(lines, line) end local chars_left = char_count local fixed_x = 55 -- Fixed X coordinate for left alignment local current_y = 100 -- Starting Y coordinate -- Vertically center single line if #lines == 1 then current_y = 104 end for i, line in ipairs(lines) do if chars_left > 0 then local draw_len = math.min(#line, chars_left) local display_text = string.sub(line, 1, draw_len) -- Draw the current line with fixed left alignment print(display_text, fixed_x, current_y, 15, false, 1) chars_left = chars_left - draw_len end current_y = current_y + 12 end local typing_finished = char_count >= #actual_text -- 3. Show Input Reminder (#53/#54) and handle input if typing_finished then local prompt_sid = 53 + (Game.time // 15) % 2 spr(prompt_sid, 180, 126, 0) if btnp(4) then vbank(1) cls(0) music() -- Stop map music sfx(52,53,180,3,15,-4) -- Confirm sound Game.mode_5_sync = false Game.mode = 1 end end end end elseif Game.mode==6 then -- warp_scene -- Prepare fresh palette for vbank 0 (warp backdrop) loadPalette(CACHED_PALETTES.WARP, 0) loadPalette(CACHED_PALETTES.WARP, 1) if Game.fader then Game.fader:apply({0, 1}) end vbank(1) cls(0) vbank(0) cls(1) if not Game.mode_6_sync then music() -- Stop music when leaving title sync(3,7,false) -- sync tiles&sprites from bank7 Game.mode_6_sync = true Game.mode_6_start_time = Game.time particle_systems = {} make_3dwarp_ps() sfx(48,13,120,0,15,0) -- Play warp effect end update_psystems() draw_psystems() --draw top black bar spr(270,0,-2,0,1,0,-1,1,16) spr(270,128,-2,0,1,0,-1,1,16) --draw bottem black bar spr(270,0,128,0,1,0,-1,1,16) spr(270,128,128,0,1,0,-1,1,16) --draw background image spr(0,0,4,0,1,0,0,16,16) spr(256,128,4,0,1,0,0,14,16) -- Constant intensive shake during warp Game.shake = 10 local elapsed = Game.time - Game.mode_6_start_time -- 120 frames = 2 seconds at 60fps if elapsed >= 120 and not Game.next_mode then if Game.fader then Game.fader:fade_out(0.04) Game.next_mode = 5 Game.shake = 0 -- Reset shake immediately on transition else Game.mode = 5 Game.mode_6_sync = false Game.shake = 0 end end vbank(0) -- Reset to default vbank end Game.time=Game.time+1 end --==================================================================================-- -- PARTICLE SYSTEM LIBRARY (Integrated from pslib) --==================================================================================-- particle_systems = {} function make_psystem(minlife, maxlife, minstartsize, maxstartsize, minendsize, maxendsize) local ps = { autoremove = true, minlife = minlife, maxlife = maxlife, minstartsize = minstartsize, maxstartsize = maxstartsize, minendsize = minendsize, maxendsize = maxendsize, particles = {}, emittimers = {}, emitters = {}, drawfuncs = {}, affectors = {} } table.insert(particle_systems, ps) return ps end function update_psystems() local timenow = Game.time for key = #particle_systems, 1, -1 do update_ps(particle_systems[key], timenow) end end function update_ps(ps, timenow) for key = #ps.emittimers, 1, -1 do local et = ps.emittimers[key] local keep = et.timerfunc(ps, et.params) if (keep==false) then table.remove(ps.emittimers, key) end end for key = #ps.particles, 1, -1 do local p = ps.particles[key] p.phase = (timenow-p.starttime)/(p.deathtime-p.starttime) for i = 1, #ps.affectors do local a = ps.affectors[i] a.affectfunc(p, a.params) end p.x = p.x + p.vx p.y = p.y + p.vy local dead = false if (p.x<0 or p.x>240 or p.y<0 or p.y>136) then dead = true end if (timenow>=p.deathtime) then dead = true end if (dead==true) then table.remove(ps.particles, key) end end if (ps.autoremove==true and #ps.particles<=0) then for pskey = #particle_systems, 1, -1 do local pps = particle_systems[pskey] if pps==ps then table.remove(particle_systems, pskey) return end end end end function draw_ps(ps, params) for i = 1, #ps.drawfuncs do local df = ps.drawfuncs[i] df.drawfunc(ps, df.params) end end function draw_psystems() for i = 1, #particle_systems do draw_ps(particle_systems[i]) end end function emit_particle(psystem) local p = {} local e = psystem.emitters[math.random(#psystem.emitters)] e.emitfunc(p, e.params) p.phase = 0 p.starttime = Game.time p.deathtime = Game.time+(math.random()*(psystem.maxlife-psystem.minlife))+psystem.minlife p.startsize = (math.random()*(psystem.maxstartsize-psystem.minstartsize))+psystem.minstartsize p.endsize = (math.random()*(psystem.maxendsize-psystem.minendsize))+psystem.minendsize table.insert(psystem.particles, p) end function emittimer_burst(ps, params) for i=1,params.num do emit_particle(ps) end return false end function emittimer_constant(ps, params) if (params.nextemittime<=Game.time) then emit_particle(ps) params.nextemittime = params.nextemittime + params.speed end return true end function emitter_point(p, params) p.x = params.x p.y = params.y p.vx = (math.random()*(params.maxstartvx-params.minstartvx))+params.minstartvx p.vy = (math.random()*(params.maxstartvy-params.minstartvy))+params.minstartvy end function emitter_box(p, params) p.x = (math.random()*(params.maxx-params.minx))+params.minx p.y = (math.random()*(params.maxy-params.miny))+params.miny p.vx = (math.random()*(params.maxstartvx-params.minstartvx))+params.minstartvx p.vy = (math.random()*(params.maxstartvy-params.minstartvy))+params.minstartvy end function affect_force(p, params) p.vx = p.vx + params.fx p.vy = p.vy + params.fy end function affect_attract(p, params) local dx = p.x - params.x local dy = p.y - params.y if (math.abs(dx) + math.abs(dy) < params.mradius) then p.vx = p.vx + dx * params.strength p.vy = p.vy + dy * params.strength end end function draw_ps_fillcirc(ps, params) for i = 1, #ps.particles do local p = ps.particles[i] local c = math.floor(p.phase*#params.colors)+1 local r = (1-p.phase)*p.startsize+p.phase*p.endsize circ(p.x,p.y,r,params.colors[c]) end end function draw_ps_pixel(ps, params) for i = 1, #ps.particles do local p = ps.particles[i] local c = math.floor(p.phase*#params.colors)+1 pix(p.x,p.y,params.colors[c]) end end function draw_ps_streak(ps, params) for i = 1, #ps.particles do local p = ps.particles[i] local c = math.floor(p.phase*#params.colors)+1 line(p.x,p.y,p.x-p.vx,p.y-p.vy,params.colors[c]) end end function make_explosion_ps(ex,ey) local ps = make_psystem(6,30,9,18,1,3) -- Adjusted time unit to frames (60fps) instead of ms table.insert(ps.emittimers, { timerfunc = emittimer_burst, params = { num = 4 } }) table.insert(ps.emitters, { emitfunc = emitter_box, params = { minx = ex-4, maxx = ex+4, miny = ey-4, maxy= ey+4, minstartvx = 0, maxstartvx = 0, minstartvy = 0, maxstartvy=0 } }) table.insert(ps.drawfuncs, { drawfunc = draw_ps_fillcirc, params = { colors = {15,0,14,9,9,4} } }) end function make_explosparks_ps(ex,ey) local ps = make_psystem(18,42, 1,3,0.5,0.5) -- Adjusted time unit to frames table.insert(ps.emittimers, { timerfunc = emittimer_burst, params = { num = 10 } }) table.insert(ps.emitters, { emitfunc = emitter_point, params = { x = ex, y = ey, minstartvx = -1.5, maxstartvx = 1.5, minstartvy = -1.5, maxstartvy=1.5 } }) table.insert(ps.drawfuncs, { drawfunc = draw_ps_fillcirc, params = { colors = {15,14,4,15,8,2} } }) table.insert(ps.affectors, { affectfunc = affect_force, params = { fx = 0, fy = 0.0 } }) end function make_explosparks_item(ex,ey) local ps = make_psystem(6,25, 2,4,0.5,0.5) -- Adjusted time unit to frames table.insert(ps.emittimers, { timerfunc = emittimer_burst, params = { num = 10 } }) table.insert(ps.emitters, { emitfunc = emitter_point, params = { x = ex, y = ey, minstartvx = -1.5, maxstartvx = 1.5, minstartvy = -1.5, maxstartvy=1.5 } }) table.insert(ps.drawfuncs, { drawfunc = draw_ps_fillcirc, params = { colors = {15,14,4,15,8,2} } }) table.insert(ps.affectors, { affectfunc = affect_force, params = { fx = 0, fy = 0.0 } }) end function make_explosion_map(ex,ey) local ps = make_psystem(15,14,12,7,1,2) -- Adjusted time unit to frames (60fps) instead of ms table.insert(ps.emittimers, { timerfunc = emittimer_burst, params = { num = 4 } }) table.insert(ps.emitters, { emitfunc = emitter_box, params = { minx = ex-4, maxx = ex+4, miny = ey-4, maxy= ey+4, minstartvx = 0, maxstartvx = 0, minstartvy = 0, maxstartvy=0 } }) table.insert(ps.drawfuncs, { drawfunc = draw_ps_fillcirc, params = { colors = {15,0,14,9,9,4} } }) end function make_3dwarp_ps() local ps = make_psystem(60,120, 1,2,0.5,0.5) ps.autoremove = false table.insert(ps.emittimers, { timerfunc = emittimer_constant, params = {nextemittime = Game.time, speed = 0.5} } ) table.insert(ps.emitters, { emitfunc = emitter_box, params = { minx = 118, maxx = 122, miny = 63, maxy= 67, minstartvx = 0, maxstartvx = 0, minstartvy = 0, maxstartvy=0 } } ) table.insert(ps.affectors, { affectfunc = affect_attract, params = { x = 120, y = 65, mradius = 64, strength = 0.01 } } ) table.insert(ps.drawfuncs, { drawfunc = draw_ps_streak, params = { colors = {10,10,10,10,10,9,9,9,9,9,10,15,10,10,10,10,15,10,10,15,10,15,15} } } ) end function draw_image(x, y, width, height, pixel_data, color_key) color_key = color_key or -1 for i = 0, width - 1 do for j = 0, height - 1 do local index = j * width + i + 1 if pixel_data[index] ~= color_key then pix(x + i, y + j, pixel_data[index]) end end end end