3FFɕKKϐĿLiY3233CM"@4D43"o=3wwMUVUUUUUUUUUU3CTufvwwUȌEC3̻\Uuw̶fVUUlU̼M˙E1'33CDDD"4D3# eM=3MkwgDUfUUUUUUUUUUCCTvfvwwěFC3̻{\UewUU]UUlU̼D˙E"833CDDDD"4D3#0f=3Dvw]efeUUUUUUUU3CTffvweƌ̸33̼g\YMDD]UlU\Dy4<3DDDDD""#33"f43DmwwDUUUUUUUUUUUUU3CTffwwWěx53̼WǔVݽlU̻Mx3L3DDDCD""#3#Df4@Mm|w]UUUUUUUUUU]#CTffwwWUȌHx63WD3̼\u̼Mx03\"DDDCD3" 3#eVwWMM]UUUUUUUUUU]#3TfvwwVUIGCgVMD33̼\Mg=33|%CDDCDC#03#UVwNDMD]UUUUUUUUUU0TkvgwUuGlCW{WDCDDS̼XEg3Dό5CDDCD!3$"02"@TVtwFMDD[UUUUUUUUUU0TkvfvUUfGx0U|U]eDDM̼UEW34Dό&BDDDCD 3#""UwDMDlUUUUUUUUUUU0TkffvUU͈E\iMFMD̼]EW3DDČGBDDCD$ 33" SUwMDMDT]UUUUUUU00TkffgUUMWxF3Suݻ̵̻ۜVCDDHCDDMCD$ 3C#0TUNgDDDM53C]UuwwgfffV3@Tkffg]UI]gˌf4CDD̻|ۜfCDDČWCDDM3C$ 33#"0TUMմfMDDDM433CDDDTUUUfwwgU3TkffVMUUL]ֽ0{E3@ݻ̻̼ۜV3D|kDDD43C- 333"0TUMݴgMDDD43343DDDDMDDTkffVMeTU]WDMk@Խ˙̶̻ۜVDD|eDDDD3D- 334"@]UEݴgDDDDDM]U]033CDD3CDDTfffDUeUg5dTEDԽ̼ۜFCDݴxU4DD=3D= 33C"@UUugMDDDUUUUMDD433333SfffD[eV|gVDTMMDDۜwFDDݴxU=DD=CDM 33C#PUUeMDDDDUUUUMDDDD333SfffD[X]UVewVDTDDD@D̻wFDDݴlU4DD=CDM 33C#"@UUUDMDDDDMTUUUUDDD43SfffD[\]UUewfEDDDdED̉fED|U4CD=DDM 33C#"0]UUMDDTMMD4DUUUUDDD33SfffD[X]fVUvgVDDMDDD\[yUEDlU=@D=DDM"33C4"]UU]MDDwgM4DDDUUUUUMDDDDSfffMC[յX]fWeggMCU̼˙[̻ifDDWU=@D4DDD"33C4"SUU]MDvwwxM4DDDUUUUUMDDDDffVM3Uݽ|UnwvgM˜6̻U̼iUDDV]=CDDDDD"33CD#0UUfD|www]MDDDDUUUUMDSkfVD4T]|]fkMT\uiU]fy#2C̻YiUDDMED=CDDMD"33CD$22UvV݆|ǙUM]wVUDDD]]USVfDDDMˌUU-\DeVUUUeff4""34̘iVDMEECDMD"33CD42#UfUwVUMwgfDDDDUUffFDMC4͌XegfUnjUUelvf"23"CD#40dyDM|4ECDDD#33CDD3#eV՝yU~wwgUUDDDDUC[VEDEDD3͌XfvgUǘfh63C3M03yMDT|4MDDMD#33CDD3#fU̙U|wwwwUUUDDDD0UMDDDUl=efwVUU]vg3DDDM""""#334yDUW3MMDDMD#33CDD4C2DŚiu||wwgUUUDDD@kDDDDD{l]T]evUeMtvf3DDDDDDDD"""""""CDdiDMV3EMDDMD#33CDD4C2Dx|wwwwwVUUEDD3CUUDDDDDxE4UUUUUUMDT{gUwCDDDDD434 "BDMDDD4iDDkM3CMMDDMD$33CDDDC3t||wwwffVUUD3@kUDDEDCDxFTUUUUE3DDvWew0CDDDDD433"2@DDDDD4iD]Dw4CCMMDDMD$33CDDD333|̙|wwwgfVUUMDD1@]UDMDEMT]UUM333SvgEHCDDDD4D3323CDDDDCIMdf3DMMDDMD$33CDDD4D3gfeUDDD1@]DTDMgU]UE 33uwgDF3CDDDD4C4303CDDDDMCeIMkGCDD=MDDMD$33CDDD43C̙|fVUUMDD!@]EDDEDUTkFM33S{wFD{3CD4C43333033DDDTMeITfDCDD3DDDMD$33CDDDD4||fUUUEMD43@kE@4@3EmVD3@33kgf>w333DD4333343DDDE4eIe3D3DDDDMD$33DDDDD3ƚ|gUU]MCM0@[E@DEUkM403wVeDg333DD3#33T D4DDUD4SeIDU=CDDCCDDDDMD43CDDDDD3wgfUUMCD013@[]k=@DDVMwF3ggeFT7433C2"2PffC4UU43TeJDDMD4D4CDDDDD43CDDDDD3̙|wwfUU3033@[k D4mVEDfVeVefUVCdf=33333"4dfff33CD334]U >?0?P??p?????????????11;2`2{040p0`7@0@:<=>????????????????        2LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLQ    D  Y @0 @@@@@@@BCE F I K N L J E C @ @@@AACDGKNO4 MLMMMMMMMMMMMMMMMMMMMMMMMMMMMM@@@@@OONO/OOO=@\@ @ @ @NONOOOOOOOOOOMK HFE C BA-A=@M@\@K@;@*@@@@BCDE"F2G2H!IJLMO@@@@@@@@ @ @0@@@@@@APA@B@B@C@D@E0F0G IJJKLMNO  6,      a           Q QQ         b bb b b bb b   p pa  p pa      APP A A APPPaP AAAAp a a aaa " " """ a aaa aaaDdDD d d D  D   dd       a a aaa " " " ""ap  apap p PpB "" B BBB "" B BBBfߨ ) Ǩ3]3]?UUU@D43333C"03CDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD3333#""""33""32"!"UUU5DDD33334"DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD333C4""""" 03"2DDDDDD0#"UUUU@DDD433D# DDDDDDDDDDDDDDMDDDDDDDDDDDDDDDDDDD33333" "" "33"""DDDDDDD4#UUUU0DDDDDDDD#BDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD433333""" """03333#2DDDDDDDD$UUUUUCDDDDDDD42DDDDDDDDDDDDDMMDDDDDDDDDDDDDDDDDDDD433333$"" """2#33333#CDDDDDDDDDUUUUU0DDDDDDDDDDDDDDDDDDDDDMDDDDDDDDDDDDDDDDDDDDD43333CD""*"""233333333CDDDDDDDDD#UUUUUUCDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD33333CC*"*"""2333333333DDDDDDDDDD$UUUUUUE0CDDDDDDDDDDDDD43333DDMMDDDDDDDDDDDDDDDDDDDDDD33333CD*"""* 3333333333DDDDDDDDDD$UUUUUUU=CCDDDDDDDDDDD4333333DDMDDDDDDDDDDDDDDDDDDDDDDD3C33334@"**"*333333333CDDDDDDDDDDD4UUUUUUUC4DDDDDDDDDDDD433333CDDDDDDDDDDDDDDDDDDDDDDDDDDDDC333CD@@ **"":333333333DDDDDDDDDDDD4UfeUUUUe-4DDDDDDDDDDDD4333333CDDDDDCDDDDDDDDDDDDDDDDDDD4D33343@"**"":333333333CDDDDDDDDDDD4ffuUUUUUECCDDDDDDDDDDDD3333333DDDDD4DDDDDDDDDDDDDDDDDDDDDD33C44D@D""*":333333343DDDDDDDDDDDD4nfvUUUUUeCCDDDDDDDDDDDD3D3333CDDD43DDDDDDDDDDDDDDDDDDDD43434434"""""**333333343CDD33DDDDDDDD3fffUUUUeV$03DDDDDDDDDDDDD443333DD33DDDDDDDDDDDDDDDDDDDDD43CD3J3CD""""**333333333D4333DDDDDDDD#fffUUUUUf0CDDDDDDDDD3DDCDDD3333CDDDDDDDDDDDDDDDDDDDD43CD3*3CD """*""*333##3:C43333DDDDDDD4#effUUUefE4DDDDDDDDTM3443333333CDDDDDDDDDDDDDDDDD""JD43CD""J """***":33"323*333333CDDDDDD$ effUUUeU 4DDDDDDDU3433333333DDDDDDDDDDDDDDDDDD**D343**"" ""**":33233"333333DDDDDDDUfvVUUU20DDDDDDDDDDD43333333DDDDDDDDDDDDDDDDD""C3"**"*"****"*333333*"":3333DDDDDDDDڭffvfUUe50CD3DDDDDDDDDDD3333D43DDDDDDDDDDDDDDDDD"":D4*******"**:33333"*":333DDDDDDDD4ڭVfffVUU$CDDDCDDDDDDDDDM3333C33DDDDDDDDDDDDDDDDD*D3C***""**":333#""*":33DDDDDDDDD4 ݭVfffVUU4"DDDD4CDDDDDDDDDD433333CCDDDDDDDDDDDDDDDD*JDCC"""*"""**3333"""*":33DDDDDDDDD3 UfffVU#2DDDDDCDDDDDDDDDDD3333DDDDDDDDDDDDDDDDD*DDC"""*"**C3323#"":CDDDDDDDDDDCݪ UfffffE#CDDDDDD3 3CDDDDDDDDD4 3333DDDDDDDDDDDDDDDD"*DD*:3*""***""":333#33*CDDDDDDDDDD42ڪڭeffffV="CD43DDDD433DDDDDDDDDD=33CDDDDDDDDDDDDDDDDD*"""J4*C**"""*"":33#333*DDDDDDDDDDD3ڪڭ effffV 0D33333DDD$"3DDDDDDDDDD033DDDDDDDDDDDDDDDDD""D"D3***" *""*""333:"JDDDDDDDDDDDjfffffe0D33DD3DDD#0CDDDDDDDDDD 33CDDDDDDDDDDDDDDDDDJJD"""*"""**"3":3"""JDDDDDDDDDD4f ffffff04C]UDDD4"0DDDDDDDDMD033DDDDDDDDDDDDDDDDDDDDDDD*"""":*  "3#033**""DDDDDDDJD#f*"ffffff@DM]UUUDD4"0DDDDDDDDDD3333DDDDDDDDDDDDDDDDDDDC3*""3*J""#333D#" 33""""JDDDDDDD*D**"jf*" efffff@DMD]UUEDD40DDDDDDDDD333DDDDDDDDDDDDDDDDDDDDDd:4*:$ "333CDD$233"***"JDDDDDD""**"""b*"fffffDDMD]UUDDD43DDDDDD303DDDDDDDDDDDDDDDDD͌DDfvLT$D*:CD$23C4CDD33C*""""JDDDDDD***"""j&*":""gffff@DDMDDTUUUEDDD30DDDDDDM40DDDDDDDDDDDDDDDDxxDvȄD$*:C33CDDM4DDD**""*JDDDDD"*""""""*":gfffk@DDDU4DTUUUUU4DD4DDDDDD3430DDDDDDDDDDD{HILDN*:CB3333DDDDDDD***JDDD**"""****"JDMlff0DUU4DTUUUUUUDD4CDDMDM3ȜDDDDDDDDD͙J̄g*:C 333CDDDDDD***""JDDD*""*""""*"Jlff0DTUU4DUUUUUUUDDD3CDMDɜEDDDDDDDDLDx*:#"333DDDDDDD"*""JDDD"""""**""**"gfff@DDUUU4DUUUUUUUUU4D03D]ˈȜ̘6DDDDDDLL"Z3" 34DDDDDDDDD*""*"DDDD"""""*****"JD3Mffff@DUUU4DUUUUUUUUUUDD0DMMUȌH3DDDDHD"j%233DCDDDDDDDD"""JDDD*JD*""""***"*"G33fffV0DDUUU3DUUUUUUUUUUVDD4CMUifDDDDʉwĬ"jV32334CDDDDDDDDD*"*JDDD*"""**"*"ZfV3nffNDDMUUUU4DUUUfUVUUUUUUDDD0CMUěgfDC̪xx*"ZU3333CDDDDDDDDDDD"*JDDD"**" "*""***"ffVfff0DDDUUUU4DUUUkffUUUUUUUEDD0@DTU|fff5|E˘xẅ*"Z3333DDDDDDDDDDDD"*JDDD"""":""j*"*""ffUUfffBDDDUUUU4CUUUkffefUUUUUUD3D0Muwff̌x˩wvw*"Z3333DDDDDDDDDDDD*"*JDDD*""""3̪"*"jfVDkfVfCDDDUUUU4CTUUkffffffUUUUUUMDDM#0M|Ԍ|Ljywwwfvẅ*"jC433DDDDDDDDDDDD**JDD*""1]D̬|wfMffffCDDDUUUU4CTUUkfffffffVUUUUEDDuD#CDDDTED||wwwwgvwẅ*"jFC43CDDDDDDDDDDD"*JD C]EDwWwwfM]fffkCDDDUUUU=CT]UEeffUfffffvgffUDefM3DDDDDDEwwxwgw|̈*"j533DDDDDDDDDDDDD"" UUUVUU|VԵfnVV0DDDDUUUUU5CUUEmffVVfffVvgfgfFDDTUUDDDDDDDDTMVwwwgwẅ̧":23DDDDDDDDDDDDDD""UDUյUgյkfefF0DDDDUUUUUE3UUEdffVfffffevffvfDDUUUMDDDDDDDD33]wgwwwww":3CDDDDDDDDDDDDDD"" ]=CDDWݶgfVfE0DDDDUUUUeFCTUUEffVeffffVfffffuDDMUUUE43CDDDDDD3˘|wwwwv":CDDDDDDDDDDDDDDD"*" 04CDDDԻwgggfVf52DDDUUUUf3TUUEfffUffffVefUUUUVMD]UUUED43CDDDDD3DDduwwwwf*JDDDDDDDDDDDDDDDD*"*CDDD[|wwgvvfff=0DDDUUUVf3TUUETkffUefffffUUUUUUUUUUEDD30DDDDDDDDD3CgUfM*:}wwEDDDDDDDDDD*"" @UEDDDUD|wwffgff=@DDDUUeffV3UUDkffEefffffVUUUUUUUUUMDMDD3DDDDDDDDDD#23DD*jwwwWDDDDDDDDD""]UDDDUDD|wwwfgff3@DDDUUffff4UUDmkfUTefffffVUUUeUUUUUVMDDD3DDDDDDDDDD]vweEDDDDD4"""2LDD̻̻MDU[efvfgnf4CDDDDUefff4UVDffVD]ffffffUUUUUeUUUUUM4DD433DDDDDDDDDDvwwgfVDDD33!"@DD3EUCDDDDD̼KD[UUgvfeV#CDDDUeffffEUUUMTffVET]efffgVUfUefVfUUMD4CTDD40#"233DDDDDDdfvvUM"@DDUUEC4VDDGDD̻[̼M˻[]wwgfV"CDDUeffffEUVUMTfvfCeffgfUefffffVUDDCTUU$03""2#3CD] CUUU5C4VDCFDD̻[ݻ˻UUwwfff"DDMegfffffCUV]MDfvV4DUffgfVUVeUVUMDE3CUU$CD]UUU400[ " UUU4D4ED3ED̻[˻kUwwffVDDMUwfffffCTV]MDefff4DDDUffvffefffUE4CU4DC[UU$CD]U]##U2#23"UUU3MMDD4DD̼MOTۿ[U|wgfV DDMTe|ffffvDU]MDmfffE3DDDDDewffUUUD4CfEDDCUUU43DTUD]E433223"2#033 3]]UUCM4DDM̼DDD̻ܴ[|wgU0DDMTegfff{EeDDfff3CDDDDDUffUUEDDTfD]MCUUU=3DTDUE04C4223"2#03#""3]U]U54TDDD̼DDM̻[|wfUDDMTewfffwFDeDDlffV433DDDDDeffUUUVef4UgCUUU=3DTUDTUDM"23"233"""33]U]=4TDDDDDM̻[Mջwg0DDM{gfvvDMDD[fffD33DDDDDDTffUffV4CgD[U53CTUUUCDD#233"3 #""!3331]]U4=TMU|DDDDDUԽ̼MF]f0DDD{w|fwwgDWEDDeffE34DDDݻUveCDED]54CUeUU@DD$233"3 """ 3330]]CMTMݻ{DDDDD]UD̼MKDݵf2DDD{gwwwDVEDDUgfV4CU]kF3Df]EDCUMTfUV5CDD$233"30" 34#@DMTEMDDDDDUE4[[D[f0DCD{̌wvwwETXMDDUvff4C[Tk52DDDWMDCDUM]fUVECDD3333"#0"""234#DMMTEDDDDDDUDCD̻kEm34CD{w|wwwGMETEC[f%CDݽgMD3DUMeeUVU"@DD@333#2 """234#DDDMMDDDDDDTEDD̻MKԻ[C4C4{w|wwwDMu[Deə4[f CDݴg]M3D]MefUfU@D4@333#  #"3234#MDDս[DEDDDDTDDMD̻MKD[MC434S{w̌wwwlD]MTT̜lM[dg40DݴhTM4DMkfUfDD$@3333!33$3C4#offUDMԴKWCDDDDDDEDC434S{wxww|MDTkM̉ԵUkDDDDT|DE4DMffMeVKD033333343C4#offVDMDKCDDD3D[E[D[C33D4S{|wwFCDT̼T̜\T[CUfM|UEUDDffMdf03333#CD4CD4offNDEFDDD4DTVDMD[D33D4T{w̌wwLCDT̻ݵ̜VM̻eD|E{UKTE]dffEf4]=D43332"CD4DD4offEDD{5DDDCMDT|VMMDD33DT{w̌|\4MC̼V]ɉUD̼U]T|]VEUmffEfEM43332#CD4DD4#_ffDDKC|DDDDMmUUݵ]MDMD040DT{wx|4DMCU\lU̻Դ[VM]ewgEkV#!333333@D4DD4"ofNEwDDDMUUUUUUUU04DTvw|x6CDC̼[XYUMU̼[ULDU̙3V 233334@D4DDD"oVMMDwUDUUUeUUUUU03DSvwww|6CC[\ŜX]^̻[VDDeɌ#h##2#33DDD4DDD#ofMEkDgDUUVeVUUUUU0#DT{wgwWwGCC3˼̻|̌VU]˼̼U=Dkɉ33#33CCDD"D4DDD#oVD=KmEݴUUUUfUUUU@3DTvwgfV̌DD3Ż̵̻g\UU\\UD˙=323CDD"@4DD3"oD^3DUfVUUUUUUUUC#1DTvwvvwV̌x4C3̼˻̶gVVe\\ue\D˙EA>233CM"@4D43"o=3wwMUVUUUUUUUUUU3CTufvwwUȌEC3̻\Uuw̶fVUUlU̼M˙E1'33CDDD"4D3# eM=3MkwgDUfUUUUUUUUUUCCTvfvwwěFC3̻{\UewUU]UUlU̼D˙E"833CDDDD"4D3#0f=3Dvw]efeUUUUUUUU3CTffvweƌ̸33̼g\YMDD]UlU\Dy4<3DDDDD""#33"f43DmwwDUUUUUUUUUUUUU3CTffwwWěx53̼WǔVݽlU̻Mx3L3DDDCD""#3#Df4@Mm|w]UUUUUUUUUU]#CTffwwWUȌHx63WD3̼\u̼Mx03\"DDDCD3" 3#eVwWMM]UUUUUUUUUU]#3TfvwwVUIGCgVMD33̼\Mg=33|%CDDCDC#03#UVwNDMD]UUUUUUUUUU0TkvgwUuGlCW{WDCDDS̼XEg3Dό5CDDCD!3$"02"@TVtwFMDD[UUUUUUUUUU0TkvfvUUfGx0U|U]eDDM̼UEW34Dό&BDDDCD 3#""UwDMDlUUUUUUUUUUU0TkffvUU͈E\iMFMD̼]EW3DDČGBDDCD$ 33" SUwMDMDT]UUUUUUU00TkffgUUMWxF3Suݻ̵̻ۜVCDDHCDDMCD$ 3C#0TUNgDDDM53C]UuwwgfffV3@Tkffg]UI]gˌf4CDD̻|ۜfCDDČWCDDM3C$ 33#"0TUMմfMDDDM433CDDDTUUUfwwgU3TkffVMUUL]ֽ0{E3@ݻ̻̼ۜV3D|kDDD43C- 333"0TUMݴgMDDD43343DDDDMDDTkffVMeTU]WDMk@Խ˙̶̻ۜVDD|eDDDD3D- 334"@]UEݴgDDDDDM]U]033CDD3CDDTfffDUeUg5dTEDԽ̼ۜFCDݴxU4DD=3D= 33C"@UUugMDDDUUUUMDD433333SfffD[eV|gVDTMMDDۜwFDDݴxU=DD=CDM 33C#PUUeMDDDDUUUUMDDDD333SfffD[X]UVewVDTDDD@D̻wFDDݴlU4DD=CDM 33C#"@UUUDMDDDDMTUUUUDDD43SfffD[\]UUewfEDDDdED̉fED|U4CD=DDM 33C#"0]UUMDDTMMD4DUUUUDDD33SfffD[X]fVUvgVDDMDDD\[yUEDlU=@D=DDM"33C4"]UU]MDDwgM4DDDUUUUUMDDDDSfffMC[յX]fWeggMCU̼˙[̻ifDDWU=@D4DDD"33C4"SUU]MDvwwxM4DDDUUUUUMDDDDffVM3Uݽ|UnwvgM˜6̻U̼iUDDV]=CDDDDD"33CD#0UUfD|www]MDDDDUUUUMDSkfVD4T]|]fkMT\uiU]fy#2C̻YiUDDMED=CDDMD"33CD$22UvV݆|ǙUM]wVUDDD]]USVfDDDMˌUU-\DeVUUUeff4""34̘iVDMEECDMD"33CD42#UfUwVUMwgfDDDDUUffFDMC4͌XegfUnjUUelvf"23"CD#40dyDM|4ECDDD#33CDD3#eV՝yU~wwgUUDDDDUC[VEDEDD3͌XfvgUǘfh63C3M03yMDT|4MDDMD#33CDD3#fU̙U|wwwwUUUDDDD0UMDDDUl=efwVUU]vg3DDDM""""#334yDUW3MMDDMD#33CDD4C2DŚiu||wwgUUUDDD@kDDDDD{l]T]evUeMtvf3DDDDDDDD"""""""CDdiDMV3EMDDMD#33CDD4C2Dx|wwwwwVUUEDD3CUUDDDDDxE4UUUUUUMDT{gUwCDDDDD434 "BDMDDD4iDDkM3CMMDDMD$33CDDDC3t||wwwffVUUD3@kUDDEDCDxFTUUUUE3DDvWew0CDDDDD433"2@DDDDD4iD]Dw4CCMMDDMD$33CDDD333|̙|wwwgfVUUMDD1@]UDMDEMT]UUM333SvgEHCDDDD4D3323CDDDDCIMdf3DMMDDMD$33CDDD4D3gfeUDDD1@]DTDMgU]UE 33uwgDF3CDDDD4C4303CDDDDMCeIMkGCDD=MDDMD$33CDDD43C̙|fVUUMDD!@]EDDEDUTkFM33S{wFD{3CD4C43333033DDDTMeITfDCDD3DDDMD$33CDDDD4||fUUUEMD43@kE@4@3EmVD3@33kgf>w333DD4333343DDDE4eIe3D3DDDDMD$33DDDDD3ƚ|gUU]MCM0@[E@DEUkM403wVeDg333DD3#33T D4DDUD4SeIDU=CDDCCDDDDMD43CDDDDD3wgfUUMCD013@[]k=@DDVMwF3ggeFT7433C2"2PffC4UU43TeJDDMD4D4CDDDDD43CDDDDD3̙|wwfUU3033@[k D4mVEDfVeVefUVCdf=33333"4dfff33CD334]U= a and x < b end math.clamp = function(x, a, b) if a == nil and b == nil then a, b = 0, 1 end return max(a, min(x, b)) end math.bool2int = function(a) return a and 1 or 0 end math.mod = function(x, a) return x - floor(x/a)*a end math.sign = function(x) return x > 0 and 1 or -1 end math.maprange = function(x, a1, b1, a2, b2) return (x - a1)/(b1 - a1)*(b2 - a2) + a2 end math.isnan = function(x) return x ~= x end math.round = function(x) return math.floor(x + 0.5) end math.lerp = function(x, a, b) return (1-x)*a + x*b end math.isin = function(a, list) local flag = false for i = 1,#list do if list[i] == a then flag = true break end end return flag end math.randlist = function(list) local n = #list local i = random(1, n) return list[i] end math.randprob = function(list, prob) local a = random() local cumsum = 0 for i = 1,#list do if a <= prob[i] then return a end cumsum = cumsum + prob[i+1] end end math.rmdupe = function(list) local hash = {} local res = {} for _,v in ipairs(list) do if (not hash[v]) then res[#res+1] = v hash[v] = true end end return res end math.sum = function(list) local tot = 0 for i = 1,#list do tot = tot + list[i] end return tot end math.maxf = function(list, fun) local v = list[1] local k = 1 for i = 2,#list do if fun(v, list[i]) then v = list[i] k = i end end return v, k end -- verified (trust me 15/10/22) math.isccw = function(x1, y1, x2, y2, x3, y3) local a1 = x2 - x1 local a2 = y2 - y1 local b1 = x3 - x1 local b2 = y3 - y1 return (a1*b2 - b1*a2) < 0 end -- box-muller transform math.randnorm = function() local U1 = random() local U2 = random() local R = sqrt(-2*math.log(U1)) local th = 6.2832*U2 return R*cos(th), R*sin(th) end -- -- higher level functions convexhull = function(points) local hull = {} -- min y-value local p, k = math.maxf(points, function(u,v) return u[2] > v[2] end) for i = 1,#points do table.insert(hull, p) local q -- q cant equal p for _,r in ipairs(points) do q = r if q[1] ~= p[1] or q[2] ~= p[2] then break end end -- find most ccw point for _,r in ipairs(points) do if math.isccw(p[1], p[2], q[1], q[2], r[1], r[2]) then q = r end end if q == hull[1] then break end p = q end return hull end polyb = function(points, col) for i = 1,#points-1 do line(points[i][1], points[i][2], points[i+1][1], points[i+1][2], 15) end line(points[1][1], points[1][2], points[#points][1], points[#points][2], 0) end point_in_hull = function(p, poly) for i = 1,#poly-1 do if math.isccw(p[1], p[2], poly[i][1], poly[i][2], poly[i+1][1], poly[i+1][2]) then return false end end return not math.isccw(p[1], p[2], poly[#poly][1], poly[#poly][2], poly[1][1], poly[1][2]) end find = function(x, list) local index = nil for j = 1,#list do if x == list[j] then index = j end end return index end -- time = { t = 0, dt = 0, last_t = 0, count = 0, fps = 60, ts = 0, show = true, timers = {}, update = function(self) if keyp(20) then self.show = not self.show end local t = systime() self.dt = t - self.t self.t = t self.ts = self.t // 1000 if t - self.last_t > 1000 then self.last_t = t self.fps = self.count self.count = 0 else self.count = self.count + 1 end for i,timer in ipairs(self.timers) do timer:update() end end, draw = function(self) if self.show then print(self.fps, 228, 1, 0, true) end end, new = function(time) local timer = {t=0, finish=false} setmetatable(timer, time.mt) return timer end, mt = { __index = { update = function(self) if self.finish then return end self.t = self.t + 1 if self.t >= self.finish then self.finish = true end end, reset = function() self.t = 0 self.finish = false end } } } mouse = { x = 120, y = 68, dx = 0, dy = 0, md = false, mp = false, mu = false, right = false, middle = false, scroll = 0, update = function(self) local mx, my, md, middle, right, scrollx, scrolly = sysmouse() self.dx = mx - self.x self.dy = my - self.y self.x = mx self.y = my self.mp = not self.md and md self.mu = self.md and not md self.md = md self.right = right self.middle = middle self.scroll = scrolly end, draw = function(self) line(0, self.y, WIDTH, self.y, 6) line(self.x, 0, self.x, HEIGHT, 6) print('(' .. self.x .. ',' .. self.y .. ')', self.x+2, self.y+2, 6) local w = print(self.x, 0, -6) print(self.x, self.x-w, 0, 6) print(self.y, 0, self.y-6 , 6) end, } Vec = { new = function(x, y, z) local v = {x, y, z} setmetatable(v, Vec.mt) return v end, mt = { __add = function (u, v) return Vec.new(u[1]+v[1], u[2]+v[2], u[3]+v[3]) end, __sub = function(u, v) return Vec.new(u[1]-v[1], u[2]-v[2], u[3]-v[3]) end, __mul = function(k, v) if type(k) == "table" then return Vec.new(k[1]*v[1], k[2]*v[2], k[3]*v[3]) else return Vec.new(k*v[1], k*v[2], k*v[3]) end end, __div = function(v, k) if type(k) == "table" then return Vec.new(v[1]/k[1], v[2]/k[2], v[3]/k[3]) else return Vec.new(v[1]/k, v[2]/k, v[3]/k) end end, __pow = function(v, k) return Vec.new(v[1]^k, v[2]^k, v[3]^k) end, __eq = function(u, v) return u[1] == v[1] and u[2] == v[2] and u[3] == v[3] end, __tostring = function(v) return "(" .. v[1] .. "," .. v[2] .. "," .. v[3] .. ")" end, __concat = function(s, v) return s .. "(" .. v[1] .. "," .. v[2] .. "," .. v[3] .. ")" end, __index = { normalise = function(self) local r = Vec.norm(self) self[1] = self[1] / r self[2] = self[2] / r self[3] = self[3] / r return self end, ortho_proj = function(self, camera) local x = Vec.dot(self, camera.x) + 86 local y = Vec.dot(self, camera.y) - H2 return x, -y end, -- accurate pers_proj = function(self, camera) local x = Vec.dot(self, camera.x) local y = Vec.dot(self, camera.y) local r = Vec.dist(self, camera.o) local r2 = sqrt(r^2 - x^2 - y^2) x = camera.depth * x / r2 + W2 y = camera.depth * y / r2 - H2 return x, -y end, -- simple proj proj = function(self, camera) local x = Vec.dot(self, camera.x) local y = Vec.dot(self, camera.y) local r = Vec.dist(self, camera.o) x = camera.depth * x / r + W2 y = camera.depth * y / r - H2 return x, -y end, rotate = function(self, ax, ay, az) local cx = cos(ax) local sx = sin(ax) local cy = cos(ay) local sy = sin(ay) local cz = cos(az) local sz = sin(az) local x1 = self[1] local y1 = self[2] * cx - self[3] * sx local z1 = self[2] * sx + self[3] * cx local x2 = x1 * cy + z1 * sy local y2 = y1 local z2 = -x1 * sy + z1 * cy self[1] = x2 * cz - y2 * sz self[2] = x2 * sz + y2 * cz self[3] = z2 return self end, qrotate = function(self, theta, u) u = u / Vec.norm(u) local c = cos(theta/2) local s = sin(theta/2) local k = Vec.dot(s*u, self) local w = Vec.cross(s*u, self) local v = 2*k*s*u + (c*c-s*s)*self + 2*c*w self[1] = v[1] self[2] = v[2] self[3] = v[3] return self end, draw = function(self) local x, y = self:proj(camera) circb(x, y, 2, 4) end, } }, norm = function(v) return sqrt(v[1]*v[1] + v[2]*v[2] + v[3]*v[3]) end, dot = function(u, v) return u[1]*v[1] + u[2]*v[2] + u[3]*v[3] end, cross = function(u, v) return Vec.new(u[2]*v[3]-v[2]*u[3], v[1]*u[3]-u[1]*v[3], u[1]*v[2]-v[1]*u[2]) end, avg = function(u) return (u[1] + u[2] + u[3]) / 3 end, dist = function(u, v) return Vec.norm(u - v) end, mid = function(u, v) return (u + v) / 2 end, round = function(u) return Vec.new(math.round(u[1]), math.round(u[2]), math.round(u[3])) end, line = function(u, v, camera, col) local x1, y1 = u:proj(camera) local x2, y2 = v:proj(camera) line(x1, y1, x2, y2, col) end, ysort = function(u, v) return u[2] < v[2] end } camera = { o = Vec.new(0, 0, 100), x = Vec.new(1, 0, 0), y = Vec.new(0, 1, 0), ax = 0, ay = 0.5, r = 100, depth = 100, load = function(self) self.o = Vec.new(0, 0, 200) self.ay = 3 self.r = 200 -- learping targets self.axt = -0.5 self.ayt = 0 self.rt = 100 -- perspective projection Vec.mt.__index.proj = Vec.mt.__index.ortho_proj end, update = function(self) -- update target -- if mouse.md and textbox.hide then if mouse.md then self.axt = math.clamp(self.axt - 0.03*mouse.dy, -PI/2, PI/2) self.ayt = self.ayt - 0.03*mouse.dx end if textbox.hide == false then -- constant rotation self.ayt = self.ayt + 0.01 end self.rt = math.clamp(self.rt - 7*mouse.scroll, 60, 300) -- lerping self.ax = math.lerp(0.1, self.ax, self.axt) self.ay = math.lerp(0.1, self.ay, self.ayt) self.r = math.lerp(0.1, self.r, self.rt) -- update camera self.o = Vec.new(0, 0, self.r):rotate(self.ax, self.ay, 0) self.x = Vec.new(1, 0, 0):rotate(0, self.ay, 0) self.y = Vec.new(0, 1, 0):rotate(self.ax, self.ay, 0) -- reset camera (num 0) if keyp(27) then self:reset() end -- toggle perspective (num 5) if keyp(32) then self.togglePers() end end, togglePers = function() if Vec.mt.__index.proj == Vec.mt.__index.pers_proj then Vec.mt.__index.proj = Vec.mt.__index.ortho_proj else Vec.mt.__index.proj = Vec.mt.__index.pers_proj end end, reset = function(self) self.ax = 0 self.ay = 3 self.axt = -0.5 self.ayt = 0 self.rt = 100 Vec.mt.__index.proj = Vec.mt.__index.ortho_proj end } color = { addr = 0x03FC0, new = function(r, g, b) local c = Vec.new(r, g, b) return c end, getcol = function(col) local addr = color.addr + 3*col local r = peek(addr) local g = peek(addr+1) local b = peek(addr+3) return Vec.new(r, g, b) end, setcol = function(c, col) local addr = color.addr + 3*col poke(addr, c[1]) poke(addr+1, c[2]) poke(addr+2, c[3]) end, draw = function() for i = 0,15 do rect(184+i%8*7, 7+i//8*7, 6, 6, i) end local col = pix(mouse.x, mouse.y) print(col, mouse.x+7, mouse.y, 15, 0, 1, 1) end } Face = { new = function(verts, col) local f = {verts=verts, col=col} setmetatable(f, Face.mt) return f end, mt = { __index = { draw = function(self) local x0, y0 = table.unpack(self.verts[1].p) local x1, y1 = table.unpack(self.verts[2].p) local x2, y2 for i = 3,#self.verts do x2, y2 = table.unpack(self.verts[i].p) tri(x0, y0, x1, y1, x2, y2, self.col) x1, y1 = x2, y2 end end, drawWire = function(self) local x1, y1 = table.unpack(self.verts[#self.verts].p) local x2, y2 = table.unpack(self.verts[1].p) line(x1, y1, x2, y2, self.col) for i = 2,#self.verts do x1, y1 = x2, y2 x2, y2 = table.unpack(self.verts[i].p) line(x1, y1, x2, y2, self.col) end end, -- furthest point -- seto = function(self) -- local m = self.verts[1].p[3] -- for i = 2,#self.verts do -- if m < self.verts[i].p[3] then -- m = self.verts[i].p[3] -- end -- end -- self.o = m -- end, -- mid point seto = function(self) local m = self.verts[1].p[3] for i = 2,#self.verts do m = m + self.verts[i].p[3] end self.o = m / #self.verts end, isccw = function(self) local x1, y1 = table.unpack(self.verts[1].p) local x2, y2 = table.unpack(self.verts[2].p) local x3, y3 = table.unpack(self.verts[3].p) return math.isccw(x1, y1, x2, y2, x3, y3) end, proj = function(self) for _,v in ipairs(self.verts) do -- v.p = (x, y, r) v.p[1], v.p[2] = v:proj(camera) v.p[3] = Vec.dist(v, camera.o) -- v.p[3] = camera.o[3] - v[3] end end } } } Tface = { new = function(verts, uv) local f = {verts=verts, uv=uv} setmetatable(f, Tface.mt) return f end, mt = { __index = { draw = function(self) local x0, y0, z0 = table.unpack(self.verts[1].p) local u0, v0 = self.uv[1], self.uv[2] local x1, y1, z1 = table.unpack(self.verts[2].p) local u1, v1 = self.uv[3], self.uv[4] local x2, y2, z2, u2, v2 for i = 3,#self.verts do x2, y2, z2 = table.unpack(self.verts[i].p) u2, v2 = self.uv[2*i-1], self.uv[2*i] ttri(x0, y0, x1, y1, x2, y2, u0, v0, u1, v1, u2, v2, 0, 0, z0, z1, z2) x1, y1, z1 = x2, y2, z2 u1, v1 = u2, v2 end end, seto = Face.mt.__index.seto, isccw = Face.mt.__index.isccw, proj = Face.mt.__index.proj, } } } Mesh = { cols = {}, instances = {}, nface = 0, new = function(obj, hide, selectable, onclick) local scale = obj.scale or 30 local mesh = { selectable = selectable or false, hide = hide or false, onclick = onclick or nil, } -- load verticies mesh.verts = {} for i,v in ipairs(obj.verts) do mesh.verts[i] = scale * Vec.new(table.unpack(v)) mesh.verts[i].p = Vec.new(0, 0, 0) end -- load faces mesh.faces = {} for i,f in ipairs(obj.faces) do -- pointers to vertices local vj = {} for _,j in ipairs(f) do table.insert(vj, mesh.verts[j]) end -- mesh.faces[i] = Face.new(vj, #Mesh.cols+obj.fcols[i]+7) -- color local col = obj.cols[obj.fcols[i]] setmetatable(col, Vec.mt) local k = find(col, Mesh.cols) if k == nil then color.setcol(col, #Mesh.cols+8) mesh.faces[i] = Face.new(vj, #Mesh.cols+8) table.insert(Mesh.cols, col) else mesh.faces[i] = Face.new(vj, k+7) end end setmetatable(mesh, Mesh.mt) -- color -- for i,c in ipairs(obj.cols) do -- local j = find(c, Mesh.cols) -- if j ~= nil then -- -- need to fix for multiple colors -- for k,f in ipairs(mesh.faces) do -- f.col = j + 7 -- end -- else -- color.setcol(c, #Mesh.cols+8) -- setmetatable(c, Vec.mt) -- table.insert(Mesh.cols, c) -- end -- end table.insert(Mesh.instances, mesh) return mesh end, mt = { __index = { proj = function(self) for _,v in ipairs(self.verts) do -- v.p = (x, y, r) v.p[1], v.p[2] = v:proj(camera) v.p[3] = Vec.dist(v, camera.o) end end, update = function(self) self.ishovered = false self.ispressed = false if coroutine.status(transition.co) == "suspended" then return end if not textbox.hide then return end if not self.selectable then return end if self.hide then return end self.hull = convexhull(self:get_proj()) self.ishovered = point_in_hull({mouse.x, mouse.y}, self.hull) if not self.ishovered then return end if mouse.mp and type(self.onclick) == "function" then self:onclick() end if mouse.md then self.ispressed = true end end, draw = function(self) -- project vertex self:proj() -- backface culling local dfaces = {} for _,f in ipairs(self.faces) do if f:isccw() then table.insert(dfaces, f) end end -- compute depth, sort, and draw for _,f in ipairs(dfaces) do f:seto() end table.sort(dfaces, Mesh.zsort) for i,f in ipairs(dfaces) do f:draw() end end, draw_twosided = function(self) self:proj() -- compute depth, sort, and draw for _,f in ipairs(self.faces) do f:seto() end table.sort(self.faces, Mesh.zsort) for i,f in ipairs(self.faces) do f:draw() end end, get_proj = function(self) local s = {} for i,v in ipairs(self.verts) do s[i] = v.p end return s end, } }, zsort = function(u, v) return u.o > v.o end, clear = function(self) self.cols = {} self.instances = {} Points.instances = {} end, update = function(self) for i,m in ipairs(self.instances) do m:update() end end, draw = function(self) -- faces to draw local dfaces = {} for i,m in ipairs(self.instances) do if not m.hide then m:proj() -- backface culling for _,f in ipairs(m.faces) do if f:isccw() then table.insert(dfaces, f) end end end end -- add particles for _,p in ipairs(Points.instances) do table.insert(dfaces, p) end -- compute depth, sort, and draw for _,f in ipairs(dfaces) do f:seto() end table.sort(dfaces, self.zsort) for i,f in ipairs(dfaces) do f:draw() end self.nface = #dfaces -- draw outline for i,m in ipairs(self.instances) do if m.ishovered then polyb(m.hull, 0) end end end, hide_and_proceed = function(self) self.hide = true coroutine.resume(dialog.co) end } Points = { instances = {}, new = function(self) local p = {} p.v = self.randpoint() p.t = random(400) p.id = random(337, 338) setmetatable(p, Points.mt) table.insert(self.instances, p) return p end, mt = { __index = { draw = function(self) local x, y = self.v:proj(camera) -- pix(x, y, 15) self.t = self.t + 1 spr(self.id, x-3, y-3+2*sin(self.t/20), 0) end, seto = function(self) self.o = Vec.dist(self.v, camera.o) -- self.o = camera.o[3] - self.v[3] end } }, load = function(self) self.instances = {} for i = 1,random(3,7) do self:new() end end, randpoint = function() local x, y = math.randnorm() local z = math.randnorm() local v = Vec.new(x, abs(y), z):normalise() v = 40 * v return v end, } textbox = { _x = 20, _y = 150, x = 20, y = 150, speed = 0.5, width = 156, pad = 10, hide = true, text = "", len = 0, visible = 0, finish = false, instruction = "", spell = false, say = function(self, text) -- add line break local t = "" local tnew = "" for word in text:gmatch("%S+") do tnew = t .. word .. " " local w = print(tnew, 0, -6) if w > self.width then tnew = t .. "\n".. word .. " " end t = tnew end self.text = tnew self.len = #tnew if self.hide then self.visible = -16 self.hide = false else self.visible = 0 end self.finish = false end, update = function(self) if self.hide then textbox.y = 150 else textbox.y = 100 end -- lerp position self._x = self.x self._y = math.lerp(0.1, self._y, self.y) -- break when text finish if self.finish then return end -- spacebar -- if keyp(48) then -- self:jump() -- return -- end -- increment text self.visible = self.visible + self.speed if self.visible >= self.len then self.finish = true end end, draw = function(self) print(self.instruction, 12, 122-time.ts%2, 1) rect(self._x-self.pad+2, self._y-self.pad, self.width+2*self.pad-4, 18+2*self.pad, 0) rect(self._x-self.pad, self._y-self.pad+2, 4, 18+2*self.pad-4, 0) rectb(self._x-self.pad+2, self._y-self.pad+2, self.width+2*self.pad-4, 18+2*self.pad-4, 2) spr(1, self._x-self.pad, self._y-self.pad, 3) spr(1, self._x-self.pad, self._y+20, 3, 1, 0, 3) spr(2, self._x+self.width/2, self._y-self.pad, -1, 1, 0, 0, 2, 1) if self.hide then return end local a = max(0, floor(self.visible)) local s = self.text:sub(1, a) if character.sage.talk then print(s, self._x, self._y+1, 1) elseif character.salt.talk then print(s, self._x, self._y+1, 14) elseif character.spell then print(s, self._x, self._y+1, 7) end print(s, self._x, self._y, 3) end, jump = function(self) self.visible = self.len self.finish = true end, } dialog = { active = true, command = { hide = function() textbox.hide = true end, textbox_hide = function() textbox.hide = true character.sage.talk = false character.salt.talk = false end, transition_in = function() transition:unwipe() end,  transition_out = function() transition:wipe() end, transition_hide = function() for i = 1,#transition.instances do local p = transition.instances[i] p.fac = 1 end end, sage = function() character.sage.talk = true character.salt.talk = false character.spell = false character.sage:jump() end, salt = function() character.sage.talk = false character.salt.talk = true character.spell = false character.salt:jump() end, narr = function() character.sage.talk = false character.salt.talk = false character.spell = false end, spell = function() character.sage.talk = false character.salt.talk = false character.spell = true end, mug_drink = function() textbox.instruction = "Drink the latte" end, mug_break = function() textbox.instruction = "Break the mug" end, yarn_untangle = function() textbox.instruction = "Untangle the knots" end, next = function() textbox.instruction = "Click anywhere to proceed" dialog.flag = "text" end, wait = function() dialog.flag = "text" end, pen_study = function() textbox.instruction = "Study the pen" end, pen_nib = function() textbox.instruction = "Fix the pen nib" end, umbrella_open = function() textbox.instruction = "Open the umbrella" end, umbrella_close = function() textbox.instruction = "Reopen the umbrella" end, reset = function() reset() end, cg = function() Scene.scenes.ending.current = Scene.scenes.ending.current + 1 end, sage_happy = function() character.sage.expression = 112 end, sage_sad = function() character.sage.expression = 115 end, sage_surprised = function() character.sage.expression = 118 end, sage_annoyed = function() character.sage.expression = 163 end, sage_confused = function() character.sage.expression = 163 end, sage_tearful = function() character.sage.expression = 166 end, sage_neutral = function() character.sage.expression = 208 end, sage_angry = function() character.sage.expression = 211 end, sage_uncomfortable = function() character.sage.expression = 214 end, salt_sad = function() character.salt.expression = 121 end, salt_neutral = function() character.salt.expression = 400 end, salt_happy = function() character.salt.expression = 185 end, salt_surprised = function() character.salt.expression = 58 end, salt_angry = function() character.salt.expression = 336 end, salt_hesitant = function() character.salt.expression = 54 end, sleigh_fix = function() textbox.instruction = "Assemble the planks" end, sleigh_glow = function() Scene.scenes.sled.ps.active = true music() end, music_stop = function() music() end, music_play = function() sync(24, 0) -- music music(1) end }, flag = nil, load = function(self, data) self.co = coroutine.create(self.routine) coroutine.resume(self.co, data) end, update = function(self) -- if not self.active then return end -- mouse press if mouse.mp or keyp(48) then if not textbox.finish then textbox:jump() elseif self.flag == "text" then coroutine.resume(self.co) end end -- Z key if btnp(4) then if self.flag == "event" then coroutine.resume(self.co) end end textbox:update() character:update() end, draw = function(self) textbox:draw() if not self.active then return end hud:draw() character:draw() end, routine = function(data) -- run all data for i,v in ipairs(data) do local char = v:sub(1,1) if char == "$" then dialog.flag = "event" local key = v:sub(2) if type(dialog.command[key]) == "function" then dialog.command[key]() end coroutine.yield() textbox.instruction = "" elseif char == "&" then local key = v:sub(2) if type(dialog.command[key]) == "function" then dialog.command[key]() end else -- default text textbox:say(v) dialog.flag = "text" coroutine.yield() sfx(63, nil, 20, 3) end end Scene:next() end, } transition = { instances = {}, co = coroutine.create(function() end), new = function(x, y) local p = {x=x, y=y} p.u = p.x / WIDTH p.v = p.y / HEIGHT p.fac = 0 p.r = 16 setmetatable(p, transition.mt) table.insert(transition.instances, p) end, mt = { __index = { draw = function(self) circ(self.x, self.y, self.fac*self.r-1, 0) end, } }, load = function(self) for i = 0,240,20 do for j = 0,136,20 do transition.new(i+5, j+5) end end end, draw = function(self) vbank(1) poke(0x03FF8, 15) cls(15) for i,v in ipairs(self.instances) do v:draw() end vbank(0) end, update = function(self) coroutine.resume(self.co) if keyp(10) then self:wipe() end if keyp(11) then self:unwipe() end end, wipe = function(self) for i = 1,#transition.instances do local p = transition.instances[i] p.fac = 0 end self.co = coroutine.create(self.routine1) sfx(61, nil, 20, 3) end, unwipe = function(self) for i = 1,#transition.instances do local p = transition.instances[i] p.fac = 1 end self.co = coroutine.create(self.routine2) end, routine1 = function() local t = 0 local dt = 0.1 -- speed local m = 2 -- slope -- f(u) = t - m*u while t < m + 1.3 do t = t + dt for i = 1,#transition.instances do local p = transition.instances[i] local f = t - m*p.u p.fac = math.lerp(0.2, p.fac, f) p.fac = math.clamp(p.fac) end coroutine.yield() end coroutine.resume(dialog.co) end, routine2 = function() local t = 0 local dt = 0.1 -- speed local m = 2 -- slope -- f(u) = -t + m*u + 1 while t < m + 1.3 do t = t + dt for i = 1,#transition.instances do local p = transition.instances[i] local f = -t + m*p.u + 1 p.fac = math.lerp(0.2, p.fac, f) p.fac = math.clamp(p.fac) end coroutine.yield() end coroutine.resume(dialog.co) end } character = { sage = { talk = false, x = 150, y = 40, u = 0, expression = 208, draw = function(self) -- sprite spr(324, self.x, self.y, 15, 1, 0, 0, 12, 12) spr(self.expression, self.x+35, self.y+35, -1, 1, 0, 0, 3, 3) if self.talk then -- speach bubble local k = time.ts % 2 elli(159, 80+k, 10, 7, 1) ellib(159, 80+k, 9, 6, 2) print(string.sub("...", 1, time.t//389%4), 155, 77+k, 3) end end, update = function(self) self.u = self.u + 0.15*(-5) self.y = min(self.y - 0.15*self.u, 40) end, jump = function(self) self.u = 4 end, }, salt = { talk = false, y = 3, u = 0, expression = 384, draw = function(self) -- spr(264, character.sage.x+40, character.sage.y+7, 15, 1, 0, 0, 3, 4) spr(self.expression, character.sage.x+33, character.sage.y+self.y, 0, 1, 0, 0, 4, 4) if self.talk then -- speach bubble local k = time.ts % 2 elli(221, 57+k, 10, 7, 6) ellib(221, 57+k, 9, 6, 3) print(string.sub("...", 1, time.t//389%4), 217, 54+k, 3) end end, update = function(self) self.u = self.u + 0.15*(-5) self.y = min(self.y - 0.15*self.u, 3) end, jump = function(self) self.u = 4 end }, -- draw = function(self) -- spr(self.id, self.x, self.y, self.colorkey, 1, 0, 0, self.w, self.h) -- if self.sagetalk then -- local k = time.ts % 2 -- elli(159, 80+k, 10, 7, 1) -- ellib(159, 80+k, 9, 6, 2) -- print(string.sub("...", 1, time.t//389%4), 155, 77+k, 3) -- end -- if self.salttalk then -- local k = time.ts % 2 -- elli(221, 57+k, 10, 7, 6) -- ellib(221, 57+k, 9, 6, 3) -- print(string.sub("...", 1, time.t//389%4), 217, 54+k, 3) -- end -- end, draw = function(self) self.sage:draw() self.salt:draw() end, update = function(self) self.sage:update() self.salt:update() end, } hud = { fac = 0.0, draw = function(self) map(19, 10, 11, 7, 166, 80, 0) rect(0, 0, WIDTH, 4, 3) rect(0, 132, WIDTH, 4, 3) rect(0, 0, 4, HEIGHT, 3) rect(236, 0, 4, HEIGHT, 3) rectb(4, 4, 232, 128, 1) -- progress bar -- rect(16, 9, 50, 2, 1) -- rect(16, 9, 50*self.fac, 2, 2) -- spr(4, 4, 4, 0, 1, 0, 0, 8, 2) -- name tags if character.sage.talk then spr(48, textbox._x-14-2, textbox._y+18-1, 15, 1, 0, 0, 6, 2) print("Sage", textbox._x-6, textbox._y+23, 3) elseif character.salt.talk then spr(80, textbox._x-14-2, textbox._y+18-1, 15, 1, 0, 0, 6, 2) print("Salt", textbox._x-6, textbox._y+23, 3) end end, reset = function(self) self.fac = 0.0 end } ps_star = { particles = {}, update = function(self) if mouse.mp then self:emit() end for i,p in ipairs(self.particles) do p.fac = (time.t - p.start) / p.lifespan -- remove if p.fac > 1 then table.remove(self.particles, i) end end end, emit = function(self) local p = {} local a = random() * 2*PI p.u = 10*sin(a) p.v = 10*cos(a) p.x = mouse.x p.y = mouse.y p.fac = 0 p.start = time.t p.lifespan = 200 table.insert(self.particles, p) end, draw = function(self) for i,p in ipairs(self.particles) do p.x = p.x + 0.1*p.u p.y = p.y + 0.1*p.v spr(320, p.x-3, p.y-3, 0, 1, 0, floor(4*p.fac)) end end, } Pslib = { instances = {}, new = function() local ps = { active = true, o = Vec.new(0,0,0), particles = {}, t = 0, next = 10, maxlife = 1000, minlife = 1000, v = Vec.new(0,0,0), mt = nil, } setmetatable(ps, Pslib.mt) table.insert(Pslib.instances, ps) return ps end, mt = { __index = { update = function(self) if self.active then self:emittimer() self.t = self.t - 1 end -- update particles for i,p in ipairs(self.particles) do p.o = p.o + p.v p.fac = (time.t - p.start) / p.lifespan -- remove if p.fac > 1 then table.remove(self.particles, i) end end end, emittimer = function(self) if self.t <= 0 then self:emit() self.t = self.t + self.next end end, emit = function(self) local p = { fac = 0, start = time.t, o = Vec.new(0,0,0) + self.o, lifespan = (self.maxlife-self.minlife)*random() + self.minlife, v = self.v } table.insert(self.particles, p) if self.mt ~= nil then setmetatable(p, self.mt) end end, draw = function(self) if self.mt ~= nil then for _,p in ipairs(self.particles) do local x, y = p.o:ortho_proj(camera) p:draw(x, y) end else for _,p in ipairs(self.particles) do local x, y = p.o:ortho_proj(camera) pix(x, y, 15) end end end, } }, update = function(self) for _,ps in ipairs(self.instances) do ps:update() end end, draw = function(self) for _,ps in ipairs(self.instances) do ps:draw() end end, clear = function(self) self.instances = {} end } rain = { col = {149, 198, 198}, particles = {}, maxlife = 20, minlife = 10, minradius = 5, maxradius = 10, next = 0, t = 0, active = true, load = function(self) color.setcol(self.col, 6) -- table.insert(self.particles, {o = Vec.new(0,0,0), r = 1.7*30, fac=1}) end, update = function(self) if self.active then self:emittimer() self.t = self.t + 1 end -- update particles for i,p in ipairs(self.particles) do p.fac = (time.t - p.start) / p.lifespan -- remove if p.fac > 1 then table.remove(self.particles, i) end end end, emittimer = function(self) if self.next <= self.t then self:emit() self.next = self.next + 3 end end, emit = function(self) local p = {} local r = random() * 50 + 30 local a = random() * 2*PI p.o = Vec.new(r*sin(a), 0, r*cos(a)) p.fac = 0 p.r = random() * (self.maxradius - self.minradius) + self.minradius p.start = time.t p.lifespan = 400 table.insert(self.particles, p) end, draw = function(self) for _,p in ipairs(self.particles) do local x, y = p.o:ortho_proj(camera) -- local d = Vec.dist(camera.o, p.o) -- local s = camera.o[2] / d -- local r = p.r * p.fac -- ellib(x, y, r, r*s, 6) local r = p.r * p.fac ellib(x, y, r, r*sin(abs(camera.ax)), 6) end end, } debug = { log = {}, show = true, update = function(self) -- self.log[1] = "vertices: " .. #Mesh.verts self.log[1] = "camera: " .. camera.axt .. ", " .. camera.ayt self.log[2] = "faces: " .. Mesh.nface self.log[3] = "dialog flag: " .. dialog.flag -- self.log[4] = Mesh.instances[2].ispressed if keyp(44) then self.show = not self.show end end, draw = function(self) if not self.show then return end for i,v in ipairs(self.log) do print(v, 1, (i-1)*6+1, 15, 0, 1, 1) end end } Scene = { scenes = { title = { next = "intro", script = { "$transition_in", "$wait", "$transition_out", }, load = function(self) dialog.active = false music() end, draw = function(self) sync(4, 0) sync(32, 2) sync(1+2, 1) memcpy(0, 0xbfc0, 16320) spr(291, 100, time.ts%2, 0, 1, 0, 0, 8, 7) spr(404, 155, 13+time.ts%2, 0, 1, 0, 0, 10, 5) print("Click anywhere to start", 12+1, 122-time.ts%2, 9) print("Click anywhere to start", 12-1, 122-time.ts%2, 9) print("Click anywhere to start", 12, 122+1-time.ts%2, 9) print("Click anywhere to start", 12, 122-1-time.ts%2, 9) print("Click anywhere to start", 12, 122-time.ts%2, 4) end, }, intro = { next = "umbrella", script = { "$transition_in", "&salt", "&salt_neutral", -- "Is that suppose to be me?", "Sage greetings to you, Sage.", "&narr", "A familiar voice, crisp like a snowflake.", "&sage", "&sage_surprised", "Salt!", "&narr", "&sage_neutral", "Every winter, your ordinary life transforms,", "into a not-so-ordinary whirlwind of secrets, words and magic.", "Secrets to be uncovered, then kept.", "&sage_confused", "Words to be misused, or twisted so they could be of use.", "&sage_happy", "Magic to infuse everyday life with a flurry of winter excitement.", "As bare branches appear, you look forward to the first snowfall-", "&sage_neutral", "when your rabbit familiar appears with their mysterious sack", "&sage_happy", "full of objects in need of your healing words.", "&salt", "Don't tell me you missed me?", "&narr", "&sage_uncomfortable", "You make a face.", "Miss a mysterious, icy cold rabbit crouching on my hat? Never.", "&sage_neutral", "To be truthful, you like the change in the everyday tedium of life.", "The excitement of not knowing what problems you'll uncover,", "or what you'll need to do to solve them.", "&sage_annoyed", "But you would never admit that to Salt-", "-a snow rabbit tending towards salty, rather than sweet-", "and on occasion, jokingly, your \"sworn nemesis\".", "Mostly when he's brought objects that have inconvenienced you...", "...or made a mess.", "Like the time, years ago...", "when he brought a recorder with two notes out of tune, then hid your actual one.", "You were banned from playing in class for an entire term.", "&sage", "&sage_neutral", "How many are there this year?", "&narr", "Objects spill out.", "&salt", "&salt_surprised", "Not many... and it's your lucky number, five.", "&sage", "&sage_surprised", "So few?", "Did your overlord find more workers?", "&sage_annoyed", "Or did people complain since they weren't getting paid.", "&narr", "&salt_hesitant", "The snow rabbit makes an ambiguous sound.", "You feel an emotion coming off them - uncertainty, tinged with regret.", "Or sadness maybe?", "&sage_confused", "You frown, then dismiss it.", "&sage_happy", "Time to get to your very own holiday part time job-", "-saving the world (i.e. five people), one slightly broken object at a time!", "&textbox_hide", "$transition_out", }, load = function(self) sync(0, 0) music(1) dialog.active = true -- border color poke4(0x3FF8*2, 15) Mesh.new(Floor, true) Mesh.new(Rabbit) end, draw = function(self) Mesh.instances[1]:draw() Mesh:draw() end }, umbrella = { next = "pen", script = { "$transition_in", "&salt", "&salt_hesitant", "So I know you hate rain...", "&sage", "&sage_annoyed", "Almost as much I hate overly hot sun,", "wind that blows my hair in my face,", "and snow that melts in my boots.", "&sage_happy", "Umbrellas don't make me particularly emotional, though...", "What's wrong with this one?", "&narr", "&sage_neutral", "No response.", "You touch the umbrella.", "&sage_confused", "It's damp, even though it's warm and cosy inside your living room.", "&sage", "I didn't realise it was raining today?", "&salt", "&salt_neutral", "Why don't you try opening it?", "&textbox_hide", "$umbrella_open", "&narr", "&sage_surprised", "The umbrella springs open in a splash of raindrops.", "They fall steadily, making a small pool in the middle of the room.", "You jump away.", "&sage", "&sage_annoyed", "You might have warned me!", "&salt", "&salt_happy", "I did! I said you hate rain.", "&narr", "Salt looks amused.", "You narrow your eyes.", "However messy this was making your apartment, this should be simple enough.", "&sage_sad", "Umbrellas that wouldn't dry were usually symptomatic of a melancholy heart.", "One that poured rain...", "the owner of this object was likely possessed of internal tears...", "...that they refused to release.", "&sage", "&sage_annoyed", "I have to make a stranger cry?", "&narr", "You have never been allowed to know the identity of the objects' owners.", "It would ruin the effectiveness of the magic, apparently.", "&salt", "I don't know, you're the expert.", "&sage", "&sage_annoyed", "At crying? How dare you.", "&narr", "The umbrella is still pouring rain.", "&sage_confused", "You had better think fast...", "&sage", "Is it because they have a heart that is broken?", "&salt", "&salt_neutral", "Time to use those words that serve you so well.", "&sage", "&sage_neutral", "\"The sky is crying, so is your heart,\"", "\"Yes, look inside, it's time to start,\"", "\"You have a secret sadness yet,\"", "&sage_annoyed", "(it's making my living room wet...)", "&sage_happy", "\"You feel it will break you apart, but it won't.\"", "&salt", "&salt_happy", "What a terrible rhyme.", "&textbox_hide", "$umbrella_close", "&narr", "&sage_neutral", "This time, there's no rain in sight.", "&sage_happy", "You're glad to see that you haven't lost the magic touch, so to speak.", "&sage", "Shut up.", "It worked, didn't it?", "So does that mean she cried?", "&salt", "&salt_hesitant", "Probably?", "People always think their hearts are broken into pieces,", "when they are only cracked and can be healed through tears.", "&sage", "&sage_annoyed", "So wise for a bunny made out of snow!", "&narr", "You think grumpily to yourself...", "...as you look for towels to clean up the mess.", "&textbox_hide", "$next", "$transition_out", }, load = function(self) Mesh.new(Floor, true) Mesh.new(Outter2, false, true, self.click1) Mesh.new(Inner2) Mesh.new(Outter, true, true, self.click2) Mesh.new(Inner, true) Mesh.new(Stick) Mesh.new(Handle) rain:load() rain.active = false sync(24, 1) music(0) end, update = function(self) rain:update() end, draw = function(self) Mesh.instances[1]:draw() rain:draw() Mesh:draw() end, click1 = function() rain.active = true Mesh.instances[2].hide = true Mesh.instances[3].hide = true Mesh.instances[4].hide = false Mesh.instances[5].hide = false -- Mesh.instances[6].hide = false coroutine.resume(dialog.co) sfx(60, nil, 20, 3) sfx(59, nil, -1, 2) -- rain end, click2 = function() rain.active = false Mesh.instances[2].hide = false Mesh.instances[3].hide = false Mesh.instances[4].hide = true Mesh.instances[5].hide = true -- Mesh.instances[6].hide = true Mesh.instances[2].onclick = Scene.scenes.umbrella.click3 sfx(60, nil, 20, 3) sfx(-1, nil, nil, 2) -- rain end, click3 = function() Mesh.instances[2].hide = true Mesh.instances[3].hide = true Mesh.instances[4].hide = false Mesh.instances[5].hide = false -- Mesh.instances[6].hide = false coroutine.resume(dialog.co) Mesh.instances[4].selectable = false sfx(60, nil, 20, 3) end }, pen = { next = "mug", script = { "$transition_in", "&sage", "&sage_neutral", "A pen?", "Let me guess, it won't write?", "&narr", "You pick up the pen, and write \"snow\".", "&sage_surprised", "It comes out \"no\".", "&sage", "What a disagreeable pen.", "&salt", "&salt_neutral", "So now that you've tried that experiment,", "you can see that it only writes partial words.", "&narr", "You try a few more words.", "Predictably, hello turns into hell...", "sage turns into sag...", "and worst of all, spell turns into pee in your loopy cursive.", "&sage", "&sage_happy", "Quite a useful pen in its way...", "&salt", "&salt_hesitant", "I never understood why humans need pens anyway.", "Don't people just use their phones these days?", "&sage", "&sage_annoyed", "Some people like to write down their thoughts on paper.", "&narr", "&sage_sad", "Except...", "...it's horrible for your thoughts to be translated into partial words.", "Like stuttering every time you try to speak.", "&sage", "I relate to the owner of this pen.", "&narr", "&salt_neutral", "The snow rabbit looks contemplative.", "&salt", "Go on.", "&sage", "Sometimes when I talk to people,", "I find that I struggle to say what I mean.", "&salt", "What stops you, do you think?", "&sage", "&sage_tearful", "Like, I'll think I'm praising them, but it comes out all sarcastic.", "&salt", "&salt_hesitant", "Isn't that intonation rather than the actual words themselves?", "&sage", "Yes, but sometimes it's the words as well.", "I've never told my sister that I love her.", "&narr", "&salt_sad", "You are both silent, thinking about this.", "&sage", "&sage_sad", "I actually wasn't sure you were coming this year.", "&salt", "&salt_hesitant", "Are you going to say that you love me?", "&sage", "&sage_annoyed", "I was going to say that I missed you...", "...and I was worried that you wouldn't be here.", "Not here to make my life more exciting this winter.", "&narr", "Silence.", "&sage", "&sage_tearful", "It's been a hard year...", "&salt", "&salt_sad", "I missed you, too.", "&narr", "His eyes are suspiciously bright.", "&sage_tearful", "You find yourself blinking back tears, too.", "Quickly you study the pen.", "&textbox_hide", "$pen_study", "&sage", "Nothing wrong there.", "&textbox_hide", "$pen_nib", "&salt", "&salt_hesitant", "Check if it's working.", "&sage", "&sage_surprised", "Oh it's working!", "How strange.", "Don't I usually have to say some kind of rhyme?", "&salt", "&salt_neutral", "I think this time, maybe our conversation around the pen was enough.", "Objects like to feel understood.", "&sage", "So you think we listened to the pen and it started working?", "&salt", "&salt_happy", "&sage_happy", "It was probably afraid of not being understood.", "Now that it is, it's able to write full words.", "Well done!", "&textbox_hide", "$next", "$transition_out", }, load = function(self) color.setcol({158, 187, 161}, 12) Mesh.new(Floor, true) color.setcol({222, 213, 201}, 8) Mesh.new(Shaft, false, true, self.click) Mesh.new(Nib, false, false, self.click2) end, update = function(self) self.ps_drawing:update() end, draw = function(self) Mesh.instances[1]:draw() self.ps_drawing:draw() Mesh:draw() end, click = function() coroutine.resume(dialog.co) Mesh.instances[3].selectable = true Mesh.instances[2].selectable = false sfx(58, nil, 20, 3) end, click2 = function() coroutine.resume(dialog.co) Mesh.instances[3].selectable = false Scene.scenes.pen.ps_drawing.active = true sfx(58, nil, 20, 3) end, ps_drawing = { v = 30*Vec.new(-0.104, 0, 0.342), active = false, instances = {}, t = 0, v = 0, th = 0, new = function(self) local p = {} p.v = 30*Vec.new(-0.104, 0, 0.342) p.t = 0 table.insert(self.instances, p) end, -- update = function(self) -- for i,p in ipairs(self.instances) do -- -- p.v[3] = p.v[3] - 1 -- p.t = p.t + 0.1 -- p.v = Vec.new(5*p.t*sin(p.t-time.t/600), 0, -10*p.t) + self.v -- if p.t > 5 then -- table.remove(self.instances, i) -- end -- end -- if self.fac < 1 and time.ts % 2 == 0 then -- return -- elseif textbox.hide then -- return -- else -- self:new() -- end -- end, draw = function(self) for i = 2,#self.instances do local u = self.instances[i-1].v local v = self.instances[i].v Vec.line(u, v, camera, 12) end end, update = function(self) if self.active == false then return end local dt = 1 local a = 0.02*(random() - 0.5) self.v = math.clamp(self.v + a*dt, -0.1, 0.1) self.th = self.th + self.v*dt local dx = 0.7*cos(self.th) local dy = 0.7*sin(self.th) for i,p in ipairs(self.instances) do p.t = p.t + 1 p.v[1] = p.v[1] + dx p.v[3] = p.v[3] + dy if p.t > 500 then table.remove(self.instances, i) end end -- if self.fac < 1 and time.t//1200 % 2 == 0 then -- return -- elseif textbox.hide then -- return -- else -- end self.t = self.t+1 if self.t % 3 == 0 then self:new() end end, } }, mug = { next = "yarn", script = { "$transition_in", "&sage", "&sage_annoyed", "Is it time for coffee yet?", "&narr", "Usually you require coffee before doing anything...", "...but you were distracted today by the sparkling of snow.", "&salt_neutral", "The snow rabbit wordlessly hands you a mug that you don't recognise.", "&sage", "&sage_surprised", "You brought me my favourite coffee?", "I didn't realise you knew how to make it.", "&narr", "&sage_happy", "An oat milk dirty matcha latte with honey and cinnamon.", "It smells so deliciously fragrant, you want to take a sip right away.", "&textbox_hide", "$mug_drink", "&sage", "&sage_uncomfortable", "Ugh!", "&narr", "You exclaim as a bitter taste floods your mouth.", "It tastes like an extremely bitter bitter melon.", "&sage", "&sage_annoyed", "What-", "Did you put soap in this cup or something?", "&narr", "&salt_neutral", "The snow rabbit tries to look concerned...", "...but you are certain you detect a smile.", "&sage", "&sage_surprised", "Let me guess, the coffee cup makes everything in it taste bitter.", "&narr", "&salt_happy", "The snow rabbit is hiding his laughter behind his paws.", "&sage", "&sage_annoyed", "Why are you always pranking me!", "&salt", "&salt_neutral", "Your coffee choice is overly complex.", "Why not just drink water?", "&sage", "Maybe I'll put you in the microwave and then drink snow rabbit water.", "&salt", "I don't melt in microwaves.", "Anyway, now that you've discovered its secret...", "...aren't you intrigued by this mug?", "A mug that imparts flavour is extremely rare to encounter.", "&sage", "Let me make another cup first.", "&narr", "&sage_confused", "As you sift bright green powder, you consider...", "...bitter coffee still lingering restlessly on your tongue.", "The phrase 'eating bitterness' comes to mind as you open the fridge.", "&sage_surprised", "You feel a sense of doom as white-hot rage floods your veins.", "&sage", "&sage_annoyed", "There's no oat milk left!", "&salt", "&salt_happy", "You're annoyed, aren't you?", "&sage", "I'm furious, actually.", "You used up the last of the oat milk for a prank?!", "&salt", "How does it feel?", "&sage", "How does it feel?", "I need my special coffee!!", "&salt", "Can't you just use water?", "&sage", "It's not the same!!", "&narr", "&sage_surprised", "Your voice comes out in a strangled shriek and you start to cough.", "Automatically you drink some coffee to clear your throat.", "It tastes horrible, the words turned to ash in your mouth.", "&sage_confused", "Belatedly you realise you have picked the offending mug...", "...now slightly less full of bitter greenish liquid.", "&sage", "So the owner's angry.", "Let's destroy the mug.", "&salt", "&salt_surprised", "What?", "&sage", "You heard me.", "It's not a useful mug, it makes things taste bitter.", "As I know well since you tricked me into drinking from it!!", "&narr", "The rabbit is silent.", "&sage", "Would you prefer I destroy you?", "&salt", "&salt_hesitant", "How will this help the owner stop being angry?", "&sage", "&sage_tearful", "We don't want them to stop being angry... ", "...we want them to express their anger.", "This mug is a symbol for the trap they find themselves.", "Their trapped rage...", "...which they will not permit to release and which grows in bitterness.", "&narr", "&sage_neutral", "Quickly you reach for the mug, which you had put down to recover from the taste.", "&sage", "I'm going to do it.", "&textbox_hide", "$mug_break", "&narr", "&sage_surprised", "&salt_surprised", "The mug shatters with a noise like an exploding coffee machine.", "&salt", "That was scary.", "&sage", "&sage_sad", "I think the owner thought so.", "That's why they kept their anger inside.", "They probably didn't realise it would gradually colour their everyday life...", "like an overly bitter coffee.", "&narr", "&sage_neutral", "&salt_neutral", "You feel a weight lift off your chest.", "&sage", "I'm just happy that they will be able to enjoy coffee again.", "&salt", "This might make you even happier.", "Look in the sack.", "&narr", "&sage_confused", "You pull out a rectangular carton-", "&sage_happy", "It's your favourite brand of oat milk!", "&sage", "&sage_annoyed", "You had more all along?!", "&salt", "&salt_surprised", "Aren't you grateful?", "&narr", "&sage_happy", "You start to say something about being tricked,", "but swallow your words and head to the kitchen", "to make another cup - this time, a slightly less angry one.", "&textbox_hide", "$next", "$transition_out", }, load = function(self) Mesh.new(Floor, true) color.setcol({207, 151, 149}, 8) Mesh.new(Mug, false, false) Mesh.new(Handle2) Mesh.new(Coffee, false, true, self.click1) Mesh.new(Broken, true) self.ps = Pslib:new() self.ps.o = Vec.new(0, 30, 0) self.ps.v = 0.1*Vec.new(1, 2, -1) self.ps.next = 35 self.ps.maxlife = 2000 self.ps.minlife = 2000 self.ps.mt = self.steam.mt sync(24, 0) -- music music(1) end, click1 = function(self) Mesh.instances[4].hide = true coroutine.resume(dialog.co) Mesh.instances[2].selectable = true Pslib:clear() sfx(55, nil, 20, 3) end, click2 = function(self) Mesh.instances[2].hide = true Mesh.instances[3].hide = true Mesh.instances[5].hide = false coroutine.resume(dialog.co) end, draw = function(self) Mesh.instances[1]:draw() Mesh:draw() Pslib:draw() end, update = function(self) if Mesh.instances[2].ispressed then hud.fac = math.clamp(hud.fac + 0.01) poke(0x3FF9, math.random(-1,1)) poke(0x3FF9+1, math.random(-1,1)) sfx(54, nil, 1, 3) else hud.fac = math.clamp(hud.fac - 0.005) poke(0x3FF9, 0) poke(0x3FF9+1, 0) end if hud.fac >= 1 then Mesh.instances[2].hide = true Mesh.instances[3].hide = true Mesh.instances[5].hide = false hud:reset() coroutine.resume(dialog.co) sfx(62, nil, 20, 3) end Pslib:update() end, steam = { mt = { __index = { draw = function(self, x, y) local r = 7*sin(self.fac*PI) circ(x, y, r, 3) end } } } }, yarn = { next = "sled", script = { "$transition_in", "&narr", "&sage_happy", "&salt_hesitant", "Magical matcha coffee drink in hand, I'm now slightly less grumpy.", "Salt is still trying to make up for the coffee incident, however.", "&salt", "Last winter I recalled you were always knitting...", "...so I brought this for you as a gift.", "&sage", "&sage_neutral", "You mean I knitted one row every two days,", "and never finished the scarf I was making,", "let alone the crochet coasters I saved the tutorial for on Pinterest.", "Look, it's still there-", "&narr", "You point at a corner of the couch,", "where a bag peeks out from under the cushions.", "&salt", "&salt_surprised", "Why would you leave knitting needles on your couch...", "...where anyone could sit there and become grievously injured?", "&sage", "&sage_annoyed", "&salt_angry", "I like to scatter hazards through my house for unwitting intruders...", "...like you.", "&narr", "&sage_neutral", "You pick up the wool and examine it more closely.", "&sage", "&sage_surprised", "This looks really tangled.", "How would you even knit with this?", "You'd have to untangle it every two seconds.", "&narr", "&sage_annoyed", "You grab the end but find that you can't even pull out a workable length,", "encountering a knot somewhere in the depths of the wool.", "&sage", "&sage_surprised", "&salt_neutral", "Wait, this is like those tangled earphones...", "...that you brought over the first year I met you!", "I wonder if I can remember the spell I used then-", "&sage_confused", "&salt_happy", "\"Your thoughts are in tangles,\"", "\"You're on your phone too much.\"", "&sage_neutral", "\"A phone is newfangled,\"", "&sage_happy", "\"You need to stop using it so much.\"", "&narr", "The yarn stays in place, implacably, still tangled.", "&sage", "&sage_confused", "What? Why didn't it work?", "&salt", "...", "&sage", "Maybe this is different from back then.", "&salt", "&salt_neutral", "Maybe. Also, on reflection, that rhyme wasn't particularly good.", "Frankly, I'm surprised that it worked the first time.", "&sage", "&sage_annoyed", "Hey! I was really young then.", "&salt", "&salt_happy", "It reads like you just learnt the word \"newfangled\".", "&narr", "&sage_confused", "You study the tangles of yarn, thinking.", "&salt", "&salt_neutral", "The idea you had back then of thoughts being in tangles...", "...could you draw that out further?", "&sage", "Sometimes when I'm really stressed, I start to get confused.", "Tangled, if you will...", "&salt", "Like now.", "&sage", "No.", "Not like now.", "Sometimes I'll even get people tangled up in my head.", "Like I'll get so confused that I'll call someone by the wrong name.", "Usually it happens when I'm really preoccupied with my work though.", "&salt", "That's a thing, isn't it?", "When you think of people in similar groups, it's called a Freudian slip.", "&sage", "&sage_sad", "But it doesn't feel good to slip up like that.", "Sometimes it means nothing...", "but sometimes it means I'm getting them tangled up in my mind, too.", "I could accidentally get angry at someone...", "...especially when I'm really angry at another person.", "&sage_tearful", "It can be hard to be on the receiving end of that too.", "It feels like they don't even know who you are,", "my parents are always doing that-", "&narr", "Your words curl in your throat.", "&sage", "&sage_surprised", "Is this... my mother's?", "She said she was knitting me a sweater for Christmas...", "...because I always borrow hers.", "But it must be someone else borrowing them...", "...because I hate how scratchy sweaters are.", "&narr", "You look back at the yarn.", "&textbox_hide", "$yarn_untangle", "$yarn_untangle", "$yarn_untangle", "&sage", "&sage_confused", "That felt so uncomfortable to do.", "Can they stay tangled?", "&salt", "&salt_surprised", "You like to think of everyone as a mass of people?", "&sage", "&sage_sad", "Well. No.", "But it can be easier.", "I don't like to think of my mother realising how often she tangles up people.", "&salt", "&salt_hesitant", "Remember, these problems that the owners want to solve,", "whether consciously or not.", "&sage", "&sage_annoyed", "So it was my mother's....", "&salt", "&salt_neutral", "You can think of it that way if it makes you feel better.", "&sage", "&sage_sad", "I think it helps me make sense of things if I think of it that way.", "But I'd still like to know the truth.", "&narr", "You fiddle with the yarn, embarrassed.", "&salt", "&salt_hesitant", "As an all-powerful snow rabbit, I believe in objective truth -", "accepting that humans tend to view the truth from different angles, subjectively.", "&sage", "&sage_annoyed", "Sometimes I want to believe my subjective truth is objective!", "Is that so bad?", "&salt", "It could result in tangles of the nature we just discussed.", "&sage", "&sage_happy", "Fine, I'll give up my subjective truth for more coffee.", "&salt", "&salt_neutral", "Done.", "&narr", "A small price to pay...", "&textbox_hide", "$next", "$transition_out", }, load = function(self) Mesh.new(Floor, true) color.setcol({180, 204, 138}, 8) Mesh.new(Yarn) Mesh.new(Dust1, false, true, self.click) Mesh.new(Dust2, false, true, self.click) Mesh.new(Dust3, false, true, self.click) sync(24, 1) -- music music(0) end, draw = function(self) Mesh.instances[1]:draw() Mesh:draw() end, click = function(self) self.hide = true coroutine.resume(dialog.co) sfx(53, nil, 20, 3) end }, sled = { next = "ending", script = { "$transition_in", "&sage", "&sage_surprised", "What's this?", "&salt", "&salt_neutral", "I understand it may be difficult to discern at this point in time,", "but it appears to be some sort of wooden object broken into pieces.", "&sage", "& sage_annoyed", "Thanks...", "&narr", "&sage_confused", "Something about it calls to you -", "beckons to something buried deeply in your soul.", "&sage_sad", "Why does this feel so...", "&salt", "&salt_sad", "I have to tell you something.", "&sage", "In a minute, let me look at this more closely.", "&textbox_hide", "$sleigh_fix", "$sleigh_fix", "$sleigh_fix", "&narr", "As you feel the pieces connect...", "falling eagerly into place like ice cubes in a glass, you realise-", "it spills out of you", "like the whoosh of joy from fast movement through snowy trees-", "&sage", "&sage_surprised", "This is mine, isn't it?", "&music_stop", "&salt", "I have to tell you that this is the last winter I'll be visiting-", "&sage", "&sage_neutral", "I didn't recognise it since it broke when I was young, but-", "&sage_surprised", "What did you say?", "&narr", "The rabbit is silent.", "&sage", "I thought-", "&salt", "It was never intended to continue forever.", "&sage", "&sage_sad", "I knew it.", "I knew this year was different, there were so few objects!", "&salt", "I am only here in the winter time...", "...but it was evident to me that you struggled in recent years-,", "it was time for the last push of healing-", "&salt_neutral", "for yourself to be healed.", "&sage", "&sage_confused", "Was that the point all along?", "&sleigh_glow", "&music_play", "&sage", "Salt, I remember this sleigh. I only rode it a few times, but it was really fun-", "&sage_sad", "although there was this time where I crashed into a tree and it broke, I guess.", "I was banned from doing most fun activities after that.", "I just read books instead.", "&sage_tearful", "I guess my parents thought I was too clumsy for them.", "It sounds silly, but I guess that made a big impact on me...", "It felt like fun things were for other people, but not for me...", "&narr", "&sage_neutral", "Except in the winter, you think.", "When Salt visited.", "&sage_happy", "On snowy days you'd always had the thrill of magic,", "a forever secret from your parents.", "&sage", "&sage_tearful", "You helped me with that,", "&salt", "&salt_sad", "You're allowed to be happy,", "You care for others so much.", "It's time to care for yourself.", "&narr", "You look back at the sleigh.", "&sage", "&sage_sad", "Should we give it a test run?", "&narr", "&sage_neutral", "You sit on the sleigh,", "tentatively feel your happiness rushing down like a slope,", "bittersweet and sad, but with a lift of hope.", "&sage", "&sage_tearful", "Will you ever come back again?", "&salt", "You don't need me to.", "&sage", "But maybe I want you to.", "&salt", "&salt_neutral", "You have coffee, you don't need me anymore.", "&sage", "&sage_happy", "You make a good point...", "&textbox_hide", "$next", "$transition_out", }, load = function(self) Mesh.new(Floor, true) color.setcol({250, 252, 254}, 8) -- Mesh.new(Sled, false, true, self.click) Mesh.new(Plank1, false, true, self.click1) Mesh.new(Plank2) Mesh.new(Plank3, false, true, self.click1) Mesh.new(Plank4, false, true, self.click1) Mesh.new(Plank5) Mesh.new(Plank6, false, true, self.click2) Mesh.new(Plank7, false, true, self.click2) Mesh.new(Plank8, false, true, self.click2) Mesh.new(Plank9, false, true, self.click3) Mesh.new(Plank10, false, true, self.click3) Mesh.new(Plank11, false, true, self.click3) Mesh.new(Plank12, false, true, self.click3) Mesh.new(Sled1, true) Mesh.new(Sled2, true) Mesh.new(Sled3, true) -- particle system color.setcol({251, 240, 203}, 12) self.ps = Pslib.new() self.ps.v = 0.8*Vec.new(0, 1, 0) self.ps.next = 2 self.ps.maxlife = 1000 self.ps.minlife = 500 setmetatable(self.ps, self.ps_mt) self.ps.active = false end, update = function(self) Pslib:update() end, draw = function(self) Mesh.instances[1]:draw() Mesh:draw() Pslib:draw() end, click = function(self) coroutine.resume(dialog.co) end, click1 = function(self) Mesh.instances[2].hide = true Mesh.instances[3].hide = true Mesh.instances[4].hide = true Mesh.instances[5].hide = true Mesh.instances[16].hide = false coroutine.resume(dialog.co) sfx(56, nil, 20, 3) end, click2 = function(self) Mesh.instances[6].hide = true Mesh.instances[7].hide = true Mesh.instances[8].hide = true Mesh.instances[9].hide = true Mesh.instances[14].hide = false coroutine.resume(dialog.co) sfx(56, nil, 20, 3) end, click3 = function(self) Mesh.instances[10].hide = true Mesh.instances[11].hide = true Mesh.instances[12].hide = true Mesh.instances[13].hide = true Mesh.instances[15].hide = false coroutine.resume(dialog.co) sfx(56, nil, 20, 3) end, ps_mt = { __index = { update = function(self) if self.active then self:emittimer() self.t = self.t - 1 end -- update particles for i,p in ipairs(self.particles) do p.o = p.o + p.v p.fac = (time.t - p.start) / p.lifespan -- remove if p.fac > 1 then table.remove(self.particles, i) end end end, emittimer = function(self) if self.t <= 0 then self:emit() self.t = self.t + self.next end end, emit = function(self) local p = { fac = 0, start = time.t, lifespan = (self.maxlife-self.minlife)*random() + self.minlife, v = self.v } local r = 10*random()+35 local th = random()*2*PI p.o = Vec.new(r*cos(th), 0, r*sin(th)) table.insert(self.particles, p) if self.mt ~= nil then setmetatable(p, self.mt) end end, draw = function(self) for _,p in ipairs(self.particles) do local x, y = p.o:ortho_proj(camera) circ(x, y, 1.99-2*p.fac, 12) end end } } }, ending = { next = "title", current = 0, t = 180, script = { "$transition_in", "$textbox_hide", "$transition_out", "&cg", "$transition_in", "$textbox_hide", "$cg", "$transition_out", "&cg", "$transition_in", "$textbox_hide", "$transition_out", "&cg", "$transition_in", "$textbox_hide", -- "$cg", "$cg", "$cg", "$transition_out", "&cg", "$transition_in", "$textbox_hide", "$cg", "$cg", "$next", "$transition_out", }, seq = { [0] = function() local w = print("THE END?") cls(6) print("THE END?", 120-w/2, 61+time.ts%2, 5) print("THE END?", 120-w/2, 60+time.ts%2, 3) end, [1] = function() sync(4, 1) memcpy(0, 0x08000, 16320) print("Writing\n Starling", 14, 41, 5) print("Writing\n Starling", 14, 40, 4) end, [2] = function() sync(4, 1) memcpy(0, 0x08000, 16320) spr(16, 67, 20+time.ts%2, 0, 1, 0, 0, 3, 5) print("Writing\n Starling", 14, 41, 5) print("Writing\n Starling", 14, 40, 4) end, [3] = function() sync(4, 2) memcpy(0, 0x08000, 16320) print("Programming\n pke1029", 64, 11, 6) print("Programming\n pke1029", 64, 10, 5) end, [4] = function() sync(4, 3) memcpy(0, 0x08000, 16320) print("Art\n N0ceru", 136, 31, 5) print("Art\n N0ceru", 136, 30, 4) end, [5] = function() sync(4, 3) memcpy(0, 0x08000, 16320) spr(20, 89, 36+time.ts%2, 0, 1, 0, 0, 7, 4) print("Art\n N0ceru", 136, 31, 5) print("Art\n N0ceru", 136, 30, 4) end, [6] = function() sync(4, 4) memcpy(0, 0x08000, 16320) -- print("Art\n N0ceru", 136, 31, 5) -- print("Art\n N0ceru", 136, 30, 4) print("Web design\n Ruminio", 150, 91, 5) print("Web design\n Ruminio", 150, 90, 4) end, [7] = function() sync(4, 5) memcpy(0, 0x08000, 16320) end, [8] = function() sync(4, 6) memcpy(0, 0x08000, 16320) print("Thank you for playing", 110, 11, 5) print("Thank you for playing", 110, 10, 4) end, [9] = function() sync(4, 6) memcpy(0, 0x08000, 16320) spr(128, 92, 44+time.ts%2, 0, 1, 0, 0, 9, 8) print("Thank you for playing", 110, 11, 5) print("Thank you for playing", 110, 10, 4) end }, load = function(self) -- border color poke4(0x3FF8*2, 0) -- tiles and palette sync(1+2+32, 1) dialog.active = false end, draw = function(self) self.seq[self.current]() end, update = function(self) self.t = self.t - 1 if self.t <= 0 then self.t = 180 if dialog.flag == "event" then coroutine.resume(dialog.co) end end end } }, load = function(self, scene) self.current = self.scenes[scene] -- dialog script if self.current.script == nil then local script = { "$transition_in", "$hide", "$transition_out" } dialog:load(script) else dialog:load(self.current.script) end -- mesh data Pslib:clear() Mesh:clear() if type(self.current.load) == "function" then self.current:load() end camera:reset() -- reset expressions character.sage.expression = 208 character.salt.expression = 400 end, update = function(self) Mesh:update() if type(self.current.update) == "function" then self.current:update() end dialog:update() ps_star:update() transition:update() end, draw = function(self) -- bg texture cls(2) local d = 6 for i = 3,233,d do local upper = 14*sin(i/240*2*PI+2.4) + 35 for j = 7,upper,d do pix(i, j+i//d%2*d/2, 1) end end -- draw mesh if type(self.current.draw) == "function" then self.current:draw() else Mesh:draw() end -- others vbank(0) dialog:draw() ps_star:draw() transition:draw() end, next = function(self) if self.current.next == nil then return end self:load(self.current.next) end } camera:load() Scene:load("title") transition:load() function TIC() cls(7) time:update() mouse:update() Scene:update() camera:update() debug:update() Scene:draw() -- mouse:draw() -- time:draw() -- color:draw() -- debug:draw() end