0,]']>S}Wup8d%qy)6o;]AsVl3>>>>>>>  *~(}#l k"[$Z%I&H'8'7''(&(()***************[PPRTUVWXXYZZZZZZZ[[[[[[[[[[[[  ''''(()**+,,-.//////////////// oO?//''-..../////////////////////// oO?//oO?//  G O G O G O G O G O G O G O G O GOGOGOGOGOGOGOA !##&&///////////////////////// $ -$-$))p)//////////////////////55$-$ -$$')+.//////////////////// e$ -$-$//   % % ( + . %%(+.%%%(+.%%(+%%%%%%'),/c`E`EdEgEhEiEijjjkkllllmmmmmmmmmmmmmncnmffgijknnnnnnnnnooooooooooooo**,.)*++,---.....////)-...//// ,              DDDD""""""""""""""pP`PPP@P0P P  DDd d                  e e e eeee e e e e e e eeee e e e`0@"""""""" g gggg g g gggg g g gggg g g ggggg gggggg gggggg gggggg gggqPqPggg gggggg gggggg gggggg gggaPggg gggggg gggPggg gggggg gggg gg g g g g gg g g g g gg g g g g gg g g g g ggg g g gggg g g gggg g g gggg g g gggg g g gggg g g gggg g g gggg g g ggggg gggggg ggg<"""""""""""""""    iiii i iiiiiii i ii i""""""""iiii i iiiiiii i ii iiiii i iiiiiii i ii i00@@0@00000(0,00 P` (@@@@@@@@@@@@@@@@B@@@@@?3333333333333333ffffffff"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333ffffffffffff3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDD4333"""""""wwwwwwwww3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDD43"","|w|w3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDD4<,,||w|ww3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDD4<<,,""|ww|ww3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D#3<",",|www|ww3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D#3""","|ww||ww3333fwffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDD4333"""""""wwwwwwwwww3333fwffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333fwfffffffwff3333ffffffff3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDD4"""""""wwwwwwwwwwww3333ffffffff3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDD4"""www|www3""33""3fwffwfwf3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD34"","|w w| ww3""33""3fwffwfwf3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD34"","|w ww|w3333333333333333ffffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDD4",www|w|wwww3333333333333333ffffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDD4,","wwwww|w3333333333333333ffffffffDD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD39333333DD3DDDD3"333333#33"3333"wwwwwwwwwwww3333333333333333ffffffffDD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD39333333DD3DDDD3"3333333DD3DDDD3ffffffffffff3""33""3ffwffwffDDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDDffwffwffffwf3""33""3ffwffwffDDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDDffwffwffffwf3333ffffffffDDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDDffffffffffff3333ffffffffDDDDDDDD33333"33DDDDDDDD33333"33DDDDDDJ33333"33DDDDDDDD33333"33DDDDDDDDffffffffffff3333ffffwffwDD3D3DDD33333333DD3D3DDD33333333DD3D3Dʬ33333333DD3D3DDD33333333DD3D3DDDffffwffwffff3333ffffwffwDD3D3DDD33333333DD3D3DDD33333333DD3D3Dʬ33333333DD3D3DDD33333333DD3D3DDDffffwffwffff3333fwffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDJ3"333333DDDDDDDD3"333333DDDDDDDDfwfffffffwff3333fwffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDOD3"333333DDDDDDDD3"333333DDDDDDDDfwfffffffwff3333ffffffffDDDDDDDD3333333"DDDDDDDD3333333"DDDDDD333333"DDDDDDDD3333333"DDDDDDDDffffffffffff3333ffffffffDDDDDDDD3333333"DDDDDDDD3333333"DDDDD333333"DDDDDDDD3333333"DDDDDDDDffffffffffff3""33""3fwffwfwf3DD3DD3D3333"3333DD3DD3D3333"3333DDӝD3333"3333DD3DD3D3333"3333DD3DD3Dfwffwfwffwff3""33""3fwffwfwf3DD3DD3D3333"3333DD3DD3D3333"3333DDؘ9D3333"3333DD3DD3D3333"3333DD3DD3Dfwffwfwffwff3333333333333333ffffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDD3"33333DDDDDDDD3"333333DDDDDDDDffffffffffff3333333333333333ffffffffDDDDDDDD3"333333DDDDDDDD3"333333DߘIM3:3333DDDDDDDD3"333333DDDDDDDDffffffffffff3333333333333333ffffffff"3333333DD3DDDD3"3333333DD3DDDD3"HJDDD3"3333333DD3DDDD3"3333333ffffffffffff3333333333333333ffffffff"3333333DD3DDDD3"3333333DD3DDDD3"3JNDDDD3"3333333DD3DDDD3"3333333ffffffffffff3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD33IDNDDDDDD333"3333DDDDDDDD333"3333ffwffwffffwf3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD333"MDDDDDDD333"3333DDDDDDDD333"3333ffwffwffffwf3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD3333$MDDDDDDD33333"33DDDDDDDD33333"33ffffffffffff3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD3333DMDDDDDDD33333"33DDDDDDDD33333"33ffffffffffff3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD3333DD3D3DDD33333333DD3D3DDD33333333ffffwffwffff3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD3333>3DD3D3DDD33333333DD3D3DDD33333333ffffwffwffff3333fwffffff3"333333DDDDDDDD3"333333DDDDDDDD3"33>33DDDDDDDD3"333333DDDDDDDD3"333333fwfffffffwff3333fwffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333?33DDDDDDDD3"333333DDDDDDDD3"333333fwfffffffwff3333ffffffff3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"ffffffffffff3333ffffffff3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"ffffffffffff3""33""3fwffwfwf3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"333fwffwfwffwff3""33""3fwffwfwf3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"333fwffwfwffwff3333333333333333ffffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333ffffffffffff3333333333333333ffffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333ffffffffffff3333333333333333ffffffffDD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3ffffffffffff3333333333333333ffffffffDD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3ffffffffffff3""33""3ffwffwffDDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDDffwffwffffwf3""33""3ffwffwffDDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDDffwffwffffwf3333ffffffffDDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDDffffffffffff3333ffffffffDDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDDffffffffffff3333ffffwffwDD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDDffffwffwffff3333ffffwffwDD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDDffffwffwffff3333fwffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDfwfffffffwff3333fwffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDfwfffffffwff3333ffffffffDDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDDffffffffffff3333ffffffffDDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDDffffffffffff3""33""3fwffwfwf3DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3Dfwffwfwffwff3""33""3fwffwfwf3DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3Dfwffwfwffwff3333333333333333ffffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDffffffffffff3333333333333333ffffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDffffffffffff3333333333333333ffffffff"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333ffffffffffff3333333333333333ffffffff"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333ffffffffffff3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333ffwffwffffwf3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333ffwffwffffwf3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33ffffffffffff3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33ffffffffffff3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333ffffwffwffff3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333ffffwffwffff3333fwffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333fwfffffffwff3333fwffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333fwfffffffwff3333ffffffff3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"ffffffffffff3333ffffffff3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"ffffffffffff3""33""3fwffwfwf3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"333fwffwfwffwff3""33""3fwffwfwf3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"333fwffwfwffwff3333333333333333ffffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333ffffffffffff3333333333333333ffffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333ffffffffffff3333333333333333ffffffffDD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3ffffffffffff3333333333333333ffffffffDD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3ffffffffffff3""33""3ffwffwffDDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDDffwffwffffwf3""33""3ffwffwffDDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDDffwffwffffwf3333ffffffffDDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDDffffffffffff3333ffffffffDDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDDffffffffffff3333ffffwffwDD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDDffffwffwffff3333ffffwffwDD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDDffffwffwffff3333fwffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDfwfffffffwff3333fwffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDfwfffffffwff3333ffffffffDDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDDffffffffffff3333ffffffffDDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDDffffffffffff3""33""3fwffwfwf3DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3Dfwffwfwffwff3""33""3fwffwfwf3DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3Dfwffwfwffwff3333333333333333ffffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDffffffffffff3333333333333333ffffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDffffffffffff3333333333333333ffffffff"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333ffffffffffff3333333333333333ffffffff"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333ffffffffffff3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333ffwffwffffwf3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333ffwffwffffwf3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33ffffffffffff3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33ffffffffffff3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333ffffwffwffff3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333ffffwffwffff3333fwffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333fwfffffffwff3333fwffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333fwfffffffwff3333ffffffff3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"ffffffffffff3333ffffffff3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"ffffffffffff3""33""3fwffwfwf3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"333fwffwfwffwff3""33""3fwffwfwf3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"333fwffwfwffwff3333333333333333ffffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333ffffffffffff3333333333333333ffffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333ffffffffffff3333333333333333ffffffffDD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3ffffffffffff3333333333333333ffffffffDD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3ffffffffffff3""33""3ffwffwffDDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDDffwffwffffwf3""33""3ffwffwffDDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDDffwffwffffwf3333ffffffffDDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDDffffffffffff3333ffffffffDDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDDffffffffffff3333ffffwffwDD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDDffffwffwffff3333ffffwffwDD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDDffffwffwffff3333fwffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDfwfffffffwff3333fwffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDfwfffffffwff3333ffffffffDDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDDffffffffffff3333ffffffffDDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDD3333333"DDDDDDDDffffffffffff3""33""3fwffwfwf3DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3Dfwffwfwffwff3""33""3fwffwfwf3DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3D3333"3333DD3DD3Dfwffwfwffwff3333333333333333ffffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDffffffffffff3333333333333333ffffffffDDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDDffffffffffff3333333333333333ffffffff"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333ffffffffffff3333333333333333ffffffff"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333DD3DDDD3"3333333ffffffffffff3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333ffwffwffffwf3""33""3ffwffwff333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333DDDDDDDD333"3333ffwffwffffwf3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33ffffffffffff3333ffffffff33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33DDDDDDDD33333"33ffffffffffff3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333ffffwffwffff3333ffffwffw33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333DD3D3DDD33333333ffffwffwffff3333fwffffff3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333DDDDDDDD3"333333fwfffffffwff$~QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQttQQQQQQQQttQQQQQQQQQQQQQQQQQttQQQQQQQQttQQQQQQQQQQQQQQQQrrrrrrrrrrrrQQQQQQQQQQQQQQQrrrrrrrrrrrrQQQQQQQQQQQQQQrrrrrrrrrrrrQQQQQQQQQQQQQrrrrrrrrrrrrQQQQQQQQQQQQrrrrrrrrrrrrQQQQQQQQQQQQrrrrrrrrrrrrQQQQQQQQQQQQrrrrrrrrrrrrQQQQQQQQQQQQQQQQQQQQQQQQQQQttQQQQQQQQttQQQQQQQQQQQQQQQQQQQQQQQQQQQttQQQQQQQQttQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQYYijD5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAґAAAAAAґAAAAAAґAґAґґґґґ҂ттсссссстт҂҂ijw``````````````````````````````````````````````````````````PPPPPPPPPPPPPPPPPP```PPPPPP````PPPPPPPPPPPPPPPPPP```PPPPPP````PPPPPPPPPPPPPPPPPPPP```PPPPPPPP````PPPPPPPPPPPPPPPPPPPP```PPPPPPPP````PPPPPPPPPPPPPPPPPPPPPP``PPPPPPPPPP````PPPPPPPPPPPPPPPPPPPPPP``PPPPPPPPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP``````````````````````````PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP``````````````````````````PPPPPP``````````````PPPPPPPPPP``PPPPPPPPPPPPPPPPPPPPPPPPPPPP````PPPPPP``````````````PPPPPPPPPP``PPPPPPPPPPPPPPPPPPPPPPPPPPPP````PPPPPP````PPPPPPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPP````PPPPPP````PPPPPPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPP``PPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPP``PPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPP````PPPP````PPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPP````PPPP````PPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPP``PPPP````````````````````````````````PPPPPP``PPPP````````````````````````````````PPPPPP````PPPP``PPPPPP``PPPP``PPPPPPPPPP````PPPP``PPPP````PPPP````PPPP``PPPP``PPPP``PPPPPPPP``````````````PPPP``PPPP``````````````PPPP````PPPP````````````````````````````PPPP``PPPP````````````````````````````PPPPPPPPPP````````````````````PPPPPPPPPPPPPPPP````PPPP``PPPPPP````````````````````PPPPPPPPPPPPPPPP````PPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP``````PPPP``PPPPPPPPPPPPPPPPPPPPPPPPPP````````````PPPPPP``````PPPP``PPPPPPPPPPPPPPPPPPPPPPPPPP``````PPPPPPPP````PPPP````PPPPPPPPPPPPPPPPPPPPPPPPPP``PPPPPPPP````PPPP````PPPPPPPPPPPPPPPPPPPPPP```PPPPPPPP``````PPPP``PPPPPPPPPPPPPPPPPPPPPPP````````PPPPPPPP``````PPPP``PPPPPPPPPPPPPPPPPPPPPPPPP``````````PPPPPPPPPP````PPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP````PPPP``````````````````````PPPPPPPPPPPPPPPPPPPPPPPPP````PPPP``````````````````````PPPPPPPPPPPPPPPPPPPPPPP````PPPP`````````````````````PPPP``PPPP```````````````````````PPPP``PPPP````````````````PPPP``PPPP``````````````PPPP``PPPPPPPP``PPPP``PPPP``PP````````````````````PPPP``PPPP``````````````````PPPP``PPPP``PPPPPPPPPPPPP``PPPP``PP````PPPPPPPPPPPPP``PPPP``PP````PPPPPPPPPPPPPPPP````PPPP``PPPP``PPPPPPPPPPPPPPPP````PPPP``PPPP``PPPPPPPPPPPPPPPPPP````````````````````PPPP``PPPP````PPPPPPPPPPPPPPPPPP````````````````````PPPP``PPPP````PPPPPPPPPPPPPPPPPPPP````PPPPPPPPPPPPPPPP````PPPP``PPPP``PPPPPPPPPPPPPPPPPPPP````PPPPPPPPPPPPPPPP````PPPP``PPPP``PPPP``````````````PPPP``PPPPPPPP````PPPPP``PPPP``PPPP````PPPP``````````````PPPP``PPPPPPPP````PPPPP``PPPP``PPPP````PPPP````PP````PPPPPPPPPP```PPPPPP````PP``PPPP``PPPP````PP````PPPPPPPPPP```PPPPPP````PP``PPPP``PPPP``PPPPPPPPPPPPPPPPPPPPPPPPPP``PPPP``PP````PPPP``PPPPPPPPPPPPPPPPPPPPPPPPPP``PPPP``PP````PPPP````PP````PP````````````````PPPP````PP``PPPP``PPPP````PP````PP````````````````PPPP````PP``PPPPPPPP``PPPPPPPP``PPPP``PP``PP``PPPP``PPPPPPPP``PPPP``PP``PPPP````PPPP``PP````PP````PPPP``PPPP``PPPP``PPPP``PP````PP````PPPP``PPPP``PPPPPPPP``PPPP``PPPPPPPP``PP``PPPP``PPPP``PPPP``PPPPPPPP``PPPP``PPPP````PPPP``PPPPPPPP````PPPP``PPPP``PPPP``PPPP``PPPPPPPP````PPPP``PPPP``PPPPPPPP``PPPPPPPP``PPPP``PPPP``PPPP``PPPP``PPPPPPPP``PPPP``PPPP``PPPP````PPPP``PPPPPP````PPPP``PPPP``PP``PPPP``PPPPPP````PPPP``PPPP``PPPPPP````PPPP``PPPPPP````````PPPP``PPPP``PPPP````PPPP``PPPPPP````````PPPP``PPPP````PPPP``````````PPPPPP``````PPPPPP``PP``PPPP``````````PPPPPP``````PPPPPP``PPPPPPPPPP````PPPPPP````PPPPPP``PPPP``PPPPPP````PPPPPP````PPPPPP``PPPP````PPPPPP````PPPPPP``PPPPPP````PPPP``PPPPPP````PPPPPP``PPPPPP````PPPP``PPPPPP````PPPPPPPPPPPP````PPPP````PPPPPP````PPPPPPPPPPPP````PPPP````PPPPPP````````````````````PPPPPPPPPP````PPPPPP``PPPPPP````````````````````PPPPPPPPPP````PPPPPP``PPPPPPPPPPPPPPPPPPPPPP````PPPPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````PPPPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````PPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````PPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````````````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````````````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````PPPPPP````````````````````PPPPPP````PPPPPP````````````````````PPPPPP````PPPPPP````PPPPPP````````````````````PPPPPP````PPPPPP````````````````````PPPPPP````PPPPPPPPPPPPPPPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````````````PPPPPP````PPPPPPPPPPPPPPPPPPPPPP````````````PPPPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP````PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP``````````````````````PPPPPPPPPPPPPPPPPP``````````````````````PPPPPPPPPPPPPPPPPP````PPPPPPPPPPPPPPPP````PPPPPPPPPPPPPPPP````PPPPPPPPPPPPPP````PPPPPPPPPPPPPP```````````````````````````````````rijijijijijóijijijijijijijó%-- title: SRBKart -- author: Indev -- desc: SRB2Kart but in TIC-80 -- license: GPLv3 -- version: 0.1 -- saveid: SRBKART_V1 -- input: gamepad -- script: lua -- menu: RETRY ABORT RETURN TO TITLE --showcover = 1 --showallmaps = 1 -- Bypasses "hell unlocked" check -- A reminder for myself: -- !!! ALL COORDS ARE IN MAP UNITS, NOT PIXELS !!! local maps = { { name = "Green Hills Zone", bank = 0, preview = 14, music = 0, numlaps = 5, -- Record Attack Gold (we don't have ghosts -- so just assigning reasonable values here :p ra_goldtime = 60*60 + 5*60, -- 01'05"00 -- sigh i give up nobotdrift = true, bots = {"sonic", "metalsonic", "knuckles"}, starts = { { x = 56, y = 38, }, { x = 56, y = 40, }, { x = 56, y = 42, }, }, checkpoints = { { x = 77, y = 40, r = 7 }, { x = 90, y = 40, r = 7 }, { x = 97, y = 26, r = 6 }, { x = 88, y = 8, r = 8 }, { x = 66, y = 9, r = 5 }, { x = 50, y = 18, r = 5 }, { x = 35, y = 11, r = 7 }, { x = 20, y = 5, r = 4 }, { x = 7, y = 17, r = 6 }, { x = 10, y = 27, r = 5 }, { x = 22, y = 39, r = 4 }, }, finish = { x = 58, y = 34, w = 2, h = 17, }, things = { { x = 93.5, y = 31, name = "randomitem" }, { x = 94.5, y = 31, name = "randomitem" }, { x = 95.5, y = 31, name = "randomitem" }, { x = 96.5, y = 31, name = "randomitem" }, { x = 97.5, y = 31, name = "randomitem" }, { x = 98.5, y = 31, name = "randomitem" }, { x = 99.5, y = 31, name = "randomitem" }, } }, { name = "Toxic Palace Zone", bank = 0, preview = 46, numlaps = 4, ra_goldtime = 60*60 + 20*60, -- 01'20"00 -- Yeah i should just remove bot drifting, i'm too dumb -- to make it work properly -- just kinda wanna keep it for future nobotdrift = true, bots = {"eggman", "tails", "knuckles"}, starts = { { x = 34, y = 89, }, { x = 34, y = 91, }, { x = 34, y = 93, }, }, checkpoints = { { x = 45, y = 91, r = 6 }, { x = 77, y = 91, r = 6 }, { x = 109, y = 93, r = 7 }, { x = 105, y = 108, r = 5 }, { x = 94, y = 105, r = 5 }, { x = 95, y = 73, r = 6 }, { x = 105, y = 67, r = 6 }, { x = 114, y = 74, r = 6 }, { x = 123, y = 80, r = 4 }, { x = 131, y = 83, r = 5 }, { x = 132, y = 115, r = 5 }, { x = 122, y = 124, r = 6 }, { x = 102, y = 124, r = 4 }, { x = 82, y = 124, r = 4 }, { x = 78, y = 115, r = 5 }, { x = 72, y = 105, r = 3 }, { x = 58, y = 106, r = 5 }, { x = 31, y = 110, r = 5 }, { x = 22, y = 102, r = 6 }, { x = 26, y = 91, r = 6 }, }, finish = { x = 39, y = 86, w = 2, h = 11, }, things = { { x = 130.5, y = 96, name = "randomitem", }, { x = 131.5, y = 96, name = "randomitem", }, { x = 132.5, y = 96, name = "randomitem", }, { x = 133.5, y = 96, name = "randomitem", }, { x = 134.5, y = 96, name = "randomitem", }, { x = 135.5, y = 96, name = "randomitem", }, { x = 20.5, y = 98, name = "randomitem", }, { x = 21.5, y = 98, name = "randomitem", }, { x = 22.5, y = 98, name = "randomitem", }, { x = 23.5, y = 98, name = "randomitem", }, { x = 24.5, y = 98, name = "randomitem", }, { x = 25.5, y = 98, name = "randomitem", }, { x = 26.5, y = 98, name = "randomitem", }, }, }, { name = "Sunny Islands Zone", bank = 0, numlaps = 4, ra_goldtime = 60*60 + 30*60, -- 01'30"00 preview = 78, music = 3, -- no lmao nobotdrift = true, bots = {"eggman", "tails", "knuckles"}, starts = { { x = 178, y = 58, }, { x = 178, y = 60, }, { x = 178, y = 62, }, }, checkpoints = { { x = 190, y = 61, r = 4 }, { x = 205, y = 61, r = 4 }, { x = 212, y = 56, r = 4 }, { x = 213, y = 45, r = 3 }, { x = 214, y = 16, r = 4 }, { x = 213, y = 7, r = 4 }, { x = 201, y = 8, r = 4 }, { x = 199, y = 17, r = 5 }, { x = 199, y = 33, r = 4 }, { x = 200, y = 40, r = 3 }, { x = 192, y = 46, r = 3 }, { x = 182, y = 45, r = 3 }, { x = 179, y = 42, r = 3 }, { x = 179, y = 31, r = 3 }, { x = 179, y = 8, r = 4 }, { x = 167, y = 5, r = 4 }, { x = 155, y = 12, r = 5 }, { x = 137, y = 13, r = 4 }, { x = 130, y = 12, r = 4 }, { x = 125, y = 21, r = 3 }, { x = 126, y = 29, r = 4 }, { x = 138, y = 31, r = 3 }, { x = 159, y = 32, r = 4 }, { x = 163, y = 39, r = 4 }, { x = 158, y = 44, r = 3 }, { x = 141, y = 45, r = 5 }, { x = 137, y = 56, r = 5 }, { x = 148, y = 61, r = 4 }, { x = 173, y = 61, r = 4 }, }, finish = { x = 180, y = 56, w = 2, h = 10, }, things = { { x = 210, y = 50, name = "randomitem", }, { x = 211, y = 50, name = "randomitem", }, { x = 212, y = 50, name = "randomitem", }, { x = 213, y = 50, name = "randomitem", }, { x = 214, y = 50, name = "randomitem", }, { x = 215, y = 50, name = "randomitem", }, { x = 139, y = 29, name = "randomitem", }, { x = 139, y = 30, name = "randomitem", }, { x = 139, y = 31, name = "randomitem", }, { x = 139, y = 32, name = "randomitem", }, { x = 139, y = 33, name = "randomitem", }, { x = 139, y = 34, name = "randomitem", }, }, }, { name = "Lava Reef Zone", bank = 1, numlaps = 5, ra_goldtime = 60*60 + 15*60, -- 01'15"00 preview = 110, -- It almost works here... Maybe will turn back on -- when i feel like tweaking checkpoints more nobotdrift = true, bots = {"metalsonic", "eggman", "knuckles"}, starts = { { x = 43, y = 21, }, { x = 43, y = 23, }, { x = 43, y = 25, }, }, checkpoints = { { x = 54, y = 23, r = 4 }, -- 1 { x = 74, y = 23, r = 4 }, -- 2 { x = 94, y = 23, r = 5 }, -- 3 { x = 106, y = 23, r = 4 }, -- 4 { x = 112, y = 27, r = 4 }, -- 5 { x = 112, y = 40, r = 4 }, -- 6 { x = 106, y = 46, r = 4 }, -- 7 { x = 85, y = 45, r = 4 }, -- 8 { x = 73, y = 46, r = 4 }, -- 9 { x = 60, y = 54, r = 3 }, -- 10 { x = 48, y = 50, r = 4 }, -- 11 { x = 39, y = 45, r = 4 }, -- 12 { x = 22, y = 51, r = 4 }, -- 13 { x = 16, y = 56, r = 4 }, -- 14 { x = 9, y = 54, r = 4 }, -- 15 { x = 7, y = 49, r = 5 }, -- 16 { x = 9, y = 38, r = 5 }, -- 17 { x = 20, y = 24, r = 5 }, -- 18 { x = 37, y = 23, r = 4 }, -- 18 }, finish = { x = 45, y = 19, w = 2, h = 9, }, things = { { x = 104, y = 20, name = "randomitem" }, { x = 104, y = 21, name = "randomitem" }, { x = 104, y = 22, name = "randomitem" }, { x = 104, y = 23, name = "randomitem" }, { x = 104, y = 24, name = "randomitem" }, { x = 104, y = 25, name = "randomitem" }, { x = 104, y = 26, name = "randomitem" }, }, }, { name = "Verdant Ruins Zone", bank = 1, numlaps = 4, ra_goldtime = 60*60 + 15*60, -- 01'15"00 preview = 142, nobotdrift = true, bots = {"sonic", "knuckles", "metalsonic"}, starts = { { x = 19, y = 104, }, { x = 19, y = 106, }, { x = 19, y = 108, }, }, checkpoints = { { x = 37, y = 106, r = 6 }, -- 1 { x = 60, y = 106, r = 4 }, -- 2 { x = 69, y = 109, r = 4 }, -- 3 { x = 71, y = 121, r = 6 }, -- 4 { x = 80, y = 126, r = 4 }, -- 5 { x = 107, y = 126, r = 5 }, -- 6 { x = 111, y = 111, r = 6 }, -- 7 { x = 120, y = 97, r = 6 }, -- 8 { x = 138, y = 97, r = 5 }, -- 9 { x = 146, y = 90, r = 5 }, -- 10 { x = 136, y = 84, r = 6 }, -- 11 { x = 121, y = 84, r = 6 }, -- 12 { x = 96, y = 85, r = 8 }, -- 13 { x = 74, y = 87, r = 6 }, -- 14 { x = 46, y = 91, r = 4 }, -- 15 { x = 28, y = 91, r = 5 }, -- 16 { x = 10, y = 92, r = 5 }, -- 17 { x = 4, y = 102, r = 4 }, -- 18 { x = 14, y = 106, r = 4 }, -- 19 }, finish = { x = 21, y = 100, w = 2, h = 14, }, things = { { x = 58, y = 103.5, name = "randomitem", }, { x = 58, y = 104.5, name = "randomitem", }, { x = 58, y = 105.5, name = "randomitem", }, { x = 58, y = 106.5, name = "randomitem", }, { x = 58, y = 107.5, name = "randomitem", }, { x = 58, y = 108.5, name = "randomitem", }, { x = 95, y = 82.5, name = "randomitem", }, { x = 95, y = 83.5, name = "randomitem", }, { x = 95, y = 84.5, name = "randomitem", }, { x = 95, y = 85.5, name = "randomitem", }, { x = 95, y = 86.5, name = "randomitem", }, { x = 95, y = 87.5, name = "randomitem", }, }, }, { name = "Desert Bus Zone", bank = 7, numlaps = 20, ra_goldtime = 4*60*60 + 20*60, -- 04'20"00 -- NIIIIICE preview = 238, hell = true, -- >:3 -- Not really sure about it... They won't try anyway -- but nobotdrift gives them slight rubberband boost -- so they don't stay behind player 99% if time -- (unless you are sonic :p) nobotdrift = true, bots = {"sonic", "knuckles", "metalsonic"}, starts = { { x = 22, y = 108, }, { x = 22, y = 110, }, { x = 22, y = 112, }, }, checkpoints = { { x = 40, y = 110, r = 5 }, { x = 60, y = 110, r = 5 }, { x = 80, y = 110, r = 5 }, { x = 100, y = 110, r = 5 }, { x = 120, y = 110, r = 5 }, { x = 140, y = 110, r = 5 }, { x = 160, y = 110, r = 5 }, { x = 180, y = 110, r = 5 }, { x = 220, y = 110, r = 5 }, -- Hack so bots continue driving after teleporting back -- This is actually 1 same checkpoint! { x = 245, y = 110, r = 1 }, { x = 5, y = 110, r = 5 }, { x = 20, y = 110, r = 5 }, }, finish = { x = 24, y = 106, w = 2, h = 9, }, things = { { x = 218, y = 107.5, name = "randomitem" }, { x = 218, y = 108.5, name = "randomitem" }, { x = 218, y = 109.5, name = "randomitem" }, { x = 218, y = 110.5, name = "randomitem" }, { x = 218, y = 111.5, name = "randomitem" }, { x = 218, y = 112.5, name = "randomitem" }, { x = 9, y = 90, name = "something" }, -- behind you }, wind = { x = 0, y = 0.035, }, }, { name = "Egg Rock Zone", bank = 2, numlaps = 5, ra_goldtime = 1*60*60 + 25*60, -- 01'25"00 preview = 174, -- Might as well just delete bot drift completely at this point? nobotdrift = true, bots = {"tails", "knuckles", "eggman"}, starts = { { x = 99, y = 16, }, { x = 99, y = 18, }, { x = 99, y = 20, }, }, checkpoints = { { x = 109, y = 18, r = 4 }, -- 1 { x = 129, y = 18, r = 4 }, -- 2 { x = 154, y = 18, r = 4 }, -- 3 { x = 161, y = 25, r = 4 }, -- 4 { x = 157, y = 32, r = 3 }, -- 5 { x = 131, y = 32, r = 4 }, -- 6 { x = 112, y = 33, r = 4 }, -- 7 { x = 107, y = 45, r = 4 }, -- 8 { x = 103, y = 52, r = 4 }, -- 9 { x = 82, y = 52, r = 4 }, -- 10 { x = 71, y = 42, r = 3 }, -- 11 { x = 54, y = 49, r = 3 }, -- 12 { x = 43, y = 49, r = 3 }, -- 13 { x = 36, y = 40, r = 4 }, -- 14 { x = 36, y = 16, r = 4 }, -- 15 { x = 42, y = 10, r = 4 }, -- 16 { x = 48, y = 18, r = 4 }, -- 17 { x = 83, y = 18, r = 4 }, -- 18 }, finish = { x = 101, y = 14, w = 2, h = 9, }, things = { { x = 122, y = 29.5, name = "randomitem" }, { x = 122, y = 30.5, name = "randomitem" }, { x = 122, y = 31.5, name = "randomitem" }, { x = 122, y = 32.5, name = "randomitem" }, { x = 122, y = 33.5, name = "randomitem" }, { x = 122, y = 34.5, name = "randomitem" }, }, }, { name = "Sealed Star Zone", bank = 6, numlaps = 2, ra_goldtime = 60*60 + 20*60, -- 1'20"00 preview = 206, music = 7, hell = true, nobotdrift = true, bots = {"sonic", "metalsonic", "knuckles"}, starts = { { x = 75, y = 43, }, { x = 75, y = 45, }, { x = 75, y = 47, }, }, checkpoints = { { x = 90, y = 45, r = 7 }, -- 1 { x = 114, y = 50, r = 4 }, -- 2 { x = 157, y = 49, r = 6 }, -- 3 { x = 163, y = 62, r = 6 }, -- 4 { x = 163, y = 88, r = 5 }, -- 5 { x = 155, y = 98, r = 5 }, -- 6 { x = 142, y = 96, r = 6 }, -- 7 { x = 135, y = 84, r = 6 }, -- 8 { x = 130, y = 72, r = 5 }, -- 9 { x = 103, y = 69, r = 6 }, -- 10 { x = 93, y = 76, r = 5 }, -- 11 { x = 90, y = 86, r = 4 }, -- 12 { x = 82, y = 83, r = 5 }, -- 13 { x = 82, y = 70, r = 5 }, -- 14 { x = 75, y = 63, r = 7 }, -- 15 { x = 49, y = 64, r = 6 }, -- 16 { x = 38, y = 77, r = 6 }, -- 17 { x = 39, y = 90, r = 5 }, -- 18 { x = 54, y = 104, r = 6 }, -- 19 { x = 80, y = 103, r = 7 }, -- 20 { x = 97, y = 115, r = 7 }, -- 21 { x = 130, y = 116, r = 6 }, -- 22 { x = 145, y = 121, r = 7 }, -- 23 { x = 160, y = 120, r = 6 }, -- 24 { x = 182, y = 97, r = 7 }, -- 25 { x = 183, y = 80, r = 4 }, -- 26 { x = 183, y = 33, r = 6 }, -- 27 { x = 170, y = 24, r = 5 }, -- 28 { x = 141, y = 23, r = 6 }, -- 29 { x = 124, y = 19, r = 5 }, -- 30 { x = 115, y = 17, r = 5 }, -- 31 { x = 107, y = 24, r = 5 }, -- 32 { x = 99, y = 17, r = 6 }, -- 33 { x = 69, y = 16, r = 6 }, -- 34 { x = 59, y = 29, r = 6 }, -- 35 { x = 61, y = 42, r = 5 }, -- 36 }, finish = { x = 77, y = 39, w = 2, h = 15, }, things = { { x = 159.5, y = 58, name = "randomitem" }, { x = 160.5, y = 58, name = "randomitem" }, { x = 161.5, y = 58, name = "randomitem" }, { x = 162.5, y = 58, name = "randomitem" }, { x = 163.5, y = 58, name = "randomitem" }, { x = 164.5, y = 58, name = "randomitem" }, { x = 165.5, y = 58, name = "randomitem" }, { x = 61, y = 60.5, name = "randomitem" }, { x = 61, y = 61.5, name = "randomitem" }, { x = 61, y = 62.5, name = "randomitem" }, { x = 61, y = 63.5, name = "randomitem" }, { x = 61, y = 64.5, name = "randomitem" }, { x = 61, y = 65.5, name = "randomitem" }, { x = 61, y = 66.5, name = "randomitem" }, { x = 179.5, y = 36, name = "randomitem" }, { x = 180.5, y = 36, name = "randomitem" }, { x = 181.5, y = 36, name = "randomitem" }, { x = 182.5, y = 36, name = "randomitem" }, { x = 183.5, y = 36, name = "randomitem" }, { x = 184.5, y = 36, name = "randomitem" }, { x = 185.5, y = 36, name = "randomitem" }, }, }, } skins = {} skincolors = {} function cvar_new(name, value, onchange) return { name = name, value = value, onchange = onchange } end local function cvar_set(cv, value) local oldvalue = cv.value cv.value = value if cv.onchange then cv:onchange(oldvalue) end end local function listfunc(list, func) return function(cv, oldvalue) if cv.value <= 0 then cv.value = #list elseif cv.value > #list then cv.value = 1 end if func then func(cv, oldvalue) end end end local function optlistfunc(list, func) return function(cv, oldvalue) if cv.value < 0 then cv.value = #list elseif cv.value > #list then cv.value = 0 end if func then func(cv, oldvalue) end end end local function wrapfunc(min, max, func) return function(cv, oldvalue) if cv.value < min then cv.value = max end if cv.value > max then cv.value = min end if func then func(cv, oldvalue) end end end SKINMASK = 0x000000ff COLORSHIFT = 8 COLORMASK = 0x0000ff00 -- First bit responsible for settings (comes after this one) SETTINGSSHIFT = 16 function saveskin(cv) local options = pmem(0) & ~SKINMASK pmem(0, options|cv.value) end function loadskin() local s = pmem(0) & SKINMASK -- ...! This will be 0 by default, we don't like that if s == 0 then s = 1 end return s end function savecolor(cv) local options = pmem(0) & ~COLORMASK pmem(0, options|(cv.value<>COLORSHIFT end local settings_bit = SETTINGSSHIFT local function cvar_newbit(name, default, onchange) -- no matter what we specify as default, bits will always -- be 0 by default so here's trick: settings bit -- will represent not an actual setting value, but -- whether it was changed from default or not. -- Comes with a problem that we can't really change -- default value without it affecting settings from -- older version :p settings_bit = settings_bit+1 if settings_bit == 33 then error("Too many settings for 32bit integer!") end local bit = settings_bit local xorval = default and 1 or 0 local cv = cvar_new(name, default, function(cv, oldvalue) local v = cv.value and 1 or 0 v = (v ~ xorval)<>bit & 0x1 if v == 1 then cv.value = not default end return cv end options = { skin = cvar_new("skin", loadskin(), listfunc(skins, saveskin)), skincolor = cvar_new("color", loadcolor(), optlistfunc(skincolors, savecolor)), music = cvar_newbit("music", false, function() game_updatemusic() end), threat = cvar_newbit("threat display", true), autoaccel = cvar_newbit("auto accelerate", false), -- Those are more just for ui, and are not saved between sessions difficulty = cvar_new("difficulty", 2, wrapfunc(1, 3)), map = cvar_new("level", 1, function(cv, oldvalue) -- Skip hellmaps if they aren't unlocked yet local step = cv.value - oldvalue cv.value = menu_nextmapindex(oldvalue, step) end), } local game_states = {} local game_curstate = "menu" game_finishthinker = nil local WIDTH = 240 local HEIGHT = 136 local MAXSPEED = 5 local NOCONTEST = (1<<32) local leveltime = 0 local gamemap = 1 local starttime = 3*60 + 30 local p local camera local players = {} -- TODO - more sfx local enginesfx = { [1] = 16, } -- Bunch of matrix multiplications unfolded, basically local function _transform(x0, y0, u, v, x, y) return u*(x0-x) + v*(y0-y) + x, -v*(x0-x) + u*(y0-y) + y end local function sprrot(id, x, y, angle, chromakey, w, h, s) if w == nil then w = 1 end if h == nil then h = 1 end if s == nil then s = 1 end -- Needs negative angle for some reason... -- i'm good at math trust me! -- actually figured out it might be because transform -- is created for case when y axis points up, but in -- tic80 it points down. Not sure but most likely its that local u = s*math.cos(-angle) local v = s*math.sin(-angle) local cx = x+4*w local cy = y+4*h local x1, y1 = _transform(x, y, u, v, cx, cy) local x2, y2 = _transform(x+w*8, y, u, v, cx, cy) local x3, y3 = _transform(x+w*8, y+h*8, u, v, cx, cy) local x4, y4 = _transform(x, y+h*8, u, v, cx, cy) local u1, v1 = (id % 16)*8, math.floor(id/16)*8 local u2, v2 = u1+w*8, v1 local u3, v3 = u1+w*8, v1+h*8 local u4, v4 = u1, v1+h*8 ttri(x1, y1, x2, y2, x3, y3, u1, v1, u2, v2, u3, v3, 0, chromakey) ttri(x3, y3, x4, y4, x1, y1, u3, v3, u4, v4, u1, v1, 0, chromakey) end function remaprect(x, y, w, h, cmap) if x > WIDTH or y > HEIGHT then return end if x < 0 then w = w+x; x = 0 end if x+w > WIDTH then w = WIDTH-x end if y < 0 then h = h+y; y = 0 end if y+h > HEIGHT then h = HEIGHT-y end for u = x, x+w-1 do for v = y, y+h-1 do local p = pix(u, v) if cmap[p] then pix(u, v, cmap[p]) end end end end function remapcirc(x, y, r, cmap) x = math.floor(x) y = math.floor(y) r = math.floor(r) local r2 = r*r for u = math.max(0, x-r), math.min(x+r, WIDTH) do for v = math.max(0, y-r), math.min(y+r, HEIGHT) do local dx = u-x local dy = v-y if dx*dx + dy*dy <= r2 then local p = pix(u, v) if cmap[p] then pix(u, v, cmap[p]) end end end end end function misc_timestr(t) if t == NOCONTEST then return "NO CONTEST" end if t then local min = math.floor(t/60/60) local sec = math.floor(t/60)%60 local cen = math.floor(t/60*100)%100 if min < 60 then return string.format("%.2d'%.2d\"%.2d", min, sec, cen) else return " SUCKS" -- Haha funny DOOM reference end end return "--'--\"--" end local function misc_rescale(value, oldmin, oldmax, newmin, newmax) return newmin + (value-oldmin)/(oldmax-oldmin)*(newmax-newmin) end local function misc_rescalecl(value, oldmin, oldmax, newmin, newmax) value = math.min(oldmax, math.max(oldmin, value)) return misc_rescale(value, oldmin, oldmax, newmin, newmax) end local function misc_stepify(x, step) return math.floor(x / step) * step end local function misc_sign(x) if x < 0 then return -1 end if x > 0 then return 1 end return 0 end local function misc_rectcollide(r1, r2) if r1.x+r1.w < r2.x or r2.x+r2.w < r1.x or r1.y+r1.h < r2.y or r2.y+r2.h < r1.y then return false end return true end local function misc_hypot(x, y) return math.sqrt(x*x + y*y) end local function misc_dir(x, y) local hypot = misc_hypot(x, y) if hypot > 0 then return x/hypot, y/hypot else return 0, 1 -- Return something shrug end end local function misc_pointtodist2(x1, y1, x2, y2) return misc_hypot(x1-x2, y1-y2) end local function misc_pointtodir2(x1, y1, x2, y2) return misc_dir(x2-x1, y2-y1) end local function misc_objdist(o1, o2) return misc_pointtodist2(o1.x, o1.y, o2.x, o2.y) end local function misc_objdir(o1, o2) return misc_pointtodir2(o1.x, o1.y, o2.x, o2.y) end -- Shoutouts to OneLoneCoder aka javidx9 local function misc_period(val, pstart, pend) local period = pend-pstart while val < pstart do val = val + period end while val >= pend do val = val - period end return val end -- Picks index in array, wrapping around if needed local function misc_index(val, arr) return misc_period(val, 1, #arr+1) end local function misc_perioddiff(val1, val2, pstart, pend) local diff1 = misc_period(val2-val1, pstart, pend) local diff2 = misc_period(val1-val2, pstart, pend) if diff2 < diff1 then return -diff2 else return diff1 end end function misc_checkpointdist(c1, c2) return misc_hypot( misc_perioddiff(c1.x, c2.x, 0, 240), misc_perioddiff(c1.y, c2.y, 0, 136) ) end local function misc_angadd(ang1, ang2) return misc_period(ang1+ang2, 0, 2*math.pi) end local function misc_angsub(ang1, ang2) return misc_angadd(ang1, -ang2) end local function misc_angdiff(ang1, ang2) return misc_perioddiff(ang1, ang2, 0, 2*math.pi) end local function misc_dotimer(t, name) if t[name] > 0 then t[name] = t[name] - 1 return t[name] <= 0 end return false end render_frame = nil function render_storeframe() if render_frame == nil then render_frame = {} end for i = 0, 0x03FC0-1 do render_frame[i] = peek(i) end end function render_blitframe() if not render_frame then return end for i = 0, 0x03FC0-1 do poke(i, render_frame[i]) end end -- Bunch of draw calls that get called after everything else is rendered local posteffects = {} function render_posteffect(func, ...) local args = {...} table.insert(posteffects, function() func(table.unpack(args)) end) end function render_draweffects() for _, effect in ipairs(posteffects) do effect() end posteffects = {} end local function skincolor(name, c1, c2, c3) table.insert(skincolors, { name = name, colormap = { [5] = c1, [6] = c2, [7] = c3, }, textcolor = c1, }) _G["SKINCOLOR_"..name:upper()] = #skincolors end -- Its hard to come up with color combinations -- when palette is so limited :^( skincolor("Platinum", 12, 13, 14) skincolor("Grey", 13, 14, 15) skincolor("Blueberry", 9, 8, 0) skincolor("Blue", 10, 9, 8) skincolor("Caribbean", 5, 6, 9) skincolor("Green", 5, 6, 7) skincolor("Gold", 12, 3, 4) skincolor("Orange", 4, 3, 2) skincolor("Red", 3, 2, 1) skincolor("Purple", 2, 1, 0) skincolor("Pastel", 11, 10, 1) function render_skincolormap(i) return skincolors[i].colormap end shademap = { [1] = 0, [2] = 1, [3] = 2, [4] = 3, [5] = 6, [6] = 7, [7] = 0, [8] = 0, [9] = 8, [10] = 9, [11] = 10, [12] = 13, [13] = 14, [14] = 15, [15] = 0, } function render_cmapcombine(c1, c2) local c = {} for i = 0, 15 do c[i] = c2[c1[i] or i] or i end return c end darkshademap = render_cmapcombine(shademap, shademap) local function render_palcolor(i) local r = peek(0x3FC0+i*3) local g = peek(0x3FC0+i*3+1) local b = peek(0x3FC0+i*3+2) return r, g, b end local function render_closestpalcolor(r, g, b) local closest = 0 local closestdist = 3*255*255 for i = 0, 15 do local r2, g2, b2 = render_palcolor(i) local dr = r-r2 local dg = g-g2 local db = b-b2 local dist = dr*dr + dg*dg + db*db if dist < closestdist then closest = i closestdist = dist end end return closest end local invmap = {} for i = 0, 15 do local r, g, b = render_palcolor(i) invmap[i] = render_closestpalcolor(255-r, 255-g, 255-b) end local blinkmaps = { red = { [4] = 12, [5] = 3, [6] = 2, [7] = 1, [8] = 1, [9] = 2, [10] = 3, [11] = 12, [13] = 3, [14] = 2, [15] = 1, }, grey = { [1] = 15, [2] = 14, [3] = 13, [4] = 12, [5] = 13, [6] = 14, [7] = 15, [8] = 15, [9] = 14, [10] = 13, [11] = 12, }, } local fullcolormaps = {} local function render_fullcolormap(c) local cmap = fullcolormaps[c] if cmap == nil then cmap = {} for i = 0, 15 do cmap[i] = c end fullcolormaps[c] = cmap end return cmap end local function render_rainbowmap() return render_skincolormap(math.floor(leveltime/2)%#skincolors+1) end -- True-ish rainbow local fcrainbow = { 2, 3, 4, 6, 10, 9, 1 } local function render_fcrainbowmap() return render_fullcolormap(fcrainbow[math.floor(leveltime/2)%#fcrainbow+1]) end local lastcolormap local function render_setcolormap(colormap) if colormap == lastcolormap then return end lastcolormap = colormap for i = 0, 15 do poke4(0x3FF0*2+i, colormap and colormap[i] or i) end end -- Fade code, its a little messy because i was aiming more -- for accuracy with SRB2Kart local fade_active = false local fade_laststep = nil local fade_time = 0 local fade_endtime = 0 local fade_color = 12 local fade_speed = 1 local fade_dobackward = false local fade_nowait = false function render_startfade(color, speed, dobackward) fade_active = true fade_laststep = nil fade_time = 0 fade_endtime = 0 fade_color = color or 12 fade_speed = speed or 1 fade_dobackward = dobackward fade_nowait = false end function render_startinterfade(color, speed) render_startfade(color, speed) fade_nowait = true end function render_stopfade() fade_active = false fade_time = 0 end -- Gaaah this is ugliest code i've ever wrote ;-; local function render_fade() local FADE_STEPTIME = 4/fade_speed fade_time = fade_time + 1 local fade_step = math.floor(fade_time/FADE_STEPTIME) if fade_step == fade_laststep then return end fade_laststep = fade_step local y = HEIGHT - fade_step*16 local rect = (type(fade_color) == "number") and rect or remaprect if y > -48 then for x = (y%32)-16, WIDTH, 32 do rect(x, y, 16, 16, fade_color) end elseif fade_nowait then render_stopfade() elseif fade_dobackward and y+HEIGHT+32 >= -48 then -- First frame of "backward" fade, draw 1 frame to store it if y + 16 > -48 then game_states[game_curstate]() render_storeframe() else render_blitframe() end y = y + HEIGHT+32 for i = 0, 2 do local u = y+16*i for x = (u%32)-16, WIDTH, 32 do rect(x, u, 16, 16, fade_color) end end if y <= -48 then render_stopfade() end rect(0, 0, WIDTH, y, fade_color) return else fade_endtime = fade_endtime + FADE_STEPTIME if fade_endtime > 60 then render_stopfade() end end if y+48 < HEIGHT then y = y+48 for x = (y%32)-32, WIDTH, 32 do rect(x, y, 16, 16, fade_color) end end end local vissprites = {} -- cache local numvissprites = 0 local spritestodraw = {} local function render_clearsprites() spritestodraw = {} numsprites = 0 end local function render_drawrotated(sprite) sprrot(sprite.id, sprite.x, sprite.y, sprite.a, 0, sprite.w, sprite.h, sprite.s) end local function render_newsprite() numvissprites = numvissprites+1 local s = vissprites[numvissprites] if s == nil then s = { x = 0, y = 0, z = 0, } vissprites[numvissprites] = s end table.insert(spritestodraw, s) return s end local function render_clipsprite() local s = spritestodraw[#spritestodraw] if s.x + s.w*8 < 0 or s.x > WIDTH or s.y + s.h*8 < 0 or s.y > HEIGHT then table.remove(spritestodraw) return true end return false end function render_drawsprites() table.sort(spritestodraw, function(a, b) return a.z < b.z end) for _, sprite in ipairs(spritestodraw) do render_setcolormap(sprite.cmap) sprite:draw() end render_clearsprites() render_setcolormap(nil) end -- Tile animation local animframetime = {} local animframes = {} local defaultframetime = 10 local function render_remap(tile) if not fget(tile, 6) then return tile end local frames = animframes[tile] if animframes[tile] == nil then frames = 1 for i = 1, 255 do if fget(tile+i, 6) then frames = frames + 1 else break end end animframes[tile] = frames end local frametime = animframetime[tile] or defaultframetime tile = tile + math.floor(leveltime/defaultframetime) % frames return tile end local channels = { { timer = 0, volume = 0, }, { timer = 0, volume = 0, }, { timer = 0, volume = 0, }, { timer = 0, volume = 0, }, } local music_playing = nil local function sound_getchannel(sound, volume, duration, o, tryreplace) for i, channel in ipairs(channels) do -- Last 2 channels are reserved for music, if its enabled if i == 3 and music_playing then break end if channel.timer <= 0 or (tryreplace and channel.volume < volume) then channel.timer = duration channel.volume = volume channel.sound = sound channel.o = o return i-1 end end -- Couldn't find free channel, maybe we can replace sound -- that is more quiet? if not tryreplace then return sound_getchannel(sound, volume, duration, o, true) end end local fadedist = 3*WIDTH/4 function sound_start(snd, note, duration, volume, o, kart) if kart ~= nil and kart ~= p then return end if duration == nil then duration = -1 end if volume == nil then volume = 15 end if o ~= nil and camera ~= nil then local center = { x = camera.x + WIDTH/2, y = camera.y + HEIGHT/2 } local dist = misc_objdist(o, center) if dist > fadedist then volume = math.floor(volume*(fadedist/dist)) end end if volume == 0 then return end local channel = sound_getchannel(snd, volume, duration, o) if channel == nil then return end sfx(snd, note, duration, channel, volume) end function sound_playing(snd, o) for i, channel in ipairs(channels) do if channel.timer > 0 and channel.sound == snd and (o == nil or channel.o == o) then return true, i-1 end end return false end function sound_stop(snd, o) local playing, ch = sound_playing(snd, o) if not playing then return end sfx(0, 0, 0, ch, 0) end function sound_beep() sound_start(63, "B-7", 2, 7) end local function sound_update() for _, channel in ipairs(channels) do misc_dotimer(channel, "timer") end end function sound_startnm(...) if not options.music.value then sound_start(...) end end function sound_playmusic(track) if options.music.value and track ~= music_playing then music(track, -1, -1, true) music_playing = track end end function sound_stopmusic() music() music_playing = nil end local con_lines = {} local CON_MAXLINES = 4 local CON_LINETIMER = 60*4 local CON_FADETIME = 60 function con_printf(fmt, ...) local s = string.format(fmt, ...) trace(s) table.insert(con_lines, { string = s, timer = CON_LINETIMER, }) if #con_lines > CON_MAXLINES then table.remove(con_lines, 1) end end function con_clear() con_lines = {} end local function con_draw() for i, entry in ipairs(con_lines) do local color = entry.timer < CON_FADETIME and 15 or 12 print(entry.string, 4, i*6, color) end for i = #con_lines, 1, -1 do if misc_dotimer(con_lines[i], "timer") then table.remove(con_lines, i) end end end local function map_pmem(mapid) return 1 + (mapid-1)*2 end function map_besttime(mapid, newtime) local slot = map_pmem(mapid) local v = pmem(slot) if newtime and (v == 0 or v > newtime) then pmem(slot, newtime) end if v == 0 then return nil end return v end MAP_RAGOLD = 0x1 MAP_VSSILVER = 0x2 -- 2nd/1st on at least normal difficulty MAP_VSGOLD = 0x4 -- 1st on at least normal difficulty MAP_VSGOLDHARD = 0x8 -- 1st on hard difficulty function map_flags(mapid, newflags) local slot = map_pmem(mapid)+1 local v = pmem(slot) if newflags then pmem(slot, newflags) end return v end function map_addflag(mapid, flag) local v = map_flags(mapid) map_flags(mapid, v|flag) end function map_getflag(mapid, flag) return (map_flags(mapid) & flag) == flag end function map_wipescores() for i = 1, #maps do map_besttime(i, 0) map_flags(i, 0) end end local function map_checkpoint(i) local checkpoints = maps[gamemap].checkpoints if #checkpoints == 0 then return end i = misc_index(i, checkpoints) return checkpoints[i] end local function map_laplength() local mapinfo = maps[gamemap] local length = mapinfo.laplength if length == nil then length = 0 local checkpoints = mapinfo.checkpoints for i = 1, #checkpoints do local c1 = map_checkpoint(i) local c2 = map_checkpoint(i+1) length = length + misc_checkpointdist(c1, c2)*16 -- Position of a checkpoint inside lap c1.lapposition = length end mapinfo.laplength = length+misc_checkpointdist( map_checkpoint(0), map_checkpoint(1) )*16 end return length end local function map_checkpointpos(i) local c = map_checkpoint(i) -- If not calculated yet, trigger it if c.lapposition == nil then map_laplength() end return c.lapposition end local function ticcmd_create() return { drift = false, brake = false, -- for brakedrift item = false, forward = 0, turning = 0, throwdir = 0, } end local function ticcmd_autoaccel(cmd) if not options.autoaccel.value then return false end if leveltime < starttime then return false end if not cmd.last_aa and btn(4) then return false end cmd.last_aa = true return true end local function ticcmd_p1(cmd) -- AAAAAAAAAAAAAAA local aa = ticcmd_autoaccel(cmd) local forward = aa and 1 or 0 local turning = 0 local throwdir = 0 cmd.brake = false if btn(4) then forward = forward + (aa and -1 or 1) end if btn(6) then forward = forward - ((aa and not btn(4)) and 2 or 1) cmd.brake = true end if btn(0) then throwdir = throwdir + 1 end if btn(1) then throwdir = throwdir - 1 end if btn(2) then turning = turning - 1 end if btn(3) then turning = turning + 1 end cmd.forward = forward cmd.throwdir = throwdir cmd.turning = turning cmd.drift = btn(5) cmd.item = btn(7) end local objectinfo = {} local OBJ_DEFAULT = { w = 1, h = 1, zh = 8, friction = 0.9, gravity = 4/60, kartitem = false, } local function obj_newtype(name, def) objectinfo[name] = setmetatable(def, { __index = OBJ_DEFAULT }) end local objects = {} local function obj_clear() objects = {} end local function obj_create(x, y, name) local info = assert(objectinfo[name]) local o = { name = name, spawntic = leveltime, x = x, y = y, z = 0, -- For jumps zh = info.zh, -- Vertical height, for interactions ground = 0, w = info.w, h = info.h, hitbox = info.hitbox, scale = info.scale or 1, drawshadow = info.drawshadow, interact = info.interact, -- Can interact with other objects interactive = info.interactive, -- Can be interacted with momx = 0, momy = 0, momz = 0, gravity = info.gravity, angle = 0, angle_offset = 0, -- for temp effects friction = info.friction, sprite = info.sprite, think = info.think, remove = info.remove, kartitem = info.kartitem, onground = true, nodraw = false, -- for flashing or stuff like that valid = true, } if info.init then info.init(o) end table.insert(objects, o) return o end local function obj_exists(o) return o and o.valid end local function obj_remove(o) if not obj_exists(o) then return end o.valid = false -- Prevents any sort of recursion if o.remove then o:remove() end -- Clear some potentially recursive references o.owner = nil o.target = nil o.hnext = nil o.hprev = nil end local function obj_center(o) return o.x + o.w*4, o.y + o.h*4 end local function obj_speed(o) return misc_hypot(o.momx, o.momy) end local function obj_createfrom(orig, name) local o = obj_create(orig.x + orig.w*4, orig.y + orig.h*4, name) o.x = o.x - o.w*4 o.y = o.y - o.h*4 o.z = orig.z o.target = orig o.scale = orig.scale return o end local function obj_hitbox(o, noscale) local hb = { x = o.x + (o.hitbox and o.hitbox.x or 0), y = o.y + (o.hitbox and o.hitbox.y or 0), w = o.hitbox and o.hitbox.w or o.w*8, h = o.hitbox and o.hitbox.h or o.h*8, } -- Smol optimization if not noscale then local neww = hb.w*o.scale local newh = hb.h*o.scale local dw = neww - hb.w local dh = newh - hb.h hb.x = hb.x - dw/2 hb.y = hb.y - dh/2 hb.w = neww hb.h = newh end return hb end local function obj_touching(o1, o2) -- Check height if (o1.z + o1.zh < o2.z) or (o2.z + o2.zh < o1.z) then return false end local h1 = obj_hitbox(o1) local h2 = obj_hitbox(o2) return misc_rectcollide(h1, h2) end local function obj_checkmap(o, pred, resp, full, scale) local startx, starty, endx, endy local hb = obj_hitbox(o, not scale) startx = (hb.x)/16 starty = (hb.y)/16 endx = startx + hb.w/16 endy = starty + hb.h/16 local collide = true local x = startx while x < endx do local y = starty while y < endy do local mx, my = math.floor(x)%240, math.floor(y)%136 if pred(mx, my) then if resp then resp(o, mx, my) end if not full then return true end else collide = false end y = y + math.min(1, o.hitbox.h/16)-0.001 end x = x + math.min(1, o.hitbox.w/16)-0.001 end return collide end local function obj_checkcollision(o, flag, resp, full) return obj_checkmap(o, function(mx, my) return fget(mget(mx, my), flag) end, resp, full, flag ~= 0) end local function obj_update() for _, o in ipairs(objects) do if not o.valid then goto skipobj end if o.think then o:think() end local ground = 0 local justbumpedwall = false local function makeresp(by_x) return function(o, mx, my) -- We're too high if o.z > 4 then ground = 8 return end justbumpedwall = true if o.momx < 0 then if by_x then o.x = (mx+1)*16 - o.hitbox.x end elseif o.momx > 0 then if by_x then o.x = mx*16 - o.hitbox.w - 1 end end if o.momy < 0 then if not by_x then o.y = (my+1)*16 - o.hitbox.y end elseif o.momy > 0 then if not by_x then o.y = my*16 - o.hitbox.h - 1 end end end end o.x = o.x + o.momx while o.x < 0 do o.x = o.x + 240*16 end while o.x > 240*16 do o.x = o.x - 240*16 end if o.hitbox then obj_checkcollision(o, 0, makeresp(true)) end o.y = o.y + o.momy while o.y < 0 do o.y = o.y + 136*16 end while o.y > 136*16 do o.y = o.y - 136*16 end if o.hitbox then obj_checkcollision(o, 0, makeresp(false)) end -- Didn't figure out bumpcode yet :^( if justbumpedwall then o.momx = o.momx*0.95 o.momy = o.momy*0.95 end o.justbumpedwall = justbumpedwall -- Gravity o.momz = o.momz - o.gravity o.z = o.z + o.momz if o.z <= ground then o.z = ground o.momz = math.max(o.momz, 0) end o.ground = ground o.onground = o.z <= ground ::skipobj:: end for _, obj in ipairs(objects) do if obj.valid and obj.interact then for _, obj2 in ipairs(objects) do if obj2.interactive and obj ~= obj2 and obj2.valid and obj_touching(obj, obj2) then obj:interact(obj2) end end end end for i = #objects, 1, -1 do if not objects[i].valid then table.remove(objects, i) end end end -- Returns how much object is scaled for its z position local function obj_jumpscale(o) return misc_rescalecl(o.z, 0, 16, 1, 2) end local function obj_draw() local c = camera or { x = 0, y = 0 } for _, o in ipairs(objects) do local sprite = o.skin and o.skin.sprite or o.sprite if not o.nodraw and sprite ~= nil then local s = render_newsprite() local jumpf = obj_jumpscale(o) s.id = sprite s.cmap = o.colormap -- Take looping maps into account s.x = misc_perioddiff(c.x, o.x, 0, 240*16) s.y = misc_perioddiff(c.y, o.y, 0, 136*16) s.z = o.z s.a = o.angle+o.angle_offset s.w = o.w s.h = o.h s.s = o.scale*jumpf s.draw = render_drawrotated render_clipsprite() -- Draw a shadow if o.drawshadow and o.z > 0.01 then local shadow = render_newsprite() shadow.id = sprite shadow.cmap = render_fullcolormap(0) shadow.x = s.x shadow.y = s.y + o.z/2 shadow.z = 0 shadow.a = s.a shadow.w = s.w shadow.h = s.h shadow.s = s.s shadow.draw = s.draw render_clipsprite() end end end render_drawsprites() end function obj_radmove(o, angle, move, origin) origin = origin or o local cx, cy = obj_center(origin) o.x = cx + move*math.cos(angle) - o.w*4 o.y = cy + move*math.sin(angle) - o.h*4 end function obj_thrust(o, angle, speed) o.momx = o.momx + speed*math.cos(angle) o.momy = o.momy + speed*math.sin(angle) end function obj_instathrust(o, angle, speed) o.momx = 0 o.momy = 0 obj_thrust(o, angle, speed) end function obj_speedmul(o, mul) o.momx = o.momx*mul o.momy = o.momy*mul end local threat_closest = { o = nil, x = 0, y = 0, distx = 0, disty = 0 } local threat_maxdist = 256 local function threat_add(o) if not options.threat.value then return end if camera == nil then return end local cx, cy = obj_center(o) local dirx, diry = misc_pointtodir2(p.x, p.y, cx, cy) local movedirx, movediry = misc_dir(p.momx, p.momy) -- Ignore threats that are behind us if movedirx ~= nil and (dirx*movedirx + diry*movediry) < 0.25 then return end local x = cx - camera.x local y = cy - camera.y -- Already on screen if x >= 0 and x <= WIDTH and y >= 0 and y <= WIDTH then return end if x < -threat_maxdist or x-WIDTH > threat_maxdist or y < -threat_maxdist or y-HEIGHT > threat_maxdist then return end local distx, disty = x, y if distx > WIDTH then distx = distx - WIDTH end if disty > HEIGHT then disty = disty - HEIGHT end if threat_closest.o == nil or (math.abs(distx) < math.abs(threat_closest.distx) and math.abs(disty) < math.abs(threat_closest.disty)) then threat_closest.o = o threat_closest.x = x threat_closest.y = y threat_closest.distx = distx threat_closest.disty = disty end end local function threat_clear() threat_closest = { o = nil, x = 0, y = 0, distx = 0, disty = 0 } end local function threat_draw() if threat_closest.o == nil then return end local x = threat_closest.x local y = threat_closest.y if x < 0 then x = 0 end if x > WIDTH then x = WIDTH-16 end if y < 0 then y = 0 end if y > HEIGHT then y = HEIGHT-16 end if leveltime % 30 < 15 then render_setcolormap(render_fullcolormap(1)) end spr(448, x, y, 0, 2) render_setcolormap() end -- Shortcut local function skin_new(name, realname, sprite, icon, prefcolor, spd, wgt) local skin = { name = name, realname = realname, prefcolor = prefcolor, sprite = sprite, icon = icon, kartspeed = spd, kartweight = wgt, } table.insert(skins, skin) end local function skin_find(name) if type(name) == "number" then return skins[name] end for _, skin in ipairs(skins) do if skin.name == name then return skin end end end skin_new("sonic", "Sonic", 256, 303, SKINCOLOR_BLUE, 8, 2) skin_new("tails", "Tails", 258, 319, SKINCOLOR_ORANGE, 2, 2) skin_new("knuckles", "Knuckles", 260, 335, SKINCOLOR_RED, 5, 5) skin_new("eggman", "Dr. Eggman", 262, 351, SKINCOLOR_RED, 2, 8) skin_new("metalsonic", "Metal Sonic", 264, 367, SKINCOLOR_BLUE, 8, 8) local function kart_checkitemchain(kart) local itemcount = 0 local item = kart.hnext while obj_exists(item) do itemcount = itemcount + 1 item = item.hnext end kart.itemamount = itemcount end local function kart_repairitemchain(removed, keepamount) local next = removed.hnext local prev = removed.hprev if next then next.hprev = prev end if prev then prev.hnext = next end if removed.owner and not keepamount then kart_checkitemchain(removed.owner) end end local function kart_itemchainpop(kart) local o = kart.hnext if not o then return end kart_repairitemchain(o) o.hnext = nil o.hprev = nil return o end local function kart_itemchainput(prev, o) prev.hnext = o o.hprev = prev end local function kart_dragitem(o) local dist = misc_objdist(o, o.target) if dist > o.dragdist then local dirx, diry = misc_objdir(o, o.target) o.x = o.x + dirx*(dist-o.dragdist) o.y = o.y + diry*(dist-o.dragdist) end o.z = (o.target.z+o.z)/2 o.momz = 0 end local function kart_dosneaker(kart) kart.sneakertimer = math.max(kart.sneakertimer, 60) end local function kart_hurtable(kart, ignoreflashing) return ( kart.invincibilitytimer <= 0 and kart.growtimer <= 0 and (kart.flashing <= 0 or ignoreflashing) ) end local function kart_dropitemchain(kart, keepamount) local item = kart.hnext while obj_exists(item) do local next = item.hnext item.target = nil item.hnext = nil item.hprev = nil item = next end kart_repairitemchain(kart, keepamount) end local function kart_stripitems(kart) kart.itemtype = 0 kart.itemamount = 0 kart.rocketsneakertimer = 0 kart_dropitemchain(kart, true) end local function kart_canspin(kart) return kart_hurtable(kart) and kart.spinouttimer <= 0 end local function kart_dospin(kart, force) if not (force or kart_canspin(kart)) then return end kart.spinouttimer = 90 kart.drift = 0 kart.driftcharge = 0 end function kart_spawnexplosion(kart) sound_start(25, "F-1", 30, 12, kart) for i = 1, 10 do local o = obj_createfrom(kart, "explosion") o.x = o.x + math.random(-2, 2) o.y = o.y + math.random(-2, 2) end end local function kart_doexplode(kart, force) if not (force or kart_hurtable(kart, true)) then return end kart_dospin(kart, true) kart_stripitems(kart) kart.momx = 0 kart.momy = 0 kart.momz = 1.5 end local function kart_explosion(o, radius) local x, y = obj_center(o) kart_spawnexplosion(o) for _, o in ipairs(objects) do if not (o.valid and o.interactive) then goto skipobj end local cx, cy = obj_center(o) if misc_pointtodist2(cx, cy, x, y) < radius then if o.kartitem then obj_remove(o) elseif o.name == "kart" then kart_doexplode(o) end end ::skipobj:: end end -- Shortcut local function kart_itembutton(kart) if kart.spinouttimer > 0 then return false end return not kart.itemheld and kart.cmd.item end KITEM_NONE = 0 KITEM_SAD = -1 local kartitems = { [KITEM_SAD] = { icon = 479, think = function(kart) if kart_itembutton(kart) then kart.itemamount = kart.itemamount - 1 end end, } } function kart_newitem(name, icon, think, draw) table.insert(kartitems, { icon = icon, draw = draw, think = think, }) _G["KITEM_"..name:upper()] = #kartitems end function kart_drawitemicon(x, y, item, cmap) if cmap then render_setcolormap(cmap) end spr(item.icon, x, y, 0, 2) render_setcolormap() end function kart_drawitembar(x, y, fill) local size = 16 rect(x, y+17, size, 3, 0) rect(x-1, y+18, size+2, 1, 0) size = math.ceil(fill*size) rect(x, y+17, size, 3, 14) rect(x-1, y+18, size+1, 1, 13) end kart_newitem("sneaker", 480, function(kart) if not kart_itembutton(kart) then return end if not kart.onground then return end kart_dosneaker(kart) kart.itemamount = kart.itemamount - 1 end) RSTIMER = 24*60 kart_newitem("rocketsneaker", 481, function(kart) -- Always check timer if not kart_itembutton(kart) then goto checktimer end if not kart.onground then goto checktimer end if not obj_exists(kart.hnext) then local prev = kart for side = -1, 1, 2 do local sneaker = obj_createfrom(kart, "rocketsneaker") sneaker.side = side kart_itemchainput(prev, sneaker) prev = sneaker end kart.rocketsneakertimer = RSTIMER else kart_dosneaker(kart) kart.rocketsneakertimer = kart.rocketsneakertimer - 2*60 end ::checktimer:: -- :oh: we don't need to if not obj_exists(kart.hnext) then return end if kart.rocketsneakertimer <= 0 then kart.itemamount = kart.itemamount - 1 kart_dropitemchain(kart) end end, function(self, x, y, amount, cmap) kart_drawitemicon(x, y, self, cmap) -- FIXME - prob shouldn't use globals to pass player... if obj_exists(p.hnext) then kart_drawitembar(x, y, p.rocketsneakertimer/RSTIMER) end end) kart_newitem("invincibility", 482, function(kart) if not kart_itembutton(kart) then return end kart.invincibilitytimer = math.max(kart.invincibilitytimer, 10*60) kart.itemamount = kart.itemamount - 1 end, function(self, x, y, amount, cmap) kart_drawitemicon(x, y, self, cmap or render_rainbowmap()) end) function kart_throwobj(kart, obj) obj_radmove(obj, kart.angle, 12, kart) obj_instathrust(obj, kart.angle, math.max(7, 5+obj_speed(kart))) obj.z = kart.z+4 obj.momz = 0.8 end kart_newitem("banana", 483, function(kart) if not kart_itembutton(kart) then return end if not obj_exists(kart.hnext) then local prev = kart for i = 1, kart.itemamount do local banana = obj_createfrom(kart, "banana") banana.target = prev banana.owner = kart if prev == kart then banana.dragdist = 16 end kart_itemchainput(prev, banana) prev = banana end else local banana = kart.hnext while obj_exists(banana.hnext) do banana = banana.hnext end kart_repairitemchain(banana) banana.target = nil if kart.cmd.throwdir == 1 then kart_throwobj(kart, banana) end end end) kart_newitem("eggman", 484, function(kart) if not kart_itembutton(kart) then return end if not obj_exists(kart.hnext) then local item = obj_createfrom(kart, "randomitem") item.eggman = true item.target = kart item.owner = kart kart_itemchainput(kart, item) else local item = kart.hnext kart_repairitemchain(item, true) item.target = nil kart.itemamount = kart.itemamount - 1 if kart.cmd.throwdir == 1 then kart_throwobj(kart, item) item.gravity = OBJ_DEFAULT.gravity end end end) kart_newitem("orbinaut", 485, function(kart) if not kart_itembutton(kart) then return end if not obj_exists(kart.hnext) then local prev = kart for i = 1, kart.itemamount do local orbi = obj_createfrom(kart, "orbinaut") orbi.target = kart orbi.owner = kart orbi.angle = 2*math.pi/kart.itemamount*i kart_itemchainput(prev, orbi) prev = orbi end else local orbi = kart_itemchainpop(kart) local angle = kart.angle local speed = math.max(4, 2 + obj_speed(kart)) if kart.cmd.throwdir == -1 then angle = misc_angadd(angle, math.pi) speed = speed*0.5 end obj_radmove(orbi, angle, 24, kart) obj_instathrust(orbi, angle, speed) orbi.target = nil end end) kart_newitem("mine", 487, function(kart) if not kart_itembutton(kart) then return end if not obj_exists(kart.hnext) then local item = obj_createfrom(kart, "mine") item.target = kart item.owner = kart kart_itemchainput(kart, item) else local item = kart.hnext kart_repairitemchain(item, true) item.target = nil kart.itemamount = kart.itemamount - 1 end end) local GROWCANCEL = 30 kart_newitem("grow", 490, function(kart) if not kart_itembutton(kart) then return end kart.growtimer = math.max(kart.growtimer, 12*60) kart.itemamount = kart.itemamount - 1 end, function(self, x, y, amount, cmap) if p.growtimer <= 0 or (leveltime % 4 < 2) then if not cmap and leveltime % 20 < 5 then cmap = render_fullcolormap(10) end render_setcolormap(cmap) spr(self.icon, x, y, 0, 2) render_setcolormap() end if p.growcancel > 0 then kart_drawitembar(x, y, p.growcancel/GROWCANCEL) end end) local function kart_setskin(kart, skin) local skin = assert(skin_find(skin)) kart.skin = skin kart.skincolor = skin.prefcolor kart.kartspeed = skin.kartspeed kart.kartweight = skin.kartweight end local function kart_create(x, y, skin) local o = obj_create(x, y, "kart") kart_setskin(o, skin) -- Always exists, appears when you have drift/sneaker boost obj_createfrom(o, "boostflame") return o end local function kart_getsparkrate(kart) local BESTRATE=1 local WORSTRATE=0.7 return misc_rescale(kart.kartweight + (10 - kart.kartspeed)*2, 3, 27, WORSTRATE, BESTRATE) end local function kart_getturning(kart) local BASETURN = math.pi/80 local BESTBONUS = BASETURN/4 local WORSTBONUS = -BESTBONUS local turning = BASETURN if kart.cmd.drift then turning = turning + misc_rescale(kart.kartweight, 1, 9, BESTBONUS, WORSTBONUS) end -- Sorry, otherwise bots suck at sunny islands too much, -- figuring out waypoints is hard ;-; if kart.ai_rubberband > 0 then turning = turning*misc_rescalecl(kart.ai_rubberband, 0, 100, 1, 8) end return turning end local function kart_getaccel(kart) local BESTACCEL = 5/60 local WORSTACCEL = 5*BESTACCEL/9 local accel = misc_rescale(kart.kartspeed, 1, 9, BESTACCEL, WORSTACCEL) return accel*(1 + kart.accelboost) end local function kart_getspeed(kart, doboost) local BESTSPEED = MAXSPEED local WORSTSPEED = BESTSPEED*0.85 local speed =misc_rescale(kart.kartspeed, 1, 9, WORSTSPEED, BESTSPEED) if doboost then speed = speed*(kart.boostpower + kart.speedboost) end return speed end local function kart_getdriftspeed(kart) local speed = kart_getspeed(kart, false)/3 -- So heavy weights don't have easier time recovering speed = speed - misc_rescale(kart.kartweight, 1, 9, 0, speed/6) return speed end -- Called for player and some items function kart_commonspecials(o) if not o.onground then return end -- Spring panel if obj_checkcollision(o, 3) then o.momz = 1.5 end -- Redirect speed into x or y if obj_checkcollision(o, 4) then o.momx = o.momx + misc_sign(o.momx)*math.abs(o.momy) o.momy = 0 o.angle = o.momx > 0 and 0 or math.pi elseif obj_checkcollision(o, 5) then o.momy = o.momy + misc_sign(o.momy)*math.abs(o.momx) o.momx = 0 o.angle = o.momy > 0 and math.pi/2 or 3*math.pi/2 end if o.name ~= "kart" and obj_checkcollision(o, 7, nil, true) then obj_remove(o) end end local function kart_checkspecials(kart) if not kart.onground then return end -- Offroad if obj_checkcollision(kart, 1) then kart.offroad = kart.offroad + 1 else kart.offroad = 0 end -- Sneaker panel if obj_checkcollision(kart, 2) then kart_dosneaker(kart) end kart_commonspecials(kart) -- Deathpit if obj_checkcollision(kart, 7, nil, true) then kart.respawn.active = true end end local function kart_doboost(boost, spd, acc, pwr) boost.spd = math.max(spd, boost.spd) boost.acc = math.max(acc, boost.acc) boost.pwr = boost.pwr*(pwr or 1) end local function kart_boost(kart) local boost = { spd = 0, acc = 0, pwr = 1 } if kart.spinouttimer > 0 then kart.speedboost = 0 kart.accelboost = 0 kart.boostpower = 1 return end if kart.offroad > 0 and not ( kart.sneakertimer > 0 or kart.invincibilitytimer > 0 ) then kart_doboost(boost, 0, 0, misc_rescalecl(kart.offroad, 0, 30, 1, 0.5)) end if kart.driftboost > 0 then kart_doboost(boost, 0.15, 2) end if kart.startboost > 0 then kart_doboost(boost, 0.15, 2) end if kart.sneakertimer > 0 then kart_doboost(boost, 0.25, 3) end if kart.invincibilitytimer > 0 then kart_doboost(boost, 0.3, 1) end if kart.growtimer > 0 then kart_doboost(boost, 0.2, 0, 1.5) end if kart.bot then local rb = kart.ai_rubberband -- Small "pity" compensation if maps[gamemap].nobotdrift then rb = rb + 5 end if options.difficulty.value == 1 then rb = math.min(rb, 40) - 10 elseif options.difficulty.value == 3 then rb = rb + 20 end -- Make rubberbanding weaker on final lap if rb > 0 then local laplength = map_laplength() local finishdist = laplength*maps[gamemap].numlaps-kart.lapposition rb = rb * misc_rescalecl(finishdist, 0, laplength, 0.25, 1) end local spd = misc_rescale(rb, 0, 100, 0, 0.4) local acc = misc_rescale(rb, 0, 100, 0, 4) -- Allow negative rubberbanding so player can catch up local pwr = misc_rescalecl(rb, -100, 0, 0.5, 1) kart_doboost(boost, spd, acc, pwr) end -- Value smoothing (yep exactly same as in kart :p) if kart.speedboost < boost.spd then kart.speedboost = boost.spd else kart.speedboost = kart.speedboost - (kart.speedboost - boost.spd)/30 end kart.accelboost = boost.acc kart.boostpower = boost.pwr end local function kart_control(kart) if kart.spinouttimer <= 0 then local turning = kart.cmd.turning if kart.drift ~= 0 and kart.onground then turning = kart.drift end kart.angle = misc_angadd(kart.angle, turning*kart_getturning(kart)) end if not kart.onground then return end local accel = kart_getaccel(kart) local forward = 0 if kart.spinouttimer <= 0 then forward = kart.cmd.forward end if kart.sneakertimer > 0 then forward = 1 end obj_thrust(kart, kart.angle, forward*accel) local speed = obj_speed(kart) local maxspeed = kart_getspeed(kart, true) if speed > maxspeed then kart.momx = kart.momx/speed*maxspeed kart.momy = kart.momy/speed*maxspeed end end local function kart_drive(kart) local speed = obj_speed(kart) if speed < 0.001 then kart.momx = 0 kart.momy = 0 return end if not kart.onground then return end local sin, cos = math.sin(kart.angle), math.cos(kart.angle) local dirx, diry = misc_dir(kart.momx, kart.momy) local dot = cos*dirx + sin*diry local takespeed = speed/10 local readdspeed = takespeed*math.max(0.5, dot*dot)*kart.friction if dot < 0 then sin = -sin cos = -cos end kart.momx = kart.momx - takespeed*dirx + cos*readdspeed kart.momy = kart.momy - takespeed*diry + sin*readdspeed end function kart_driftrelease(kart, boost) if boost then local amt = 0 if kart.driftcharge >= 400 then amt = 4*60 elseif kart.driftcharge >= 200 then amt = 2*60 elseif kart.driftcharge >= 100 then amt = 60 end kart.driftboost = math.max(kart.driftboost, amt) end kart.angle = misc_angsub(kart.angle, kart.drift*kart_getturning(kart)) kart.driftcharge = 0 kart.drift = 0 end -- Outward, neutral and inward drift local drift_mult = { 0.25, 0.5, 1 } local function kart_drift(kart) if not kart.onground then return end local speed = obj_speed(kart) if speed < kart_getdriftspeed(kart) then kart_driftrelease(kart, false) return end if kart.drift == 0 then -- Nothing to do here if not (kart.cmd.drift and kart.cmd.turning ~= 0) then return end kart.drift = kart.cmd.turning kart_drift(kart) -- set kart.drift to proper value kart.angle = misc_angsub(kart.angle, misc_sign(kart.drift)*math.pi/12) else -- Release drift if not kart.cmd.drift then kart_driftrelease(kart, true) return end local dir = 2 -- Holding opposite direction of drift if kart.drift*kart.cmd.turning < 0 then dir = 1 elseif kart.cmd.turning ~= 0 then dir = 3 end kart.drift = drift_mult[dir]*misc_sign(kart.drift) if kart.cmd.brake then kart.drift = kart.drift*1.25 end if kart.justbumpedwall then kart.driftcharge = math.min(kart.driftcharge, 100) end if kart.offroad == 0 then kart.driftcharge = kart.driftcharge + dir*kart_getsparkrate(kart) end end end local function kart_timers(kart) misc_dotimer(kart, "driftboost") misc_dotimer(kart, "startboost") misc_dotimer(kart, "sneakertimer") misc_dotimer(kart, "rocketsneakertimer") misc_dotimer(kart, "invincibilitytimer") misc_dotimer(kart, "growtimer") misc_dotimer(kart, "bumptimer") if misc_dotimer(kart, "eggmanexplode") then kart_explosion(kart, 48) end if not kart.touchingplayer then misc_dotimer(kart, "flashing") end kart.touchingplayer = false -- Allow the funny speen if we have sneaker timer if kart.sneakertimer <= 0 and misc_dotimer(kart, "spinouttimer") then kart.flashing = 3*60 end end local function kart_visuals(kart) if kart.spinouttimer > 0 then local speed = math.max(1, math.min(math.floor(kart.spinouttimer/18), 8)) if speed == 1 and math.abs(misc_angdiff(kart.angle_offset, 0)) < math.pi/32 then kart.angle_offset = 0 else kart.angle_offset = misc_angadd(kart.angle_offset, speed*math.pi/32) end elseif kart.drift < 0 then kart.angle_offset = -math.pi/4 elseif kart.drift > 0 then kart.angle_offset = math.pi/4 else kart.angle_offset = 0 end if kart.drift ~= 0 and ((kart.driftcharge >= 100 and leveltime % 2 == 0) or (leveltime % 6 == 0)) then local side = misc_sign(kart.drift) for i = 1, 3, 2 do local angle = misc_angadd(kart.angle, kart.angle_offset + side*i*math.pi/4) local o = obj_createfrom(kart, "driftspark") obj_radmove(o, angle, 8*kart.scale) if kart.driftcharge >= 400 then o.colormap = render_rainbowmap() elseif kart.driftcharge >= 200 then o.colormap = render_skincolormap(SKINCOLOR_ORANGE) elseif kart.driftcharge >= 100 then o.colormap = render_skincolormap(SKINCOLOR_BLUE) else o.colormap = render_skincolormap(SKINCOLOR_GREY) end end end local cmap = skincolors[kart.skincolor].colormap if kart.invincibilitytimer > 0 then if leveltime % 3 == 0 then obj_createfrom(kart, "invstar") end if leveltime % 2 == 0 then cmap = render_fcrainbowmap() end if kart.invincibilitytimer == 1 then sound_stop(19, kart) elseif not sound_playing(19, kart) then sound_startnm(19, "G-5", 2.65*60, 3, kart) end elseif kart.eggmanexplode > 0 then local flashtime = 4< 0 then -- More like blinkslow :p local blinkspeed = 1 if kart.growtimer < 60 then blinkspeed = 4 elseif kart.growtimer < 3*60 then blinkspeed = 2 end if leveltime % (8*blinkspeed) < (4*blinkspeed) then cmap = render_fullcolormap(10) end if kart.growtimer == 1 then sound_stop(24, kart) elseif not sound_playing(24, kart) then sound_startnm(24, "C-3", 1.4*60, 6, kart) end end kart.colormap = cmap if kart.startboostcharge > 0 then kart.scale = misc_rescalecl(kart.startboostcharge, 0, 2*60, 1, 1.5) elseif kart.growtimer > 0 then kart.scale = 1.5 else kart.scale = 1 end if kart.flashing > 0 then kart.nodraw = leveltime % 4 < 2 else kart.nodraw = false end end local function kart_engineclass(kart) -- TODO return 1 end local function kart_engine(kart) local sound = enginesfx[kart_engineclass(kart)] local duration = 7 if leveltime % duration ~= 0 then return end local speed = misc_hypot(kart.momx, kart.momy) local maxspeed = kart_getspeed(kart, false)*1.2 local targetengine = math.floor(misc_rescalecl(speed, 0, maxspeed, 1, 10.9)) if kart.engine < targetengine then kart.engine = kart.engine + 1 elseif kart.engine > targetengine then kart.engine = kart.engine - 1 end sound_start(sound, kart.engine, duration, (kart == p) and 3 or 2, kart) end local function kart_lapposition(kart) local checkpos = map_checkpointpos(kart.checkpoint+1) local check = map_checkpoint(kart.checkpoint+1) local checkdist = misc_checkpointdist({ x = kart.x/16, y = kart.y/16 }, check) local lapadd = 0 if kart.checkpoint+1 > #maps[gamemap].checkpoints then lapadd = 1 end kart.lapposition = map_laplength()*(kart.laps+lapadd) + checkpos - checkdist end local function kart_touching_checkpoint(kart, check) local dist = misc_pointtodist2(kart.x, kart.y, check.x*16, check.y*16) return dist < check.r*16 end local function kart_touching_finish(kart) local f = maps[gamemap].finish return misc_rectcollide( obj_hitbox(kart), { x = f.x*16, y = f.y*16, w = f.w*16, h = f.h*16, } ) end function kart_eliminatelast() if #players < 2 then return end local last for _, player in ipairs(players) do if not player.finishtime then if last == nil then last = player else -- At least 2 players are still racing do nothing return end end end -- Who's bro eliminating if not last then return end last.nocontest = leveltime last.finishtime = leveltime-starttime if last == p then sound_stopmusic() end end function kart_finish(kart) -- Don't finish twice lmao if kart.finishtime ~= nil then return end kart.bot = true kart.finishtime = leveltime-starttime -- Check if we should "no contest" someone kart_eliminatelast() end -- Used for bots to give them fake times after player finishes race function kart_faketime(kart) if kart.finishtime then return end if kart.nocontest then return end -- * insert f-zero results music * local dist = map_laplength()*maps[gamemap].numlaps - kart.lapposition kart.finishtime = leveltime-starttime + dist/(kart_getspeed(kart, false)*1.5) -- HURRY UP GODDAMN end local function kart_checkpoints(kart) local checkpoints = maps[gamemap].checkpoints local allowskip = 3 for i = 1, allowskip+1 do local check = checkpoints[kart.checkpoint+i] if check and kart_touching_checkpoint(kart, check) then kart.checkpoint = kart.checkpoint + i kart.respawn.x = kart.x kart.respawn.y = kart.y kart.respawn.angle = kart.angle break -- No more checkpoints this frame end end if kart_touching_finish(kart) then if #checkpoints-kart.checkpoint < 2 then kart.checkpoint = 0 kart.laps = kart.laps + 1 if kart.laps >= maps[gamemap].numlaps then kart_finish(kart) end end end -- Update lap position kart_lapposition(kart) end function kart_ai_itemdelay(kart) local item = kart.itemtype if kart.offroad > 0 and (item == KITEM_INVINCIBILITY or item == KITEM_SNEAKER) then -- Use ASAP return 1 end -- Something in chain, drop that quicker if obj_exists(kart.hnext) then return math.random(10, 20) end return math.random(30, 180) end function kart_ai_throwdir(kart) kart.cmd.throwdir = 0 -- Always throw stuff back when in first if kart.position == 1 then kart.cmd.throwdir = -1 end -- Only do smart stuff when on hard difficulty if options.difficulty.value < 3 then return end local dist = misc_objdist(kart, p) local maxdist = 16*4 if kart.itemtype == KITEM_ORBINAUT then maxfrontdist = 16*240 -- "Infinite" distance end local dirx, diry = misc_objdir(kart, p) local mydirx, mydiry = math.cos(kart.angle), math.sin(kart.angle) local dot = dirx*mydirx + diry*mydiry if dot > 0.75 and dist < maxdist then -- Player is in front of us kart.cmd.throwdir = 1 elseif dot < -0.75 then -- Player is behind (no dist check is intended) kart.cmd.throwdir = -1 end end local function kart_ai_items(kart) kart.cmd.item = false if kart.itemamount <= 0 then kart.ai_itemdelay = 0 end if kart.ai_itemdelay <= 0 then kart.ai_itemdelay = kart_ai_itemdelay(kart) elseif misc_dotimer(kart, "ai_itemdelay") then kart.cmd.item = true kart_ai_throwdir(kart) end end -- Anglediff is difference between kart angle and next waypoint angle local function kart_ai_turn(kart, anglediff) misc_dotimer(kart, "ai_driftdelay") local w = kart.kartweight local drifting = kart.drift ~= 0 if kart.justbumpedwall then kart.ai_driftdelay = 2*60 end local sin = math.sin(anglediff) -- FIXME - dirty hack to fix bots "giving up" -- because sin starts to decrease after 90 degrees... if math.cos(anglediff) < 0 then sin = misc_sign(sin) end local allowdiff = 0.1 local turning = 1 -- Bots are not very good at managing red drifts... local allowdrift = kart.driftcharge < 100 and obj_speed(kart) > kart_getdriftspeed(kart)*2 -- Trying to drift is viable and not maps[gamemap].nobotdrift -- :( and kart.startboost <= 0 -- Don't drift after respawn and kart.invincibilitytimer <= 0 -- Or with invincibility and kart.offroad <= 0 -- or in offroad and kart.ai_rubberband < 60 -- No need to drift when we have strong rubberband boost and kart.ai_driftdelay <= 0 if allowdrift and drifting then if kart.driftcharge < 100 then allowdiff = misc_rescale(w, 1, 9, 0, -0.05) end local dir = misc_sign(kart.drift) if sin*dir < -misc_rescale(w, 1, 9, 0.3, 0.8) then turning = -1 elseif sin*dir > misc_rescale(w, 1, 9, 0.6, 0.2) then turning = 1 else turning = 0 end end if sin > allowdiff then kart.cmd.drift = allowdrift kart.cmd.turning = drifting and turning or 1 elseif sin < -allowdiff then kart.cmd.drift = allowdrift kart.cmd.turning = drifting and turning or -1 else kart.ai_driftdelay = 10 kart.cmd.drift = false kart.cmd.turning = 0 end -- Add delay between each attempt to drift if drifting and not kart.cmd.drift then kart.ai_driftdelay = 40 end end local function kart_ai(kart) if leveltime <= starttime then if leveltime > starttime-80 then kart.cmd.forward = 1 end return end -- Handle item usage if kart.spinouttimer <= 0 then kart_ai_items(kart) end local w = map_checkpoint(kart.checkpoint+1) local cx, cy = obj_center(kart) -- Add a bit of randomness into bot movement if kart.ai_last_checkpoint ~= kart.checkpoint then kart.ai_last_checkpoint = kart.checkpoint kart.ai_checkpoint_rndx = math.random(-8, 8) kart.ai_checkpoint_rndy = math.random(-8, 8) end local wx = w.x*16 local wy = w.y*16 -- ...only if we're not in offroad tho if kart.offroad <= 0 then wx = wx + kart.ai_checkpoint_rndx wy = wy + kart.ai_checkpoint_rndy end local dirx, diry = misc_pointtodir2(cx, cy, wx, wy) local angle = math.atan(diry, dirx) local anglediff = angle - kart.angle kart.cmd.brake = false if math.cos(anglediff) > 0.5 then kart.cmd.forward = 1 else kart.cmd.forward = obj_speed(kart) > kart_getspeed(kart, false)/4 and -1 or 0 kart.cmd.brake = true end kart_ai_turn(kart, anglediff) -- No need for rubberbanding if player has finished already if p.finishtime then return end local playerdist = (p.lapposition - kart.lapposition)/16 local rbadd = 0 local rbmin = 0 if playerdist > 16 then rbadd = misc_rescalecl(playerdist, 8, 64, 0.25, 0.5) else rbadd = misc_rescalecl(playerdist, 0, 16, -1.5, 0) end if playerdist < -16 then rbmin = -20 else rbmin = math.min(0, kart.ai_rubberband+1) end kart.ai_rubberband = math.max(rbmin, math.min(100, kart.ai_rubberband + rbadd)) end local function kart_respawn(kart) local RESPAWNSPEED = 240/60 kart.momx = 0 kart.momy = 0 kart.momz = 0 kart.z = 9 kart_driftrelease(kart, false) kart.drift = 0 if leveltime % 15 == 0 then local o = obj_createfrom(kart, "respawndonut") o.z = kart.z - 2 end local respawn = kart.respawn local dist = misc_objdist(kart, respawn) if dist > 8 then respawn.state = "move" elseif respawn.state ~= "wait" then respawn.state = "wait" respawn.timer = 60 kart.angle = respawn.angle end if respawn.state == "move" then local dirx, diry = misc_objdir(kart, respawn) kart.x = kart.x + dirx*RESPAWNSPEED kart.y = kart.y + diry*RESPAWNSPEED kart.angle = math.atan(diry, dirx) elseif respawn.state == "wait" then if misc_dotimer(respawn, "timer") then respawn.active = false if kart.cmd.forward then kart.startboost = 60 end end end end local function kart_cangetitem(kart) return not ( kart.itemroulette > 0 or kart.eggmanexplode > 0 or kart.itemtype ~= KITEM_NONE or kart.growtimer > 0 ) end local function kart_cangeteggman(kart) return kart.eggmanexplode <= 0 and not kart.eggmanroulette and kart_hurtable(kart) end local NUMROULETTEPOOLS = 4 local rouletteresults = {} local odds = {} for i = 1, NUMROULETTEPOOLS do odds[i] = {} end local function kart_rouletteresult(item, amount, ...) local itemodds = {...} assert(#itemodds == NUMROULETTEPOOLS) table.insert(rouletteresults, { item = item, amount = amount }) for i = 1, NUMROULETTEPOOLS do odds[i][#rouletteresults] = itemodds[i] end end kart_rouletteresult(KITEM_SNEAKER, 1, 0, 3, 0, 0) kart_rouletteresult(KITEM_SNEAKER, 3, 0, 1, 4, 3) kart_rouletteresult(KITEM_ROCKETSNEAKER, 1, 0, 0, 4, 3) kart_rouletteresult(KITEM_INVINCIBILITY, 1, 0, 0, 2, 5) kart_rouletteresult(KITEM_BANANA, 1, 1, 0, 0, 0) kart_rouletteresult(KITEM_BANANA, 3, 0, 1, 0, 0) kart_rouletteresult(KITEM_EGGMAN, 1, 1, 0, 0, 0) kart_rouletteresult(KITEM_ORBINAUT, 1, 1, 1, 0, 0) kart_rouletteresult(KITEM_ORBINAUT, 3, 0, 2, 1, 0) kart_rouletteresult(KITEM_MINE, 1, 0, 2, 1, 0) kart_rouletteresult(KITEM_GROW, 1, 0, 0, 1, 2) local function kart_getrouletteresult(kart, mashed) if kart.eggmanroulette then sound_start(22, 'E-6', 1.3*60, 7, nil, kart) kart.eggmanexplode = 60*4 kart.eggmanroulette = false return end sound_start(mashed and 23 or 21, 'E-6', mashed and 60 or 30, 7, nil, kart) -- Record Attack if #players == 1 then kart.itemtype = KITEM_SNEAKER kart.itemamount = 1 return end local first for _, p in ipairs(players) do if first == nil or first.lapposition < p.lapposition then first = p end end assert(first) local dist = (first.lapposition - kart.lapposition)/16 -- Mashing item roulette gives slightly worse items if mashed then dist = dist*0.7 end local useodds = math.floor(misc_rescalecl(dist, 0, 64, 1, NUMROULETTEPOOLS+0.5)) local oddsum = 0 for i = 1, #rouletteresults do oddsum = oddsum + odds[useodds][i] end local rnd = math.random(0, oddsum*100)/100 local item, amount = KITEM_SAD, 1 for i = 1, #rouletteresults do if rnd <= odds[useodds][i] then local r = rouletteresults[i] item = r.item amount = r.amount break end rnd = rnd - odds[useodds][i] end if item == KITEM_SAD then con_printf("useodds=%d rnd=%.2f gave sad item", useodds, rnd) end kart.itemtype = item kart.itemamount = amount end local roulettesounds = { 0, 2, 4, 6, 4, 6, 4, 2 } local function kart_item(kart) if kart.itemroulette > 0 then local i = math.floor(kart.itemroulette/6) % #roulettesounds + 1 if kart.itemroulette % 6 == 1 then sound_start(20, 54+roulettesounds[i], 6, 7, nil, kart) end kart.itemroulette = kart.itemroulette + 1 local mashed = false if #players > 1 and kart.itemroulette > 2*60 and kart.cmd.item then mashed = true kart.itemroulette = 3*60+20 kart.itemheld = true -- Prevent using item instantly end if kart.itemroulette >= 3*60+20 then kart_getrouletteresult(kart, mashed) kart.itemroulette = 0 end end if kart.eggmanexplode > 0 and kart.eggmanexplode < 3*60 and kart_itembutton(kart) then kart.eggmanexplode = 1 end if kart.itemamount == 0 then kart.itemtype = KITEM_NONE end if kart.growtimer > 0 and ( -- This check prevents canceling grow right after you used it (kart.growcancel == 0 and kart_itembutton(kart)) or (kart.growcancel > 0 and kart.cmd.item) ) then kart.growcancel = kart.growcancel + 1 if kart.growcancel >= GROWCANCEL then kart.growtimer = 1 end else kart.growcancel = 0 end if kart.spinouttimer <= 0 and kart.itemtype ~= KITEM_NONE then kartitems[kart.itemtype].think(kart) end kart.itemheld = kart.cmd.item end -- The funny explosion from ring racers... Kinda local function kart_nocontest(kart) kart.momx = 0 kart.momy = 0 kart.momz = 0 local tics = leveltime - kart.nocontest local t = tics/30 if tics == 1 then sound_start(16, "A-2", 10, 12, kart) end if t <= 1 then local cx, cy = obj_center(kart) cx = cx - camera.x cy = cy - camera.y local mult = math.sin(math.pi*t) render_posteffect(remapcirc, cx, cy, 10*16*mult, invmap) end -- Spawn explosion a bit earlier if tics == 25 then kart_spawnexplosion(kart) kart.colormap = blinkmaps.grey end end local function kart_think(kart) if kart.nocontest then kart_nocontest(kart) return end if kart.bot then kart_ai(kart) end kart_engine(kart) kart_visuals(kart) kart_item(kart) if leveltime < starttime then if kart.cmd.forward > 0 then kart.startboostcharge = kart.startboostcharge + 1 else kart.startboostcharge = 0 end return elseif leveltime == starttime then local PERFECT = 60 local PERFECTWINDOW = 2 local OVERCHARGE = 90 if kart.startboostcharge < PERFECT-PERFECTWINDOW then -- "Put that down..." elseif kart.startboostcharge <= PERFECT+PERFECTWINDOW then -- "...it's a prototype..." kart.sneakertimer = 60 elseif kart.startboostcharge < OVERCHARGE then -- "Don't let it overcharge!" kart.startboost = 60 else -- "What do you mean overch- *BOOM*" kart_doexplode(kart) kart_spawnexplosion(kart) obj_instathrust(kart, kart.angle, 1) kart.momz = 1 end kart.startboostcharge = 0 end if kart.respawn.active then kart_respawn(kart) if kart.respawn.state == "wait" then kart_control(kart) -- Allow turning when respawning end return end kart_checkspecials(kart) kart_drift(kart) kart_boost(kart) kart_control(kart) kart_drive(kart) kart_checkpoints(kart) kart_timers(kart) local wind = maps[gamemap].wind if wind and not kart.respawn.active then kart.momx = kart.momx + wind.x kart.momy = kart.momy + wind.y end end local function kart_bump(kart, other) if kart.bumptimer > 0 then return end local dirx, diry = misc_objdir(other, kart) local WEAKBUMP = 0.5 local STRONGBUMP = 2 local kartweight = other.kartweight or 5 local weightdiff if kart.spinouttimer > 0 or (other.name == "kart" and other.growtimer > 0) then weightdiff = -8 else weightdiff = kart.kartweight - kartweight end local strength = misc_rescale(weightdiff, -8, 8, STRONGBUMP, WEAKBUMP) kart.momx = kart.momx + dirx*strength kart.momy = kart.momy + diry*strength kart.bumptimer = 10 end obj_newtype("kart", { init = function(o) o.cmd = ticcmd_create() -- Drift o.drift = 0 o.driftcharge = 0 o.driftboost = 0 o.sneakertimer = 0 o.rocketsneakertimer = 0 o.startboostcharge = 0 o.startboost = 0 o.invincibilitytimer = 0 o.growtimer = 0 o.growcancel = 0 -- Boost values o.speedboost = 0 o.accelboost = 0 o.boostpower = 1 o.offroad = 0 -- Checkpoints o.checkpoint = 1 o.lapposition = 0 o.position = 0 -- Position in race (1st, 2nd, 3rd) o.laps = 0 -- ENgine sounds o.engine = 0 -- Other karts collision o.bumptimer = 0 -- Item stuff o.itemroulette = 0 o.eggmanroulette = false o.itemtype = KITEM_NONE o.itemamount = 0 o.itemheld = false -- Player pain o.spinouttimer = 0 o.flashing = 0 o.eggmanexplode = 0 o.nocontest = nil o.respawn = { active = false, timer = 0, state = "move", x = o.x, y = o.y, angle = o.angle, } -- Bot stuff o.ai_itemdelay = 0 o.ai_lastcheckpoint = 0 o.ai_checkpoint_rndy = 0 o.ai_checkpoint_rndx = 0 o.ai_driftdelay = 0 o.ai_rubberband = 0 end, think = kart_think, interactive = true, interact = function(self, other) if other.name ~= "kart" then return end if self.bumptimer > 0 then return end if self.flashing > 0 or other.flashing > 0 then self.touchingplayer = true -- makes flashtics not tic when we're "inside" other player return end if other.invincibilitytimer > 0 or other.growtimer > 0 then kart_dospin(self) end if other.spinouttimer > 0 then return end kart_bump(self, other) end, w = 2, h = 2, hitbox = { x = 1, y = 1, w = 14, h = 14 }, drawshadow = true, }) local function kart_itemdespawn(o) if misc_dotimer(o, "despawntimer") then obj_remove(o) return true elseif o.despawntimer < 60 then o.nodraw = o.despawntimer % 4 < 2 end return false end -- Prevents items in same chain from colliding with each other local function kart_chaincollide(o, o2) return (o.name == o2.name and (o.target or o.fuse > 0) and (o2.target or o2.fuse > 0)) end local function kart_itemcollide(o, o2) if o2.kartitem and not kart_chaincollide(o, o2) then obj_remove(o) obj_remove(o2) end end obj_newtype("boostflame", { gravity = 0, init = function(o) o.dist = 12 end, think = function(o) local kart = o.target if not obj_exists(kart) then obj_remove(o) return end local db = kart.driftboost or 0 local st = kart.sneakertimer or 0 if db <= 0 and st <= 0 then o.sprite = nil return end local do_rainbow = false if db > 2*60 then do_rainbow = true elseif db >= 1*60 then o.colormap = render_skincolormap(SKINCOLOR_ORANGE) elseif st > 0 then o.colormap = render_skincolormap(SKINCOLOR_RED) else o.colormap = render_skincolormap(SKINCOLOR_BLUE) end if do_rainbow then o.colormap = render_rainbowmap() end o.sprite = 288 + math.floor(leveltime/(10/6))%6 local angle = misc_angadd(kart.angle, kart.angle_offset) obj_radmove(o, angle+math.pi, o.dist*kart.scale*obj_jumpscale(kart), kart) o.angle = angle o.scale = kart.scale o.z = kart.z end, }) obj_newtype("driftspark", { think = function(o) local t = leveltime - o.spawntic if t > 3 then obj_remove(o) return end o.sprite = 304 + t end, }) obj_newtype("invstar", { init = function(o) o.orbitdist = math.random(8, 16) o.orbitdistspeed = o.orbitdist/(30+10) -- Slightly slower so it doesn't reach center o.angle = math.random(1, 360)*math.pi/180 o.orbitdir = (math.random(0, 1) == 0) and -1 or 1 o.colormap = render_rainbowmap() end, think = function(o) local t = leveltime - o.spawntic if t >= 30 or not obj_exists(o.target) then obj_remove(o) return end o.orbitdist = o.orbitdist-o.orbitdistspeed o.sprite = 296 + math.floor(t/15) o.angle = misc_angadd(o.angle, math.pi/10*o.orbitdir) obj_radmove(o, o.angle, o.orbitdist*obj_jumpscale(o.target), o.target) o.z = o.target.z + 2 end, }) obj_newtype("rocketsneaker", { drawshadow = true, sprite = 312, init = function(o) o.lastmomx = 0 o.lastmomy = 0 o.hadtarget = true o.sneakertimer = 0 o.side = -1 o.fuse = 60 o.momang = math.pi/10 local flame = obj_createfrom(o, "boostflame") flame.dist = 4 end, think = function(o) if not obj_exists(o.target) then if o.hadtarget then o.hadtarget = false o.momx = o.lastmomx o.momy = o.lastmomy o.momz = 1 obj_thrust(o, o.angle + o.side*math.pi/6, 1) end o.sneakertimer = 0 o.angle = misc_angadd(o.angle, o.momang) if o.onground then o.nodraw = leveltime % 4 < 2 obj_speedmul(o, 0.95) o.momang = o.momang*0.85 if misc_dotimer(o, "fuse") then obj_remove(o) end end return end local dist = 12*obj_jumpscale(o.target)*misc_rescalecl(leveltime-o.spawntic, 0, 15, 0.1, 1) obj_radmove(o, o.target.angle+o.side*math.pi/2, dist, o.target) o.colormap = render_skincolormap(o.target.skincolor or SKINCOLOR_ORANGE) o.angle = o.target.angle o.sneakertimer = o.target.sneakertimer o.lastmomx = o.target.momx o.lastmomy = o.target.momy o.z = o.target.z + 4 + math.sin(math.pi*2*leveltime/60)*2 o.momz = 0 end, }) obj_newtype("respawndonut", { init = function(o) sound_start(1, "B-2", 8, 8, o) end, think = function(o) o.z = o.z + 1 if (leveltime - o.spawntic) > 15 then obj_remove(o) end end, sprite = 294, w = 2, h = 2, }) -- Also doubles as eggman item obj_newtype("randomitem", { sprite = 320, gravity = 0, w = 2, h = 2, hitbox = { x = 1, y = 1, w = 14, h = 14, }, drawshadow = true, init = function(o) o.angle = math.random(1, 360)*math.pi/180 o.z = math.random()*8 o.respawntimer = 0 o.fuse = 40 -- See banana o.despawntimer = 30*60 o.dragdist = 18 end, interact = function(o, kart) if kart.name ~= "kart" then return end if o.eggman then if o.target and o.target == kart then return end if kart == o.owner and o.fuse > 0 then return end end if o.eggman and not kart_cangeteggman(kart) then return end if not (o.eggman or kart_cangetitem(kart)) then return end if o.respawntimer > 0 then return end kart.itemroulette = 1 o.respawntimer = 1*60 local ox, oy = obj_center(o) for i = 1, 8 do local o = obj_create(ox, oy, "randomitemdebree") o.momx = o.momx + 4*kart.momx/3 o.momy = o.momy + 4*kart.momy/3 end if o.eggman then obj_remove(o) kart_stripitems(kart) kart.eggmanroulette = true end end, think = function(o) if obj_exists(o.target) then kart_dragitem(o) end misc_dotimer(o, "fuse") if o.eggman then if kart_itemdespawn(o) then return end threat_add(o) end if o.respawntimer > 0 then misc_dotimer(o, "respawntimer") o.sprite = nil o.momz = 0 o.z = 0 else o.sprite = 320 -- When eggbox is thrown if o.gravity > 0 then if o.onground then o.gravity = 0 o.momx = 0 o.momy = 0 end else if o.z < o.ground+4 then o.momz = o.momz + 0.01 else o.momz = o.momz - 0.01 end end if o.eggman then o.colormap = leveltime % 30 < 15 and render_skincolormap(SKINCOLOR_RED) or render_fullcolormap(2) end o.angle = misc_angadd(o.angle, 2*math.pi/90) end end, remove = function(o) if o.target then kart_repairitemchain(o) end end, }) obj_newtype("randomitemdebree", { sprite = 322, drawshadow = true, init = function(o) o.fuse = 60 o.momx = math.random(-10, 10)/10 o.momy = math.random(-10, 10)/10 o.momz = 2 o.angle = math.random(0, 360)/180*math.pi o.momang = math.pi/60*((math.random(2)%2 == 0) and 1 or -1) end, think = function(o) if o.onground then if misc_dotimer(o, "fuse") then obj_remove(o) else o.nodraw = (o.fuse % 4) < 2 end o.momang = o.momang*0.7 o.momx = o.momx*0.9 o.momy = o.momy*0.9 end o.angle = misc_angadd(o.angle, o.momang) end, }) local explodeanim = {2, 1, 0, 1, 2, 3, 4} obj_newtype("explosion", { gravity = 0, drawshadow = true, init = function(o) o.momx = math.random(-4, 4)/10 o.momy = math.random(-4, 4)/10 o.delay = math.random(4, 60) o.angle = math.random(0, 360)/180*math.pi end, think = function(o) if o.delay > 0 then misc_dotimer(o, "delay") return end local anim = math.floor((leveltime - o.spawntic)/10) + 1 if anim > #explodeanim then obj_remove(o) return end o.sprite = 323 + explodeanim[anim] o.momz = 2 end, }) obj_newtype("banana", { sprite = 352, w = 2, h = 2, hitbox = { x = 3, y = 3, w = 10, h = 10, }, drawshadow = true, kartitem = true, interactive = true, init = function(o) o.dragdist = 12 o.fuse = 40 -- Its much harder to avoid bananas in top-down perspective -- so since they are a common item lets just despawn them after some time o.despawntimer = 20*60 end, think = function(o) threat_add(o) if not obj_exists(o.target) then misc_dotimer(o, "fuse") kart_itemdespawn(o) if o.onground then obj_speedmul(o, 0.7) o.angle = 0 else o.angle = misc_angadd(o.angle, 2*math.pi/10) end kart_commonspecials(o) return end kart_dragitem(o) end, interact = function(o, o2) if o2.name ~= "kart" then kart_itemcollide(o, o2) return end local kart = o2 if o.owner == kart and (o.fuse > 0 or o.target) then return end if kart.flashing <= 0 and kart.spinouttimer <= 0 then obj_remove(o) end kart_dospin(kart) end, remove = function(o) if o.target then local next = o.hnext while next do next.target = nil next.hprev = nil next = next.hnext end o.hnext = nil kart_repairitemchain(o) end end, }) obj_newtype("orbinaut", { sprite = 354, w = 2, h = 2, hitbox = { x = 2, y = 2, w = 12, h = 12, }, interactive = true, drawshadow = true, init = function(o) o.orbitdist = 16 o.fuse = 40 end, think = function(o) threat_add(o) if obj_exists(o.target) then -- Float a little o.z = o.target.z + 2 o.momz = 0 o.angle = misc_angadd(o.angle, math.pi/120) obj_radmove(o, o.angle, o.orbitdist, o.target) else kart_commonspecials(o) -- speen o.angle = misc_angadd(o.angle, math.pi/30) misc_dotimer(o, "fuse") -- I'm too dumb to figure out math for proper wall bounce code :( if o.justbumpedwall then obj_remove(o) end end end, interact = function(o, o2) if o2.name ~= "kart" then kart_itemcollide(o, o2) return end local kart = o2 if o.owner == kart and (o.fuse > 0 or o.target) then return end if not kart_canspin(kart) and o.target then return end kart_dospin(kart) kart_bump(kart, o) -- Also partially transfer orbi speed local speed = obj_speed(o) if speed > 0.01 then obj_thrust(kart, math.atan(o.momy, o.momx), speed*0.5) end obj_remove(o) end, remove = function(o) if o.target then kart_repairitemchain(o) end end, }) obj_newtype("mine", { sprite = 356, w = 2, h = 2, hitbox = { x = 3, y = 3, w = 10, h = 10, }, kartitem = true, interactive = true, init = function(o) o.dragdist = 12 o.fuse = 60 end, think = function(o) if not (o.target and o.target.valid) then threat_add(o) misc_dotimer(o, "fuse") return end kart_dragitem(o) end, interact = function(o, o2) if o2.name ~= "kart" then kart_itemcollide(o, o2) -- Was removed by item, still spawn explosion if not o.valid then kart_spawnexplosion(o) end return end local kart = o2 if o.owner == kart and (o.fuse > 0 or o.target) then return end -- Explosion is created in code below obj_remove(o) end, remove = function(o) kart_explosion(o, 32) -- Blows up?! end, }) obj_newtype("something", { w = 1, h = 2, scale = 1.5, init = function(o) o.colormap = render_skincolormap(SKINCOLOR_PURPLE) end, think = function(o) if not o.animstart then o.sprite = 165 if misc_objdist(o, p) < 64 then o.animstart = leveltime end else local t = math.floor((leveltime - o.animstart)/5) if t >= 4 then obj_remove(o) else o.sprite = 165 + t end end end, }) function game_updatemusic() if not options.music.value then sound_stopmusic() return end if game_curstate == "menu" then if menus[menu_current].music then sound_playmusic(menus[menu_current].music) elseif menu_tic < 80 or (music_playing and music_playing ~= menu_music) then sound_stopmusic() else sound_playmusic(menu_music) end elseif game_curstate == "intermission" then sound_playmusic(inter_music) elseif game_curstate == "level" then if maps[gamemap].music then sound_playmusic(maps[gamemap].music) else sound_stopmusic() end else sound_stopmusic() end end local function game_changemap(mapid, retry) local info = assert(maps[mapid], ("Internal map '%q' not found"):format(mapid)) obj_clear() gamemap = mapid leveltime = 0 players = {} sync(4, info.bank, false) if retry then render_startfade(12, 2, true) else render_startfade() end camera = nil game_curstate = "level" game_spawnthings() if retry then game_updatemusic() -- If we're restarting from intermission, gotta do that else sound_stopmusic() end end function game_spawnthings() local things = maps[gamemap].things if not things then return end for _, thing in ipairs(things) do local o = obj_create(thing.x*16, thing.y*16, thing.name) end end function game_spawnkart(i, skin) local info = maps[gamemap] while i > #info.starts do i = i - #info.starts end local kart = kart_create(info.starts[i].x*16, info.starts[i].y*16, skin) return kart end function game_spawnplayer(i, skin) local kart = game_spawnkart(i, skin) if options.skincolor.value ~= 0 then kart.skincolor = options.skincolor.value end return kart end function game_spawnbot(i, skin) local kart = game_spawnkart(i, skin) kart.bot = true return kart end local function game_vsbots(mapid, bots, retry) cpp_active = false game_changemap(mapid, retry) local info = assert(maps[mapid]) --cpp_list = info.checkpoints p = game_spawnplayer(2, options.skin.value) table.insert(players, p) local botskins = {} if info.bots then for i, skinname in ipairs(info.bots) do if skinname ~= skins[options.skin.value].name then table.insert(botskins, skinname) end end else for skinnum, _ in ipairs(skins) do if skinnum ~= options.skin.value then table.insert(botskins, skinnum) end end end for i = 1, bots do local botskin = table.remove(botskins, math.random(1, #botskins)) if botskin == nil then botskin = options.skin.value end table.insert(players, game_spawnbot(i+2, botskin)) end game_finishthinker = function() local t = math.floor(leveltime-starttime - p.finishtime) if t == 2 and options.difficulty.value > 1 then local medals = 0 if p.position <= 2 and not map_getflag(gamemap, MAP_VSSILVER) then map_addflag(gamemap, MAP_VSSILVER) medals = medals + 1 end if p.position == 1 and not map_getflag(gamemap, MAP_VSGOLD) then map_addflag(gamemap, MAP_VSGOLD) medals = medals + 1 end if p.position == 1 and options.difficulty.value == 3 and not map_getflag(gamemap, MAP_VSGOLDHARD) then map_addflag(gamemap, MAP_VSGOLDHARD) medals = medals + 1 end if medals > 0 then con_printf("Got %d race medal%s!", medals, medals > 1 and "s" or "") end end if t >= 3*60 then for _, player in ipairs(players) do if player.position < #players then kart_faketime(player) -- Create fake times if necessary end end -- Needed in case bots were way too slow to catch up to player kart_eliminatelast() inter_start() end end end local function game_ra(mapid, retry) cpp_active = false game_changemap(mapid, retry) local info = assert(maps[mapid]) --cpp_list = info.checkpoints local info = assert(maps[mapid]) p = game_spawnplayer(1, options.skin.value) table.insert(players, p) game_finishthinker = function() local t = math.floor(leveltime-starttime - p.finishtime) if t == 2 then local ret = map_besttime(gamemap, p.finishtime) if ret == nil or ret > p.finishtime then con_printf("New record!") end if maps[gamemap].ra_goldtime and p.finishtime <= maps[gamemap].ra_goldtime and not map_getflag(gamemap, MAP_RAGOLD) then local hadhell = menu_hellunlocked() map_addflag(gamemap, MAP_RAGOLD) con_printf("Earned record attack medal!") if not hadhell and menu_hellunlocked() then con_printf("Hell maps have been unlocked!") end end end if t >= 3*60 then inter_start() end end end local function game_cpp(mapid) game_changemap(mapid) local info = assert(maps[mapid]) cpp_active = true p = kart_create(info.starts[1].x*16, info.starts[1].y*16, options.skin.value) table.insert(players, p) end -- Check Point Place (CPP) :3 -- -- Awful tool to place checkpoints, slightly -- more convinient than doing so by hand tho -- -- press item to place center, drive away to increase radius -- making checkpoint of radius < 2 prints all placed checkpoints -- to console instead, so you can copy-paste them into maps table cpp_list = {} local cpp_cur = nil cpp_active = false local function cpp_printone(c, num) con_printf(" { x = %d, y = %d, r = %d },%s", c.x, c.y, c.r, (num and " -- "..num or "")) end local function cpp_print() con_printf(" checkpoints = {") for i = 1, #cpp_list do cpp_printone(cpp_list[i], i) end con_printf(" },") end local function cpp_rad(c) local cx, cy = obj_center(p) cx = cx/16 cy = cy/16 return math.floor(misc_pointtodist2(c.x, c.y, cx, cy)) end local function cpp_update() if not cpp_active then return end if cpp_cur == nil then local cx, cy = obj_center(p) cx = math.floor(cx/16) cy = math.floor(cy/16) cpp_cur = { x = cx, y = cy, } else local r = cpp_rad(cpp_cur) if r < 2 then cpp_print() cpp_cur = nil return end cpp_cur.r = r cpp_cur.x = math.floor(cpp_cur.x) cpp_cur.y = math.floor(cpp_cur.y) cpp_printone(cpp_cur) table.insert(cpp_list, cpp_cur) cpp_cur = nil end end local function cpp_draw() --remaprect(WIDTH/2-40, 1, 80, 9, shademap) --printcenter(string.format("%.2f %.2f", p.x/16, p.y/16), 3, 12) for i = 1, #cpp_list do local c = cpp_list[i] local r = cpp_rad(c) if r < c.r*2 then local x, y = c.x*16-camera.x, c.y*16-camera.y circb(x, y, c.r*16, 1) print(tostring(i), x, y, 1) end end if cpp_cur then circb(cpp_cur.x*16-camera.x, cpp_cur.y*16-camera.y, cpp_rad(cpp_cur)*16, 4) end end local function game_update() threat_clear() leveltime = leveltime + 1 if leveltime == starttime + 30 then game_updatemusic() end ticcmd_p1(p.cmd) if btnp(7) then cpp_update() end obj_update() if p.finishtime or p.nocontest then if game_finishthinker then game_finishthinker() end end local playerscopy = {} for _, player in ipairs(players) do table.insert(playerscopy, player) end table.sort(playerscopy, function(a, b) return a.lapposition > b.lapposition end) for i, player in ipairs(playerscopy) do player.position = i end end function hud_draw_lamps(lamp, x, y, sid) local w = lamp.w local h = lamp.h for j = 0, w-1 do for i = 0, h-1 do local idx = 1+i*w + j local s = lamp.pattern:sub(idx, idx) if s ~= " " then spr(sid, x+j*9, y+i*10, 0) end end end end function lamppattern(...) local t = {...} local w = #t[1] local h = #t return { pattern = table.concat(t), w = w, h = h } end local LAMP = { [3] = lamppattern( " ooo ", "o o", " o ", "o o", " ooo " ), [2] = lamppattern( " ooo ", "o o", " o ", " o ", "ooooo" ), [1] = lamppattern( " o", "oo", " o", " o", " o" ), [0] = lamppattern( " ooo ooo o", "o o o o o", "o o o o", "o ooo o o o", "o o o o ", " ooo ooo o" ), finish = lamppattern( "ooooo o o o o ooo o o", "o o o o o o o o o", "o o oo o o o o o", "oooo o o o o o ooo ooooo", "o o o oo o o o o", "o o o o o o o o o", "o o o o o ooo o o" ), } function hud_drawkartitem(item, amount, cmap) local x, y = 1, 1 spr(464, x, y, 0, 2) if item == 0 then return end if kartitems[item].draw then if kartitems[item]:draw(x, y, amount, cmap) then return end else kart_drawitemicon(x, y, kartitems[item], cmap) end if amount > 1 then print("x"..amount, x+13, y+13, 12) end end local function hud_draw_medal(x, y, good, skincolor) local medal_cmap = render_skincolormap(skincolor or SKINCOLOR_GOLD) if not good then medal_cmap = render_fullcolormap(0) end render_setcolormap(medal_cmap) spr(449, x, y, 0) render_setcolormap() end local function hud_draw() -- Countdown if leveltime < starttime+60 then local countdown = starttime - 3*60 if leveltime >= countdown then local i = 3 - math.floor((leveltime-countdown)/60) -- Not the best place for sounds but shrug :) if (leveltime - countdown) % 60 == 0 then if i == 0 then sound_start(20, "B-3", 16, 4) else sound_start(20, "B-2", 8, 4) end end local lamp = LAMP[i] hud_draw_lamps(lamp, WIDTH/2-lamp.w*4, HEIGHT/2-lamp.h*4, 496 + math.floor(leveltime/15)%2) end else -- Current time + laps local t = p.finishtime if not t then t = leveltime-starttime end local timestr = misc_timestr(t) local size = 46 remaprect(WIDTH-size-1, 1, size+1, 7, shademap) print(timestr, WIDTH-size, 2, 12) local y = 9 -- Draw "gold time" or best time in record attack if #players == 1 then local gold = maps[gamemap].ra_goldtime or 0 local best = map_besttime(gamemap) or 0 if best > 0 and ((gold == 0) or (gold > 0 and best < gold)) then gold = best end if gold > 0 then remaprect(WIDTH-size-8, y, size+8, 7, shademap) print(misc_timestr(gold), WIDTH-size, y+1, 12) hud_draw_medal(WIDTH-size-8, y, t <= gold, gold == best and SKINCOLOR_PLATINUM) y = y + 8 end end local lapstr if not p.finishtime then lapstr = string.format("LAP %d/%d", p.laps+1, maps[gamemap].numlaps) elseif p.nocontest then lapstr = "NO CONTEST" else lapstr = "FINISH" end size = #lapstr*6 remaprect(WIDTH-size-1, y, size+1, 7, shademap) print(lapstr, WIDTH-size, y+1, 12) -- FINISH if p.finishtime and not p.nocontest then local t = leveltime - (p.finishtime+starttime) local lamp = LAMP.finish local x = WIDTH-t*4 local y = HEIGHT/2 - lamp.h*4 hud_draw_lamps(lamp, x, y, 496 + math.floor(leveltime/15)%2) end -- Minirankings if #players > 1 and not p.finishtime then for _, player in ipairs(players) do local i = player.position-1 local y = HEIGHT/2 - 15 + 10*i local x = 2 -- speeeeen local spin = player.spinouttimer if spin > 0 then local t = 2*(leveltime/60)*math.pi*2 local amp = misc_rescalecl(spin, 0, 60, 0, 10) x = x + amp*math.sin(t) end if player == p then local c = skincolors[player.skincolor].colormap[5] rectb(x-1, y-1, 10, 10, c or 12) end render_setcolormap(player.colormap or render_skincolormap(player.skincolor)) spr(player.skin.icon, x, y) render_setcolormap() end end end threat_draw() if p.itemroulette > 0 then hud_drawkartitem((math.floor(p.itemroulette/6)%#kartitems)+1, 1, blinkmaps.grey) elseif p.eggmanexplode > 0 then hud_drawkartitem(KITEM_EGGMAN, 1, (leveltime % 30 < 15) and render_fullcolormap(2)) elseif p.growtimer > 0 then hud_drawkartitem(KITEM_GROW, 1, render_skincolormap(SKINCOLOR_BLUE)) elseif p.itemtype ~= KITEM_NONE then hud_drawkartitem(p.itemtype, p.itemamount, nil) end end local function camera_follow() if p == nil or p.finishtime then return end local SPDOFFX = WIDTH/2 - 10 local SPDOFFY = HEIGHT/2 - 5 local offx = misc_rescalecl(p.momx, -MAXSPEED, MAXSPEED, -SPDOFFX, SPDOFFX) local offy = misc_rescalecl(p.momy, -MAXSPEED, MAXSPEED, -SPDOFFY, SPDOFFY) local camx = p.x - WIDTH/2 + p.w*8/2 + offx local camy = p.y - HEIGHT/2 + p.h*8/2 + offy if not camera then camera = { x = camx, y = camy, } else camera.x = camx camera.y = camy end end function game_draw(nohud) -- Hack for intermission if game_curstate ~= "level" then return end camera_follow() map( math.floor(camera.x/16), math.floor(camera.y/16), math.ceil(WIDTH/16)+2, math.ceil(HEIGHT/16)+2, -math.floor(camera.x%16), -math.floor(camera.y%16), -1, 2, render_remap ) obj_draw() if not nohud then cpp_draw() hud_draw() con_draw() end render_draweffects() end game_states.level = function() game_update() game_draw() end inter_scores = {} inter_tic = 0 inter_time = 0 inter_map = 1 inter_music = 2 inter_exitf = nil function inter_start(inttime, exitf) sound_stopmusic() -- Draw one frame without hud for background game_draw(true) render_startinterfade(shademap, 2) game_curstate = "intermission" game_updatemusic() inter_scores = {} inter_tic = 0 inter_time = inttime or 8*60 inter_map = gamemap inter_exitf = exitf or function() render_startfade(0, 2, true) game_curstate = "menu" game_updatemusic() end for _, player in ipairs(players) do local finishtime = (not player.nocontest) and player.finishtime or NOCONTEST table.insert(inter_scores, { highlight = (player == p) and skincolors[p.skincolor].colormap[5], name = (player == p) and "You" or player.skin.realname, icon = player.skin.icon, colormap = render_skincolormap(player.skincolor), realtime = finishtime, time = misc_timestr(finishtime), }) end table.sort(inter_scores, function(a, b) return a.realtime < b.realtime end) end function inter_update() inter_tic = inter_tic + 1 if inter_tic >= inter_time then inter_exitf() end end function inter_draw() if inter_tic == 1 then render_storeframe() else render_blitframe() end printcenter("* "..maps[inter_map].name.." *", 2, 12) local BARWIDTH = WIDTH-32 local BARX = (WIDTH-BARWIDTH)/2 rect(BARX, 18, BARWIDTH, 1, 12) print("#", BARX, 10, 12) print("NAME", BARX+28, 10, 12) print("TIME", BARX+BARWIDTH-24, 10, 12, true) for i, score in ipairs(inter_scores) do local y = 21+(i-1)*10 local c = score.highlight or 12 print(i, BARX, y, c) render_setcolormap(score.colormap) spr(score.icon, BARX+10, y-1) render_setcolormap() print(score.name, BARX+28, y, c) print(score.time, BARX+BARWIDTH-(#score.time)*6, y, c, true) end con_draw() end game_states.intermission = function() inter_update() inter_draw() end menu_music = 1 menu_current = "empty" menu_item = 1 menu_tic = 0 local function menu_cvaritem(cv, getname, y, offset) getname = getname or function(item) local cv = item.cv local name = cv.name..": " if type(cv.value) == "boolean" then name = name..(cv.value and "on" or "off") else name = name..tostring(cv.value) end return name end return { getname = getname, cv = cv, y = y, offset = offset, handle = function(self) if type(cv.value) == "boolean" then if btnp(2) or btnp(3) then cvar_set(cv, not cv.value) sound_beep() end elseif type(cv.value) == "number" then if btnp(2) then cvar_set(cv, cv.value-1) sound_beep() end if btnp(3) then cvar_set(cv, cv.value+1) sound_beep() end end end, } end function printcenter(s, y, c, offx) local x = WIDTH/2 - #s*3 - (offx or 0) print(s, x, y, c, true) end function menu_drawskin(x, y) local skin = skins[options.skin.value] local color = options.skincolor.value if color == 0 then color = skin.prefcolor end local colormap = skincolors[color].colormap render_setcolormap(colormap) spr(skin.sprite, x, y, 0, 1, 0, 0, 2, 2) render_setcolormap() menu_drawstats(x-14, y-18, "speed", skin.kartspeed) menu_drawstats(x-14, y-9, "weight", skin.kartweight) end function menu_drawstats(x, y, type, value) local sprdn = type == "speed" and 416 or 417 local sprup = sprdn + 16 local c = type == "speed" and SKINCOLOR_BLUE or SKINCOLOR_RED local cmap = render_skincolormap(c) spr(sprup, x, y, 5) x = x + 7 for i = 1, 9 do local sid = 418 local flip = 0 if i == 1 then flip = 1 elseif i ~= value then sid = 419 end if i > value then render_setcolormap(render_fullcolormap(0)) else render_setcolormap(cmap) end spr(sid, x+(i-1)*3, y, 0, 1, flip) end render_setcolormap() x = x + 9*3 + 4 spr(sprdn, x, y, 5) end function menu_drawitems(items, y) for i, item in ipairs(items) do local name = item.getname and item:getname() or item.name if name then if i == menu_item then name = ">"..name.."<" end printcenter(name, item.y or y, item.color or 12, item.offset) end y = y + 8 end end function menu_hellunlocked() if showallmaps then return true end for i, map in ipairs(maps) do if not (map.hell or map_getflag(i, MAP_RAGOLD)) then return false end end return true end function menu_nextmapindex(i, step) if step == 0 then return i end if menu_hellunlocked() then return misc_index(i+step, maps) end local start = i local add = misc_sign(step) repeat i = misc_index(i + add, maps) if not maps[i].hell then step = step - add end until step == 0 or i == start return i end function menu_nextmap(i, step) return maps[menu_nextmapindex(i, step)] end local function menu_drawlevels(cur) local x = WIDTH/2 - 8 local y = HEIGHT/2+16 render_setcolormap(shademap) for i = 2, 0, -1 do local m1 = menu_nextmap(cur, i) if i == 0 then render_setcolormap() rectb(x-1, y-1, 18, 18, (menu_tic % 16 < 8) and 12 or 11) end spr(m1.preview, x+i*17, y, -1, 1, 0, 0, 2, 2) if i > 0 then local m2 = menu_nextmap(cur, -i) spr(m2.preview, x-i*17, y, -1, 1, 0, 0, 2, 2) end end end -- Shortcut function menuitem(name, arg, c) local i = { name = name, color = c } if type(arg) == "function" then i.func = arg else i.submenu = arg end return i end function unlockitem(name, check, tip) local i = { name = name, color = 12, getname = function(self) -- Uuh its kinda hacky but we actually use this just to update color self.color = check() and 12 or 13 return name end, handle = function() if btnp(4) then con_printf(tip) -- TODO - proper pop up for this? end end, } return i end function secretitem(name, unlockedname, check) local i = menuitem(name) i.getname = function() return check() and unlockedname or name end return i end function gamemenu(def) local m = { title = def.title, music = def.music or 2, y = HEIGHT-40, items = { menu_cvaritem(options.skin, function(i) return "Character: "..skins[i.cv.value].realname end, HEIGHT/2-24), menu_cvaritem(options.skincolor, function(i) local name = "Skin Default" if i.cv.value ~= 0 then name = skincolors[i.cv.value].name end return "Color: "..name end, HEIGHT/2-16), menu_cvaritem(options.map, function(i) return "Level: "..maps[i.cv.value].name end, HEIGHT/2+6), menuitem("Start", function() def:start(options.map.value) end), }, enter = def.enter or function(self) render_startfade(0, 2, true) end, leave = def.leave or function(self) render_startfade(0, 2) menu_reset() return true end, draw = function(self) cls(def.bg or 9) printcenter(self.title, 2, 12) menu_drawskin(WIDTH/2-8, HEIGHT/2-41) local curmap = options.map.value if def.draw then def:draw(curmap) end menu_drawlevels(curmap) menu_drawitems(self.items, self.y) return true end, } if def.items then for _, item in ipairs(def.items) do table.insert(m.items, 4, item) m.y = m.y - 8 end end return m end menus = { empty = { items = { menuitem(nil, function() for i = 4, 7 do if btnp(i) then menu_go("main_menu") return end end end), }, }, main_menu = { title = nil, y = HEIGHT-48, items = { menuitem("Record Attack", "ra"), menuitem("Match Race", "match"), menuitem("Controls", "controls"), menuitem("Options", "options"), menuitem("Unlocks", "unlocks"), menuitem("Play Credits", function() render_startfade(0, 2, true) credits_tic = 0 game_curstate = "credits" end), --menuitem("Place Checkpoints", function() game_cpp(8) end), }, }, controls = { title = "CONTROLS", hidegametitle = true, y = HEIGHT/2-32, items = { menuitem("A - Accelerate"), menuitem("B - Drift"), menuitem("X - Brake"), menuitem("Y - Item"), menuitem(), menuitem("You can enable"), menuitem("auto acceleration in options"), menuitem(), menuitem("Press brake to go back"), }, }, unlocks = { title = "UNLOCKS", hidegametitle = true, y = HEIGHT/2-8, items = { unlockitem("Hell Maps", menu_hellunlocked, "Get record attack medals on all maps") }, }, options = { title = "OPTIONS", hidegametitle = true, y = HEIGHT/2-16, items = { menu_cvaritem(options.music), menu_cvaritem(options.threat), menu_cvaritem(options.autoaccel), menuitem("Wipe Scores", "wipedataconfirm", 2), }, }, wipedataconfirm = { draw = function(self) rect(WIDTH/2-96, HEIGHT/2-8, 96*2, 32, 9) printcenter("Are you sure?", HEIGHT/2-4, 12) printcenter("This action cannot be reverted!", HEIGHT/2+4, 12) printcenter("(accel - proceed, brake - deny)", HEIGHT/2+12, 12) return true end, update = function(self) if btnp(4) then map_wipescores() sound_start(22, "C-6", 1.3*60, 7) menu_prev() con_printf("Scores wiped...") end end, }, ra = gamemenu { title = "RECORD ATTACK", bg = 9, start = function(self, curmap) game_ra(curmap) end, draw = function(self, curmap) rect(0, 60, WIDTH, 10, 8) local besttime = map_besttime(curmap) local str = misc_timestr(besttime) printcenter("Best time: "..str, 62, 12) if maps[curmap].ra_goldtime then hud_draw_medal(WIDTH/2+58, 61, map_getflag(curmap, MAP_RAGOLD)) end end, }, match = gamemenu { title = "MATCH RACE", bg = 6, start = function(self, curmap) game_vsbots(curmap, 2) end, draw = function(self, curmap) rect(0, 60, WIDTH, 10, 7) local xoff = 0 if options.difficulty.value == 3 then xoff = -4 hud_draw_medal(WIDTH/2+8+xoff, 61, map_getflag(curmap, MAP_VSGOLDHARD), SKINCOLOR_PLATINUM) end hud_draw_medal(WIDTH/2-8+xoff, 61, map_getflag(curmap, MAP_VSSILVER), SKINCOLOR_GREY) hud_draw_medal(WIDTH/2+xoff, 61, map_getflag(curmap, MAP_VSGOLD)) end, items = { menu_cvaritem(options.difficulty, function(i) local names = {"easy", "normal", "hard"} return "difficulty: "..(names[i.cv.value] or "???") end) }, }, } function menu_reset() menu_current = "empty" menu_item = 1 menu_tic = 0 end function menu_go(name, nosetprev) local menu = assert(menus[name]) if not nosetprev then menu.prevmenu = menu_current end menus[menu_current].previtem = menu_item menu_current = name menu_item = menu.previtem or 1 game_updatemusic() if menu.enter then menu:enter() end end function menu_prev() local menu = menus[menu_current] if menu.leave and menu:leave() then game_updatemusic() -- pls do this at least return end if menu.prevmenu == nil then return end menu_go(menu.prevmenu, true) end local function menu_draw() local m = menus[menu_current] cls(menu_tic < 80 and 0 or 15) local anim = menu_tic*0.4 local t = math.floor(anim) if menu_tic >= 80 then for h = -32, HEIGHT+32, 16 do local x = h % 32 local y = h - t%32 rect(x, y, 16, 16, 0) x = WIDTH-16-x y = h + t%32 rect(x, y, 16, 16, 0) end end if m.draw and m:draw() then return end if menu_tic == 5 then sound_start(18, "D-7", 30, 4) end if menu_tic == 80 then sound_start(2, "G-1", 30, 8) sound_playmusic(menu_music) end local titlecolor = menu_tic >= 80 and menu_tic <= 90 and 4 or 12 if not m.hidegametitle then printcenter("SRB", HEIGHT/2-10, titlecolor) printcenter("KART", HEIGHT/2, titlecolor, 8*math.max(20-menu_tic, 0)) end if m.title then printcenter(m.title, m.y-12, 12) end if m.items then menu_drawitems(m.items, m.y or 0) end end local function menu_update() menu_tic = menu_tic + 1 local m = menus[menu_current] if m.update and m:update() then return end local items = m.items if items then if btnp(0) and #items > 1 then menu_item = menu_item - 1 if menu_item <= 0 then menu_item = #items end sound_beep() end if btnp(1) and #items > 1 then menu_item = menu_item + 1 if menu_item > #items then menu_item = 1 end sound_beep() end local item = items[menu_item] if btnp(4) then if item.submenu then menu_go(item.submenu) end if item.func then item.func() end sound_beep() end if item.handle then item:handle() end end if btnp(6) then -- Don't beep if we have nowhere to go :p if m.prevmenu or m.leave then sound_beep() end menu_prev() end end game_states.menu = function() menu_draw() con_draw() menu_update() end credits_tic = 0 credits_list = { "* SRB Kart credits *", "", "", "", "* Original Game *", "", "SRB2Kart by KartKrewDev", "", "", "", "* Graphics Origins *", "", "Stat Display from SRB2Kart Saturn", "By scizor300", "", "Most of other graphics are either", "inspired from SRB2Kart", "or are original... -ish :p", "", "", "", "* Level Origins *", "", "Green Hills from Sonic The Hedgehog", "Lava Reef from Sonic & Knuckles", "by Sonic Team", "", "Toxic Palace from SRB2Kart", "Sealed Star", "from Dr. Robotnik's Ring Racers", "by KartKrewDev", "", "Egg Rock from SRB2", "by Sonic Team Jr", "", "Desert Bus from SRB2Kart", "by Spee", "", "", "", "* Music *", "", "Main Menu", "Simon Stalenhag - Fluvial Beat Deposits", "", "Level Select and Intermission", "Metropolis Street Racer - Menu", "", "Green Hills Zone", "Sonic CD - You Can Do Anything", "", "Sunny Islands Zone", "Rain World - Threat - Shoreline", "", "Sealed Star Zone", "Dr. Robotnik's Ring Racers - UFO Smasher", "", "", "", "* Produced by Indev *", "", ":3", "", "", "", "Thank You", "For Playing!", } local credits_speed = 0.1 local credits_bg_speed = 0.05 credits_endtic = (HEIGHT+(#credits_list)*6)/credits_speed function credits_draw() cls(8) for x = -1, math.ceil(WIDTH/16)+1 do for y = -1, math.ceil(HEIGHT/16)+1 do if (x+1+y+1) % 2 == 0 then rect(x*16, y*16-(credits_tic*credits_bg_speed)%32, 16, 16, 0) end end end local y = HEIGHT - math.floor(credits_tic*credits_speed) for i = 1, #credits_list do -- Keep "thanks for playing" at screen center if i == #credits_list-1 and y < HEIGHT/2-6 then y = HEIGHT/2-6 end if y < -6 or y > HEIGHT then goto continue end printcenter(credits_list[i], y, 12) ::continue:: y = y + 6 end end function credits_update() credits_tic = credits_tic + (btn(4) and 8 or 1) if credits_tic >= credits_endtic+60 or btnp(6) then game_curstate = "menu" menu_reset() render_startfade(0, 2) game_updatemusic() end end game_states.credits = function() credits_draw() credits_update() end con_clear() render_startfade(0, 2) function TIC() if showcover then return end if fade_active then render_fade() return end game_states[game_curstate]() sound_update() end function MENU(index) if index == 0 then -- RETRY -- Allows retrying from intermission too if game_curstate == "menu" or game_curstate == "credits" then return end if #players > 1 then game_vsbots(gamemap, #players-1, true) else game_ra(gamemap, true) end elseif index == 1 then -- ABORT if game_curstate == "menu" then return end render_startfade(0, 2, true) game_curstate = "menu" game_updatemusic() elseif index == 2 then -- RETURN TO TITLE game_curstate = "menu" menu_reset() render_startfade(0, 2) game_updatemusic() end end