` &:6=77PRsnf`HW`YJlw[kx//0BBQRfa_^P]c`ӋZ}izӺЈ 2TvͫgE#2Tv2TvfV0vάg5 eSΉGRWޫh$!d1vgEDDc:L!'B6dxFTee00 &333300000000003333wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwBIIIBIIIBDDDBDDDIIIIIIIIDDDDDDDDTUUUUwwwUuww%U33%R33UUww%Rww%RUUUUwwwwwwww33333333wwwwwwwwwwUUDDwwUDwwEswEsww[www[www[www[UUUQuwwUwwwuwwwuwwwuwwwuwwwuwwwUUUUwwwwwwwwwwwwwwwwwwwwwwwwwwwwUUUwwWwwwUwwwWwwwWwwwWwwwWwwwWuwwwuwwwuwwwuwwwuwwwUwwwQuwwUUUwwwwwwwwwwwwwwwwwwwwwwwwwwwwUUUUwwwWwwwWwwwWwwwWwwwWwwwUwwWUUUUUUQuwwUwwwuwwwuwwwu{wwu{wwu{UUUUwwwwwwwwwwwwwwwwwwwwwwww{UUUwwWwwwUwwwWwwwWwwWwwW{WBDDDRDTDBTDTBEEERTTTREUEBUEURUUUDDDDTDTDDTDTEEEETTTTUEUEEUEUUUUUBDDDBDDDBDDDBDDDBDDDBDDDBDDDBDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD%R%Rww%Rww%R%R%Rww%Rww%Rwwwwwwwwwwwwwwwwwwwwwwwwwww[www[www[www[www[www[www[www[wwwwwwsw7wwsw7wwwswwww""""BBBBB11BD11BDBDDDAADDDDffff"" j :2f "23#"3:33::::3cj3c23f" #"u{u{wwu{wwuwwwuwwwU{wwQUUU{wwwwwwwwwwwwwwwwwwwwUUUU{WwwWwwWwwwWwwwWwwU[UUURUUURUUURUUURUUURUUURUUURUUURUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUURUUURUUURUUURUUURUUURUUURUUURUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU%R%RwwUUww%R33%U33UuwwUwwwTUUUwwwwwwwwww33333333wwwwwwwwUUUUwww[www[www[sww[swEwwEwwUDUUDD""""UUUUUUUUUUUUUUUUUUUUUU 9 3 93 33 ""93333333333333333333333""""3333""3Q133Q3wwwWwwwWwwwWwwwWwwwWwwwWwwwWwwwW $@H $!@RRRRRRRRUUUUUUUUUQUQUUUUUUUUUUUUUUUUUUUUUUQUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUD11DDDDDDAA33DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD$"""DDDDDAAAAA"""BA@H @ $@ RRRRRRR"""RUUU""""UUUUUQQQQQ"""RUUUU" ::""""::::ccfc!!"""*3:!"Ϫ*:#" ::UZ "": U*S33U35Z"/#T:3"333:333@3:"6&cc&UTf333333DDAAЍ 3 :! ":!:3j!!"!"""""("҂""D!BB"""!(""""-"D"""$(N""::3#::ϯ3:!3"  ::SZEU22025J0PjVZUdPP53Z::36ac3 eQ6SQc3Q63dcc0fPfEef&3Td&Z@&33)93)3))""333333333333""""3333钙""UUqqQQUU   c!":!:! ! "B"""D"H""!""$($"DH"""(-!""!:#Ϫ#̪&c&":&"&f:j2SU:2 :U Z 3 2 Z`PTT@:VUdS@0 0@ac3Q6ce3V6ccc3363cUd3Z@ ## #@&VEe&ffV%fVUc%"DD)MD)D))ݒ""DDDDDDDDDDDD""""DDDԒDݒߒ"")""""""uwwwuwwwuwwwuwwwuwwwuwwwuwwwuwww  "!񣪪::::c6""bf "̪:::j6fff&f""":&ccff""2U 3:"c2 :cSUD6666bccc f"f""3 d 3V@fcUdcfff%RU% "cSVU"BB@ ($""B(B ("҈"M DMDԈD"DD"BDDDDDHD$DDDDDDHDDD$D"H$"DH""H"B("D("" "B$B DDBHB"H$HD$HHDHDDDDHDDHDHDDHDMDHDDDDDHH(DDDD"($HDDD$"$D """HD"("(""" $B@P ]]UU\ """ 333 #"" # # #"""333""2222 " # # # # #"22222 """ 333 #"" # # #"""333""" " # # # # #"22222P -UUP߈UU]]̈UȌXUUǓU\U\ U # # # # # # # #""2222222""2 # # # # # # # #22222222 # # # # # # # #""""" # # # # # # # #""2222222""2UPUXP݅ͅUXẌ݌\̈U\XXU 333 #"" # # # # # #333"2"22222# # # # # # # # # #22222222 333 """333""2222222 333 #"" # # # # # #333""2222222XPUUǓU\U\UUU PX # # # # # # # " # # # 3222" # # # # # #"" 333 """22222""2333""" """ 333 """22222""2333""" # # # # # # # "2222222" R \\ /%/]\ "\ X LU LU L%L%X " /  U URR, , \" \% \"ERE//"/-R"-T,(T% PUU5S>:>< 0 ppppp`foo`f :: >2# 92#  >2#2# 92#2# RURRUBRRDR"BB" _,,%,%U,%$U%"%D%$ $"X XH%H LTU%"  ,( ,-R,L,UE"R, R\LR\,R\,RL,RBBBB ""%H%\%\%$ $% $$""PUUYɜ pwpp  >  92# )2#  ""\U \UU \UU_UUUU""U"U%"UU"UU"UU" " 32b & #2 3#"3#&b  #223"" {\ wRU" """"\\\U%RU !"""" '̲{wRrwwR w'" `jj`  20&*2c3:3c333c233& 3c2&  20&/2c:3c?33c233& 3c2&  20&*2c33c?3c33& 3c2&###"#"933333# 93#* b*b*b*b* **)    )*- B-B-B-B- --(    (-RUUURUUU UUU RUU""" ""UU"U%"U%"U""""" f& &2  &2b f "bf #bb #&f" "2c& :c 2# "  &*6f#2j "2"2# c2:&c62:623* "3""" U |[ [ \%   20&*o3:c3?c23& ?c2&  20&*2c3:3o33o23& o&  /// /)))2#999)b) bi)bi)bi)bi) b)*/    /*B( ԄBH(BH(BH(BH( ԄB(-/    /-""" U" !'! %" !W"""!!"UR"QQR \ \UUu[ \UU,%,U'%,RR,% R"\"\ UR+%Q"_%R"+ !R% %! %U!!U"" R,!Q,_\!Q!!_"" "R% _U \!RUUR,U U RU"%%"!{%w!%" \RUU \UU"U% UU"UUU "R%R"!%"RR%U"" \RUU !|U UwU! "RR%R"'%"+R%"" " \ _qwRU"U%QUQ%%%\Q"\%!"" % !Q % UR%%"%%""Q[uR\U%%%%%""" UU!R ! R ,RRR,"%%RR%%" µ"_U"R \"U"Q"%+ R \%R!\R URRRP "PPU"%Q%%%%"%%%%UUU %R" U!U"rU"R"RR%R%"'UUU %R" U!U"RU"R"rRR%R'"%R\RU URR! U"" "U"Q%Q"QwQ"!%!"!%"""""" U" !!! %" !Q"""!!"UrR"[QuR \ \UU\ ,U%uU[%UW,U R | "ſ%%%,"R! 'R% %! %rU!'U""u R,!Q,_\+Wr''_"" UR Q     !%Q"%"%"%!%!%! R"\ %R"%%\R%U\%% "RU U\ R UR%R"%%\U%U"U%% " \ _RU"U%QUQ%%%\wQ"\%!" % !W % UR%%"%%""QQR\U%%%%%""" R! Q R ,RR,%%"Q%%!R\| "U uQR +\!"",," R \%R!\  U \U\U\U\U\"""P"R%QUR% %" %\UU"" ,R,R\% U""\\\%U%U"R\RwU UqRR! U"" "U"Q%Q"QQ"!%!"!%"""0303c939c39999399933?>333>333fcf>393?3>3c033000003c3>3330360?3c333003000303300606o00939i33603 0 6 9999900``903093 0 0 903i0906`000`0`0390ic?933c0c33333>33333>>33f?3933333030000036`9399030>30030030o?0000??30f09c393?30 9f3i 3 0?93 00cc039396939 0`30``""" U" !%! %W2 !U3""f"!a3S3:631cS \ \UUu[ \UU,%,U'%,RR,% R"36"<: UR<'%1"#:"3 !R% %! %U1!U"" 2"#R3?#!:f33l:V\!?!j\"" "R% UU U\!RU5R: "RbfUe"&"e%&c213333%#" \RUU \UU"U% UU"UUU&#6R6%2R"!%"RR%Uc"" \RUU !Uq UqU%"&#6R2%R",%"R%c" " \q _a6cq"U%QUQ%%wQ"{w!"w" %W !# %{`3|q2|,*&a6õR<\?000020000@00000000200000000000000@00000@0000@0000@00000002003@0000000@000002000000000000000A000000000000000000000200000000B40002@000000200000000000@0000001100000000000@000000000000000000133000000000000000000000000000200AB00000000000002000000000000000002000020000000002000000000000010@020000020000000200000200@000A00000@0000000@000000200000020000000@0000000000000000@0000000A456789000000002000000020@00000000000B0000000000000@00000000000200001A4EFFFG0@00000000000000000000000000001A000000020000000000020000000000A33EFFFG0002000000000000000000020000000000000000@0000000000000000000AC u/u/u/u/u/ u/ '() !!!!!!!!!!!!!!!!!!!!!!!" &&&&&& *-/ /////////////////////"((=(L(<(,(+(+(((((((/////////////////    UUUUUUUUUUUUUUUUUUU U U U U U UpUpUpUpUp_<`    @@@@````pppp @!"/3 30303030303 333333333333333 3@3P3P3P5`9p= prio then sfx(self.id , self.note, self.duration, self.channel, self.volume, self.speed) lazy.tic.set_channel_prio(self.channel, self.prio) end end end, ["lazylib-tic.animation_renderer"] = function() -- Depricated -- animation_renderer = {} -- animation_renderer.meta = {} end, ["lazylib-tic.mod_animate"] = function() --- An animation class using modulus -- An fast way to do an animation using modulus -- this method makes all frames take the same time -- and it's very inacurrate. ModAnimate = lazy.class("ModAnimate") -- TODO add sync for synced animation function ModAnimate:constructor(ids, frame_time, tile) self.ids = ids self.frame_time = frame_time self.total_time = frame_time * #ids self.offset = math.random(0, self.total_time) self.tile = tile or 1 end --- Renders the animation --- @param frame_total number the total number of frames since application started --- @param x number the x coordinate to draw the sprite --- @param y number the y coordinate to draw the sprite --- @param size number|nil draw size --- @param flip number|nil optional draw flip function ModAnimate:render(frame_total, x, y, size, flip) size = size or 1 flip = flip or 0 frame_total = frame_total + self.offset local current_frame = 1 + lazy.math.round((frame_total % self.total_time) / self.total_time * (#self.ids - 1)) local current_id = self.ids[current_frame] spr( current_id, x, y, 0, size, flip, 0, self.tile, self.tile ) end function ModAnimate:renderFrame(index, x, y, size, flip) size = size or 1 flip = flip or 0 --index = (index > #self.ids and #self.ids or index) local current_id = self.ids[index] spr( current_id, x, y, 0, size, flip, 0, self.tile, self.tile ) end end, ["lazylib-tic.init"] = function() --- A library for rapid game development in lua. -- The library is build for TIC80 but there's also a port to LOVE2D lazy = {} -- metadata lazy.meta = { _NAME = "lazylib-tic", _VERSION = "1.0", _AUTHOR = "Esteban RPM (Jaded Puma)", _URL = "jadedpuma.com", _LICENSE = "MIT" } -- setup lazy.log = trace -- load dependencies require("lazylib-tic.base.class_base") require("lazylib-tic.base.class_extend") require("lazylib-tic.base.debug") require("lazylib-tic.base.enum") require("lazylib-tic.base.math") require("lazylib-tic.base.math_vector") require("lazylib-tic.base.util") require("lazylib-tic.base.util_array2d") require("lazylib-tic.base.tween") -- states require("lazylib-tic.base.state") require("lazylib-tic.base.state_manager") require("lazylib-tic.base.state_stack") require("lazylib-tic.base.state_transition") -- entity require("lazylib-tic.base.entity") require("lazylib-tic.base.entity_handler") require("lazylib-tic.base.bounding_box") -- late dependencies require("lazylib-tic.base.particle_base") -- tic require("lazylib-tic.mod_animate") require("lazylib-tic.tic-util") require("lazylib-tic.animation_single_data") require("lazylib-tic.animation_single") --require("lazlib-tic.animation_data") --require("lazlib-tic.animation") --require("lazlib-tic.animation_renderer") require("lazylib-tic.sfx_data") require("lazylib-tic.base.camera") require("lazylib-tic.transition_base") require("lazylib-tic.transition_slide") -- successful load lazy.log("Loaded "..lazy.meta._NAME.." "..lazy.meta._VERSION) end, ["lazylib-tic.animation_single_data"] = function() -- Data aniimation class for single state animations AnimationSingleData = lazy.class("AnimationSingleData") function AnimationSingleData:constructor(frames, times, size) assert(#frames == #times, "frames and times in AnimationSingleData does not match.") self.frames = frames self.times = times self.size = size and size or 1 end function AnimationSingleData:getIntanced() return AnimationSingle(self) end end, ["lazylib-tic.transition_slide"] = function() TransitionSlide = lazy.extend("TransitionSlide", TransitionBase) local TRANSITION_COLOR = 0 function TransitionSlide:constructor(state_manager, time_max, old_state, new_state) TransitionSlide.super:initialize(self, state_manager, time_max, old_state, new_state) end function TransitionSlide:update() TransitionSlide.super.update(self) end function TransitionSlide:render() if self.state == self.State.INTRO then self.old_state:render() self.old_state:overlay() local w = math.floor(0.5 + self.a * SCREEN.WIDTH) rect(0, 0, w, SCREEN.HEIGHT, TRANSITION_COLOR) elseif self.state == self.State.OUTRO then self.new_state:render() self.new_state:overlay() local x = math.floor(0.5 + self.a * SCREEN.WIDTH) local w = SCREEN.WIDTH - x rect(x, 0, w, SCREEN.HEIGHT, TRANSITION_COLOR) end end end, ["lazylib-tic.animation"] = function() -- Basic animation class with a lot of limitations. Animation = lazy.class("Animation") function Animation:constructor(animation_data) self.data = animation_data.data self.index = "" self.time = 0 self.frame = 1 end -- this starts a specific animation function Animation:start(index) self.index = index self.frame = 1 self.time = self.data[self.index].frames[self.frame].ticks end function Animation:update() self.time = self.time - 1 if self.time == 0 then if self.data[self.index].loop then if self.frame ~= #self.data[self.index].frames then self.frame = self.frame + 1 else self.frame = 1 end else if self.frame ~= #self.data[self.index].frames then self.frame = self.frame + 1 else -- ignore / end on non-looping animation end end self.time = self.data[self.index].frames[self.frame].ticks end end function Animation:draw(...) self.id = self.data[self.index].frames[self.frame].id spr(self.id, ...) end end, ["lazylib-tic.animation_single"] = function() -- Single state animation class AnimationSingle = lazy.class("AnimationSingle") function AnimationSingle:constructor(AnimationSingleData) self.frames = AnimationSingleData.frames self.times = AnimationSingleData.times self.size = AnimationSingleData.size self.frame_index = 1 self.current_time = 0 self.max_times = 0 for _, frame_time in ipairs(self.times) do self.max_times = self.max_times + frame_time end self.current_frame_id = self.frames[self.frame_index] self.current_frame_time = self.times[self.frame_index] self.is_done = false end function AnimationSingle:update(dt) self.current_time = lazy.math.bound(self.current_time + 1, 0, self.current_frame_time) if self.frame_index == #self.frames and self.current_time == self.current_frame_time then self.is_done = true elseif self.current_time == self.current_frame_time then self.frame_index = self.frame_index + 1 self.current_time = 0 self.current_frame_id = self.frames[self.frame_index] self.current_frame_time = self.times[self.frame_index] end end function AnimationSingle:render(x, y) spr( self.current_frame_id, x, y, 0, 1, 0, 0, self.size , self.size ) end function AnimationSingle:isDone() return self.is_done end end, ["lazylib-tic.tic-util"] = function() -- Utility library for TIC-80 API local tic = {} lazy.tic = tic tic._default_palette = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } tic._palette = default_palette tic._palette_map = 0x3FF0 tic._color_bit = 4 tic._channel_prio = {-1, -1, -1, -1} function tic.rspr(sx, sy, scale, angle, mx, my, mw, mh, key, useMap) angle = -rad(angle) -- this is fixed , to make a textured quad -- X , Y , U , V local sv = { { -1, -1, 0, 0 }, { 1, -1, 0.999, 0 }, { -1, 1, 0, 0.999 }, { 1, 1, 0.999, 0.999 } } -- local rp = {} -- rotated points storage -- the scale is mw ( map width ) * 4 * scale -- mapwidth is * 4 because we actually want HALF width to center the image local scalex = (mw << 2) * scale local scaley = (mh << 2) * scale -- rotate the quad points for p = 1, #sv do -- apply scale local _sx = sv[p][1] * scalex local _sy = sv[p][2] * scaley -- apply rotation local a = -angle local rx = _sx * math.cos(a) - _sy * math.sin(a) local ry = _sx * math.sin(a) + _sy * math.cos(a) -- apply transform sv[p][1] = rx + sx sv[p][2] = ry + sy -- scale UV's sv[p][3] = (mx << 3) + (sv[p][3] * (mw << 3)) sv[p][4] = (my << 3) + (sv[p][4] * (mh << 3)) end -- draw two triangles for the quad textri(sv[1][1], sv[1][2], sv[2][1], sv[2][2], sv[3][1], sv[3][2], sv[1][3], sv[1][4], sv[2][3], sv[2][4], sv[3][3], sv[3][4], useMap, key) textri(sv[2][1], sv[2][2], sv[3][1], sv[3][2], sv[4][1], sv[4][2], sv[2][3], sv[2][4], sv[3][3], sv[3][4], sv[4][3], sv[4][4], useMap, key) end tic.border_color = function(color) poke(0x03FF8, color) end tic.replace_color = function(color1, color2) poke4(tic._palette_map * 2 + color1, color2) end tic.palette_reset = function() for i = 0, 15 do poke4(tic._palette_map * 2 + i, i) end tic._palette = tic._default_palette end tic.set_palette_from_table = function(palette_table) palette_table = palette_table and palette_table or tic._palette for i = 0, #palette_table - 1 do tic.replace_color(i, palette_table[i + 1]) end tic._palette = palette_table end tic.spr_textri = function(id, sw, sh, x1, y1, x2, y2, x3, y3, x4, y4, color_key) color_key = color_key and color_key or 0 sw = sw * 8 sh = sh * 8 local uvx = (id % 16) * 8 local uvy = (id // 16) * 8 local t1u1 = uvx local t1v1 = uvy local t1u2 = uvx + sw local t1v2 = uvy local t1u3 = uvx local t1v3 = uvy + sh local t2u1 = uvx + sw local t2v1 = uvy local t2u2 = uvx local t2v2 = uvy + sh local t2u3 = uvx + sw local t2v3 = uvy + sh --t1 textri( --screen x1, y1, x2, y2, x3, y3, --uv map t1u1, t1v1, t1u2, t1v2, t1u3, t1v3, --use map false, --trans color_key) --t2 textri( --screen x2, y2, x3, y3, x4, y4, --uv map t2u1, t2v1, t2u2, t2v2, t2u3, t2v3, --use map false, --trans color_key) end tic.spr_dist = function(id, sw, sh, x, y, w, h, colkey) colkey = colkey and colkey or 0 sw = sw * 8 sh = sh * 8 local uvx = (id % 16) * 8 local uvy = (id // 16) * 8 --tri1 local t1x1 = x local t1y1 = y local t1x2 = x local t1y2 = y + h local t1x3 = x + w local t1y3 = y local t1u1 = uvx local t1v1 = uvy local t1u2 = uvx local t1v2 = uvy + sh local t1u3 = uvx + sw local t1v3 = uvy --tri2 local t2x1 = x local t2y1 = y + h local t2x2 = x + w local t2y2 = y local t2x3 = x + w local t2y3 = y + h local t2u1 = uvx local t2v1 = uvy + sh local t2u2 = uvx + sw local t2v2 = uvy local t2u3 = uvx + sw local t2v3 = uvy + sh --t1 textri( --screen t1x1, t1y1, t1x2, t1y2, t1x3, t1y3, --uv map t1u1, t1v1, t1u2, t1v2, t1u3, t1v3, --use map false, --trans colkey) --t2 textri( --screen t2x1, t2y1, t2x2, t2y2, t2x3, t2y3, --uv map t2u1, t2v1, t2u2, t2v2, t2u3, t2v3, --use map false, --trans colkey) end tic.draw_box_class = function(box, color, bb_key) if bb_key then local bb = box:getBoundingBox(bb_key) rectb(box.x + bb.x, box.y + bb.y, bb.w, bb.h, color) else rectb(box.x, box.y, box.w, box.h, color) end end tic.get_channel_data = function(channel) local value = peek(0xFF9C+18*channel+1)<<8|peek(0xFF9C+18*channel) local frequency = (value&0x0fff) local volume = (value&0xf000)>>12 return frequency, volume end tic.set_channel_prio = function(channel, prio) tic._channel_prio[channel] = prio end tic.get_channel_prio = function (channel) return tic._channel_prio[channel] end end, ["lazylib-tic.transition_base"] = function() local transition_color=0 TransitionBase = lazy.extend("TransitionBase", State) function TransitionBase:constructor(state_manager, time_max, old_state, new_state) self.State = lazy.enum("INTRO","OUTRO") self.state_manager = state_manager self.old_state = old_state self.new_state = new_state self.state = self.State.INTRO self.time = 0 self.time_max = time_max self.a = 0 end function TransitionBase:update() self.a = lazy.math.bound(self.time / self.time_max, 0, 1) self.time = self.time + 1 if self.a == 1 then if self.state == self.State.INTRO then self.state = self.State.OUTRO self.time = 0 self.a = 0 self.old_state:finish() self.new_state:start() elseif self.state == self.State.OUTRO then self.state_manager:continue(self.new_state) end end end end, ["lazylib-tic.animation_data"] = function() -- Data for Animation class AnimationData = lazy.class("AnimationData") --- Animation constrcutor -- The animation constrcutor takes in any number of parameters function AnimationData:constructor(...) local args = { ... } for _, anim in ipairs(args) do if type(anim[1]) ~= "string" then error("Incorrect animation data, expected string") end local anim_name = anim[1] for i = 2,#anim do end end -- local state = 1 -- 1:string 2:frame/time or bool to end -- local index = "" -- self.data = {} -- for i, v in ipairs(args) do -- -- get string name of animation -- if state == 1 then -- assert(type(v) == "string", "tic.animation_data expected a string value but got " .. type(v)) -- index = v -- self.data[index] = {} -- self.data[index].frames = {} -- state = 2 -- elseif state == 2 then -- if type(v) == "table" then -- assert(type(v[1]) == "number" and type(v[2]) == "number", "Incorrect number paramenters in tic.animation_data.") -- table.insert(self.data[index].frames, { id = v[1], ticks = v[2] }) -- elseif type(v) == "boolean" then -- self.data[index].loop = v -- state = 1 -- else -- error("Incorrect data type in tic.animation_data: " .. type(v)) -- end -- end -- end end end, ["lazylib-tic.base.entity"] = function() -- Abstract Entity class Entity = lazy.class("Entity") function Entity:constructor(x, y, w, h) self.x = x self.y = y self.w = w self.h = h self.r = 0 self.ox = w / 2 self.oy = h / 2 self.sx = 1 self.sy = 1 self.color = {1, 1, 1, 1} self.bounding_boxes = {} self.delete = false end --- @deprecated function Entity:getScaledRectangle() local w, h = math.floor(self.w * self.sx), math.floor(self.h * self.sy) local ox, oy = w / 2, h / 2 return self.x - ox, self.y - oy, w, h, ox, oy end function Entity:update(dt) error(self._class_name.." does not override :update(dt) from Entity class") end function Entity:render() error(self._class_name.." does not override :render() from Entity class") end function Entity:addBoundingBox(name, rx, ry, w, h) self.bounding_boxes[name] = BoundingBox(rx, ry, w, h) end function Entity:getBoundingBox(name) return self.bounding_boxes[name] end end, ["lazylib-tic.base.math"] = function() -- Extended math utility API local maths = {} lazy.math = maths local PI2 = math.pi * 2 local min, max, abs, sqrt = math.min, math.max, math.abs, math.sqrt ---@param a number ---@param start number ---@param finish number ---@return number maths.lerp = function(a, start, finish) return start + (finish - start) * a end maths.nlerp = function(func, start, finish, value) local x = (value - start) / (finish - start) local a = func(x) return start + (finish - start) * a end maths.remap = function() end -- x -> converts min to max to map min to max --- return the sign of the number as 1 or -1 ---@param value number ---@return number function maths.sign(value) return (value >= 0 and 1) or -1 end maths.random_sign = function () if math.random() <= 0.5 then return 1 else return -1 end end -- round number to nearest bracket ---@param value number ---@param bracket number @express the value as e.x. 10 to round up, or e.x. 0.1 to round down. ---@return number ---@overload fun(value: number): number function maths.round(value, bracket) bracket = bracket or 1 return math.floor(value /bracket + maths.sign(value) * 0.5) * bracket end --- bound the value to a range ---@param value number ---@param low number ---@param high number ---@return number maths.bound = function(value, low, high) if value < low then return low elseif value > high then return high else return value end end maths.lower_bound = function(value, low) if value < low then return low else return value end end maths.upper_bound = function (value, high) if value > high then return high else return value end end maths.distance = function(x0, y0, x1, y1) local dx = x1 - x0 local dy = y1 - y0 dx, dy = abs(dx), abs(dy) return max(dx, dy) * 0.9609 + min(dx, dy) * 0.3984 end --- calculate normal vector from two points ---@param x0 number ---@param y0 number ---@param x1 number ---@param y1 number ---@return table maths.normal = function(x0, y0, x1, y1) local dx = x1 - x0 local dy = y1 - y0 local m = math.sqrt(dx^2 + dy^2) return {x = dx/m, y = dy/m} end --- takes in angle in degrees and return normal maths.normalAngleDegree = function(theta) local rads = math.rad(theta) return {x = math.cos(rads), y = math.sin(rads)} end maths.normalAngleRad = function(theta) return {x = math.cos(theta), y = math.sin(theta)} end maths.normalizeAngleRad = function(angle) while angle < 0 do angle = angle + PI2 end while angle >= PI2 do angle = angle - PI2 end return angle end maths.rollingAverage = function(avg, new_sample, n) avg = avg - avg / n; avg = avg + new_sample / n; return avg; end end, ["lazylib-tic.base.state_manager"] = function() -- Class that handles "State" StateManager = lazy.class("StateManager") function StateManager:constructor(start_state) self.state = start_state self.state:start() end function StateManager:next(state) -- old state self.state:finish() -- new state self.state = state self.state:start() end function StateManager:continue(state) self.state = state end function StateManager:update(dt) self.state:update(dt) end function StateManager:render() self.state:render() end function StateManager:scanline(scanline) self.state:scanline(scanline) end function StateManager:overlay() self.state:overlay() end end, ["lazylib-tic.base.class_base"] = function() -- Class implementation for Lua lazy.class = function(class_name) local mt = { __call = function(ClassGeneric, ...) local instance = ClassGeneric:_instantiate() ClassGeneric.constructor(instance, ...) return instance end } local ClassDefinition = { super = nil, _class_name = class_name, constructor = function() error(class_name.." class called without a constructor.") end, _instantiate = function(ClassGeneric) local instance = {} instance._class_name = ClassGeneric._class_name for k,v in pairs(ClassGeneric) do if type(v) == "function" then if k ~= "constructor" and k ~= "_instantiate" and k ~= "initialize" then instance[k] = v end end end return instance end, initialize = function(SuperClass, instance, ...) SuperClass.constructor(instance, ...) ---@param k string for k,v in pairs(SuperClass) do if type(v) == "function" then if k ~= "constructor" and k ~= "_instantiate" and k ~= "initialize" then if not instance[k] then instance[k] = v end end end end return instance end, typeof = function(self, Klass) return self._class_name == Klass._class_name end } setmetatable(ClassDefinition, mt) return ClassDefinition end end, ["lazylib-tic.base.math_vector"] = function() -- Incomplete vector class Vector = lazy.class("Vector") function Vector:constructor(x, y) self.x = x self.y = y end function Vector:add(x, y) end function Vector:addVector(vector) end end, ["lazylib-tic.base.camera"] = function() -- Camera class to handle scrolling and camera effects Camera = lazy.class("Camera") function Camera:constructor(x, y, w, h) self.x = x self.y = y self.w = w self.h = h self.intensity = 0 self.duration = 0 self.shake_timer = 0 self.xs = x self.ys = y end function Camera:update(dt) if self.shake_timer ~= 0 then self.shake_timer = self.shake_timer - 1 local a = 1 - self.shake_timer / self.duration local t = 1 - lazy.tween.func.easeInQuart(a) local shake_x = lazy.math.round(((math.random() * 2) - 1) * self.intensity * t) local shake_y = lazy.math.round(((math.random() * 2) - 1) * self.intensity * t) self.xs = self.x + shake_x self.ys = self.y + shake_y else self.xs = self.x self.ys = self.y end end function Camera:shake(intensity, duration) self.intensity = intensity self.duration = duration self.shake_timer = duration end end, ["lazylib-tic.base.util"] = function() -- Base utility API to simplify coding local util = {} lazy.util = util util.position = function(x, y) local mt = {} local pos = {} pos.x = x pos.y = y mt.__tostring = function(self) return "{x="..self.x..", y="..self.y.."}" end setmetatable(pos, mt) return pos end util.dimension = function(w,h) local mt = {} local dim = {} dim.w = w dim.h = h mt.__tostring = function(self) return "{w="..self.w..", h="..self.h.."}" end setmetatable(dim, mt) return dim end -- Used for exiting nested loops with 'return' util.loop = function(loopfunc) loopfunc() end util.align_horizontal = function(width_base, width_obj) return math.floor(width_base/2-width_obj/2) end util.align_vertical = function(height_base, hight_obj) return math.floor(height_base/2-hight_obj/2) end util.fif = function(test, if_true, if_false) if test then return if_true else return if_false end end util.is_point_inside_box = function(px, py, bx, by, bw, bh) return px > bx and px < bx+bw and py > by and py < by+bh end util.is_box_inside_box = function(x1, y1, w1, h1, x2, y2, w2, h2) -- return x2 < x1 + w1 and x2 + w2 > x1 and y2 > y1 + h1 and y2 + h2 < y1 return x1 < x2 + w2 and x2 < x1 + w1 and y1 < y2 + h2 and y2 < y1 + h1 end util.entity_collision = function(e1, e2) return util.is_box_inside_box(e1.x, e1.y, e1.w, e1.h, e2.x, e2.y, e2.w, e2.h) end util.entity_collision_distance = function (e1, e2, dist) local dist_e = lazy.math.distance(e2.x, e2.y, e1.x, e1.y) return dist_e < dist end util.bouding_box_collision = function(key, e1, e2) local bb1 = e1:getBoundingBox(key) local bb2 = e2:getBoundingBox(key) local x1, y1, w1, h1 = bb1:apply(e1.x, e1.y) local x2, y2, w2, h2 = bb2:apply(e2.x, e2.y) return util.is_box_inside_box(x1, y1, w1, h1, x2, y2, w2, h2) end util.table_copy = function(to_table, from_table) for k,v in pairs(from_table) do to_table[k] = v end end util.single_state_action = function(state, state_var, Enum) for i,key_value in ipairs(lazy._enumdata[Enum]._reverse_ref) do if state_var == i then local func = state["_state_"..key_value] func(state) return end end error("State number "..tostring(state_var).." was not found in StateEnum") end -- Enum utility functions util.enum = {} util.enum.toString = function (Enum, value) local string_ref = lazy._enumdata[Enum]._reverse_ref[value] assert(string_ref ~= nil, "Enum value was not found.") return string_ref end end, ["lazylib-tic.base.state"] = function() -- the base state class that is used by StateManager -- State class State = lazy.class("State") State.constructor = function() end State.start = function() end State.finish = function() end State.update = function(dt) end State.render = function() end State.scanline = function(scanline) end State.overlay = function() end end, ["lazylib-tic.base.class_extend"] = function() -- Extend functionality for lazylib Class implementation lazy.extend = function(class_name, FromClass) local NewClass = lazy.class(class_name) for k,v in pairs(FromClass) do if k ~= "constructor" and k ~= "_instantiate" and k ~= "initialize" and k ~= "_class_name" then NewClass[k] = v end end NewClass.super = FromClass return NewClass end end, ["lazylib-tic.base.debug"] = function() -- Debug API local debug = {} lazy.debug = debug debug.table_tostring = function(o, get_level) local level = get_level if level == nil then level = 0 end local pad1 = "" local pad2 = " " if level ~= 0 then for i = 1, level do pad1 = pad1 .. " " pad2 = pad2 .. " " end end if type(o) == "table" then local s = '{\n' for k, v in pairs(o) do if type(k) ~= "number" then k = "'" .. k .. "'" end s = pad2 .. s .. "[" .. k .. "] = " .. lazy.debug.table_tostring(v, level + 1) .. "\n" end return pad1 .. s .. "\n}\n" else return pad2 .. tostring(o) end end debug.print = function(text, ...) lazy.log(text .. debug.table_tostring({ ... })) end end, ["lazylib-tic.base.tween"] = function() -- Tween functions API local tween = {} lazy.tween = tween tween.func = {} -- exp tweens tween.func.easeInExp = function (x, n) return x^n end tween.func.easeOutExp = function(x, n) return 1 - (1 - x)^n end -- tween fucntions tween.func.linear = function(x) return x end tween.func.easeOutQuart = function (x) return 1 - (1 - x)^4 end tween.func.easeInQuart = function(x) return x * x * x * x; end local c1 = 1.70158; local c3 = c1 + 1; tween.func.easeOutBack = function(x) return 1 + c3 * (x - 1)^3 + c1 * (x - 1)^2 end tween.func.easeInBack = function(x) return c3 * x * x * x - c1 * x * x end tween.func.easeOutCubic = function(x) return 1 - (1 - x)^3 end end, ["lazylib-tic.base.state_transition"] = function() -- helper state to allow state transitions -- Transition State class TransitionState = lazy.extend("TransitionState", State) function TransitionState.constructor(self, manager, old_state, new_state, Transition, time_max) self.manager = manager self.transition = Transition(time_max, old_state, new_state) end function TransitionState:update() self.transition:update() if not self.transition.transitioning then self.manager:replace(self.transition.new_state, false) end end function TransitionState:render() self.transition:render() end function TransitionState:overlay() self.transition:overlay() end function TransitionState:scanline(scanline) self.transition:scanline(scanline) end end, ["lazylib-tic.base.bounding_box"] = function() -- a BoundingBox designed to handle collisions BoundingBox = lazy.class("BoundingBox") function BoundingBox:constructor(x, y, w, h) self.x = x self.y = y self.w = w self.h = h end function BoundingBox:apply(x, y) return self.x + x, self.y + y, self.w, self.h end end, ["lazylib-tic.base.state_stack"] = function() -- State stack class StateStack = lazy.class("StateStack") -- TODO: Proper implementation function StateStack:constructor() self.stack = {} end function StateStack:push(state) table.insert(self.stack, state) state:start() end function StateStack:pop() assert(#self.stack > 0, "State stack cannot perform more 'pop()' operations.") local state = table.remove(self.stack) state:finish() end function StateStack:update() if #self.stack>0 then self.stack[#self.stack]:update() else self.state:update() end end function StateStack:render() self.state:render() if #self.stack>0 then for i=1,#self.stack do self.stack[i]:render() end end end function StateStack:overlay() self.state:overlay() if #self.stack>0 then for i=1,#self.stack do self.stack[i]:overlay() end end end function StateStack:scanline(scanline) if #self.stack>0 then self.stack[#self.stack]:scanline(scanline) else self.state:scanline(scanline) end end end, ["lazylib-tic.base.util_array2d"] = function() -- Basic 2d array class Array2d = lazy.class("Array2d") function Array2d:constructor(w, h, default_value) self.default_value = default_value self.w = w self.h = h self.array = {} for x=1,w do ---@type table self.array[x] = {} for y=1,h do self.array[x][y] = self.default_value end end end function Array2d:set(x, y, value) self.array[x][y] = value end function Array2d:get(x, y) return self.array[x][y] end --- clear array with default values function Array2d:clear() for x=1,self.w do for y=1,self.h do self.array[x][y] = self.default_value end end end --- loop through array with fun(value, x, y) function Array2d:loop(loopfunc) for x=1,self.w do for y=1,self.h do loopfunc(self.array[x][y], x, y) end end end end, ["lazylib-tic.base.entity_handler"] = function() -- A somewhat inefficient way to store "Entity" objects. EntityHandler = lazy.class("EntityHandler") -- TODO: sort function function EntityHandler:constructor(limit) self.limit = limit or -1 -- assert(limit == -1 or limit > 1, "EntityHandler limit must be greater than 1 or -1") self.list = {} end --- Clears the enitity list function EntityHandler:clear() self.list = {} end function EntityHandler:size() return #self.list end function EntityHandler:isEmpty() return #self.list == 0 end function EntityHandler:add(entity) if self.limit > 0 and #self.list == self.limit then return end table.insert(self.list, entity) end function EntityHandler:get(index) return self.list[index] end function EntityHandler:remove(entity) for i,ref in ipairs(self.list) do if entity == ref then table.remove(self.list, i) break end end end function EntityHandler:removeAll() for i = #self.list, 1, -1 do if self.list[i].delete then table.remove(self.list, i) end end end function EntityHandler:update(dt) for _,entity in ipairs(self.list) do entity:update(dt) end -- remove expired for i = #self.list, 1, -1 do if self.list[i].delete then table.remove(self.list, i) end end end function EntityHandler:render() for _,entity in ipairs(self.list) do entity:render() end end function EntityHandler:loop(func) for _,entity in ipairs(self.list) do func(entity) end end function EntityHandler:sort(func) table.sort(self.list, func) end function EntityHandler:render_debug() for _,entity in ipairs(self.list) do entity:render_debug() end end end, ["lazylib-tic.base.particle_base"] = function() -- Particle base class ParticleBase = lazy.extend("ParticleBase", Entity) function ParticleBase:constructor(x, y, dir_v, init_vel, acc) self.dir_v = dir_v self.init_vel = init_vel self.acc = acc ParticleBase.super:initialize( self, x, y, 1, 1 ) end end, ["lazylib-tic.base.enum"] = function() -- add a Enum "class" to lua. lazy._enumdata = {} lazy.enum = function(...) local reverse_ref = {} local ref = { ... } local enum = {} for i, v in ipairs(ref) do reverse_ref[i] = v enum[v] = i end lazy._enumdata[enum] = { _reverse_ref = reverse_ref, _ref = ref } enum.getEnum = function (self, index) return lazy._enumdata[self]._reverse_ref[index] end return enum end end, ["states.level_render"] = function() -- Rendering logic for "level" StateLevelRenderer = lazy.class("StateLevelRenderer") function StateLevelRenderer:constructor(logic) self.logic = logic self.starfield_bg = self:_create_startfield(320) self.starfield_mg = self:_create_startfield(200) self.starfield_fg = self:_create_startfield(130) self.cursor_id = 290 self.flame_animation = ModAnimate({287, 286, 271, 270}, 6) self.quasar_pos = lazy.util.position(CONFIG.GAME.VIEWPORT.W - 20 - 16, 8 * 7 - 1) self.quasar_anim = ModAnimate({128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 192, 128, 192, 128, 192}, 7, 4) -- UI -- TODO replace with box function {} self.hp_bar_pos = lazy.util.position(25*8+4, 1*8+1) self.hp_bar_dim = lazy.util.dimension(32, 5) self.exp_bar_pos = lazy.util.position(25*8+4, 3*8+1) self.exp_bar_dim = lazy.util.dimension(32, 5) self.num_level_start_pos = lazy.util.position(26*8, 5*8) self.num_wave_start_pos = lazy.util.position(26*8, 8*8) self.wave_box_pos = lazy.util.position(26*8, 9*8) self.wave_box_dim = lazy.util.dimension(16, 32) self.arrow_animation = ModAnimate({299, 315}, 25) self.upgrade_prompt_color_toggle = true self.wave_text_color_toggle = true self.gameover_text_color_toggle = true end function StateLevelRenderer:render() self:_draw_startfield() self.logic.particle_handler_bg:render() -- spaceship if self.logic.state ~= self.logic.STATES.TRANS_GAMEOVER and self.logic.state ~= self.logic.STATES.GAMEOVER then self.logic.spaceship:render() self.flame_animation:render( FRAME, self.logic.control_ball.x + 4 + self.logic.camera.xs, self.logic.spaceship.y + 16 - 2 + self.logic.camera.ys ) end -- ! deprecated -- bullets for _, proj in ipairs(self.logic.projectiles) do spr( self.logic.weapon_id, proj.x, proj.y, 0, 1, 0, 0, 1, 1 ) end -- ! deprecated for _, enemy in ipairs(self.logic.enemies) do spr( self.logic.enemy_id, enemy.x, enemy.y, 0, 1, 0, 0, 2, 2 ) end -- entity rendering self.logic.enemy_handler:sort( function(a, b) return a.render_prio < b.render_prio end ) self.logic.enemy_handler:render() self.logic.exp_handler:render() self.logic.sfx_handler:render() self.logic.particle_handler:render() self.logic.projectile_handler:render() self.logic.enemy_projectile_handler:render() if DEMO_STRING ~= nil then print(DEMO_STRING, 1, CONFIG.GAME.VIEWPORT.H - 7, 15) end self:_draw_gameover() -- debug code self:_debug() end function StateLevelRenderer:overlay() self:_draw_wave_text() self:_draw_ui() -- control ball render self.logic.control_ball:render() -- cursor if self.logic.control_ball.logic.tap_hold and self.logic.state == self.logic.STATES.GAME then local x = lazy.math.bound(CURSOR.x - 8, 0, CONFIG.GAME.VIEWPORT.W - 8 - 8) spr( self.cursor_id, x, self.logic.control_ball.y, 0, 1, 0, 0, 2, 2 ) end if not self.logic.control_ball.logic.tap_hold and self.logic.state == self.logic.STATES.GAME then local x = self.logic.control_ball.x + 8 - 4 local y = self.logic.control_ball.y - 2 + 2 * math.sin(FRAME / 15) self.arrow_animation:render(FRAME, x, y) end self:_draw_updrade_text() end function StateLevelRenderer:_debug() if not DEBUG_MODE then return end if self.logic.debug_report_timeout > 0 then local x = 2 local y = CONFIG.GAME.VIEWPORT.H - 8 print(self.logic.debug_report, x+1, y+1, 2) print(self.logic.debug_report, x, y, 15) end -- local fps = FPS:getValue() local frame = lazy.math.round((fps / 60), 0.1) if DEBUG_DISPLAY then local a = 0.1 local round = lazy.math.round print("Entity #: "..round(self.logic.entity_count_avg, a).." : "..self.logic.entity_count, 1, 1, 15) print("Projectile #: "..round(self.logic.projectile_count_avg, a).." : "..self.logic.projectile_count, 1, 1 + 8, 15) print("Particle #: "..round(self.logic.particle_count_avg, a).." : ".. self.logic.particle_count, 1, 1 + 8 * 2, 15) print("FPS #: "..round(self.logic.fps_avg).." : "..fps.. " FRM: "..round(self.logic.frame_avg, 0.1).." : "..frame, 1, 1 + 8 * 3, 15) print("MEM KB: "..round(collectgarbage("count")).." | GC STEP: "..GC_STEP, 1, 1 + 8 * 4, 15) end local c = 3 if fps > 58 then c = 11 elseif fps > 56 then c = 9 end local s = 6 if DEBUG_SHOW_COLOR_FPS then rect(0, 0, s, s, c) rectb(0, 0, s, s, 0) end if DEBUG_WAVE_DISPLAY then local wave_time = self.logic.current_wave_time local wave_time_sec = lazy.math.round(wave_time / 60, 0.01) print("W.TIME " .. wave_time_sec .. ":" .. wave_time, 1, 1) local COMPLEXITY_MAVG = lazy.math.round(self.logic.METRICS.MAVG.CAVG, 0.01) print("COMPL " .. COMPLEXITY_MAVG, 1, 1+8) end if DEBUG_BOUNDINGBOX then lazy.tic.draw_box_class(self.logic.spaceship, 15, CONFIG.GLOBAL.KEY_HITBOX) self.logic.enemy_handler:loop( function(enemy) lazy.tic.draw_box_class(enemy, 15, CONFIG.GLOBAL.KEY_HITBOX) end ) self.logic.projectile_handler:loop( function(proj) lazy.tic.draw_box_class(proj, 15, CONFIG.GLOBAL.KEY_HITBOX) end ) self.logic.enemy_projectile_handler:loop( function(proj) lazy.tic.draw_box_class(proj, 15, CONFIG.GLOBAL.KEY_HITBOX) end ) end end function StateLevelRenderer:_draw_digits(x, y, num, length) local digit_count = 0 if num > 0 then digit_count = string.len(tostring(num)) else digit_count = length or string.len(tostring(num)) end if num < 0 then local char = 1 if num == -1 then char = 11 elseif num == -2 then char = 12 end local digit_sid = DATA.SPR_NUM[char] for i = digit_count, 1, -1 do spr( digit_sid, x + 8 * (i - 1), y, 0, 1, 0, 0, 1, 1 ) end else for i = digit_count, 1, -1 do local get_digit = num % 10 num = math.floor(num / 10) local digit_sid = DATA.SPR_NUM[get_digit + 1] spr( digit_sid, x + 8 * (i - 1), y, 0, 1, 0, 0, 1, 1 ) end end end function StateLevelRenderer:_draw_startfield() self.starfield_bg:_update_warp() self.starfield_mg:_update_warp() self.starfield_fg:_update_warp() self.starfield_bg:_draw_map(self.logic.camera, 30, 0, 90, 0) self.starfield_mg:_draw_map(self.logic.camera, 150, 0, 150, 0) -- spr(128, 148, 30, 0, 2, 0, 0, 4, 4) lazy.tic.set_palette_from_table(CONFIG.GLOBAL.QUASAR_PAL) if self.logic.quasar_mode then self.quasar_anim:render(FRAME, self.quasar_pos.x, self.quasar_pos.y, nil) if FRAME % 12 == 0 then local cx = self.quasar_pos.x + 16 local cy = self.quasar_pos.y + 16 PARTICLE_FACTORY.quasar(self.logic.particle_handler_bg, cx, cy) end end lazy.tic.palette_reset() self.starfield_fg:_draw_map(self.logic.camera, 60, 0, 120, 0) -- ? quasar???? -- spr(128, 148, 30, 0, 2, 0, 0, 4, 4) -- spr(id, x, y, [colorkey=-1], [scale=1], [flip=0], [rotate=0], [w=1], [h=1]) -- self.starfield_fg.y = lazy.math.lerp(self.starfield_fg.a, 0, SCREEN.HEIGHT) -- map(60, 0, 30, 17, self.starfield_fg.x, self.starfield_fg.y - SCREEN.HEIGHT, 0) -- map(60, 0, 30, 17, self.starfield_fg.x, self.starfield_fg.y, 0) -- star field background -- local x = self.logic.camera.xs -- local y = lazy.math.round(self.starfield_1_x_scroll) + self.logic.camera.ys -- map(30, 0, 30, 17, x, y + -SCREEN.HEIGHT) -- map(30, 0, 30, 17, x, y) -- x = self.logic.camera.xs -- y = lazy.math.round(self.starfield_2_x_scroll) + self.logic.camera.ys -- map(60, 0, 30, 17, x, y + -SCREEN.HEIGHT, 0) -- map(60, 0, 30, 17, x, y, 0) -- -- starfield update -- local a = self.logic.spaceship.logic.level / CONFIG.SPACESHIP.MAX_LEVEL -- local speed_mod = 1 + STARFIELD_LEVEL_MUL * a -- self.starfield_1_x_scroll = (self.starfield_1_x_scroll + self.starfield_1_x_scroll_speed * speed_mod) % SCREEN.HEIGHT -- self.starfield_2_x_scroll = (self.starfield_2_x_scroll + self.starfield_2_x_scroll_speed * speed_mod) % SCREEN.HEIGHT end function StateLevelRenderer:_draw_ui() -- UI -- ui screen local pal = CONFIG.UI.UI_DEFAULT_PAL if self.logic.spaceship.logic.flashing:is_flashing() then pal = CONFIG.UI.UI_HURT_PAL elseif self.logic.boss_mode then pal = CONFIG.UI.UI_BOSS_PAL end map(0, 0, 30, 17, 0, 0, 0) lazy.tic.set_palette_from_table(pal) map(30, 17, 30, 17, 0, 0, 0) lazy.tic.palette_reset() -- hp bar local hp_w = math.ceil((1 - self.logic.spaceship.logic:getHpPercent()) * self.hp_bar_dim.w) rect(self.hp_bar_pos.x, self.hp_bar_pos.y, hp_w, self.hp_bar_dim.h, 1) -- exp bar local exp_w = math.ceil( (1 - self.logic.spaceship.logic:getExpPercent())^2 * self.exp_bar_dim.w) rect(self.exp_bar_pos.x, self.exp_bar_pos.y, exp_w, self.exp_bar_dim.h, 1) -- level number bar local level = self.logic.spaceship.logic.level if level == CONFIG.SPACESHIP.MAX_LEVEL then level = -2 end self:_draw_digits(self.num_level_start_pos.x, self.num_level_start_pos.y, level, 2) -- wave box ui lazy.tic.set_palette_from_table(CONFIG.UI.WAVE_NUM_PAL) local wave = (self.logic.quasar_mode and -1 or self.logic.wave) self:_draw_digits(self.num_wave_start_pos.x, self.num_wave_start_pos.y, wave, 2) lazy.tic.palette_reset() -- wave marker local x = self.wave_box_pos.x + self.wave_box_dim.w local y = self.wave_box_pos.y + lazy.math.round((self.wave_box_dim.h - 8) * (1 - self.logic.wave_percent)) spr( 46, x - 1, y, 0, 1, 0, 0, 1, 1 ) -- 25 15 -- score lazy.tic.set_palette_from_table(CONFIG.UI.SCORE_NUM_PAL) self:_draw_digits(25 * 8 + 1, 15 * 8 - 1, self.logic.score) lazy.tic.palette_reset() lazy.tic.set_palette_from_table(CONFIG.UI.SCORE_TOP_NUM_PAL) self:_draw_digits(25 * 8 + 1, 16 * 8 - 1, self.logic.score_top) lazy.tic.palette_reset() end function StateLevelRenderer:_draw_wave_text() if self.logic.wave_text_timer > 0 and self.logic.state == self.logic.STATES.GAME and self.logic.wave > 0 then if FRAME % 15 == 0 then self.wave_text_color_toggle = not self.wave_text_color_toggle end local colorbg = 2 local color1 = 13 local color2 = 15 if self.logic.quasar_mode then color1 = 10 color2 = 15 end -- quasar rush colors are different -- TODO FIXME timming issue, displays wrong text on single frame local wave_text = lazy.util.fif(self.logic.quasar_mode, "Rush", "Wave "..self.logic.wave) local t_width = print(wave_text, -32, -32, 15, false, 4, false) local t_heigh = 6 * 4 - 1 local x = CONFIG.GAME.VIEWPORT.W / 2 - t_width / 2 local y = CONFIG.GAME.VIEWPORT.H / 2 - t_heigh / 2 print(wave_text, x + 2, y + 2, colorbg, false, 4, false) if self.wave_text_color_toggle then print(wave_text, x, y, color1, false, 4, false) else print(wave_text, x, y, color2, false, 4, false) end end end function StateLevelRenderer:_draw_updrade_text() if self.logic.upgrade_prompt.time ~= 0 then local colorbg = 2 local color1 = 3 local color2 = 15 local x = 2 local y = CONFIG.GAME.VIEWPORT.H + 8 - 3 if FRAME % 5 == 0 then self.upgrade_prompt_color_toggle = not self.upgrade_prompt_color_toggle end print( self.logic.upgrade_prompt.text, x+1, y+1, colorbg, false, 1, false ) if self.upgrade_prompt_color_toggle then print( self.logic.upgrade_prompt.text, x, y, color1, false, 1, false ) else print( self.logic.upgrade_prompt.text, x, y, color2, false, 1, false ) end end end function StateLevelRenderer:_draw_gameover() if self.logic.state == self.logic.STATES.GAMEOVER then local color1 = 9 local color2 = 15 local color3 = 14 local color4 = 6 local colorbg1 = 3 local colorbg2 = 5 local colorbg3 = 2 local size = 3 local inc = 1 local width = print("GAME OVER", -100, -100, color1, false, size) local x = CONFIG.GAME.VIEWPORT.W / 2 - width / 2 local y = CONFIG.GAME.VIEWPORT.H / 2 - (size * 6 / 2 - 1) print("GAME OVER", x + inc * 3, y + inc * 3, colorbg3, false, size) print("GAME OVER", x + inc * 2, y + inc * 2, colorbg2, false, size) print("GAME OVER", x + inc * 1, y + inc * 1, colorbg1, false, size) if FRAME % 20 == 0 then self.gameover_text_color_toggle = not self.gameover_text_color_toggle end if self.gameover_text_color_toggle then print("GAME OVER", x, y, color1, false, size) else print("GAME OVER", x, y, color2, false, size) end if self.logic.has_new_score then local msg = "NEW HSCORE" local size = 2 y = y + 21 local width = print(msg, -100, -100, color1, false, size) x = CONFIG.GAME.VIEWPORT.W / 2 - width / 2 print(msg, x+1, y, color4, false, size) print(msg, x, y+1, color4, false, size) print(msg, x-1, y, color4, false, size) print(msg, x, y-1, color4, false, size) print(msg, x, y, color3, false, size) end end end function StateLevelRenderer:_create_startfield(warp_time_max) local startfield = {} startfield.x = 0 startfield.x_top = 0 startfield.y = 0 startfield.warp_time_max = warp_time_max startfield.warp_time = warp_time_max startfield.a = 1 startfield._update_warp = function(self) self.warp_time = self.warp_time - 1 self.a = 1 - self.warp_time / self.warp_time_max self.y = lazy.math.lerp(self.a, 0, SCREEN.HEIGHT) if self.warp_time == 0 then self.warp_time = self.warp_time_max self.a = 0 startfield.x = startfield.x_top startfield.x_top = -math.random(0, SCREEN.WIDTH) end end startfield._draw_map = function(self, camera, mx0, my0, mx1, my1) self.y = lazy.math.lerp(self.a, 0, SCREEN.HEIGHT) map(mx0, my0, 30, 17, self.x + camera.xs, self.y + camera.ys, 0) map(mx1, my1, 30, 17, self.x + camera.xs + SCREEN.WIDTH, self.y + camera.ys, 0) map(mx0, my0, 30, 17, self.x_top + camera.xs, self.y + camera.ys - SCREEN.HEIGHT, 0) map(mx1, my1, 30, 17, self.x_top + camera.xs + SCREEN.WIDTH, self.y + camera.ys - SCREEN.HEIGHT, 0) end return startfield end end, ["states.intro"] = function() -- Intro state: Main screen in the game StateIntro = lazy.extend("StateLevel", State) local TEXT_START = "START GAME" local TEXT_CREDITS = "Made by "..META.AUTHOR.." | jadedpuma.com" local TEXT_TOUCH = "OR TOUCH SCREEN" local TEXT_VERSION = META.VERSION function StateIntro:constructor() self.starfield_1 = { normal = lazy.math.normalAngleDegree(310), speed = 0.2, pos = lazy.util.position(0, 0) } self.starfield_2 = { normal = lazy.math.normalAngleDegree(310), speed = 0.4, pos = lazy.util.position(0, 0) } if DEMO_MODE then TEXT_CREDITS = TEXT_CREDITS.." DEMO" end self.quasar_base_id = 128 self.quasar_flash_id = 192 self.quasar_flash_timeout = 150 self.quasar_flash_time_mod = 5 self.quasar_flash_toggle = false self.quasar_flash_time_max = 25 self.quasar_flash_time = 0 -- mouse self.mouse_animation = ModAnimate({480, 482}, 40, 2) --- timeout to prevent accidental clicking self.intro_click_timeout = 15 self.top = pmem(0) music(1) end function StateIntro:update(dt) if self.intro_click_timeout ~= 0 then self.intro_click_timeout = self.intro_click_timeout - 1 else if CURSOR.left then STATE_MANAGER:next(TransitionSlide( STATE_MANAGER, CONFIG.UI.TRANSITION_TIME, self, StateLevel() )) music(-1) end end end function StateIntro:render() self:_draw_startfield() self:_draw_logo() self:_draw_credits() self:_draw_mouse() self:_draw_version() end function StateIntro:_draw_startfield() -- star field background self.starfield_1.pos.x = self.starfield_1.pos.x + self.starfield_1.speed * self.starfield_1.normal.x self.starfield_1.pos.y = self.starfield_1.pos.y + self.starfield_1.speed * self.starfield_1.normal.y self.starfield_1.pos.x = self.starfield_1.pos.x % SCREEN.WIDTH self.starfield_1.pos.y = self.starfield_1.pos.y % SCREEN.HEIGHT local x, y = self.starfield_1.pos.x, self.starfield_1.pos.y map(30, 0, 30, 17, x, y) map(30, 0, 30, 17, x - SCREEN.WIDTH, y) map(30, 0, 30, 17, x, y - SCREEN.HEIGHT) map(30, 0, 30, 17, x - SCREEN.WIDTH, y - SCREEN.HEIGHT) -- Quasar local quasar_id = self.quasar_base_id if FRAME % self.quasar_flash_timeout == 0 then self.quasar_flash_time = self.quasar_flash_time_max end if self.quasar_flash_time ~= 0 then self.quasar_flash_time = self.quasar_flash_time - 1 if FRAME % self.quasar_flash_time_mod == 0 then self.quasar_flash_toggle = not self.quasar_flash_toggle end if self.quasar_flash_toggle then quasar_id = self.quasar_flash_id end end local x = SCREEN.WIDTH - 32 - 20 spr(quasar_id, x, 35, 0, 1, 0, 0, 4, 4) self.starfield_2.pos.x = self.starfield_2.pos.x + self.starfield_2.speed * self.starfield_2.normal.x self.starfield_2.pos.y = self.starfield_2.pos.y + self.starfield_2.speed * self.starfield_2.normal.y self.starfield_2.pos.x = self.starfield_2.pos.x % SCREEN.WIDTH self.starfield_2.pos.y = self.starfield_2.pos.y % SCREEN.HEIGHT local x, y = self.starfield_2.pos.x, self.starfield_2.pos.y map(60, 0, 30, 17, x, y, 0) map(60, 0, 30, 17, x - SCREEN.WIDTH, y, 0) map(60, 0, 30, 17, x, y - SCREEN.HEIGHT, 0) map(60, 0, 30, 17, x - SCREEN.WIDTH, y - SCREEN.HEIGHT, 0) end function StateIntro:_draw_logo() local x = SCREEN.WIDTH / 2 - 32 - 16 local y = SCREEN.HEIGHT / 2 - 32 - 16 spr(132, x, y, 0, 1, 0, 0, 8, 8) spr(140, x + 64, y, 0, 1, 0, 0, 4, 4) spr(204, x + 64, y + 32, 0, 1, 0, 0, 4, 4) end function StateIntro:_draw_credits() local color = 5 local colorbg = 2 local x = 1 local y = SCREEN.HEIGHT - 8 * 2 print(TEXT_CREDITS, x+1 , y+1 + 8, colorbg) print(TEXT_CREDITS, x , y + 8, color) end function StateIntro:_draw_mouse() local x = 1 local y = 1 self.mouse_animation:render(FRAME, x, y) local color = 15 local colorbg = 2 local color_faint = 5 y = y + 2 x = x + 17 print(TEXT_START, x+1 ,y+1, colorbg) print(TEXT_START, x ,y, color) x = x + 5*12 local top = "- HSCORE "..self.top print(top, x+1 ,y+1, colorbg) print(top, x ,y, color) x = 18 y = y + 8 print(TEXT_TOUCH, x ,y, color_faint) end function StateIntro:_draw_version() -- TEXT_VERSION local width = print(TEXT_VERSION, -100, -100, 0) print(TEXT_VERSION, SCREEN.WIDTH - width, 3, 2) end end, ["states.level_logic"] = function() -- State to handle logic on the "level" StateLevelLogic = lazy.class("StateLevelLogic") local STATE_GAMEOVER_TIMEOUT_MAX = 60 * 4 local PARTICLE_LIMIT = 500 function StateLevelLogic:constructor(base_state) self.base_state = base_state self.STATES = lazy.enum( "SHIP_SELECT", "INTRO", "GAME", "NEW_WAVE", "TRANS_GAMEOVER", "GAMEOVER" ) self.state = self.STATES.INTRO self.state_timeout = 0 self.camera = Camera(0, 0, CONFIG.GAME.VIEWPORT.W, CONFIG.GAME.VIEWPORT.H) self.play_music = true self.current_track_id = -1 self.score = 0 self.score_top = pmem(0) self.current_wave_time = 0 self.wave_time_max = 0 self.wave = 0 self.current_wave = nil self.wave_percent = 0 self.quasar_mode = false self.quasar_level = -1 self.boss_mode = false self.hard_mode = DEBUG_ENABLE_HARDMODE self.wave_text_timer = CONFIG.LEVEL.WAVE_TEXT_TIMER_MAX self.has_new_score = false -- control entity self.control_ball = ControlBallEntity( self, CONFIG.GAME.VIEWPORT.W / 2 - 8, CONFIG.CONTROLBALL.Y, 16, 16 ) -- spaceship entity self.spaceship = SpaceshipEntity(self, self.control_ball.x, CONFIG.GAME.VIEWPORT.H, 16, 16) local hb = CONFIG.SPACESHIP.HITBOX self.spaceship:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hb.X, hb.Y, hb.W, hb.H) -- intro control self.intro_time_max = 12 -- 120 self.intro_time = self.intro_time_max -- ! depricated self.projectiles = {} self.weapon_id = 269 self.weapon_recharge_total = 40 self.weapon_recharge_current = 1 self.projectiles_ids = {269, 285} self.projectiles_ids_mode3 = {266, 282} self.frame_time = 3 self.projectile_spaceship_anim_base = ModAnimate({269, 285}, 3) self.projectile_spaceship_anim_sin = ModAnimate({266, 282}, 4) self.projectile_enemy_shooter_anim = ModAnimate({268, 284}, 3) self.projectile_enemy_organic_anim = ModAnimate({267, 283}, 5) self.projectile_spaceship_anim_rocket = ModAnimate({265, 281}, 5) self.enemy_projectile_ids = {268, 284} self.enemy_organic_projectile_ids = {267, 283} self.projectile_speed = 3.5 self.projectile_handler = EntityHandler() self.enemy_projectile_handler = EntityHandler() -- ! depricated self.temp_enemy_timer = 0 self.temp_enemy_timer_max = 60 * 2 self.enemy_id = 320 self.enemies = {} self.enemy_speed = 0.1 self.enemy_handler = EntityHandler() self.exp_handler = EntityHandler() self.sfx_handler = EntityHandler() self.particle_handler = EntityHandler(PARTICLE_LIMIT) self.particle_handler_bg = EntityHandler(PARTICLE_LIMIT) -- upgrade text self.upgrade_prompt = { text = "", time = 0, max_time = 60*3, x = 0, y = 0, } self.controlled_random_drop = ControlledRandom(CONFIG.GLOBAL.DROP_RATE) if DEBUG_MODE then self:_debug_set_wave(DEBUG_WAVE_CONFIG.WAVE) self:_debug_set_level(DEBUG_WAVE_CONFIG.LEVEL) self.METRICS = { MAVG = { N = 1, CAVG = 0, RESET = function (self) self.N = 1 self.CAVG = 0 end, CALC = function (self, newV) self.CAVG = self.CAVG + (newV - self.CAVG)/self.N self.N = self.N + 1 end } } -- setup hard mode if self.hard_mode then self:_debug_set_level(99) self.spaceship.logic.enable_rocket = true end end self.debug_report = "" self.debug_report_timeout = 0 --- ? debug -- self.score = 1234 -- self:_debug_set_level(99) -- self:_debug_set_wave(23) if DEBUG_MODE then self.entity_count = 0 self.projectile_count = 0 self.particle_count = 0 self.entity_count_avg = 0 self.projectile_count_avg = 0 self.particle_count_avg = 0 self.fps_avg = 0 self.frame_avg = 0 end end function StateLevelLogic:start() -- start music -- music(0) self:_play_track(DATA.TRACK.SPACE) end function StateLevelLogic:update(dt) self.camera:update(dt) if self.state_timeout > 0 then self.state_timeout = self.state_timeout - 1 end if self.state == self.STATES.INTRO then self:_state_intro() elseif self.state == self.STATES.GAME then -- debug self:_debug() -- game state self:_state_game() elseif self.state == self.STATES.NEW_WAVE then self:_state_new_wave() elseif self.state == self.STATES.TRANS_GAMEOVER then self:_state_trans_gameover() elseif self.state == self.STATES.GAMEOVER then self:_state_gameover() end end function StateLevelLogic:spawn_projectile(source, handler, x, y, w, h, normal, speed, acc, update_func, mod_animate) -- (game_logic, x, y, w, h, normal, speed, acc, update_func, mod_animate) local projectile = ProjectileEntity( source, self, x, y, normal, speed, acc, update_func, mod_animate ) projectile:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, 0, 0, w, h) handler:add(projectile) end function StateLevelLogic:particle_explosion(cx, cy) for i = 1, 13 do PARTICLE_FACTORY.explosion(self.particle_handler, cx, cy) end end local slowdown_timeout = 60 function StateLevelLogic:_debug() if not DEBUG_MODE then return end if self.current_wave_time == 0 then self.METRICS.MAVG:RESET() end if self.debug_report_timeout > 0 then self.debug_report_timeout = self.debug_report_timeout - 1 end local complexity = self.enemy_handler:size() + self.enemy_projectile_handler:size() + self.exp_handler:size() + self.projectile_handler:size() self.METRICS.MAVG:CALC(complexity) -- 27 = 0 -- 28 = 1 -- 29 = 2 -- 30 = 3 -- 31 = 4 -- 32 = 5 -- 33 = 6 -- 34 = 7 -- 35 = 8 -- 36 = 9 -- 63 = CTRL -- 64 = SHIFT -- 65 = ALT -- 01 = A -- 02 = B -- 03 = C -- 04 = D -- 05 = E -- 06 = F -- 07 = G -- 08 = H -- 09 = I -- 10 = J -- 11 = K -- 12 = L -- 13 = M -- 14 = N -- 15 = O -- 16 = P -- 17 = Q -- 18 = R -- 19 = S -- 20 = T -- 21 = U -- 22 = V -- 23 = W -- 24 = X -- 25 = Y -- 26 = Z -- command -- -- ctrl 63 local command_key = true -- 64 -- 63 if key(command_key) then if self:_debug_key(28) then DEBUG_DISPLAY = not DEBUG_DISPLAY DEBUG_WAVE_DISPLAY = false end -- bound boxes if self:_debug_key(29) then DEBUG_BOUNDINGBOX = not DEBUG_BOUNDINGBOX end -- hp debug if self:_debug_key(30) then DEBUG_HP = not DEBUG_HP if DEBUG_HP then self:_debug_report("Invincibility ON") else self:_debug_report("Invincibility OFF") end end -- wave info if self:_debug_key(31) then DEBUG_WAVE_DISPLAY = not DEBUG_WAVE_DISPLAY DEBUG_DISPLAY = false end -- DEBUG WAVE if self:_debug_key(32) then DEBUG_WAVE = false end end -- game -- -- hp -- q if self:_debug_key(17) then self.spaceship.logic.hp = lazy.math.lower_bound(self.spaceship.logic.hp - 120, 0) self:_debug_report("Damage") end if self:_debug_key(16) then self.spaceship.logic.hp = 0 self:_debug_report("Kill") end -- TODO hp gain -- E level + 1 if self:_debug_key(05) then self.spaceship.logic.exp = lazy.math.upper_bound(self.spaceship.logic.exp + CONFIG.SPACESHIP.EXP, CONFIG.SPACESHIP.EXP) self:_debug_report("+ level") end -- level + 10 if self:_debug_key(18) then -- self.spaceship.logic.exp = lazy.math.upper_bound(self.spaceship.logic.exp + CONFIG.SPACESHIP.EXP, CONFIG.SPACESHIP.EXP) self:_debug_set_level(5) self:_debug_report("+5 level") end -- level 99 if self:_debug_key(20) then self:_debug_set_level(99) self:_debug_report("Max level") end -- y 25 - next wave if self:_debug_key(25) then self.current_wave_time = 0 self:_debug_report("Next wave") self:_debug_set_level(3) end -- wave 44 = GRAVE if self:_debug_key(44) then self:_wipe_screen() self:_debug_report("Kill enemies") end if DEBUG_WAVE then -- zombie -- a if key(01) then local index = self:_get_index_from_key() local config_key = WAVE_KEY[index] if config_key then self:_debug_spawn(EnemyZombieShip, WAVE_CONFIG.ZOMBIE[config_key]) end end -- s if key(19) then local index = self:_get_index_from_key() local config_key = WAVE_KEY[index] if config_key then self:_debug_spawn(EnemyShooter, WAVE_CONFIG.SHOOTER[config_key]) end end -- d if key(04) then local index = self:_get_index_from_key() local config_key = WAVE_KEY[index] if config_key then self:_debug_spawn(EnemyPeek, WAVE_CONFIG.PEEK[config_key]) end end -- f 06 if key(06) then local index = self:_get_index_from_key() local config_key = WAVE_KEY[index] if config_key then self:_debug_spawn(EnemyMedusa, WAVE_CONFIG.MEDUSA[config_key]) end end -- g 07 if key(07) then local index = self:_get_index_from_key() local config_key = WAVE_KEY[index] if config_key then self:_debug_spawn(EnemyKamikaze, WAVE_CONFIG.KAMIKAZE[config_key]) end end -- h 08 if key(08) then local index = self:_get_index_from_key() local config_key = WAVE_KEY[index] if config_key then self:_debug_spawn(EnemyBounce, WAVE_CONFIG.BOUNCE[config_key]) end end -- j 10 if key(10) then local index = self:_get_index_from_key() local config_key = WAVE_KEY[index] if config_key then self:_debug_spawn(EnemyOrganic, WAVE_CONFIG.ORGANIC[config_key]) end end -- k 11 if key(11) then local index = self:_get_index_from_key() local config_key = WAVE_KEY[index] if config_key then self:_debug_spawn(EnemySeeker, WAVE_CONFIG.SEEKER[config_key]) end end -- 12 = L if self:_debug_key(12) then self:_debug_spawn(EnemyHeart, { 99999, 10 }) end end local fps = FPS:getValue() local frame = lazy.math.round((fps / 60), 0.1) self.entity_count = self.enemy_handler:size() + sEelf.exp_handler:size() + self.sfx_handler:size() self.projectile_count = self.projectile_handler:size() + self.enemy_projectile_handler:size() self.particle_count = self.particle_handler:size() local time = 60 * 3 self.fps_avg = lazy.math.rollingAverage(self.fps_avg, fps, time) self.frame_avg = lazy.math.rollingAverage(self.frame_avg, frame, time) time = 15 self.entity_count_avg = lazy.math.rollingAverage(self.entity_count_avg, self.entity_count, time) self.projectile_count_avg = lazy.math.rollingAverage(self.projectile_count_avg, self.projectile_count, time) self.particle_count_avg = lazy.math.rollingAverage(self.particle_count_avg, self.particle_count, time) if DEBUG_SLOWDOWN_DETECT then if slowdown_timeout > 0 then slowdown_timeout = slowdown_timeout - 1 elseif slowdown_timeout == 0 and fps < 58 then slowdown_timeout = 60 local total = self.entity_count + self.projectile_count + self.particle_count --self.entity_count_avg = lazy.math.rollingAverage(self.entity_count_avg, entity_count, time) --self.projectile_count_avg = lazy.math.rollingAverage(self.projectile_count_avg, projectile_count, time) --self.particle_count_avg = lazy.math.rollingAverage(self.particle_count_avg, particle_count, time) trace("-----") trace("SLOWDOWN @ wave "..self.wave) trace("FPS: "..fps) trace("MEM: entity:"..self.entity_count.." proj:"..self.projectile_count.." par: "..self.particle_count.." total:"..total) trace("MEM AVG: entity:"..self.entity_count_avg.." proj:"..self.projectile_count_avg.." par: "..self.particle_count_avg) end end end local KEY_HOLD = 10 local KEY_AUTO = 15 function StateLevelLogic:_debug_key(key) return keyp(key, KEY_HOLD, KEY_AUTO) end function StateLevelLogic:_debug_report(text) self.debug_report = text self.debug_report_timeout = 90 end function StateLevelLogic:_get_index_from_key() for key = 28, 36 do if keyp(key, KEY_HOLD, KEY_AUTO) then return key - 27 end end if keyp(27, KEY_HOLD, KEY_AUTO) then return 10 end return -1 end function StateLevelLogic:_debug_set_wave(wave) self.wave = lazy.math.bound(wave - 1, 0, #WAVE_DATA - 1) end function StateLevelLogic:_debug_set_level(level) for i = 1, level do self.spaceship.logic.exp = CONFIG.SPACESHIP.EXP self.spaceship.logic:checkLevelUp() UPGRADE.apply(self.spaceship, self.control_ball) end end function StateLevelLogic:_debug_spawn(Klass, config) local enemy = Klass(self, config) enemy:spawn() end function StateLevelLogic:_state_intro() -- self.state = self.STATES.GAME self.intro_time = self.intro_time - 1 local a = 1 - self.intro_time / self.intro_time_max local t = lazy.tween.func.easeOutBack(a) local y = lazy.math.lerp(t, CONFIG.GAME.VIEWPORT.H + 32, CONFIG.SPACESHIP.Y) self.spaceship.y = lazy.math.round(y) self.spaceship:update(1) if self.intro_time == 0 then self.state = self.STATES.GAME end end function StateLevelLogic:_state_game() self:_game_wave_setup() if self.wave_text_timer ~= 0 then self.wave_text_timer = self.wave_text_timer - 1 end if self.upgrade_prompt.time ~= 0 then self.upgrade_prompt.time = self.upgrade_prompt.time - 1 end -- Updates self.projectile_handler:update(dt) self.enemy_projectile_handler:update(dt) self.enemy_handler:update(dt) self.control_ball:update(dt) self.spaceship:update(dt) self.exp_handler:update(dt) self.sfx_handler:update(dt) self.particle_handler:update(dt) self.particle_handler_bg:update(dt) self:_game_enemy_update() self:_game_collisions() -- particle spawning: projectiles self.projectile_handler:loop( function(projectile) if FRAME % 2 == 0 then PARTICLE_FACTORY.bullet_trail(self.particle_handler, projectile.x, projectile.y) end end ) self.enemy_projectile_handler:loop( function(projectile) if FRAME % 3 == 0 then -- PARTICLE_FACTORY.enemy_bullet_trail(self.particle_handler, projectile.x, projectile.y) PARTICLE_FACTORY.enemy_bullet_trail(self.particle_handler, projectile.x, projectile.y) end end ) -- check level up if self.spaceship.logic:checkLevelUp() then UPGRADE.apply(self.spaceship, self.control_ball) self.upgrade_prompt.text = UPGRADE.getText(self.spaceship) self.upgrade_prompt.time = self.upgrade_prompt.max_time self.upgrade_prompt.x = CONFIG.GAME.VIEWPORT.W / 2 self.upgrade_prompt.y = CONFIG.GAME.VIEWPORT.H - 8 end -- check gameover if DEBUG_HP and self.spaceship.logic.hp == 0 then self.spaceship.logic.hp = CONFIG.SPACESHIP.HP self:_debug_report("HP is 0") end if self.spaceship.logic.hp == 0 then self.state = self.STATES.TRANS_GAMEOVER self.has_new_score = self.score > self.score_top self.upgrade_prompt.time = 0 self.wave_text_timer = 0 -- update to remove entity self.enemy_handler:update(dt) self:particle_explosion(self.spaceship.x + self.spaceship.w / 2, self.spaceship.y + self.spaceship.h / 2) self.sfx_handler:add(SfxEntity(self.spaceship.x, self.spaceship.y, DATA.GFX.EXPLOSION)) DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() self.camera:shake(5, 90) end end function StateLevelLogic:_state_trans_gameover() -- Update objects --self.projectile_handler:update(dt) --self.enemy_projectile_handler:update(dt) --self.enemy_handler:update(dt) --self.control_ball:update(dt) --self.spaceship.x = self.control_ball.x --self.spaceship:update(dt) --self.exp_handler:update(dt) self.sfx_handler:update(dt) self.particle_handler:update(dt) if self.sfx_handler:isEmpty() and self.particle_handler:isEmpty() then self.state = self.STATES.GAMEOVER self.state_timeout = STATE_GAMEOVER_TIMEOUT_MAX end end function StateLevelLogic:_state_gameover() if self.state_timeout == 0 then pmem(0, self.score) STATE_MANAGER:next(TransitionSlide( STATE_MANAGER, CONFIG.UI.TRANSITION_TIME, self.base_state, StateIntro() )) end end function StateLevelLogic:_state_new_wave() self.projectile_handler:update(1) self.enemy_handler:update(1) self.spaceship:update(1) if self.enemy_handler:isEmpty() then self.projectile_handler:clear() self.score = self.score + CONFIG.GLOBAL.SCORE.PER_WAVE self.state = self.STATES.GAME end end function StateLevelLogic:_wipe_screen() self.enemy_handler:loop( function(enemy) enemy.logic:kill() end ) self.enemy_projectile_handler:loop( function(entity) entity.delete = true end ) self.projectile_handler:loop( function(entity) -- entity.delete = true end ) if not self.enemy_handler:isEmpty() then -- DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() end end function StateLevelLogic:_game_wave_setup() if DEBUG_WAVE then return end -- colelct garbage collectgarbage("collect") collectgarbage("stop") -- setup wave if self.current_wave_time == 0 then -- set wave self.wave = self.wave + 1 -- reset demo wave if DEMO_MODE and self.wave > #WAVE_DATA then self.wave = 1 end -- reset quasar level if self.wave > #WAVE_DATA then -- self.wave = self.quasar_level self.wave = 1 self.quasar_mode = false self.hard_mode = true self.quasar_level = -1 self:_play_track(DATA.TRACK.SPACE) -- rocket check if not self.spaceship.enable_rocket then self.spaceship.logic.enable_rocket = true self.upgrade_prompt.text = "+ Rocket" self.upgrade_prompt.time = self.upgrade_prompt.max_time self.upgrade_prompt.x = CONFIG.GAME.VIEWPORT.W / 2 self.upgrade_prompt.y = CONFIG.GAME.VIEWPORT.H - 8 self.spaceship.logic.flashing:start(30) end end self.current_wave = WAVE_DATA[self.wave] -- reset track from boss mode if self.boss_mode then if self.quasar_level == -1 then -- self:_play_track(DATA.TRACK.QUASAR) self:_play_track(DATA.TRACK.SPACE) end end -- set mode self.boss_mode = false self.wave_time_max = CONFIG.LEVEL.WAVE_TIMEOUT if self.current_wave.set_boss_mode then self.boss_mode = true local last_boss_offset = (self.wave == #WAVE_DATA) and 1400 or 0 self.wave_time_max = CONFIG.LEVEL.WAVE_BOSS_TIMEOUT + last_boss_offset if self.quasar_level == -1 then self:_play_track(DATA.TRACK.BOSS) end end if DEMO_MODE then self.wave_time_max = self.wave_time_max / 2 end self.current_wave_time = self.wave_time_max if self.current_wave.set_quasar_mode then if self.quasar_level == -1 then self:_play_track(DATA.TRACK.QUASAR) end self.quasar_mode = true self.quasar_level = self.wave end self.current_wave:initiate(self) self.state = self.STATES.NEW_WAVE self:_wipe_screen() self.wave_text_timer = CONFIG.LEVEL.WAVE_TEXT_TIMER_MAX return else self.current_wave_time = self.current_wave_time - 1 end -- wave update self.wave_percent = self.current_wave_time / self.wave_time_max self.current_wave:exec(self) end function StateLevelLogic:_game_enemy_update() self.enemy_handler:loop( function(enemy_entity) -- ship vs enemy collision local death_by_player = false if not enemy_entity:typeof(EnemyPeek) and lazy.util.bouding_box_collision(CONFIG.GLOBAL.KEY_HITBOX, enemy_entity, self.spaceship) then enemy_entity.logic:kill() --self.spaceship. if enemy_entity.logic.damage ~= nil then local penalty = enemy_entity.logic.use_alt_pal and CONFIG.GLOBAL.DAMAGE_COLLISION_PENALTY or 0 local damage = enemy_entity.logic.damage + penalty self.spaceship.logic.hp = lazy.math.lower_bound(self.spaceship.logic.hp - damage, 0) else local penalty = enemy_entity.logic.use_alt_pal and CONFIG.GLOBAL.DAMAGE_COLLISION_PENALTY or 0 local damage = CONFIG.GLOBAL.DAMAGE_COLLISION + penalty self.spaceship.logic.hp = lazy.math.lower_bound(self.spaceship.logic.hp - damage, 0) -- lazy.log("WARNING: nil 'collision' damage value.") end self.spaceship.logic.flashing:start() self.camera:shake(3, 20) end -- enemy vs player projectile collision self.projectile_handler:loop( function(projectile_entity) if lazy.util.bouding_box_collision(CONFIG.GLOBAL.KEY_HITBOX, enemy_entity, projectile_entity) then if enemy_entity.logic.hp == 0 then return end projectile_entity.delete = true if projectile_entity.logic.explode then projectile_entity.game_logic:particle_explosion(projectile_entity.x + projectile_entity.w / 2, projectile_entity.y + projectile_entity.h / 2) projectile_entity.game_logic.sfx_handler:add(SfxEntity(projectile_entity.x, projectile_entity.y, DATA.GFX.EXPLOSION_KAMIKAZE)) DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() DATA.CAMERA.ENEMY_EXPLOSION:shake(projectile_entity.game_logic.camera) local damage = CONFIG.SPACESHIP.PROJECTILE_ROCKET_DAMAGE + CONFIG.SPACESHIP.PROJECTILE_ROCKET_WAVE_ADD * self.wave enemy_entity.logic.hp = lazy.math.lower_bound(enemy_entity.logic.hp - damage, 0) enemy_entity.logic.flashing:start() elseif enemy_entity.logic:is_immune() then DATA.SFX[DATA.SFX.INDEX.IMMUNE_BULLET]:play() else enemy_entity.logic.hp = lazy.math.lower_bound(enemy_entity.logic.hp - 10, 0) enemy_entity.logic.flashing:start() DATA.SFX[DATA.SFX.INDEX.BULLET_COLLIDE]:play() end death_by_player = true end end ) --- TODO needs to be in the enemy update function? if enemy_entity.logic.hp == 0 then enemy_entity.logic:kill() if death_by_player then -- local exp_entity = enemy_entity.logic:drop_item() local type_i = self.controlled_random_drop:next() local exp_entity = ExpEnitity( type_i, enemy_entity.x + 8 - 4, enemy_entity.y + 8 - 4 ) self.exp_handler:add(exp_entity) -- auto pickup item if below player if exp_entity.y > self.spaceship.y and exp_entity.state ~= exp_entity.STATES.IN then exp_entity.state = exp_entity.STATES.IN exp_entity:setInEntity(self.spaceship) end -- score self.score = self.score + CONFIG.GLOBAL.SCORE.SHIP_KILL end end -- check enemy death if false then enemy_entity.logic:kill() -- enemy_entity.delete = true -- no drops if collision if death_by_player then local type_i = UTILITY.get_weighted_value(CONFIG.GLOBAL.DROP_RATE_TEST) self.exp_handler:add(ExpEnitity( type_i, enemy_entity.x + 4, enemy_entity.y + 4 )) end if enemy_entity:typeof(EnemyPeek) then elseif enemy_entity:typeof(EnemyKamikaze) then elseif enemy_entity:typeof(EnemyOrganic) then elseif enemy_entity:typeof(EnemyZombieShip) then else self:particle_explosion(enemy_entity.x + enemy_entity.w / 2, enemy_entity.y + enemy_entity.h / 2) self.sfx_handler:add(SfxEntity(enemy_entity.x, enemy_entity.y, DATA.GFX.EXPLOSION)) end self:_game_increase_score(1) DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() self.camera:shake(1, 20) end end ) end --- TODO move to entity logic function StateLevelLogic:_game_collisions() -- collide enemy projectiles vs spaceship self.enemy_projectile_handler:loop( function(proj) if lazy.util.bouding_box_collision(CONFIG.GLOBAL.KEY_HITBOX, self.spaceship, proj) then if self.spaceship.logic.hp == 0 then return end if proj.source.logic.projectile_damage ~= nil then local penalty = proj.source.logic.use_alt_pal and CONFIG.GLOBAL.DAMAGE_PROJECTILE_PENALTY or 0 local damage = proj.source.logic.projectile_damage + penalty self.spaceship.logic.hp = lazy.math.lower_bound(self.spaceship.logic.hp - damage, 0) else local damage = CONFIG.GLOBAL.DAMAGE_PROJECTILE self.spaceship.logic.hp = lazy.math.lower_bound(self.spaceship.logic.hp - damage, 0) end self.spaceship.logic.flashing:start() proj.delete = true DATA.SFX[DATA.SFX.INDEX.BULLET_COLLIDE]:play() self.camera:shake(3, 20) end end ) -- collide exp capsule vs spaceship self.exp_handler:loop( function(exp_entity) local dist_e = lazy.math.distance( exp_entity.x + 4, exp_entity.y + 4, self.spaceship.x + 8, self.spaceship.y + 8 ) if dist_e < CONFIG.SPACESHIP.PICK_UP_RADIUS and exp_entity.state ~= exp_entity.STATES.IN then exp_entity.state = exp_entity.STATES.IN exp_entity:setInEntity(self.spaceship) end end ) -- EnemyPeek wall stop -- TODO mode to entity logic self.enemy_handler:loop( function(enemy) if enemy:typeof(EnemyPeek) then local s_y = self.spaceship.y + CONFIG.SPACESHIP.HITBOX.Y + CONFIG.SPACESHIP.HITBOX.H local e_y = enemy.y local spaceship_x0 = self.spaceship.x + CONFIG.SPACESHIP.HITBOX.X local spaceship_w = CONFIG.SPACESHIP.HITBOX.W local spaceship_x1 = spaceship_x0 + spaceship_w if e_y < s_y and spaceship_x1 > enemy.x and spaceship_x1 < enemy.x + 8 then self.control_ball.x = enemy.x - spaceship_w - CONFIG.SPACESHIP.HITBOX.X self.spaceship.x = self.control_ball.x elseif e_y < s_y and spaceship_x0 < enemy.x + 16 and spaceship_x0 > enemy.x + 8 then self.control_ball.x = enemy.x + spaceship_w + CONFIG.SPACESHIP.HITBOX.X self.spaceship.x = self.control_ball.x end end end ) end function StateLevelLogic:_game_increase_score(value) self.score = lazy.math.upper_bound(self.score + value, CONFIG.GLOBAL.SCORE_MAX) end function StateLevelLogic:_explode_enemy(enemy) -- TODO implementation end function StateLevelLogic:_play_track(id) if DEBUG_MODE and DEBUG_NO_MUSIC then return end if self.play_music then music(id) self.current_track_id = id end end end, ["states.level"] = function() -- Container class for logic anbd renderer for "level" StateLevel = lazy.extend("StateLevel", State) function StateLevel:constructor() self.logic = StateLevelLogic(self) self.renderer = StateLevelRenderer(self.logic) end function StateLevel:start() self.logic:start() end function StateLevel:update(dt) self.logic:update(dt) end function StateLevel:render() self.renderer:render() end function StateLevel:overlay() self.renderer:overlay() end end, ["core.config"] = function() -- Configuration data for game logic local SPACESHIP_DOWN_SPREAD = 60 local SPACESHIP_UP_SPREAD = 17 local SHOOTER_M2_SPREAD = 25 -- local SPACESHIP_BASE_HP = 1000 local CONVERT_PAL = function(indexes, values) local pal = {} for i = 0, 15 do table.insert(pal, i) end for i, pal_index in ipairs(indexes) do pal[pal_index+1] = values[i] end return pal end local SAME_COLOR_PAL = function(color) local pal = {} for i = 0, 15 do table.insert(pal, color) end return pal end CONFIG = { GAME = { FPS = 60, SCREEN = { W = 240, H = 136 }, VIEWPORT = { X = 0, Y = 0, W = 200, H = 120 } }, UI = { WAVE_NUM_PAL = {0, 1, 2, 3, 4, 3, 6, 14, 8, 9, 10, 11, 12, 13, 14, 15}, SCORE_NUM_PAL = {0, 1, 2, 3, 4, 7, 6, 14, 8, 9, 10, 11, 12, 13, 14, 15}, SCORE_TOP_NUM_PAL = {0, 1, 2, 3, 4, 2, 6, 5, 8, 9, 10, 11, 12, 13, 14, 15}, TRANSITION_TIME = 18, UI_DEFAULT_PAL = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, UI_HURT_PAL = CONVERT_PAL( {7}, {10} ), UI_BOSS_PAL = CONVERT_PAL( {7, 11}, {3, 9} ), }, GLOBAL = { KEY_HITBOX = "HITBOX", FLASH_TIME = 5, FLASH_PALATTE = {0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}, PAL_DEFAULT = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, PAL_MODE = { -- {0, 1, 2, 3, 4, 5, 6, L, 8, 9, 10, D, 12, 13, 14, 15}, -- d / l - 11 / 7 -- default green [1] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, -- blue l /d 8 / 13 [2] = {0, 1, 2, 3, 4, 5, 6, 8, 8, 9, 10, 13, 12, 13, 14, 15}, -- yellow d / l 14 /9 [3] = {0, 1, 2, 3, 4, 5, 6, 9, 8, 9, 10, 14, 12, 13, 14, 15}, -- red [4] = {0, 1, 2, 3, 4, 5, 6, 10, 8, 9, 10, 3, 12, 13, 14, 15}, -- purple [5] = {0, 1, 2, 3, 4, 5, 6, 6, 8, 9, 10, 4, 12, 13, 14, 15}, -- white [6] = {0, 1, 2, 3, 4, 5, 6, 15, 8, 9, 10, 15, 12, 13, 14, 15}, -- fusion [7] = CONVERT_PAL( {2, 7, 11}, {4, 6, 14}), -- hard mode [8] = CONVERT_PAL( {1, 2, 5, 12}, {6, 3, 10, 15}), -- hard mode pal -- TODO -- {0, 1, 2, 3, 4, 5, 6, 6, 8, 9, 10, 14, 12, 13, 14, 15}, }, IMMUNE_PAL = CONVERT_PAL( {1, 2, 5, 12}, {2, 4, 12, 15} ), QUASAR_PAL = CONVERT_PAL( {8, 13, 15}, {5, 8, 13} ), -- {0, 1, 4, 3, 4, 13, 6, 7, 8, 9, 10, 11, 15, 13, 14, 15}, DAMAGE_COLLISION = 100, DAMAGE_PROJECTILE = 50, DAMAGE_COLLISION_PENALTY = 50, DAMAGE_PROJECTILE_PENALTY = 30, SCORE_MAX = 99999, SCORE = { SHIP_KILL = 1, EXP_ORB = 1, EXP_MAX_ORB = 4, HEART_ORB = 2, PER_WAVE = 10, }, DROP_TYPE = lazy.enum("EXP", "LEVEL", "HEART"), DROP_RATE = { {1, 0, 0}, {2, 23, 5}, {3, 40, 5} }, DROP_RATE_TEST = {950, 40, 10}, DROP_RATE_BASE = {850, 50, 100}, DROP_RATE_CORRUPT = {700, 100, 200}, DROP_HEART_HEALTH = 400, }, HARDMODE = { PAL = 8, HP_INCREASE = 35, HP_COR_INC = 180, HP_MUL = 0.85 }, LEVEL = { WAVE_TIMEOUT = 20 * 60, WAVE_BOSS_TIMEOUT = 38 * 60, WAVE_TEXT_TIMER_MAX = 100, }, CONTROLBALL = { X = 10 * 8, Y = 15 * 8, }, SPACESHIP = { Y = 96, HP = 1000, MOVE_SPEED = 1.1, EXP = 50, EXP_PER_CAPSULE = 10, MAX_LEVEL = 99, WEAPON_RECHARGE= 40, PICK_UP_RADIUS = 20, PROJECTILE_SPEED = 3.1, PROJECTILE_SIN_SPEED = 2.6, PROJECTILE_NORMAL_UP = lazy.math.normalAngleDegree(270), PROJECTILE_NORMAL_WEAPON1_LANE1 = lazy.math.normalAngleDegree(270), PROJECTILE_NORMAL_WEAPON2_LANE1 = lazy.math.normalAngleDegree(90 - SPACESHIP_DOWN_SPREAD), PROJECTILE_NORMAL_WEAPON2_LANE2 = lazy.math.normalAngleDegree(90 + SPACESHIP_DOWN_SPREAD), PROJECTILE_NORMAL_WEAPON3_LANE1 = lazy.math.normalAngleDegree(270 - SPACESHIP_UP_SPREAD), PROJECTILE_NORMAL_WEAPON3_LANE2 = lazy.math.normalAngleDegree(270 + SPACESHIP_UP_SPREAD), PROJECTILE_NORMAL_ROCKET = lazy.math.normalAngleDegree(270), PROJECTILE_ROCKET_TIME_MOD = 75, PROJECTILE_ROCKET_TIME_MOD_SUB = 1, PROJECTILE_ROCKET_ACC = 0.05, PROJECTILE_ROCKET_SPEED = 0.2, PROJECTILE_ROCKET_DAMAGE = 80, PROJECTILE_ROCKET_WAVE_ADD = 3, MAX_UPGRADE = { MOVE_SPEED = 2.00, ATTACK_RATE = -32, BULLET_SPEED = 1.33, }, HITBOX = { X = 2, Y = 2+3, W = 12, H = 12-3 } }, ENEMY = { ZOMBIE_SHIP = { DAMAGE_COLLISION = 200, -- 5 kill DAMAGE_SIZE_2 = 400, HITBOX = { X = 2, Y = 2, W = 11, H = 11 } }, MEDUSA = { AMP = 36, DAMAGE_COLLISION = 100, -- HITBOX = { X = 2, Y = 2, W = 11, H = 11 } }, KAMIKAZE = { SPAWN_HEIGHT_MIN = 10, SPAWN_HEIGHT_MAX = 35, DAMAGE_COLLISION = 130, DAMAGE_COLLISION_CORRUPT = 100, -- adds plus 50 to 150 HITBOX = { X = 2, Y = 2, W = 11, H = 11 } }, SHOOTER = { MODE1_PROJ_NORMAL = lazy.math.normalAngleDegree(90), MODE2_PROJ_NORMAL1 = lazy.math.normalAngleDegree(90 - SHOOTER_M2_SPREAD), MODE2_PROJ_NORMAL2 = lazy.math.normalAngleDegree(90 + SHOOTER_M2_SPREAD), SPAWN_HEIGHT_MIN = 1, SPAWN_HEIGHT_MAX = 40, SHOOT_COOLDOWN = 25, SHOOT_ANTICIPATION = 35, SHOOT_BUFFER = 11, DAMAGE_PROJECTILE = 100, -- 10 kill HITBOX = { X = 2, Y = 2, W = 11, H = 11 } }, PEEK = { PEEK_TIME = 90, PEEK_AMOUNT = 16 * 3, ATTACK_TIME = 60*5, HITBOX = { X = 2, Y = 2, W = 11, H = 11 } }, ORGANIC = { AMP = 20, X_BUFFER = 14, ANGLE_TIME_MAX = 500, MOVE_SPEED_ANTICIPATION_MUL = 0, SHOOT_COOLDOWN = 25, SHOOT_ANTICIPATION = 35, DAMAGE_PROJECTILE = 150, HITBOX = { X = 2, Y = 2, W = 11, H = 29 } }, BOUNCE = { BOUNCE_COOLDOWN = 10, DAMAGE_COLLISION = 150, -- HITBOX = { X = 2, Y = 2, W = 11, H = 11 } }, SEEKER = { DAMAGE_PROJECTILE = 100, -- 10 kill HITBOX = { X = 0, Y = 0, W = 8*3, H = 8 } }, HEART = { TRANS_TIME = 35, -- PAL_IN = {1, 2, 3, 10}, -- PAL_OUT = {10, 3, 2, 1}, PAL_INTRO = SAME_COLOR_PAL(10), DAMAGE_PROJECTILE = 200, -- 5 kill HITBOX = { X = 0+2, Y = 0+4+4, W = 31-2, H = 31-6-8 } } } } end, ["core.wave_entry_pool"] = function() -- Deprecated WaveEntryPool = lazy.class("WaveEntryPool") function WaveEntryPool:constructor(...) self.entries = {...} self.current_entry = nil end function WaveEntryPool:initiate(game_logic) local rand_i = math.random(#self.entries) self.current_entry = self.entries[rand_i] self.current_entry:initiate(game_logic) end function WaveEntryPool:exec(game_logic) self.current_entry:exec(game_logic) end end, ["core.particle_factory"] = function() -- A Factory pattern to spawn particles local data = function(maxtime, color_table) return { maxtime = maxtime, color_table = color_table } end local dir = function(x, y) return { x = x, y = y } end local generic_trail = function(handler, x, y, spread, data) -- 4 x 4 size local new_x = x + math.random(0, spread) local new_y = y + math.random(0, spread) local _data = data local particle = Particle(new_x, new_y, dir(0, 0), 0, 0, _data.maxtime, _data.color_table) handler:add(particle) end local PARTICLE_DEFINITION = {} PARTICLE_DEFINITION.BULLET = data(15, {15, 11, 7, 7, 2, 1}) PARTICLE_DEFINITION.BULLET_ENEMY = data(15, {15, 10, 6, 6, 3, 1}) PARTICLE_DEFINITION.AIM_ATTACK = data(60, {13, 13, 8, 8, 4, 2, 1}) PARTICLE_DEFINITION.BOUNCE = data(60, {10, 10, 9, 3, 3, 2}) PARTICLE_DEFINITION.DEBRIS = data(0, {12, 12, 5, 5, 5, 2, 2, 1}) PARTICLE_DEFINITION.QUASAR = data(0, {6, 6, 6, 5, 5, 2, 1, 1}) PARTICLE_FACTORY = {} PARTICLE_FACTORY.bullet_trail = function(handler, x, y) generic_trail(handler, x, y, 4, PARTICLE_DEFINITION.BULLET) end PARTICLE_FACTORY.enemy_bullet_trail = function(handler, x, y) generic_trail(handler, x, y, 4, PARTICLE_DEFINITION.BULLET_ENEMY) end PARTICLE_FACTORY.aim_attack_trail = function(handler, x, y) generic_trail(handler, x, y, 2, PARTICLE_DEFINITION.AIM_ATTACK) end PARTICLE_FACTORY.bounce = function(handler, x, y) generic_trail(handler, x, y, 2, PARTICLE_DEFINITION.BOUNCE) end PARTICLE_FACTORY.explosion = function(handler, cx, cy) local t_min = 50 local t_max = 70 local t_dt = t_max - t_min local v_min = 0.1 local v_max = 0.5 local v_dt = v_max - v_min local acc = -0.0005 local tau = 2 * math.pi -- calculate normal vector local angle = math.random() * tau local new_vx = math.cos(angle) local new_vy = math.sin(angle) local color_table = PARTICLE_DEFINITION.DEBRIS.color_table local new_t = t_min + math.random() * t_dt local new_vel = v_min + math.random() * v_dt local particle = Particle(cx, cy, dir(new_vx, new_vy), new_vel, acc, new_t, color_table) handler:add(particle) end PARTICLE_FACTORY.quasar = function(handler, cx, cy) local t_min = 120 local t_max = 150 local t_dt = t_max - t_min local v_min = 0.05 local v_max = 0.24 local v_dt = v_max - v_min local acc = 0.0005 local tau = 2 * math.pi local angle = math.random() * tau local new_vx = math.cos(angle) local new_vy = math.sin(angle) local color_table = PARTICLE_DEFINITION.QUASAR.color_table local new_t = t_min + math.random() * t_dt local new_vel = v_min + math.random() * v_dt local particle = Particle(cx, cy, dir(new_vx, new_vy), new_vel, acc, new_t, color_table) handler:add(particle) end end, ["core.debug_fps"] = function() -- Golbal var to keep track of FPS FPS = { lastTime = tstamp(), frame = 0, fps = 60, getValue = function(self) self.frame = self.frame + 1 if tstamp() ~= self.lastTime then self.lastTime = tstamp() self.fps = self.frame self.frame = 0 end return self.fps end } end, ["core.wave"] = function() -- Class to handle wave spawning Wave = lazy.class("Wave") function Wave:constructor(enemy_entries, set_boss_mode, set_quasar_mode) self.enemy_entries = enemy_entries self.set_quasar_mode = set_quasar_mode or false self.set_boss_mode = set_boss_mode or false assert(type(enemy_entries) == "table", "enemy_entries not a table") end function Wave:initiate(game_logic) for _, enemy_entry in ipairs(self.enemy_entries) do enemy_entry:initiate(game_logic) end end function Wave:exec(game_logic) for _, enemy_entry in ipairs(self.enemy_entries) do enemy_entry:exec(game_logic) end end end, ["core.utility"] = function() -- Global utility for game logic UTILITY = {} UTILITY.conver_direction_spr = function(dir) if dir == 1 then return 0 else return 1 end end local INIT_PRIO = 1000000 local PRIO_MUL = 1000 local ALT_MUL = 2 local PAL_MODE_MUL = 100 local PAL_MODE_TOTAL = #CONFIG.GLOBAL.PAL_MODE local PRIO_COUNT = 0 UTILITY.set_draw_priority = function(prio, alt, pal_mode) local pal_value = (PAL_MODE_TOTAL - pal_mode) * PAL_MODE_MUL local alt_mul = (alt and ALT_MUL or 0) local prio_value = INIT_PRIO * alt_mul + prio * PRIO_MUL - pal_value - PRIO_COUNT PRIO_COUNT = PRIO_COUNT + 1 PRIO_COUNT = PRIO_COUNT % PAL_MODE_MUL return prio_value end UTILITY.get_weighted_value = function(weight_table) local total = 0 for _, weight in ipairs(weight_table) do total = total + weight end local choose = math.random(0, total) local previous = 0 for i, weight in ipairs(weight_table) do if choose <= previous + weight then return i else previous = previous + weight end end end --- FIXME: should be part of the renderer class UTILITY.get_pal = function(entity) local pal = CONFIG.GLOBAL.PAL_MODE[entity.logic.pal_mode] if entity.logic:is_immune() then pal = CONFIG.GLOBAL.IMMUNE_PAL end return pal end --- FIXME: should be part of the renderer class UTILITY.get_anim = function(entity) local anim = entity.renderer.animation if entity.logic.use_alt_pal then anim = entity.renderer.animation_alt end return anim end UTILITY.get_num_spread = function(start_value, end_value, slices) local slice_tbl = {} local slice = (end_value - start_value) / slices for i = 0, slices do table.insert(slice_tbl, start_value + i * slice) end return slice_tbl end end, ["core.wave_enemy_entry_mod_time"] = function() -- Class to spwawn enemies based on frame count intervals WaveEnemyEntryModTime = lazy.extend("WaveEnemyEntryModTime", WaveEnemyEntryBase) function WaveEnemyEntryModTime:constructor(Klass, spawn_number, spawn_data, spawn_time_max) WaveEnemyEntryModTime.super:initialize(self, Klass, spawn_number, spawn_data) self.spawn_time_max = spawn_time_max self.spawn_time = 0 assert(self.spawn_time_max > 0, "self.spawn_time_max must be positive integer") end function WaveEnemyEntryModTime:initiate(game_logic) self.spawn_time = self.spawn_time_max end function WaveEnemyEntryModTime:exec(game_logic) if self.spawn_time == 0 then self:initiate() self:spawn(game_logic) else self.spawn_time = self.spawn_time - 1 end end end, ["core.wave_enemy_entry_range"] = function() -- spawn enemies with a range of time between them. WaveEnemyEntryRange = lazy.extend("WaveEnemyEntryRange", WaveEnemyEntryBase) function WaveEnemyEntryRange:constructor(Klass, spawn_number, spawn_data, spawn_time_min, spawn_time_spread) WaveEnemyEntryRange.super:initialize(self, Klass, spawn_number, spawn_data) self.spawn_time_min = spawn_time_min self.spawn_time_max = spawn_time_min + spawn_time_spread assert(self.spawn_time_min > 0, "spawn_time_min must be positive integer") assert(self.spawn_time_max > 0, "spawn_time_max must be positive integer") self.spawn_time = 0 end function WaveEnemyEntryRange:initiate(game_logic) self.spawn_time = math.random(self.spawn_time_min, self.spawn_time_max) end function WaveEnemyEntryRange:exec(game_logic) if self.spawn_time == 0 then self:initiate() self:spawn(game_logic) else self.spawn_time = self.spawn_time - 1 end end end, ["core.wave_enemy_entry_base"] = function() -- Abstract class to control spawning enemies WaveEnemyEntryBase = lazy.class("WaveEnemyEntryBase") function WaveEnemyEntryBase:constructor(Klass, spawn_number, spawn_data) self.Klass = Klass self.spawn_number = spawn_number self.spawn_data = spawn_data assert(Klass, "Wave Klass is nil") assert(self.spawn_number > 0, "spawn_number must me positive interger") assert(type(self.spawn_data) == "table", "spawn_data must be table") end function WaveEnemyEntryBase:initiate(game_logic) end function WaveEnemyEntryBase:exec(game_logic) end function WaveEnemyEntryBase:spawn(game_logic) local enemies = {} for n = 1, self.spawn_number do table.insert(enemies, self:single_spawn(game_logic)) end return enemies end function WaveEnemyEntryBase:single_spawn(game_logic) local enemy = self.Klass(game_logic, self.spawn_data) enemy:spawn() return enemy end end, ["core.globals"] = function() -- Deprecated file GLOBAL = { TOP_SCORE = 0, LAST_SCORE = 0, UNLOCKED_SHIP = false, MUSIC = true, } end, ["core.wave_data"] = function() -- This file will contaion wave data for each wave -- this is the way to control the increase in challenge for waves WAVE_DATA = {} local ZOMBIE = WAVE_CONFIG.ZOMBIE local ZOMBIE_KAMIKAZE = WAVE_CONFIG.ZOMBIE_KAMIKAZE local SHOOTER = WAVE_CONFIG.SHOOTER local PEEK = WAVE_CONFIG.PEEK local MEDUSA = WAVE_CONFIG.MEDUSA local KAMIKAZE = WAVE_CONFIG.KAMIKAZE local BOUNCE = WAVE_CONFIG.BOUNCE local ORGANIC = WAVE_CONFIG.ORGANIC local SEEKER = WAVE_CONFIG.SEEKER local trigger = function(num) local tbl = {} local max_time = 1.0 local frac_time = max_time / (num + 1) for i = 1, num do table.insert(tbl, frac_time * i) end return tbl end local trigger_table = function(...) return { ... } end local TRIGGER = { AT_10 = trigger_table(1/10), AT_25 = trigger_table(1/4), AT_50 = trigger_table(1/2), AT_75 = trigger_table(3/4), AT_90 = trigger_table(9/10), -- WITH_1_AT_1_2_TIME = trigger(1), -- same as AT_50 WITH_2_AT_1_3_TIME = trigger(2), WITH_3_AT_1_4_TIME = trigger(3), WITH_4_AT_1_5_TIME = trigger(4), WITH_5_AT_1_6_TIME = trigger(5), } DEBUG_WAVE_CONFIG = { LEVEL = 0, WAVE = 1, SET = function(wave, level) DEBUG_WAVE_CONFIG.WAVE = wave DEBUG_WAVE_CONFIG.LEVEL = level end, } -- debug wave local DEBUG_WAVE = false if DEBUG_WAVE then table.insert(WAVE_DATA, -- Wave( -- { -- WaveEntryPool( -- WaveEnemyEntryOnStart(EnemySeeker, 1, SEEKER["GREEN_BASE"]), -- WaveEnemyEntryOnStart(EnemySeeker, 1, SEEKER["BOSS"]) -- ) -- } -- ) -- WavePool( -- { -- { -- WaveEnemyEntryOnStart(EnemySeeker, 1, SEEKER["GREEN_BASE"]), -- WaveEnemyEntryOnStart(EnemySeeker, 1, SEEKER["GREEN_BASE"]), -- }, -- { -- WaveEnemyEntryOnStart(EnemySeeker, 1, SEEKER["BOSS"]) -- } -- } -- ) Wave( { WaveEnemyEntryAlways(EnemySeeker, 1 , SEEKER["GREEN_BASE"], 600) } ) ) end if DEMO_MODE then -- DEBUG_WAVE_CONFIG.SET(10, 0) DEMO_STRING = "DEMO" table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyZombieShip, 8, ZOMBIE["GREEN_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyShooter, 3, SHOOTER["GREEN_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyPeek, 2, PEEK["GREEN_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyMedusa, 2, MEDUSA["GREEN_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyKamikaze, 2, KAMIKAZE["GREEN_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyBounce, 1, BOUNCE["GREEN_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyOrganic, 1, ORGANIC["GREEN_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyZombieShip, 1, ZOMBIE["GREEN_BASE"]), WaveEnemyEntryAlways(EnemyZombieShip, 1, ZOMBIE["BLUE_BASE"]), WaveEnemyEntryAlways(EnemyZombieShip, 1, ZOMBIE["YELLOW_BASE"]), WaveEnemyEntryAlways(EnemyZombieShip, 1, ZOMBIE["RED_BASE"]), WaveEnemyEntryAlways(EnemyZombieShip, 1, ZOMBIE["PURPLE_BASE"]), WaveEnemyEntryAlways(EnemyZombieShip, 1, ZOMBIE["WHITE_BASE"]), WaveEnemyEntryAlways(EnemyZombieShip, 1, ZOMBIE["FUSION_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyZombieShip, 1, ZOMBIE["BOSS"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyOrganic, 1, ORGANIC["IMMUNE"]), } ) ) else --[[ w3 = lvl8-10 w6 = lvl18 w9 = lvl22-24 w12 = lvl30 w15 = 34-35 w18 = 34? ]]-- local level_per_wave = 99 / 27 local wave = DEBUG_START_WAVE local level = math.floor(wave * level_per_wave) DEBUG_WAVE_CONFIG.SET(wave, level) -- ? NOTE: DON"T optimize early, just build a bunch of waves. Leave balace for last!!!!! -- ? NOTE: start with hard mobs first and reduce difficulty. -- ? NOTE: Each wave introcuces something new, introduces a new mechanic. -- ? NOTE: Make sure waves feel different and unique. -- Zombie waves 1 2 3 table.insert(WAVE_DATA, Wave( { -- ? NOTE: Teaches controls shootings and basic mechanics. WaveEnemyEntryModTime(EnemyZombieShip, 4, ZOMBIE["GREEN_BASE"], 60), WaveEnemyEntryModTime(EnemyZombieShip, 2, ZOMBIE_KAMIKAZE["GREEN_BASE"], 140), WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["GREEN_BASE"], 60 * 4, 60 * 2), } ) ) table.insert(WAVE_DATA, Wave( { -- ? NOTE: Teaches about enemy types WaveEnemyEntryModTime(EnemyZombieShip, 2, ZOMBIE["BLUE_BASE_SLOW"], 60), WaveEnemyEntryModTime(EnemyZombieShip, 2, ZOMBIE_KAMIKAZE["BLUE_BASE"], 120), WaveEnemyEntryOnTrigger(EnemyShooter, 1, SHOOTER["GREEN_BASE"], TRIGGER.WITH_3_AT_1_4_TIME), } ) ) table.insert(WAVE_DATA, Wave( { -- ? NOTE: Introducing to boss mechanics --WaveEnemyEntryOnStart(EnemyZombieShip, 1, ZOMBIE["BOSS_1"]), WaveEnemyEntryOnStart(EnemyZombieShip, 1, ZOMBIE["BOSS_1"]), WaveEnemyEntryOnTrigger(EnemyZombieShip, 1, ZOMBIE["BOSS_2"], TRIGGER.AT_50), WaveEnemyEntryModTime(EnemyZombieShip, 2, ZOMBIE_KAMIKAZE["BOSS_1"], 60*2), WaveEnemyEntryAlways(EnemyMedusa, 1, MEDUSA["GREEN_BASE"], 60*3), -- WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["GREEN_BASE"], 60*10), }, true ) ) -- Wall Waves 4 5 6 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyPeek, 1, PEEK["GREEN_BASE_MID"]), WaveEnemyEntryOnTrigger(EnemyPeek, 1, PEEK["GREEN_BASE_MID"], TRIGGER.WITH_2_AT_1_3_TIME), WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["GREEN_BASE"], 60), WaveEnemyEntryTimed(EnemyShooter, 1, SHOOTER["GREEN_BASE"], 60*4), WaveEnemyEntryAlways(EnemyMedusa, 2, MEDUSA["GREEN_BASE"], 60*3), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyPeek, 2, PEEK["GREEN_BASE"]), WaveEnemyEntryTimed(EnemyPeek, 2, PEEK["GREEN_BASE"], 60*7), WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["BLUE_BASE"]), WaveEnemyEntryTimed(EnemyShooter, 1, SHOOTER["BLUE_BASE"], 60*5), WaveEnemyEntryOnTrigger(EnemyMedusa, 1, MEDUSA["GREEN_BASE"], TRIGGER.WITH_3_AT_1_4_TIME) } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyPeek, 1, PEEK["BOSS"]), WaveEnemyEntryTimed(EnemyMedusa, 1, MEDUSA["GREEN_BASE_P2"], 60*7), WaveEnemyEntryModTime(EnemyZombieShip, 2, ZOMBIE["BLUE_BASE"], 40), WaveEnemyEntryModTime(EnemyZombieShip, 3, ZOMBIE["BLUE_BASE"], 80) }, true ) ) -- kamikaze waves 7 8 9 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyKamikaze, 1, KAMIKAZE["GREEN_BASE"]), WaveEnemyEntryAlways(EnemyKamikaze, 2, KAMIKAZE["GREEN_BASE"], 60*4), WaveEnemyEntryAlways(EnemyKamikaze, 1, KAMIKAZE["GREEN_BASE"], 60*15, 30), WaveEnemyEntryAlways(EnemyMedusa, 1, MEDUSA["BLUE_BASE"], 60*4), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyPeek, 1, PEEK["GREEN_BASE"]), WaveEnemyEntryTimed(EnemyPeek, 3, PEEK["GREEN_BASE"], 60*7), WaveEnemyEntryAlways(EnemyKamikaze, 1, KAMIKAZE["GREEN_BASE"]), WaveEnemyEntryOnTrigger(EnemyKamikaze, 1, KAMIKAZE["GREEN_BASE"], TRIGGER.WITH_5_AT_1_6_TIME), WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["YELLOW_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyKamikaze, 2, KAMIKAZE["BOSS"], 30, 10), WaveEnemyEntryAlways(EnemyMedusa, 1, MEDUSA["BLUE_BASE"], 120), WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["BLUE_BASE"], 180), }, true ) ) -- bounce waves 10 11 12 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnTrigger(EnemySeeker, 1, SEEKER["GREEN_BASE_MID"], TRIGGER.AT_50), WaveEnemyEntryAlways(EnemyBounce, 1, BOUNCE["GREEN_BASE"]), WaveEnemyEntryAlways(EnemyZombieShip, 4, ZOMBIE["YELLOW_BASE"]), WaveEnemyEntryTimed(EnemyPeek, 1, PEEK["BLUE_BASE"], 60*4), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyBounce, 1, BOUNCE["GREEN_IMMUNE"]), WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["YELLOW_BASE"]), WaveEnemyEntryOnTrigger(EnemyMedusa, 1, MEDUSA["BLUE_BASE_P5"], TRIGGER.WITH_3_AT_1_4_TIME), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyPeek, 1, PEEK["BLUE_IMMUNE_MID"]), WaveEnemyEntryAlways(EnemyBounce, 1, BOUNCE["BOSS"]), WaveEnemyEntryOnTrigger(EnemySeeker, 1, SEEKER["GREEN_BASE_IMMUNE"], TRIGGER.AT_10), WaveEnemyEntryOnTrigger(EnemySeeker, 1, SEEKER["GREEN_BASE_IMMUNE_ALT"], TRIGGER.AT_50) }, true ) ) -- shooter waves 13 14 15 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnTrigger(EnemyZombieShip, 2, ZOMBIE_KAMIKAZE["YELLOW_BASE"], TRIGGER.WITH_4_AT_1_5_TIME), WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["BLUE_BASE"]), WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["YELLOW_BASE"]), WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["WHITE_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnTrigger(EnemySeeker, 1, SEEKER["GREEN_BASE_MID"], TRIGGER.WITH_2_AT_1_3_TIME), WaveEnemyEntryAlways(EnemyBounce, 1, BOUNCE["BLUE_BASE"]), WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["WHITE_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["BOSS"], 60), WaveEnemyEntryTimed(EnemyKamikaze, 1, KAMIKAZE["BLUE_BASE"], 300), WaveEnemyEntryTimed(EnemyMedusa, 1, MEDUSA["BLUE_BASE_M2_P5"], 400), }, true ) ) -- medusa waves 16 17 18 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyMedusa, 1, MEDUSA["BLUE_BASE_M9_P8"]), WaveEnemyEntryTimed(EnemyMedusa, 1, MEDUSA["YELLOW_BASE_M3_P6"], 60*4), WaveEnemyEntryAlways(EnemySeeker, 1, SEEKER["YELLOW_BASE"], 60*8), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyMedusa, 1, MEDUSA["YELLOW_BASE_M3_P6"]), WaveEnemyEntryAlways(EnemyBounce, 1, BOUNCE["YELLOW_BASE"], 60*2), WaveEnemyEntryOnTrigger(EnemyMedusa, 1, MEDUSA["BLUE_BASE_M12_P8"], TRIGGER.WITH_3_AT_1_4_TIME), WaveEnemyEntryOnTrigger(EnemyPeek, 2, PEEK["YELLOW_BASE"], TRIGGER.AT_50), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemySeeker, 2, SEEKER["BLUE_BASE"], 60*5), WaveEnemyEntryAlways(EnemyMedusa, 1, MEDUSA["BOSS"]), WaveEnemyEntryTimed(EnemyMedusa, 1, MEDUSA["PURPLE_BASE_BOSS"], 60*6), }, true ) ) -- oganic waves 19 20 21 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyOrganic, 1, ORGANIC["GREEN_BASE"]), WaveEnemyEntryOnTrigger(EnemyMedusa, 1, MEDUSA["YELLOW_BASE_M10_P3"], TRIGGER.WITH_2_AT_1_3_TIME), WaveEnemyEntryAlways(EnemyZombieShip, 3, ZOMBIE_KAMIKAZE["RED_BASE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyOrganic, 1, ORGANIC["GREEN_BASE"], 60), WaveEnemyEntryOnTrigger(EnemyKamikaze, 1, KAMIKAZE["WHITE_BASE"], TRIGGER.WITH_4_AT_1_5_TIME), WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["RED_BASE"], 60*3), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyOrganic, 1, ORGANIC["BOSS"]), }, true ) ) -- seeker waves -- waves 22 23 24 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemySeeker, 1, SEEKER["YELLOW_BASE"], 30), WaveEnemyEntryTimed(EnemySeeker, 1, SEEKER["YELLOW_BASE"], 60*2), WaveEnemyEntryTimed(EnemyZombieShip, 1, ZOMBIE["PURPLE_BASE"], 60*2) } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemySeeker, 2, SEEKER["RED_BASE"], 60*1+30), WaveEnemyEntryTimed(EnemyZombieShip, 1, ZOMBIE["WHITE_BASE_SLOW"], 60*3), WaveEnemyEntryTimed(EnemyPeek, 1, PEEK["FUSION_BASE_S3"], 60*10) } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyZombieShip, 2, ZOMBIE_KAMIKAZE["BOSS_2"], 60*1+30), WaveEnemyEntryAlways(EnemySeeker, 1, SEEKER["BOSS"], 45), WaveEnemyEntryTimed(EnemyMedusa, 1, MEDUSA["YELLOW_BASE_M10_P3"], 60*14) }, true ) ) -- challenge waves -- set of waves combining everying, no bosses -- waves 25 26 27 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryTimed(EnemyZombieShip, 1, ZOMBIE["BLUE_BASE_SPECIAL"], 25), WaveEnemyEntryTimed(EnemyZombieShip, 1, ZOMBIE["YELLOW_BASE_SPECIAL"], 25), WaveEnemyEntryTimed(EnemyZombieShip, 1, ZOMBIE["RED_BASE_SPECIAL"], 25), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyZombieShip, 1, ZOMBIE["RED_BASE_M2"], 60*3), WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["RED_BASE"]), WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["WHITE_BASE"]), WaveEnemyEntryOnTrigger(EnemyMedusa, 1, MEDUSA["WHITE_BASE_M2_P16"], TRIGGER.WITH_2_AT_1_3_TIME), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyPeek, 1, PEEK["FUSION_BASE_S3"], 120), WaveEnemyEntryTimed(EnemyZombieShip, 1, ZOMBIE["FUSION_BASE_WALL"], 80), WaveEnemyEntryOnTrigger(EnemyMedusa, 1, MEDUSA["FUSION_BASE"], TRIGGER.WITH_5_AT_1_6_TIME), } ) ) -- 28 29 30 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryTimed(EnemyKamikaze, 1, KAMIKAZE["PURPLE_BASE"], 220), WaveEnemyEntryAlways(EnemyMedusa, 1, MEDUSA["WHITE_BASE_BLOCK"]) } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryTimed(EnemySeeker, 1, SEEKER["RED_BASE"], 285), WaveEnemyEntryAlways(EnemyBounce, 3, BOUNCE["IMMUNE"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["WHITE_BASE"]), WaveEnemyEntryAlways(EnemyShooter, 1, SHOOTER["FUSION_BASE"]), WaveEnemyEntryOnTrigger(EnemyZombieShip, 1, ZOMBIE_KAMIKAZE["WHITE_BIG"], TRIGGER.WITH_4_AT_1_5_TIME) } ) ) -- 31 32 33 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyMedusa, 2, MEDUSA["FUSION_BASE_CIRC"]), WaveEnemyEntryTimed(EnemyZombieShip, 1, ZOMBIE["WHITE_BASE"], 25), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryTimed(EnemyPeek, 1, PEEK["RED_BASE"], 450), WaveEnemyEntryAlways(EnemyOrganic, 1, ORGANIC["FUSION_BASE_M2"]), WaveEnemyEntryAlways(EnemyBounce, 3, BOUNCE["YELLOW_BASE"], 300) } ) ) -- Quasar waves local base = 1/8.5 table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnTrigger(EnemyZombieShip, 1, ZOMBIE["QUASAR_WALL"], trigger_table( base * 1, base * 2, base * 3, base * 4, --base * 5, base * 6, base * 7, base * 8, --base * 9, base * 10, base * 11, base * 12, base * 13, base * 14, base * 15, base * 16, base * 17, base * 18, base * 19, base * 20, base * 21, base * 22, base * 23, base * 24, base * 25, base * 26, base * 27, base * 28, base * 29, base * 30 )), WaveEnemyEntryOnTrigger(EnemyZombieShip, 1, ZOMBIE["QUASAR_BIG"], trigger_table( base * 5, base * 9 )) }, -- set boss mode false, -- set quasar mode true ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryTimed(EnemyPeek, 1, PEEK["QUASAR"], 60*1), WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["QUASAR_WALL"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyMedusa, 1, MEDUSA["QUASAR_1"]), WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["QUASAR_SUPPORT"], 90), WaveEnemyEntryAlways(EnemyMedusa, 1, MEDUSA["QUASAR_2"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyKamikaze, 1, KAMIKAZE["QUASAR"]), WaveEnemyEntryModTime(EnemyKamikaze, 1, KAMIKAZE["QUASAR"], 99), WaveEnemyEntryModTime(EnemyZombieShip, 2, ZOMBIE_KAMIKAZE["QUASAR"], 109), WaveEnemyEntryModTime(EnemyKamikaze, 1, KAMIKAZE["QUASAR"], 141), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyShooter, 8, SHOOTER["QUASAR_1"]), WaveEnemyEntryAlways(EnemyShooter, 2, SHOOTER["QUASAR_2"]), } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyBounce, 2, BOUNCE["QUASAR_1"], 200), WaveEnemyEntryAlways(EnemyBounce, 1, BOUNCE["QUASAR_2"], 200), WaveEnemyEntryAlways(EnemyZombieShip, 10, ZOMBIE["QUASAR_SUPPORT"]) } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryAlways(EnemyOrganic, 2, ORGANIC["QUASAR"]), WaveEnemyEntryOnTrigger(EnemyOrganic, 1, ORGANIC["QUASAR"], TRIGGER.WITH_2_AT_1_3_TIME) } ) ) table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemySeeker, 1, SEEKER["QUASAR"]), WaveEnemyEntryTimed(EnemyMedusa, 1, MEDUSA["WHITE_BASE_M2_P16"], 350) } ) ) --- table.insert(WAVE_DATA, Wave( { WaveEnemyEntryOnStart(EnemyHeart, 1, {}) }, true ) ) end end, ["core.wave_pool"] = function() -- Deprecated WavePool = lazy.class("WavePool") function WavePool:constructor(entries, set_boss_mode, set_quasar_mode) assert(type(entries) == "table", "WavePool.pool is not a table") self.pool = {} for _, entry in ipairs(entries) do table.insert(self.pool, Wave(entry)) end self.current_wave = nil self.set_quasar_mode = set_quasar_mode or false self.set_boss_mode = set_boss_mode or false end function WavePool:initiate(game_logic) local rand_i = math.random(#self.pool) self.current_wave = self.pool[rand_i] self.current_wave:initiate(game_logic) end function WavePool:exec(game_logic) self.current_wave:exec(game_logic) end end, ["core.wave_enemy_entry_timed"] = function() -- spawn enemies exactly at a time interval WaveEnemyEntryTimed = lazy.extend("WaveEnemyEntryTimed", WaveEnemyEntryBase) local GLOBAL_START_TIMEOUT = 30 function WaveEnemyEntryTimed:constructor(Klass, spawn_number, spawn_data, spawn_time_mod) WaveEnemyEntryTimed.super:initialize(self, Klass, spawn_number, spawn_data) self.spawn_time_mod = spawn_time_mod self.timeout = GLOBAL_START_TIMEOUT assert(self.spawn_time_mod > 0, "self.spawn_time_max must be positive integer") end function WaveEnemyEntryTimed:initiate(game_logic) end function WaveEnemyEntryTimed:exec(game_logic) if self.timeout ~= 0 then self.timeout = self.timeout - 1 return end if FRAME % self.spawn_time_mod == 0 then self:initiate() self:spawn(game_logic) end end end, ["core.debug"] = function() -- Depricated file DebugMode = lazy.class("DebugMode") function DebugMode:constructor() end function DebugMode:update(dt) end function DebugMode:render() end end, ["core.wave_enemy_entry_always"] = function() -- Class to always spawn enemies defined in the wave_data file. WaveEnemyEntryAlways = lazy.extend("WaveEnemyEntryAlways", WaveEnemyEntryBase) local DEFAULT_COOLDOWN = 60 function WaveEnemyEntryAlways:constructor(Klass, spawn_number, spawn_data, delay, cooldown_max) WaveEnemyEntryAlways.super:initialize(self, Klass, spawn_number, spawn_data) self.delay = delay or 0 self.cooldown_max = cooldown_max or DEFAULT_COOLDOWN self.timeout = {} for i = 1, spawn_number do self.timeout[i] = 0 end self.buffer = {} self.enemies = {} end function WaveEnemyEntryAlways:initiate(game_logic) self.enemies = {} self.timeout = self.delay end function WaveEnemyEntryAlways:exec(game_logic) if self.timeout > 0 then self.timeout = self.timeout - 1 return end -- keep trying to spawn if next(self.enemies) == nil then self.enemies = self:spawn(game_logic) end -- check buffer for i, cooldown in pairs(self.buffer) do if cooldown ~= 0 then self.buffer[i] = self.buffer[i] - 1 else self.buffer[i] = nil self.enemies[i] = self:single_spawn(game_logic) end end -- check spawn for i, enemy in ipairs(self.enemies) do if self.buffer[i] == nil then -- enemy group if next(enemy.spawn_group) ~= nil then -- check deleted local all_deleted = true for _, enemy_spawn in ipairs(enemy.spawn_group) do if not enemy_spawn.delete then all_deleted = false end end -- if group deleted, new spawn if all_deleted then -- self.enemies[i] = self:single_spawn(game_logic) self:_add_buffer(i) end -- single enemy else -- check for failed spawns local exists = false game_logic.enemy_handler:loop( function(enemy_in_handler) if enemy == enemy_in_handler then exists = true end end ) -- remove failed spawns if not exists then enemy.delete = true end -- new spawn if enemy.delete then -- self.enemies[i] = self:single_spawn(game_logic) self:_add_buffer(i) end end end end end function WaveEnemyEntryAlways:_add_buffer(i) self.buffer[i] = self.cooldown_max end end, ["core.upgrades"] = function() -- Handling upgrade logic and configuration local UPGRADE_TYPE = lazy.enum( "PLUS_WEAPON" ) UPGRADE = { SPACESHIP = { [1] = { LEVEL = { [5] = UPGRADE_TYPE.PLUS_WEAPON, [25] = UPGRADE_TYPE.PLUS_WEAPON, [50] = UPGRADE_TYPE.PLUS_WEAPON, [99] = UPGRADE_TYPE.PLUS_WEAPON, }, } }, apply = function(spaceship, ball) local a = spaceship.logic.level / CONFIG.SPACESHIP.MAX_LEVEL local t = a -- upgrade stats ball.logic.tap_speed_mod = CONFIG.SPACESHIP.MAX_UPGRADE.MOVE_SPEED * t spaceship.logic.attack_timeout_mod = lazy.math.round(CONFIG.SPACESHIP.MAX_UPGRADE.ATTACK_RATE * t) spaceship.logic.projectile_speed_mod = CONFIG.SPACESHIP.MAX_UPGRADE.BULLET_SPEED * t -- unique upgrades local LEVEL = UPGRADE.SPACESHIP[1].LEVEL local unique_upgrade = LEVEL[spaceship.logic.level] if unique_upgrade ~= nil and unique_upgrade == UPGRADE_TYPE.PLUS_WEAPON then spaceship.logic.weapons = spaceship.logic.weapons + 1 end end, getText = function(spaceship) -- unique upgrades local unique_upgrade = UPGRADE.SPACESHIP[1].LEVEL[spaceship.logic.level] if spaceship.logic.level == CONFIG.SPACESHIP.MAX_LEVEL then return "MAX LEVEL" end if unique_upgrade ~= nil then if unique_upgrade == UPGRADE_TYPE.PLUS_WEAPON then return "+ WEAPON" else return "[nil]" end else return "LEVEL UP" end end } end, ["core.wave_enemy_entry_onstart"] = function() -- Class to spawn enemies at the start of a wave WaveEnemyEntryOnStart = lazy.extend("WaveEnemyEntryOnStart", WaveEnemyEntryBase) function WaveEnemyEntryOnStart:constructor(Klass, spawn_number, spawn_data) WaveEnemyEntryOnStart.super:initialize(self, Klass, spawn_number, spawn_data) self.isStart = false end function WaveEnemyEntryOnStart:initiate(game_logic) self.isStart = true end function WaveEnemyEntryOnStart:exec(game_logic) if self.isStart then self:spawn(game_logic) self.isStart = false end end end, ["core.wave_enemy_entry_trigger"] = function() -- Class to spawn enemies based on the percent of wave completed. WaveEnemyEntryOnTrigger = lazy.extend("WaveEnemyEntryOnTrigger", WaveEnemyEntryBase) function WaveEnemyEntryOnTrigger:constructor(Klass, spawn_number, spawn_data, triggers) WaveEnemyEntryOnTrigger.super:initialize(self, Klass, spawn_number, spawn_data) self.triggers = triggers self.on_start = false self.time_triggers = {} assert(triggers, "triggers is nil") assert(type(triggers) == "table", "triggers must be %a table") end function WaveEnemyEntryOnTrigger:initiate(game_logic) self.time = 0 -- self.on_start = true self.time_triggers = {} local time_max = game_logic.wave_time_max for i, percent in ipairs(self.triggers) do self.time_triggers[i] = lazy.math.round(percent * time_max) end end function WaveEnemyEntryOnTrigger:exec(game_logic) for _, times in ipairs(self.time_triggers) do if times == self.time then self:spawn(game_logic) end end self.time = self.time + 1 end end, ["core.controlled_random"] = function() -- Class for controlling random events with a more even distribution ControlledRandom = lazy.class("ControlledRandom") local ID = 1 local COUNT = 2 local OFFSET = 3 function ControlledRandom:constructor(id_table) self.id_table = id_table self.track_table = {} for i, entry in ipairs(id_table) do self:_update_tracker(i) end end function ControlledRandom:next() for i = #self.track_table, 1, -1 do -- return default if i == 1 then return self.id_table[i][ID] end -- return weighted if self.track_table[i] < 1 then self:_update_tracker(i) return self.id_table[i][ID] else self.track_table[i] = self.track_table[i] - 1 end end end function ControlledRandom:_update_tracker(id) self.track_table[id] = self.id_table[id][COUNT] + math.random(-self.id_table[id][OFFSET], self.id_table[id][OFFSET]) end end, ["core.wave_enemy_data"] = function() -- Enemy data file which determines the attributes that can be modified in any one enemy object local NONE = "NONE" local modify = function(config, ...) local tbl = {...} assert(#tbl == #config, "Array mismatched.") for i, value in ipairs(tbl) do if value == NONE then tbl[i] = config[i] end end return tbl end -- local speed = UTILITY.get_num_spread(0.24, 1.32, 6) local speed = UTILITY.get_num_spread(0.24, 1.14, 6) local CONFIG_ZOMBIE = { -- hp/speed/pal/pal_mode/size/spawn_mode/kamikaze ["GREEN_BASE"] = {10, speed[1], false, 1, 1, 1, false}, ["BLUE_BASE"] = {20, speed[2], false, 2, 1, 1, false}, ["YELLOW_BASE"] = {20, speed[3], false, 3, 1, 1, false}, ["RED_BASE"] = {30, speed[4], false, 4, 1, 1, false}, ["PURPLE_BASE"] = {30, speed[5], false, 5, 1, 1, false}, ["WHITE_BASE"] = {40, speed[6], false, 6, 1, 1, false}, ["FUSION_BASE"] = {50, speed[7], false, 7, 1, 1, false}, ["DEBUG_CORRUPT"] = {-1, 1.32, true, 2, 1, 3, false}, ["BOSS"] = {150, 0.12, true, 2, 2, 2, false}, ["IMMUNE"] = {-1, 0.12, false, 5, 1, 1, false}, } CONFIG_ZOMBIE["GREEN_BASE_IMMUNE"] = modify(CONFIG_ZOMBIE["GREEN_BASE"], -1, NONE, NONE, NONE, NONE, NONE, NONE) CONFIG_ZOMBIE["BLUE_BASE_SLOW"] = modify(CONFIG_ZOMBIE["BLUE_BASE"], NONE, 0.20, NONE, NONE, NONE, NONE, NONE) CONFIG_ZOMBIE["FUSION_BASE_S3_SLOW"] = modify(CONFIG_ZOMBIE["FUSION_BASE"], NONE, 0.46, NONE, NONE, NONE, 3, NONE) CONFIG_ZOMBIE["RED_BASE_M2"] = modify(CONFIG_ZOMBIE["RED_BASE"], NONE, NONE, NONE, NONE, NONE, 2, NONE) CONFIG_ZOMBIE["FUSION_BASE_WALL"] = modify(CONFIG_ZOMBIE["FUSION_BASE"], NONE, NONE, NONE, NONE, NONE, 2, NONE) CONFIG_ZOMBIE["WHITE_BASE_SLOW"] = modify(CONFIG_ZOMBIE["WHITE_BASE"], NONE, 0.5, NONE, NONE, 2, NONE, NONE) CONFIG_ZOMBIE["BOSS_1"] = modify(CONFIG_ZOMBIE["BOSS"], 60, NONE, NONE, NONE, 1, NONE, NONE) CONFIG_ZOMBIE["BOSS_2"] = modify(CONFIG_ZOMBIE["BOSS"], NONE, NONE, NONE, NONE, NONE, NONE, NONE) CONFIG_ZOMBIE["QUASAR_WALL"] = modify(CONFIG_ZOMBIE["PURPLE_BASE"], -1, 0.80, true, NONE, NONE, 3, NONE) CONFIG_ZOMBIE["QUASAR_BIG"] = modify(CONFIG_ZOMBIE["PURPLE_BASE"], -1, 0.80, true, NONE, 2, 3, NONE) CONFIG_ZOMBIE["QUASAR_SUPPORT"] = modify(CONFIG_ZOMBIE["PURPLE_BASE"], 120, 0.24, true, NONE, 2, NONE, NONE) CONFIG_ZOMBIE["BLUE_BASE_SPECIAL"] = modify(CONFIG_ZOMBIE["BLUE_BASE"], 60, NONE, NONE, NONE, 2, NONE, NONE) CONFIG_ZOMBIE["YELLOW_BASE_SPECIAL"] = modify(CONFIG_ZOMBIE["YELLOW_BASE"], 60, NONE, NONE, NONE, 2, NONE, NONE) CONFIG_ZOMBIE["RED_BASE_SPECIAL"] = modify(CONFIG_ZOMBIE["RED_BASE"], 60, NONE, NONE, NONE, 2, NONE, NONE) local CONFIG_ZOMBIE_KAMIKAZE = { ["GREEN_BASE"] = modify(CONFIG_ZOMBIE["GREEN_BASE"], NONE, NONE, NONE, NONE, NONE, NONE, true), ["BLUE_BASE"] = modify(CONFIG_ZOMBIE["BLUE_BASE"], NONE, NONE, NONE, NONE, NONE, NONE, true), ["YELLOW_BASE"] = modify(CONFIG_ZOMBIE["YELLOW_BASE"], NONE, NONE, NONE, NONE, NONE, NONE, true), ["RED_BASE"] = modify(CONFIG_ZOMBIE["RED_BASE"], NONE, NONE, NONE, NONE, NONE, NONE, true), ["PURPLE_BASE"] = modify(CONFIG_ZOMBIE["PURPLE_BASE"], NONE, NONE, NONE, NONE, NONE, NONE, true), ["WHITE_BASE"] = modify(CONFIG_ZOMBIE["WHITE_BASE"], NONE, NONE, NONE, NONE, NONE, NONE, true), ["FUSION_BASE"] = modify(CONFIG_ZOMBIE["FUSION_BASE"], NONE, NONE, NONE, NONE, NONE, NONE, true), ["DEBUG_CORRUPT"] = modify(CONFIG_ZOMBIE["DEBUG_CORRUPT"], NONE, NONE, NONE, NONE, NONE, NONE, true), ["BOSS"] = modify(CONFIG_ZOMBIE["BOSS"], NONE, NONE, NONE, NONE, NONE, NONE, true), ["IMMUNE"] = modify(CONFIG_ZOMBIE["IMMUNE"], NONE, NONE, NONE, NONE, NONE, NONE, true), } CONFIG_ZOMBIE_KAMIKAZE["BOSS_1"] = modify(CONFIG_ZOMBIE["BOSS_1"], 30, 0.19, NONE, NONE, NONE, 1, true ) CONFIG_ZOMBIE_KAMIKAZE["BOSS_2"] = modify(CONFIG_ZOMBIE["BOSS_1"], 40, 0.89, NONE, NONE, NONE, 1, true) CONFIG_ZOMBIE_KAMIKAZE["WHITE_BIG"] = modify(CONFIG_ZOMBIE["WHITE_BASE"], 150, NONE, NONE, NONE, 2, NONE, true) CONFIG_ZOMBIE_KAMIKAZE["QUASAR"] = modify(CONFIG_ZOMBIE["FUSION_BASE"], 120, NONE, true, NONE, NONE, NONE, true) local speed = UTILITY.get_num_spread(0.50, 1.15, 6) local CONFIG_SHOOTER = { -- hp/speed/pal/pal_mode/atk_mode/attack_timeout/proj_speed/shoots ["GREEN_BASE"] = {10, speed[1], false, 1, 1, 90, 1.9, 1}, ["BLUE_BASE"] = {20, speed[2], false, 2, 1, 85, 1.9, 3}, ["YELLOW_BASE"] = {20, speed[3], false, 3, 2, 80, 1.9, 1}, ["RED_BASE"] = {30, speed[4], false, 4, 2, 60, 2.4, 2}, ["PURPLE_BASE"] = {40, speed[5], false, 5, 2, 50, 2.4, 3}, ["WHITE_BASE"] = {50, speed[6], false, 6, 1, 40, 2.8, 8}, ["FUSION_BASE"] = {60, speed[7], false, 7, 2, 30, 2.8, 5}, ["DEBUG_CORRUPT"] = {9999, 1.33, true, 2, 1, 90, 1.2, 2}, ["BOSS"] = {-1, 6.89, true, 2, 2, 15, 2.4, 15}, -- 0.89 ["IMMUNE"] = {-1, 1.0, false, 5, 2, 90, 0.8, 3}, } CONFIG_SHOOTER["QUASAR_WALL"] = modify(CONFIG_SHOOTER["FUSION_BASE"], -1, NONE, true, NONE, 1, 10, 4.5, 4) CONFIG_SHOOTER["QUASAR_1"] = modify(CONFIG_SHOOTER["FUSION_BASE"], NONE, NONE, true, NONE, 1, 60, 0.4, 9999) CONFIG_SHOOTER["QUASAR_2"] = modify(CONFIG_SHOOTER["FUSION_BASE"], NONE, NONE, true, 6, 2, 30, 0.6, 1) CONFIG_SHOOTER["QUASAR_SUPPORT"] = modify(CONFIG_SHOOTER["FUSION_BASE"], -1, 32, true, 6, 1, 2, 1.6, 1) local CONFIG_PEEK = { -- hp/peek_rate/pal/pal_mode/peek_amound/spawn_mode ["GREEN_BASE"] = {60, 71, false, 1, 8*0+4, 1}, ["BLUE_BASE"] = {70, 66, false, 2, 8*1, 1}, ["YELLOW_BASE"] = {80, 52, false, 3, 8*2, 1}, ["RED_BASE"] = {90, 45, false, 4, 8*3, 1}, ["PURPLE_BASE"] = {100, 40, false, 5, 8*4, 1}, ["WHITE_BASE"] = {120, 37, false, 6, 8*4, 1}, ["FUSION_BASE"] = {180, 32, false, 7, 8*5, 1}, ["DEBUG_CORRUPT"] = {9999, 100, true, 2, 8*5, 1}, ["BOSS"] = {300, 467, true, 2, 256, 3}, ["IMMUNE"] = {-1, 1.0, false, 2, 0, 1}, } CONFIG_PEEK["GREEN_BASE_MID"] = modify(CONFIG_PEEK["GREEN_BASE"], NONE, NONE, NONE, NONE, NONE, 2) CONFIG_PEEK["BLUE_IMMUNE_MID"] = modify(CONFIG_PEEK["GREEN_BASE"], -1, NONE, NONE, NONE, NONE, 2) CONFIG_PEEK["FUSION_BASE_S3"] = modify(CONFIG_PEEK["FUSION_BASE"], NONE, NONE, NONE, NONE, NONE, 3) CONFIG_PEEK["QUASAR"] = modify(CONFIG_PEEK["FUSION_BASE"], 170, NONE, true, NONE, 8*10, NONE) local CONFIG_MEDUSA = { -- hp/speed/pal/pal_mode/atk_mode/parts -- atk_mode: -- 1 sin -- 2 square -- 3 versin -- 4 1 - versin -- 5 sin + square -- 6 versin both -- 7 follow circ -- 8 wall -- 9 2xsin -- 10 top down sin -- 11 helix -- 12 opposite sin ["GREEN_BASE"] = {10, 0.52, false, 1, 1, 0}, ["BLUE_BASE"] = {10, 0.70, false, 2, 1, 2}, ["YELLOW_BASE"] = {20, 0.80, false, 3, 2, 3}, ["RED_BASE"] = {20, 0.90, false, 4, 3, 4}, ["PURPLE_BASE"] = {30, 0.99, false, 5, 3, 5}, ["WHITE_BASE"] = {30, 1.00, false, 6, 4, 7}, ["FUSION_BASE"] = {20, 0.80, false, 7, 11, 8}, ["DEBUG_CORRUPT"] = {9999, 0.50, true, 6, 7, 83}, -- speed 0.8 ["BOSS"] = {20, 0.02, true, 2, 8, 13}, ["IMMUNE"] = {-1, 0.69, false, 5, 2, 5}, } -- custom CONFIG_MEDUSA["GREEN_BASE_P2"] = modify(CONFIG_MEDUSA["GREEN_BASE"], NONE, NONE, NONE, NONE, NONE, 2) CONFIG_MEDUSA["BLUE_BASE_P5"] = modify(CONFIG_MEDUSA["BLUE_BASE"], NONE, NONE, NONE, NONE, NONE, 5) CONFIG_MEDUSA["BLUE_BASE_M2_P5"] = modify(CONFIG_MEDUSA["BLUE_BASE"], NONE, NONE, NONE, NONE, 2, 4) CONFIG_MEDUSA["BLUE_BASE_M1_P8"] = modify(CONFIG_MEDUSA["BLUE_BASE"], NONE, NONE, NONE, NONE, 1, 8) CONFIG_MEDUSA["BLUE_BASE_M12_P8"] = modify(CONFIG_MEDUSA["BLUE_BASE"], NONE, NONE, NONE, NONE, 12, 8) CONFIG_MEDUSA["BLUE_BASE_M9_P8"] = modify(CONFIG_MEDUSA["BLUE_BASE"], NONE, NONE, NONE, NONE, 9, 8) CONFIG_MEDUSA["YELLOW_BASE_M6_P6"] = modify(CONFIG_MEDUSA["YELLOW_BASE"], NONE, 0.42, NONE, NONE, 6, 6) CONFIG_MEDUSA["YELLOW_BASE_M3_P6"] = modify(CONFIG_MEDUSA["YELLOW_BASE"], NONE, 0.42, NONE, NONE, 3, 6) CONFIG_MEDUSA["YELLOW_BASE_M4_P6"] = modify(CONFIG_MEDUSA["YELLOW_BASE"], NONE, 0.42, NONE, NONE, 4, 6) CONFIG_MEDUSA["YELLOW_BASE_M10_P3"] = modify(CONFIG_MEDUSA["YELLOW_BASE"], NONE, 0.12, NONE, NONE, 10, 3) CONFIG_MEDUSA["PURPLE_BASE_BOSS"] = modify(CONFIG_MEDUSA["PURPLE_BASE"], NONE, 0.70, NONE, NONE, 1, 6) CONFIG_MEDUSA["YELLOW_BASE_M2_P5"] = modify(CONFIG_MEDUSA["YELLOW_BASE"], NONE, NONE, NONE, NONE, 2, 5) CONFIG_MEDUSA["WHITE_BASE_M2_P16"] = modify(CONFIG_MEDUSA["WHITE_BASE"], NONE, NONE, NONE, NONE, 1, 16) CONFIG_MEDUSA["WHITE_BASE_BLOCK"] = modify(CONFIG_MEDUSA["WHITE_BASE"], NONE, NONE, NONE, NONE, 9, 14) CONFIG_MEDUSA["FUSION_BASE_CIRC"] = modify(CONFIG_MEDUSA["FUSION_BASE"], 190, 0.62, NONE, NONE, 7, 90) CONFIG_MEDUSA["QUASAR_1"] = modify(CONFIG_MEDUSA["FUSION_BASE"], 30, 0, true, NONE, 9, 215) CONFIG_MEDUSA["QUASAR_2"] = modify(CONFIG_MEDUSA["FUSION_BASE"], NONE, 0.10, true, 6, 10, 0) local CONFIG_KAMIKAZE = { -- hp/speed/pal/pal_mode/aim_timout/attack_timeout ["GREEN_BASE"] = {10, 1.4, false, 1, 120, 100}, ["BLUE_BASE"] = {20, 1.4, false, 2, 120, 80}, ["YELLOW_BASE"] = {20, 1.4, false, 3, 100, 70}, ["RED_BASE"] = {30, 1.5, false, 4, 90, 70}, ["PURPLE_BASE"] = {30, 1.6, false, 5, 80, 60}, ["WHITE_BASE"] = {30, 1.7, false, 6, 70, 50}, ["FUSION_BASE"] = {40, 1.8, false, 7, 50, 50}, ["DEBUG_CORRUPT"] = {9999, 1.0, true, 2, 100, 100}, ["BOSS"] = {10, 1.8, true, 2, 30, 65}, ["IMMUNE"] = {-1, 1.0, false, 5, 100, 100}, } CONFIG_KAMIKAZE["QUASAR_SUPPORT"] = modify(CONFIG_KAMIKAZE["FUSION_BASE"], NONE, 1.2, true, NONE, NONE, 90) CONFIG_KAMIKAZE["QUASAR"] = modify(CONFIG_KAMIKAZE["FUSION_BASE"], 30, NONE, true, NONE, 5, 80) local speed = UTILITY.get_num_spread(0.7, 1.8, 6) local CONFIG_BOUNCE = { -- hp/speed/pal/pal_mode/bounces ["GREEN_BASE"] = {30, speed[1], false, 1, -1}, ["BLUE_BASE"] = {30, speed[2], false, 2, -1}, ["YELLOW_BASE"] = {40, speed[3], false, 3, -1}, ["RED_BASE"] = {50, speed[4], false, 4, -1}, ["PURPLE_BASE"] = {70, speed[5], false, 5, -1}, ["WHITE_BASE"] = {80, speed[6], false, 6, -1}, ["FUSION_BASE"] = {90, speed[7], false, 7, -1}, ["DEBUG_CORRUPT"] = {9999, 1.0, true, 2, -1}, ["BOSS"] = {120, 1.45, true, 2, -1}, ["IMMUNE"] = {-1, 1.0, false, 1, -1}, } CONFIG_BOUNCE["GREEN_IMMUNE"] = modify(CONFIG_BOUNCE["GREEN_BASE"], -1, NONE, NONE, NONE, NONE) CONFIG_BOUNCE["QUASAR_1"] = modify(CONFIG_BOUNCE["FUSION_BASE"], 80, 1.5, true, 7, NONE) CONFIG_BOUNCE["QUASAR_2"] = modify(CONFIG_BOUNCE["FUSION_BASE"], -1, 2.5, true, 7, NONE) local CONFIG_ORGANIC = { -- hp/speed/pal/pal_mode/atk_mode/attack_timeout/proj_speed ["GREEN_BASE"] = {80, 0.4, false, 1, 1, 120, 1.2}, ["BLUE_BASE"] = {90, 0.4, false, 2, 1, 110, 1.2}, ["YELLOW_BASE"] = {110, 0.4, false, 3, 1, 90, 1.2}, ["RED_BASE"] = {140, 0.4, false, 4, 1, 80, 1.2}, ["PURPLE_BASE"] = {160, 0.4, false, 5, 1, 80, 1.2}, ["WHITE_BASE"] = {160, 0.4, false, 6, 1, 70, 1.2}, ["FUSION_BASE"] = {180, 0.4, false, 7, 1, 60, 2.2}, ["DEBUG_CORRUPT"] = {9999, 1.5, true, 2, 1, 10000, 1.2}, ["BOSS"] = {250, 1.3, true, 2, 3, 75, 0.85}, ["IMMUNE"] = {-1, 0.4, false, 5, 1, 90, 1.2}, } CONFIG_ORGANIC["GREEN_BASE_WEAK_IMMUNE"] = modify(CONFIG_ORGANIC["GREEN_BASE"], -1, NONE, NONE, NONE, NONE, 160, 0.8) CONFIG_ORGANIC["FUSION_BASE_M2"] = modify(CONFIG_ORGANIC["FUSION_BASE"], NONE, NONE, NONE, NONE, 2, NONE, NONE) CONFIG_ORGANIC["QUASAR"] = modify(CONFIG_ORGANIC["FUSION_BASE"], 440, 17, true, NONE, 1, 3, 0.31) local CONFIG_SEEKER = { -- hp / pal / pal_mode / mode / proj_speed / proj_acc / atk_frame / accuracy / spawn_mode ["GREEN_BASE"] = {50, false, 1, 2, 0.70, 0.0, 190, 0.025, 2}, ["BLUE_BASE"] = {50, false, 2, 2, 0.65, 0.0, 170, 0.019, 2}, ["YELLOW_BASE"] = {60, false, 3, 2, 0.60, 0.0, 150, 0.022, 2}, ["RED_BASE"] = {70, false, 4, 2, 0.50, 0.0, 150, 0.028, 2}, ["PURPLE_BASE"] = {80, false, 5, 2, 0.35, 0.0, 150, 0.028, 2}, ["WHITE_BASE"] = {90, false, 6, 2, 3.70, 0.0, 100, 0.0, 2}, ["FUSION_BASE"] = {90, false, 7, 2, 2.70, 0.0, 150, 0.29, 2}, ["DEBUG_CORRUPT"] = {9999, true, 3, 3, 0.3, 0.0085, 15, 0.00023, 3}, ["BOSS"] = {1480, true, 2, 2, 0.0, 0.045, 65, 0.10, 3}, ["IMMUNE"] = {30, false, 1, 1, 0.7, 0.0, 170, 0.0002, 2}, } CONFIG_SEEKER["GREEN_BASE_MID"] = modify(CONFIG_SEEKER["GREEN_BASE"], NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE, 3) CONFIG_SEEKER["GREEN_BASE_IMMUNE"] = modify(CONFIG_SEEKER["GREEN_BASE"], -1, NONE, NONE, NONE, 0.75, NONE, 91, 0.0, 3) CONFIG_SEEKER["GREEN_BASE_IMMUNE_ALT"] = modify(CONFIG_SEEKER["GREEN_BASE"], -1, NONE, NONE, NONE, 0.75, NONE, 91, 0.0, 2) CONFIG_SEEKER["QUASAR"] = {9999, true, 3, 3, 1.2, 0.0135, 10, 0.037, 3} -- idea for quasar mode WAVE_KEY = { "GREEN_BASE", "BLUE_BASE", "YELLOW_BASE", "RED_BASE", "PURPLE_BASE", "WHITE_BASE", "FUSION_BASE", "DEBUG_CORRUPT", "BOSS", "IMMUNE" } WAVE_CONFIG = { ZOMBIE = CONFIG_ZOMBIE, ZOMBIE_KAMIKAZE = CONFIG_ZOMBIE_KAMIKAZE, SHOOTER = CONFIG_SHOOTER, PEEK = CONFIG_PEEK, MEDUSA = CONFIG_MEDUSA, KAMIKAZE = CONFIG_KAMIKAZE, BOUNCE = CONFIG_BOUNCE, ORGANIC = CONFIG_ORGANIC, SEEKER = CONFIG_SEEKER } end, ["core.data"] = function() -- Data for the game, basically a lua json file. DATA = {} -- temp vars local t = 0 local shake_config = function(intensity, duration) return { intensity = intensity, duration = duration, shake = function(self, camera) camera:shake(self.intensity, self.duration) end } end -- Number to sprite data -- convert decimal to sprite table DATA.SPR_NUM = {502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 486, 487} -- GFX data DATA.GFX = {} t = 5 DATA.GFX.EXPLOSION = AnimationSingleData( {384, 386, 388, 390}, {t, t+3, t+6, t+9}, 2 ) t = 1 DATA.GFX.EXPLOSION_KAMIKAZE = AnimationSingleData( {392, 394, 396, 398}, {t+6, t+6, t+3, t+3}, 2 ) -- SFX data DATA.SFX = {} DATA.SFX.INDEX = { BULLET_FIRE = "bullet_fire", BULLET_COLLIDE = "bullet_collide", EXPLODE = "explode", EXP_COLLECT = "exp_collect", LEVELUP = "levelup", ENEMY_BULLET = "enemy_bullet", HEALTH_UP = "health_up", BOUNCE = "bounce", EYE_BULLET = "eye_bullet", IMMUNE_BULLET = "immune_bullet", FOLLOW_BULLET = "follow_bullet", ROCKET_READY = "rocket_ready" } -- SFX -- channel 2 DATA.SFX[DATA.SFX.INDEX.BULLET_COLLIDE] = SfxData(1, "F#1", nil, nil, 5, 2, 1000) DATA.SFX[DATA.SFX.INDEX.IMMUNE_BULLET] = SfxData(9, "D#3", nil, nil, 8, 2, 1000) DATA.SFX[DATA.SFX.INDEX.EXP_COLLECT] = SfxData(3, "C#5", nil, nil, 7, 2, 1010) DATA.SFX[DATA.SFX.INDEX.HEALTH_UP] = SfxData(6, "C#5", nil, nil, 7, 2, 1010) DATA.SFX[DATA.SFX.INDEX.LEVELUP] = SfxData(4, "C#4", nil, nil, 0, 2, 1020) DATA.SFX[DATA.SFX.INDEX.ROCKET_READY] = SfxData(12, "F#2", nil, nil, 7, 2, 1030) -- channel 3 DATA.SFX[DATA.SFX.INDEX.BULLET_FIRE] = SfxData(0, "D#3", nil, nil, 6, 3, 1000) DATA.SFX[DATA.SFX.INDEX.EXPLODE] = SfxData(2, "A-1", nil, nil, 4, 3, 1020) DATA.SFX[DATA.SFX.INDEX.ENEMY_BULLET] = SfxData(5, "C-6", nil, nil, 6, 3, 1030) DATA.SFX[DATA.SFX.INDEX.BOUNCE] = SfxData(7, "C-3", nil, nil, 5, 3, 1030) DATA.SFX[DATA.SFX.INDEX.EYE_BULLET] = SfxData(8, "B-1", nil, nil, 8, 3, 1040) DATA.SFX[DATA.SFX.INDEX.FOLLOW_BULLET] = SfxData(11, "A-2", nil, nil, 8, 3, 1040) -- MUSIC DATA.TRACK = {} DATA.TRACK.SPACE = 0 DATA.TRACK.INTRO = 1 DATA.TRACK.QUASAR = 2 DATA.TRACK.BOSS = 3 -- camera DATA.CAMERA = { ENEMY_EXPLOSION = shake_config(1, 10), SHIP_COLLIDE_SHIP = shake_config(1, 10), SHIP_COLLIDE_PROJECTILE = shake_config(1, 10), } end, ["entities.enemy_zombie_ship_renderer"] = function() EnemyZombieShipRenderer = lazy.class("EnemyZombieShipRenderer") function EnemyZombieShipRenderer:constructor(entity) self.entity = entity self.animation = ModAnimate({320, 352}, 21, 2) self.animation_alt = ModAnimate({416, 448}, 51, 2) self.spike = ModAnimate({489, 489}, 5, 1) end function EnemyZombieShipRenderer:render() local size = self.entity.logic.size local camera = self.entity.game_logic.camera local pal = UTILITY.get_pal(self.entity) local anim = UTILITY.get_anim(self.entity) -- local pal = CONFIG.GLOBAL.PAL_MODE[self.entity.logic.pal_mode] -- if self.entity.logic.is_immune then -- pal = CONFIG.GLOBAL.IMMUNE_PAL -- end -- local anim = self.animation -- if self.entity.logic.use_alt_pal then -- anim = self.animation_alt -- end lazy.tic.set_palette_from_table(pal) self.entity.logic.flashing:render_function( function() anim:render(FRAME, self.entity.x + camera.xs, self.entity.y + camera.ys, size) if self.entity.logic.kamikaze then local x_o, y_o = (16 * size) / 2 - (8 * size) / 2, 16 * size - size - 1 self.spike:render(FRAME, self.entity.x + camera.xs + x_o, self.entity.y + camera.ys + y_o, size) end end ) lazy.tic.palette_reset() end end, ["entities.sfx_entity"] = function() SfxEntity = lazy.extend("SfxEntity", Entity) function SfxEntity:constructor(x, y, animation) self.x = x self.y = y self.animation = animation:getIntanced() end function SfxEntity:update(dt) self.animation:update(dt) if self.animation:isDone() then self.delete = true end end function SfxEntity:render() self.animation:render(self.x, self.y) end end, ["entities.enemy_seeker"] = function() EnemySeeker = lazy.extend("EnemySeeker", EnemyBase) function EnemySeeker:constructor(game_logic, config) EnemySeeker.super:initialize( self, game_logic, config, 0, 0, 24, 8 ) self.logic = EnemySeekerLogic(self) self.renderer = EnemySeekerRenderer(self) self.render_prio = UTILITY.set_draw_priority(7, self.logic.use_alt_pal, self.logic.pal_mode) end function EnemySeeker:update(dt) self.logic:update(dt) end function EnemySeeker:render() self.renderer:render() end function EnemySeeker:spawn() local enemy_handler = self.game_logic.enemy_handler -- spawn modes -- 1 normal random -- 2 follow grid / orgnized -- 3 center if self.logic.spawn_mode == 3 then local x = CONFIG.GAME.VIEWPORT.W / 2 - 12 local y = 24 self:_create_spawn(self, x, y) else for i = 1, 6 do local x = math.random(8, 8 + CONFIG.GAME.VIEWPORT.W - 16 - 24) local y = math.random(2, 8 * 2 + 4) local w = 24 local h = 8 if self.logic.spawn_mode == 2 then local grid_w = math.floor((CONFIG.GAME.VIEWPORT.W - 8 - 24) / 24) local grid_h = 2 x = 8 + math.random(0, grid_w - 1) * 24 y = 2 + math.random(0, grid_h - 1) * 8 end local isNotColliding = true enemy_handler:loop( function(e) if not e:typeof(EnemySeeker) then return end if lazy.util.is_box_inside_box(x, y, w, h, e.x, e.y, e.w, e.h) then isNotColliding = false end end ) if isNotColliding then self:_create_spawn(self, x, y) break end end end end function EnemySeeker:_create_spawn(entity, x, y) entity.x = x entity.y = y local hitbox = CONFIG.ENEMY.SEEKER.HITBOX entity:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) self.game_logic.enemy_handler:add(entity) end end, ["entities.enemy_orgranic_renderer"] = function() EnemyOrganicRenderer = lazy.class("EnemyOrganicRenderer") function EnemyOrganicRenderer:constructor(entity) self.entity = entity self.animation_head = ModAnimate({330, 332}, 140, 2) self.animation_body = ModAnimate({362, 364}, 45, 2) self.animation_head_alt = ModAnimate({426, 428}, 120, 2) self.animation_body_alt = ModAnimate({458, 460}, 25, 2) end function EnemyOrganicRenderer:render() local camera = self.entity.game_logic.camera local pal = UTILITY.get_pal(self.entity) local dir = UTILITY.conver_direction_spr(self.entity.logic.spawn_dir) local anim_head = self.animation_head local anim_body = self.animation_body if self.entity.logic.use_alt_pal then anim_head = self.animation_head_alt anim_body = self.animation_body_alt end lazy.tic.set_palette_from_table(pal) self.entity.logic.flashing:render_function( function() if self.entity.logic.state == self.entity.logic.STATES.MOVE then anim_head:renderFrame(1, self.entity.x + camera.xs, self.entity.y + camera.ys, nil, dir) else anim_head:renderFrame(2, self.entity.x + camera.xs, self.entity.y + camera.ys, nil, dir) end -- anim_head:render(FRAME, self.entity.x + camera.xs, self.entity.y + camera.ys, nil, dir) anim_body:render(FRAME, self.entity.x + camera.xs, self.entity.y + camera.ys + 16, nil, dir) end ) lazy.tic.palette_reset() end end, ["entities.enemy_peek_renderer"] = function() EnemyPeekRenderer = lazy.class("EnemyPeekRenderer") function EnemyPeekRenderer:constructor(entity) self.entity = entity self.tile = 2 self.main_spr_id = 328 self.part_spr_id = 360 self.main_spr_id_alt = 424 self.part_spr_id_alt = 456 end function EnemyPeekRenderer:render() local camera = self.entity.game_logic.camera local pal = UTILITY.get_pal(self.entity) local main_id = self.main_spr_id local part_id = self.part_spr_id if self.entity.logic.use_alt_pal then main_id = self.main_spr_id_alt part_id = self.part_spr_id_alt end lazy.tic.set_palette_from_table(pal) self.entity.logic.flashing:render_function( function() spr( main_id, self.entity.x + camera.xs, self.entity.y + camera.ys, 0, 1, 0, 0, self.tile, self.tile ) local parts_count = math.ceil((self.entity.logic.peek_amount - 16) / 16) for i = 1, parts_count do spr( part_id, self.entity.x + camera.xs, self.entity.y + camera.ys + i * 16, 0, 1, 0, 0, self.tile, self.tile ) end end ) lazy.tic.palette_reset() end end, ["entities.spaceship"] = function() SpaceshipEntity = lazy.extend("SpaceshipEntity", Entity) function SpaceshipEntity:constructor(game_logic, x, y, w, h) self.logic = SpaceshipEntityLogic(self) self.renderer = SpaceshipEntityRenderer(self) self.game_logic = game_logic SpaceshipEntity.super:initialize( self, x, y, w, h ) end function SpaceshipEntity:update(dt) self.logic:update(dt) end function SpaceshipEntity:render() self.renderer:render() end end, ["entities.enemy_heart_logic"] = function() EnemyHeartLogic = lazy.extend("EnemyHeartLogic", EnemyBaseLogic) local PI2 = 2 * math.pi local ATTACK_ACC = 0.0001 local ANTICIPATION_TIMEOUT_MAX = 30 local COOLDOWN_TIMEOUT_MAX = 120 local IDLE_TIMEOUT_MAX = 120 --250 local ATTACK_TIMEOUT_MAX = 400 local BASE_PROJECTILE_SPEED = 0.4 local BASE_ROTATION_SPEED = PI2 / (60 * 3) function EnemyHeartLogic:constructor(entity) self.entity = entity self.flashing = Flashing() self.STATES = lazy.enum("TRANS_IN", "IDLE", "ANTICIPATION", "ATTACK", "COOLDOWN", "TRANS_OUT") self.state = self.STATES.TRANS_IN local config = self.entity.config self.hp_mod = -1 self.attack_pos = lazy.util.position(14, 15) self.attack_angle = 0 self.attack_type = 0 self.theta_acc = 0 self.speed = 0 self.frame = 10 self.lane_splits = 0 self.rotation_speed = 0 self.rotation_acc = 0 self.projectile_acc = 0 self.projectile_speed = 0 self.projectile_speed_mul = 0 -- ! deprecated self.rotations = 0 self.revered_rotations = false self.degree_per_rotation = 0 --- ! deprecated self.degree_angle_mul = 0 self.projectile_damage = CONFIG.ENEMY.HEART.DAMAGE_PROJECTILE self._state_timeout = CONFIG.ENEMY.HEART.TRANS_TIME -- update logic self.hp = 10000000 end function EnemyHeartLogic:update(dt) self.flashing:update(dt) if self.state == self.STATES.TRANS_IN then self.base_y = self.entity.y if self._state_timeout == 0 then self.state = self.STATES.IDLE self._state_timeout = IDLE_TIMEOUT_MAX + 60 end elseif self.state == self.STATES.TRANS_OUT then if self._state_timeout == 0 then self.entity.delete = true end elseif self.state == self.STATES.IDLE then self.entity.y = self.base_y + lazy.math.round(math.sin(PI2 * FRAME / 180)) if self._state_timeout == 0 then self.entity.y = self.base_y self.state = self.STATES.ANTICIPATION self._state_timeout = ANTICIPATION_TIMEOUT_MAX end elseif self.state == self.STATES.ANTICIPATION then if self._state_timeout == 0 then self:_select_attack() end elseif self.state == self.STATES.ATTACK then --local theta_0 = math.deg(-self.attack_angle) if FRAME % self.frame == 0 then self.projectile_speed = self.projectile_speed + self.projectile_acc local x = self.entity.x + self.attack_pos.x local y = self.entity.y + self.attack_pos.y -- self.rotations = 0 -- self.revered_rotations = false -- self.degree_per_rotation = 0 local a_amount = 360 / self.lane_splits for i = 0, self.rotations - 1 do -- TODOL requires array to track rotations end for i = 0, self.lane_splits - 1 do local angle = i * a_amount local theta = math.deg(self.attack_angle) local normal = lazy.math.normalAngleDegree(theta + angle) self:_fire_projectile(x, y, normal, self.projectile_speed) if self.revered_rotations then theta = math.deg(-self.attack_angle) normal = lazy.math.normalAngleDegree(theta + angle) self:_fire_projectile(x, y, normal, self.projectile_speed) end end self.attack_angle = self.attack_angle + self.rotation_speed * (1 + self.theta_acc) self.theta_acc = self.theta_acc + self.rotation_acc end if self._state_timeout == 0 then self.state = self.STATES.COOLDOWN self._state_timeout = COOLDOWN_TIMEOUT_MAX end elseif self.state == self.STATES.COOLDOWN then if self._state_timeout == 0 then self.state = self.STATES.IDLE self._state_timeout = IDLE_TIMEOUT_MAX end end if self._state_timeout ~= 0 then self._state_timeout = self._state_timeout - 1 end end function EnemyHeartLogic:_fire_projectile(x, y, normal, speed) self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, x, y, 5, 5, normal, speed, 0, nil, self.entity.game_logic.projectile_enemy_organic_anim, 2 ) end function EnemyHeartLogic:_select_attack() -- cleanup self.theta_acc = 0 self.attack_angle = 0 self.speed = 0 self.attack_type = math.random(1, 5) -- self.attack_type = 5 --- ? 0 is debug attack if self.attack_type == 0 then self.frame = 11 self.lane_splits = 14 self.rotation_speed = - BASE_ROTATION_SPEED * 0.3 self.rotation_acc = 0 self.projectile_acc = 0 self.projectile_speed = BASE_PROJECTILE_SPEED - 0.1 --self.projectile_speed_mul = 0 self.rotations = 0 self.revered_rotations = false --self.degree_per_rotation = 0 self.degree_angle_mul = 0 self._state_timeout = ATTACK_TIMEOUT_MAX + 400 elseif self.attack_type == 1 then self.frame = 40 self.lane_splits = 8 self.rotation_acc = 0.7 self.rotation_speed = BASE_ROTATION_SPEED self.projectile_acc = 0 self.projectile_speed = BASE_PROJECTILE_SPEED --self.projectile_speed_mul = 0 self.rotations = 0 self.revered_rotations = false --self.degree_per_rotation = 0 self.degree_angle_mul = 0 self._state_timeout = ATTACK_TIMEOUT_MAX elseif self.attack_type == 2 then self.frame = 20 self.lane_splits = 9 self.rotation_speed = BASE_ROTATION_SPEED self.rotation_acc = 0 self.projectile_acc = 0.2 self.projectile_speed = BASE_PROJECTILE_SPEED --self.projectile_speed_mul = 0 self.rotations = 0 self.revered_rotations = false --self.degree_per_rotation = 0 self.degree_angle_mul = 0 self._state_timeout = ATTACK_TIMEOUT_MAX elseif self.attack_type == 3 then self.frame = 60 self.lane_splits = 5 self.rotation_speed = BASE_ROTATION_SPEED * 1.5 self.rotation_acc = 0 self.projectile_acc = 0 self.projectile_speed = BASE_PROJECTILE_SPEED --self.projectile_speed_mul = 0 self.rotations = 0 self.revered_rotations = true --self.degree_per_rotation = 0 self.degree_angle_mul = 0 self._state_timeout = ATTACK_TIMEOUT_MAX + 400 elseif self.attack_type == 4 then self.frame = 16 self.lane_splits = 2 self.rotation_speed = BASE_ROTATION_SPEED * 0.4 self.rotation_acc = 0.8 self.projectile_acc = 0.085 self.projectile_speed = BASE_PROJECTILE_SPEED + 0.9 --self.projectile_speed_mul = 0 self.rotations = 0 self.revered_rotations = false --self.degree_per_rotation = 0 self.degree_angle_mul = 0 self._state_timeout = ATTACK_TIMEOUT_MAX + 200 elseif self.attack_type == 5 then self.frame = 17 self.lane_splits = 11 self.rotation_speed = - BASE_ROTATION_SPEED * 0.3 self.rotation_acc = 0 self.projectile_acc = 0 self.projectile_speed = BASE_PROJECTILE_SPEED - 0.1 --self.projectile_speed_mul = 0 self.rotations = 0 self.revered_rotations = false --self.degree_per_rotation = 0 self.degree_angle_mul = 0 self._state_timeout = ATTACK_TIMEOUT_MAX + 400 end self.state = self.STATES.ATTACK end function EnemyHeartLogic:kill() self.state = self.STATES.TRANS_OUT self._state_timeout = CONFIG.ENEMY.HEART.TRANS_TIME end end, ["entities.projectile_entity_renderer"] = function() ProjectileEntityRenderer = lazy.class("ProjectileEntityRenderer") function ProjectileEntityRenderer:constructor(entity) self.entity = entity end function ProjectileEntityRenderer:render() local camera = self.entity.game_logic.camera self.entity.animate:render(FRAME, self.entity.x + camera.xs, self.entity.y + camera.ys) end end, ["entities.enemy_kamikaze_renderer"] = function() EnemyKamikazeRenderer = lazy.class("EnemyKamikazeRenderer") local AIM_COLOR = 4 function EnemyKamikazeRenderer:constructor(entity) self.entity = entity self.animation = ModAnimate({324, 356}, 22, 2) self.animation_alt = ModAnimate({420, 452}, 22, 2) end function EnemyKamikazeRenderer:render() local camera = self.entity.game_logic.camera local spaceship = self.entity.game_logic.spaceship local pal = UTILITY.get_pal(self.entity) local anim = UTILITY.get_anim(self.entity) lazy.tic.set_palette_from_table(pal) self.entity.logic.flashing:render_function( function() if self.entity.logic.state == self.entity.logic.STATES.FIND then anim:render(FRAME, self.entity.x + camera.xs, self.entity.y + camera.ys) elseif self.entity.logic.state == self.entity.logic.STATES.AIM then anim:renderFrame(1, self.entity.x + camera.xs, self.entity.y + camera.ys) elseif self.entity.logic.state == self.entity.logic.STATES.ATTACK then anim:renderFrame(2, self.entity.x + camera.xs, self.entity.y + camera.ys) end end ) lazy.tic.palette_reset() if self.entity.logic.state == self.entity.logic.STATES.AIM then local normal = lazy.math.normal(self.entity.x + 8, self.entity.y + 8, spaceship.x + 8, spaceship.y + 8) local x0 = (self.entity.x + 8) + normal.x * 10 local y0 = (self.entity.y + 8) + normal.y * 10 local x1 = x0 + normal.x * 21 local y1 = y0 + normal.y * 21 line(x0 + camera.xs, y0 + camera.ys, x1 + camera.xs, y1 + camera.ys, AIM_COLOR) end end end, ["entities.projectile_entity_logic"] = function() ProjectileEntityLogic = lazy.class("ProjectileEntityLogic") local BUFFER = 24 function ProjectileEntityLogic:constructor(entity) self.entity = entity end function ProjectileEntityLogic:update(dt) self.update_func(self) if self.entity.x < -16 - BUFFER or self.entity.x > CONFIG.GAME.VIEWPORT.W + BUFFER then self.entity.delete = true end if self.entity.y < -self.entity.h or self.entity.y > CONFIG.GAME.VIEWPORT.H + BUFFER then self.entity.delete = true end end end, ["entities.enemy_peek"] = function() EnemyPeek = lazy.extend("EnemyPeek", EnemyBase) local SPAWN_DIR_TRACKER = true local PEEK_Y_VARIATION = 12 function EnemyPeek:constructor(game_logic, config) EnemyPeek.super:initialize( self, game_logic, config, 0, 0, 16, 16 ) self.logic = EnemyPeekLogic(self) self.renderer = EnemyPeekRenderer(self) self.render_prio = UTILITY.set_draw_priority(3, self.logic.use_alt_pal, self.logic.pal_mode) end function EnemyPeek:update(dt) self.logic:update(dt) end function EnemyPeek:render() self.renderer:render() end function EnemyPeek:spawn() local enemy_handler = self.game_logic.enemy_handler local w = 16 local h = 16 if self.logic.spawn_mode == 1 then for i = 1, 6 do local x = 0 local vw = (CONFIG.GAME.VIEWPORT.W - 16) if SPAWN_DIR_TRACKER then x = math.random(1, vw / 2) else x = math.random(vw / 2, vw) end SPAWN_DIR_TRACKER = not SPAWN_DIR_TRACKER -- local x = math.random() * (CONFIG.GAME.VIEWPORT.W - 16) local y = CONFIG.GAME.VIEWPORT.H local isNotColliding = true enemy_handler:loop( function(e) if not e:typeof(EnemyPeek) then return end if lazy.util.is_box_inside_box(x, 0, w, h, e.x, 0, e.w, e.h) then isNotColliding = false end end ) if isNotColliding then self:_create_spawn(self, x, y) break end end elseif self.logic.spawn_mode == 2 then local x = CONFIG.GAME.VIEWPORT.W / 2 - 8 local y = CONFIG.GAME.VIEWPORT.H if not self:_is_wall_wall_collision(x, w, h) then self:_create_spawn(self, x, y) end elseif self.logic.spawn_mode == 3 then local x_offset = CONFIG.GAME.VIEWPORT.W / 3 local x = x_offset - 8 local y = CONFIG.GAME.VIEWPORT.H self:_create_spawn(self, x, y) local peek = EnemyPeek(self.game_logic, self.config) x = x_offset * 2 - 8 y = CONFIG.GAME.VIEWPORT.H self:_create_spawn(peek, x, y) end end --- TODO can be generalized into a n UTIL class function EnemyPeek:_is_wall_wall_collision(x, w, h) local isColliding = false self.game_logic.enemy_handler:loop( function(e) if not e:typeof(EnemyPeek) then return end if lazy.util.is_box_inside_box(x, 0, w, h, e.x, 0, e.w, e.h) then isColliding = true end end ) return isColliding end function EnemyPeek:_create_spawn(entity, x, y) entity.x = x entity.y = y entity.logic.base_y = entity.y local hitbox = CONFIG.ENEMY.PEEK.HITBOX local h_offset = entity.logic.peek_amount - hitbox.H entity:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H + h_offset) entity.game_logic.enemy_handler:add(entity) end end, ["entities.enemy_organic_logic"] = function() EnemyOrganicLogic = lazy.extend("EnemyOrganicLogic", EnemyBaseLogic) local PI2 = math.pi * 2 local BASE_ANGLE = 28 function EnemyOrganicLogic:constructor(entity) self.entity = entity self.flashing = Flashing() self.STATES = lazy.enum("MOVE", "ANTICIPATION", "SHOOT", "COOLDOWN") self.state = self.STATES.MOVE self.state_count = 0 local config = self.entity.config self.hp_mod = config[1] self.move_speed_mod = config[2] self.use_alt_pal = config[3] self.pal_mode = config[4] self.mode = config[5] self.attack_timeout_mod = config[6] self.projectile_speed_mod = config[7] -- update logic self.hp = self.hp_mod self.move_speed = self.move_speed_mod self.attack_time_max = self.attack_timeout_mod self.attack_time = math.random(1, self.attack_time_max) self.projectile_damage = CONFIG.ENEMY.ORGANIC.DAMAGE_PROJECTILE self.angle_time_max = CONFIG.ENEMY.ORGANIC.ANGLE_TIME_MAX self.angle_time = -1 -- self.strech = CONFIG.VIEWPORT.W self.base_y = CONFIG.GAME.VIEWPORT.H / 2 - 32 self.start_angle = 0 -- PI2 * math.random() -- hard mode self.entity:setup_hardmode(self) end function EnemyOrganicLogic:update(dt) self.flashing:update(dt) local speed = 0 if self.state == self.STATES.MOVE then speed = self.move_speed self.angle_time = self.angle_time + 1 if self.entity.x > 0 and self.entity.x < CONFIG.GAME.VIEWPORT.W - 16 then self.attack_time = self.attack_time - 1 if self.attack_time == 0 then self.state = self.STATES.ANTICIPATION self.state_count = CONFIG.ENEMY.ORGANIC.SHOOT_ANTICIPATION end end elseif self.state == self.STATES.ANTICIPATION then speed = self.move_speed * CONFIG.ENEMY.ORGANIC.MOVE_SPEED_ANTICIPATION_MUL self.state_count = self.state_count - 1 if self.state_count == 0 then self.state = self.STATES.SHOOT end elseif self.state == self.STATES.SHOOT then speed = self.move_speed * CONFIG.ENEMY.ORGANIC.MOVE_SPEED_ANTICIPATION_MUL self.state = self.STATES.COOLDOWN self:_fire_weapon() self.state_count = CONFIG.ENEMY.ORGANIC.SHOOT_COOLDOWN elseif self.state == self.STATES.COOLDOWN then speed = self.move_speed * CONFIG.ENEMY.ORGANIC.MOVE_SPEED_ANTICIPATION_MUL self.state_count = self.state_count - 1 if self.state_count == 0 then self.state = self.STATES.MOVE self.attack_time = self.attack_time_max end end self.entity.x = self.entity.x + self.spawn_dir * speed local angle = self.angle_time / self.angle_time_max local theta = self.start_angle + PI2 * angle local hsum = 0 if self.mode == 3 then hsum = (math.sin(10.3 * theta) / 6) - 0.4 else local h1 = math.sin(theta) local h2 = math.sin(10.3 * theta) / 6 hsum = h1 + h2 end self.entity.y = self.base_y + 12 * hsum if self.entity.x + 16 >= CONFIG.GAME.VIEWPORT.W - CONFIG.ENEMY.ORGANIC.X_BUFFER then self.spawn_dir = -1 elseif self.entity.x <= CONFIG.ENEMY.ORGANIC.X_BUFFER then self.spawn_dir = 1 end -- if self.angle_time == 0 then -- self.angle_time = self.angle_time_max -- end end function EnemyOrganicLogic:_fire_weapon() -- handler, x, y, w, h, normal, speed, acc, update_func, mod_animate) local normal = lazy.math.normal( self.entity.x + 8, self.entity.y + 4, self.entity.game_logic.spaceship.x + 8, self.entity.game_logic.spaceship.y + 8 ) if self.mode == 3 then local rate = 1/180 local amp = 38 local update_func = function (self) self.angle = self.angle + rate --self.entity.y = self.entity.y + self.speed * self.normal.y --self.entity.x = self.base_x + 20 * math.sin(self.angle) --self.speed = self.speed + self.acc self.base_x = self.base_x + self.speed * self.normal.x self.base_y = self.base_y + self.speed * self.normal.y self.entity.x = self.base_x + amp * math.cos(PI2 * self.angle) self.entity.y = self.base_y + amp * math.sin(PI2 * self.angle) end local update_func2 = function (self) self.angle = self.angle + rate --self.entity.y = self.entity.y + self.speed * self.normal.y --self.entity.x = self.base_x + 20 * math.sin(self.angle) --self.speed = self.speed + self.acc self.base_x = self.base_x + self.speed * self.normal.x self.base_y = self.base_y + self.speed * self.normal.y self.entity.x = self.base_x + amp * math.cos(math.pi + PI2 * self.angle) self.entity.y = self.base_y + amp * math.sin(math.pi + PI2 * self.angle) end self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, self.entity.x + 8, self.entity.y + 4, 4, 4, normal, self.projectile_speed_mod, 0, update_func, self.entity.game_logic.projectile_enemy_organic_anim ) self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, self.entity.x + 8, self.entity.y + 4, 4, 4, normal, self.projectile_speed_mod, 0, update_func2, self.entity.game_logic.projectile_enemy_organic_anim ) else self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, self.entity.x + 8, self.entity.y + 4, 4, 4, normal, self.projectile_speed_mod, acc, nil, self.entity.game_logic.projectile_enemy_organic_anim ) end local normal = lazy.math.normal( self.entity.x + 8, self.entity.y + 4, self.entity.game_logic.spaceship.x + 8, self.entity.game_logic.spaceship.y + 8 ) local angle = math.deg(math.acos(normal.x)) if self.mode == 2 then local normal_0 = lazy.math.normalAngleDegree(angle + BASE_ANGLE) local normal_1 = lazy.math.normalAngleDegree(angle - BASE_ANGLE) self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, self.entity.x + 8, self.entity.y + 4, 4, 4, normal_0, self.projectile_speed_mod, 0, nil, self.entity.game_logic.projectile_enemy_organic_anim ) self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, self.entity.x + 8, self.entity.y + 4, 4, 4, normal_1, self.projectile_speed_mod, 0, nil, self.entity.game_logic.projectile_enemy_organic_anim ) end if self.mode == 3 then -- local normal = {} -- normal[1] = lazy.math.normalAngleDegree(angle + BASE_ANGLE*1.5) -- normal[2] = lazy.math.normalAngleDegree(angle - BASE_ANGLE*1.5) -- normal[3] = lazy.math.normalAngleDegree(angle + BASE_ANGLE*2) -- normal[4] = lazy.math.normalAngleDegree(angle - BASE_ANGLE*2) -- normal[5] = lazy.math.normalAngleDegree(angle + BASE_ANGLE*2.5) -- normal[6] = lazy.math.normalAngleDegree(angle - BASE_ANGLE*2.5) -- for i = 1, 6 do -- self.entity.game_logic:spawn_projectile( -- self.entity, -- self.entity.game_logic.enemy_projectile_handler, -- self.entity.x + 8, self.entity.y + 4, -- 4, 4, -- normal[i], -- self.projectile_speed_mod, -- 0, -- nil, -- self.entity.game_logic.projectile_enemy_organic_anim -- ) -- end end DATA.SFX[DATA.SFX.INDEX.EYE_BULLET]:play() end function EnemyOrganicLogic:kill() self.entity.delete = true local game_logic = self.entity.game_logic game_logic:particle_explosion(self.entity.x + self.entity.w / 2, self.entity.y + self.entity.h / 2) game_logic:particle_explosion(self.entity.x + self.entity.w / 2, self.entity.y + 16 + self.entity.h / 2) game_logic.sfx_handler:add(SfxEntity(self.entity.x, self.entity.y, DATA.GFX.EXPLOSION)) game_logic.sfx_handler:add(SfxEntity(self.entity.x, self.entity.y + 16, DATA.GFX.EXPLOSION)) DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() DATA.CAMERA.ENEMY_EXPLOSION:shake(game_logic.camera) end end, ["entities.enemy_bounce_renderer"] = function() EnemyBounceRenderer = lazy.class("EnemyBounceRenderer") function EnemyBounceRenderer:constructor(entity) self.entity = entity self.animation = ModAnimate({334, 366}, 25, 2) self.animation_alt = ModAnimate({430, 462}, 35, 2) end function EnemyBounceRenderer:render() local camera = self.entity.game_logic.camera local pal = UTILITY.get_pal(self.entity) local anim = UTILITY.get_anim(self.entity) lazy.tic.set_palette_from_table(pal) self.entity.logic.flashing:render_function( function() anim:render(FRAME, self.entity.x + camera.xs, self.entity.y + camera.ys) end ) lazy.tic.palette_reset() end end, ["entities.spaceship_logic"] = function() SpaceshipEntityLogic = lazy.class("SpaceshipEntityLogic") function SpaceshipEntityLogic:constructor(entity) self.entity = entity self.max_hp = CONFIG.SPACESHIP.HP self.hp = self.max_hp self.max_exp = CONFIG.SPACESHIP.EXP self.exp = 0 self.level = 0 self.flashing = Flashing() self.attack_timeout_mod = 0 self.weapons = 1 self.weapon_recharge_count = 3 self.projectile_speed_mod = 0 self.enable_rocket = false end function SpaceshipEntityLogic:update(dt) self.flashing:update(dt) self.entity.x = self.entity.game_logic.control_ball.x if self.weapon_recharge_count == 0 then self.weapon_recharge_count = CONFIG.SPACESHIP.WEAPON_RECHARGE + self.attack_timeout_mod if self.weapons <= 2 then self:_fire_weapon(1, CONFIG.SPACESHIP.PROJECTILE_NORMAL_WEAPON1_LANE1) end if self.weapons >= 2 then self:_fire_weapon(2, CONFIG.SPACESHIP.PROJECTILE_NORMAL_WEAPON2_LANE1) self:_fire_weapon(2, CONFIG.SPACESHIP.PROJECTILE_NORMAL_WEAPON2_LANE2) end if self.weapons >= 3 then self:_fire_weapon(3, CONFIG.SPACESHIP.PROJECTILE_NORMAL_UP) end DATA.SFX[DATA.SFX.INDEX.BULLET_FIRE]:play() else self.weapon_recharge_count = self.weapon_recharge_count - 1 end if FRAME % 30 == 0 and self.weapons >= 4 then self:_fire_weapon(4, CONFIG.SPACESHIP.PROJECTILE_NORMAL_WEAPON1_LANE1) end if FRAME % 30 == 0 and self.weapons >= 5 then self:_fire_weapon(5, CONFIG.SPACESHIP.PROJECTILE_NORMAL_WEAPON1_LANE1) end local mod = CONFIG.SPACESHIP.PROJECTILE_ROCKET_TIME_MOD - CONFIG.SPACESHIP.PROJECTILE_ROCKET_TIME_MOD_SUB * self.entity.game_logic.wave if FRAME % mod == 0 and self.enable_rocket and self.entity.game_logic.state == self.entity.game_logic.STATES.GAME then local anim = self.entity.game_logic.projectile_spaceship_anim_rocket local normal = CONFIG.SPACESHIP.PROJECTILE_NORMAL_ROCKET local acc = CONFIG.SPACESHIP.PROJECTILE_ROCKET_ACC local speed = CONFIG.SPACESHIP.PROJECTILE_ROCKET_SPEED local x, y = self.entity.x + 5, self.entity.y + 6 local update = function (this) if this.entity.y < CONFIG.SPACESHIP.Y - 10 then -- this.entity.x = this.entity.x + this.speed * this.normal.x this.entity.y = this.entity.y + this.speed * this.normal.y else this.entity.x = self.entity.x + 5 this.entity.y = this.entity.y + this.speed * this.normal.y this.explode = true end -- apply acc this.speed = this.speed + this.acc end -- (game_logic, x, y, w, h, normal, speed, acc, update_func, mod_animate) self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.projectile_handler, x, y, 4, 8, normal, speed, acc, update, anim ) DATA.SFX[DATA.SFX.INDEX.ROCKET_READY]:play() end end function SpaceshipEntityLogic:addExp() self.entity.game_logic.score = self.entity.game_logic.score + CONFIG.GLOBAL.SCORE.EXP_ORB self.exp = lazy.math.upper_bound(self.exp + CONFIG.SPACESHIP.EXP_PER_CAPSULE, CONFIG.SPACESHIP.EXP) end function SpaceshipEntityLogic:addLevel() self.entity.game_logic.score = self.entity.game_logic.score + CONFIG.GLOBAL.SCORE.EXP_MAX_ORB self.exp = lazy.math.upper_bound(self.exp + CONFIG.SPACESHIP.EXP, CONFIG.SPACESHIP.EXP) end function SpaceshipEntityLogic:addHealth() self.hp = lazy.math.upper_bound(self.hp + CONFIG.GLOBAL.DROP_HEART_HEALTH, CONFIG.SPACESHIP.HP) end function SpaceshipEntityLogic:getHpPercent() return self.hp / self.max_hp end function SpaceshipEntityLogic:getExpPercent() return self.exp / self.max_exp end function SpaceshipEntityLogic:checkLevelUp() if self.exp == CONFIG.SPACESHIP.EXP and self.level < CONFIG.SPACESHIP.MAX_LEVEL then DATA.SFX[DATA.SFX.INDEX.LEVELUP]:play() self.exp = 0 self.level = self.level + 1 return true else return false end end function SpaceshipEntityLogic:_fire_weapon(weapon_type, normal) local speed = CONFIG.SPACESHIP.PROJECTILE_SPEED + self.projectile_speed_mod speed = lazy.math.upper_bound(speed, 13) local anim = self.entity.game_logic.projectile_spaceship_anim_base local x, y = 0, 0 if weapon_type == 4 or weapon_type == 5 then anim = self.entity.game_logic.projectile_spaceship_anim_sin speed = CONFIG.SPACESHIP.PROJECTILE_SIN_SPEED end if weapon_type == 3 then x = self.entity.x + 8 y = CONFIG.SPACESHIP.Y + 7 local x0, x1 = x - 7 - 2, x + 7 - 2 -- handler, x, y, w, h, normal, speed, acc, update_func, mod_animate) self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.projectile_handler, x0, y, 4, 4, normal, speed, 0, nil, anim ) self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.projectile_handler, x1, y, 4, 4, normal, speed, 0, nil, anim ) elseif weapon_type == 4 then local update_func = function (self) self.angle = self.angle + (math.pi * 2 / 30) self.entity.y = self.entity.y + self.speed * self.normal.y self.entity.x = self.base_x + 20 * math.sin(self.angle) end self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.projectile_handler, self.entity.x + 8 - 2, CONFIG.SPACESHIP.Y + 8, 4, 4, normal, speed, 0, update_func, anim ) elseif weapon_type == 5 then local update_func = function (self) self.angle = self.angle + (math.pi * 2 / 30) self.entity.y = self.entity.y + self.speed * self.normal.y self.entity.x = self.base_x + 20 * math.sin(math.pi + self.angle) end self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.projectile_handler, self.entity.x + 8 - 2, CONFIG.SPACESHIP.Y + 8, 4, 4, normal, speed, 0, update_func, anim ) else self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.projectile_handler, self.entity.x + 8 - 2, CONFIG.SPACESHIP.Y + 8, 4, 4, normal, speed, 0, nil, anim ) end end end, ["entities.enemy_peek_logic"] = function() EnemyPeekLogic = lazy.extend("EnemyPeekLogic", EnemyBaseLogic) local PEEK_MIN = CONFIG.GAME.VIEWPORT.H - CONFIG.SPACESHIP.Y - 8 local PEEK_Y_VARIATION = 32 function EnemyPeekLogic:constructor(entity) self.entity = entity self.STATES = lazy.enum("PEEK_IN", "WAIT", "HOLD") self.state = self.STATES.PEEK_IN self.flashing = Flashing() local config = self.entity.config self.hp_mod = config[1] self.peek_timeout_mod = config[2] self.use_alt_pal = config[3] self.pal_mode = config[4] self.peek_amount_mod = config[5] self.spawn_mode = config[6] -- update logic self.hp = self.hp_mod self.peek_time_max = self.peek_timeout_mod self.peek_time = self.peek_time_max self.peek_amount = CONFIG.ENEMY.PEEK.PEEK_AMOUNT + self.peek_amount_mod - math.random(0, PEEK_Y_VARIATION) self.peek_amount = lazy.math.lower_bound(self.peek_amount, PEEK_MIN) self.a = 0 self.base_y = -16 self.attack_y = 0 -- hard mode self.entity:setup_hardmode(self) end function EnemyPeekLogic:update(dt) self.flashing:update(dt) local spaceship = self.entity.game_logic.spaceship if (self.state == self.STATES.PEEK_IN or self.state == self.STATES.WAIT) and self.entity.y > spaceship.y + CONFIG.SPACESHIP.HITBOX.Y + CONFIG.SPACESHIP.HITBOX.H and (spaceship.x + 16 > self.entity.x and spaceship.x < self.entity.x + 16) then self.state = self.STATES.WAIT elseif self.state == self.STATES.WAIT then self.state = self.STATES.PEEK_IN end if self.state == self.STATES.PEEK_IN then self.peek_time = self.peek_time - 1 self.a = 1 - self.peek_time / self.peek_time_max self.entity.y = self.base_y - lazy.math.lerp(self.a, 0, self.peek_amount) if self.peek_time == 0 then self.state = self.STATES.HOLD end elseif self.state == self.STATES.HOLD then end end function EnemyPeekLogic:kill() self.entity.delete = true local parts_count = math.ceil((self.peek_amount - 16) / 16) for i = 0, parts_count do local y = self.entity.y + i * 16 self.entity.game_logic:particle_explosion(self.entity.x + self.entity.w / 2, y + self.entity.h / 2) self.entity.game_logic.sfx_handler:add(SfxEntity(self.entity.x, y, DATA.GFX.EXPLOSION)) end DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() DATA.CAMERA.ENEMY_EXPLOSION:shake(self.entity.game_logic.camera) end end, ["entities.enemy_organic"] = function() EnemyOrganic = lazy.extend("EnemyOrganic", EnemyBase) function EnemyOrganic:constructor(game_logic, config) EnemyOrganic.super:initialize( self, game_logic, config, 0, 0, 16, 16 ) self.logic = EnemyOrganicLogic(self) self.renderer = EnemyOrganicRenderer(self) self.render_prio = UTILITY.set_draw_priority(7, self.logic.use_alt_pal, self.logic.pal_mode) end function EnemyOrganic:update(dt) self.logic:update(dt) end function EnemyOrganic:render() self.renderer:render() end function EnemyOrganic:spawn() local enemy_handler = self.game_logic.enemy_handler if SPAWN_DIR_TRACKER then self.logic.spawn_dir = 1 else self.logic.spawn_dir = -1 end SPAWN_DIR_TRACKER = not SPAWN_DIR_TRACKER if self.logic.spawn_dir > 0 then self.x = -16 self.y = 0 else self.x = CONFIG.GAME.VIEWPORT.W self.y = 0 end local hitbox = CONFIG.ENEMY.ORGANIC.HITBOX self:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) enemy_handler:add(self) end end, ["entities.control_ball_logic"] = function() ControlBallEntityLogic = lazy.extend("ControlBallEntityLogic", Entity) function ControlBallEntityLogic:constructor(entity) self.control_box_x = 0 self.control_box_y = 120 - 16 * 3 self.control_box_w = 200 self.control_box_h = 16 * 4 self.entity = entity self.min_x = 0 self.max_x = 200 self.tap_x = self.entity.x self.tap_hold = false self.default_tap_speed = CONFIG.SPACESHIP.MOVE_SPEED self.tap_speed = self.default_tap_speed self.tap_speed_mod = 0 end function ControlBallEntityLogic:update(dt) self.tap_hold = false if CURSOR.left then if lazy.util.is_point_inside_box(CURSOR.x, CURSOR.y, self.control_box_x, self.control_box_y, self.control_box_w, self.control_box_h) then self.tap_hold = true end else self.tap_hold = false self.tap_x = self.entity.x end if self.tap_hold then self.tap_x = lazy.math.bound(CURSOR.x-8, self.min_x, self.max_x-16) local w = math.sqrt((self.tap_x - self.entity.x)^2) local sign = lazy.math.sign(self.tap_x - self.entity.x) self.entity.x = self.entity.x + sign * (self.default_tap_speed + self.tap_speed_mod) if sign > 0 then self.entity.x = lazy.math.bound(self.entity.x, 0, self.tap_x) else self.entity.x = lazy.math.bound(self.entity.x, self.tap_x, self.max_x) end end end end, ["entities.enemy_seeker_renderer"] = function() EnemySeekerRenderer = lazy.class("EnemySeekerRenderer") local ANIM_TIME_1 = 21 function EnemySeekerRenderer:constructor(entity) self.entity = entity self.animation_side_l = ModAnimate({292, 292, 294}, ANIM_TIME_1, 1) self.animation_side_r = ModAnimate({294, 294, 292}, ANIM_TIME_1, 1) self.animation_main = ModAnimate({293}, ANIM_TIME_1, 1) self.animation_side_l_alt = ModAnimate({308, 308, 310}, ANIM_TIME_1, 1) self.animation_side_r_alt = ModAnimate({310, 310, 308}, ANIM_TIME_1, 1) self.animation_main_alt = ModAnimate({309}, ANIM_TIME_1, 1) end function EnemySeekerRenderer:render() local camera = self.entity.game_logic.camera local pal = UTILITY.get_pal(self.entity) local anim_side_l = self.animation_side_l local anim_side_r = self.animation_side_r local anim_main = self.animation_main if self.entity.logic.use_alt_pal then anim_side_l = self.animation_side_l_alt anim_side_r = self.animation_side_r_alt anim_main = self.animation_main_alt end lazy.tic.set_palette_from_table(pal) self.entity.logic.flashing:render_function( function() local x = self.entity.x + camera.xs local y = self.entity.y + camera.ys anim_side_l:render(FRAME, x, y) anim_main:renderFrame(1, x + 8, y) anim_side_r:render(FRAME, x + 16, y, nil, 1) end ) lazy.tic.palette_reset() end end, ["entities.enemy_zombie_ship_logic"] = function() EnemyZombieShipLogic = lazy.extend("EnemyZombieShipLogic", EnemyBaseLogic) local MIN_KAMEKAZE_Y = 10 local MAX_KAMEKAZE_Y = 42 local MAX_WAIT = 70 local CHARGE_ACC = 0.1 local States = lazy.enum("MOVE", "WAIT", "CHARGE") function EnemyZombieShipLogic:constructor(entity) self.entity = entity self.flashing = Flashing() local config = self.entity.config self.hp_mod = config[1] self.move_speed_mod = config[2] self.use_alt_pal = config[3] self.pal_mode = config[4] self.size = config[5] self.spawn_mode = config[6] self.kamikaze = config[7] self.hp = self.hp_mod self.move_speed = self.move_speed_mod local damage_size = (self.size > 1) and CONFIG.ENEMY.ZOMBIE_SHIP.DAMAGE_SIZE_2 or 0 self.damage = CONFIG.ENEMY.ZOMBIE_SHIP.DAMAGE_COLLISION + damage_size self.state = States.MOVE self.kamikaze_y = MIN_KAMEKAZE_Y + math.random(0, MAX_KAMEKAZE_Y - MIN_KAMEKAZE_Y) self.wait = MAX_WAIT -- hard mode self.entity:setup_hardmode(self) end function EnemyZombieShipLogic:update(dt) if self.state == States.MOVE then self.entity.y = self.entity.y + self.move_speed if self.kamikaze and self.entity.y > self.kamikaze_y then self.state = States.WAIT self.wait = MAX_WAIT end elseif self.state == States.WAIT then self.wait = self.wait - 1 if self.wait == 0 then self.state = States.CHARGE end elseif self.state == States.CHARGE then self.entity.y = self.entity.y + self.move_speed self.move_speed = self.move_speed + CHARGE_ACC end if self.state == States.CHARGE and FRAME % 2 == 0 then PARTICLE_FACTORY.aim_attack_trail( self.entity.game_logic.particle_handler, self.entity.x + 8 - 1, self.entity.y + 8 - 1 ) end self.flashing:update(dt) if self.entity.y > CONFIG.GAME.VIEWPORT.H + (self.size - 1) * 16 then self.entity.delete = true end end function EnemyZombieShipLogic:kill() self.entity.delete = true self.entity.game_logic:particle_explosion(self.entity.x + self.entity.w / 2, self.entity.y + self.entity.h / 2) if self.size == 2 then self.entity.game_logic.sfx_handler:add(SfxEntity(self.entity.x, self.entity.y, DATA.GFX.EXPLOSION)) self.entity.game_logic.sfx_handler:add(SfxEntity(self.entity.x + 16, self.entity.y, DATA.GFX.EXPLOSION)) self.entity.game_logic.sfx_handler:add(SfxEntity(self.entity.x, self.entity.y + 16, DATA.GFX.EXPLOSION)) self.entity.game_logic.sfx_handler:add(SfxEntity(self.entity.x + 16, self.entity.y + 16, DATA.GFX.EXPLOSION)) else self.entity.game_logic.sfx_handler:add(SfxEntity(self.entity.x, self.entity.y, DATA.GFX.EXPLOSION)) end DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() DATA.CAMERA.ENEMY_EXPLOSION:shake(self.entity.game_logic.camera) end end, ["entities.projectile_entity"] = function() ProjectileEntity = lazy.extend("ProjectileEntity", Entity) function ProjectileEntity:constructor(source, game_logic, x, y, normal, speed, acc, update_func, mod_animate) self.source = source self.game_logic = game_logic self.logic = ProjectileEntityLogic(self) self.renderer = ProjectileEntityRenderer(self) self.animate = mod_animate self.logic.normal = normal self.logic.speed = speed self.logic.acc = acc self.logic.update_func = update_func or (function (self) self.entity.x = self.entity.x + self.speed * self.normal.x self.entity.y = self.entity.y + self.speed * self.normal.y end) self.explode = false self.logic.base_x = x self.logic.base_y = y self.logic.angle = 0 ProjectileEntity.super:initialize( self, x, y, 4, 4 ) end function ProjectileEntity:update(dt) self.logic:update(dt) end function ProjectileEntity:render() self.renderer:render() end end, ["entities.enemy_medusa"] = function() EnemyMedusa = lazy.extend("EnemyMedusa", EnemyBase) local SPAWN_DIR_TRACKER = true local PART_DIST_X = 16 - 8 function EnemyMedusa:constructor(game_logic, config) EnemyMedusa.super:initialize( self, game_logic, config, 0, 0, 16, 16 ) self.logic = EnemyMedusaLogic(self) self.renderer = EnemyMedusaRenderer(self) self.render_prio = UTILITY.set_draw_priority(5, self.logic.use_alt_pal, self.logic.pal_mode) end function EnemyMedusa:update(dt) self.logic:update(dt) end function EnemyMedusa:render() self.renderer:render() end function EnemyMedusa:spawn() local enemy_handler = self.game_logic.enemy_handler --self.logic.spawn_dir = lazy.math.sign((math.random() * 2) - 1) if SPAWN_DIR_TRACKER then self.logic.spawn_dir = 1 else self.logic.spawn_dir = -1 end SPAWN_DIR_TRACKER = not SPAWN_DIR_TRACKER if self.logic.mode == 12 then self.logic.spawn_dir = 1 self.logic.even = true end if self.logic.spawn_dir > 0 then self.x = -16 self.y = 0 else self.x = CONFIG.GAME.VIEWPORT.W self.y = 0 end if self.logic.mode == 10 then self.y = -16 end local hitbox = CONFIG.ENEMY.MEDUSA.HITBOX self:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) enemy_handler:add(self) if self.logic.mode == 9 or self.logic.mode == 11 or self.logic.mode == 12 then self:_create_spawn(self.logic.mode, self, 0 , 0) end if self.logic.parts > 0 then self.spawn_group = {} table.insert(self.spawn_group, self) end for i = 1, self.logic.parts do local x = 0 if self.logic.spawn_dir > 0 then x = i * -PART_DIST_X else x = i * PART_DIST_X end local part = EnemyMedusa(self.game_logic, self.config) if self.logic.mode == 10 then part.x = self.x part.y = self.y - (i * PART_DIST_X) else part.x = self.x + x part.y = self.y end part.logic.spawn_dir = self.logic.spawn_dir part.logic.start_angle = self.logic.start_angle part.logic.mode = self.logic.mode if self.logic.mode == 7 then part.logic.base_i = i part.logic.wait = 19 * i end part:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) enemy_handler:add(part) -- TODO add to function _create_spawn if self.logic.mode == 9 or self.logic.mode == 11 then part.logic.even = true local odd_part = EnemyMedusa(self.game_logic, self.config) odd_part.logic.even = false odd_part.x = self.x + x odd_part.y = self.y odd_part.logic.spawn_dir = self.logic.spawn_dir odd_part.logic.start_angle = self.logic.start_angle odd_part.logic.mode = self.logic.mode odd_part:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) enemy_handler:add(odd_part) table.insert(self.spawn_group, odd_part) elseif self.logic.mode == 12 then part.logic.even = true local odd_part = EnemyMedusa(self.game_logic, self.config) odd_part.logic.even = false odd_part.x = CONFIG.GAME.VIEWPORT.W + (i * PART_DIST_X) odd_part.y = self.y odd_part.logic.spawn_dir = -1 odd_part.logic.start_angle = self.logic.start_angle odd_part.logic.mode = self.logic.mode odd_part:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) enemy_handler:add(odd_part) table.insert(self.spawn_group, odd_part) end table.insert(self.spawn_group, part) end end function EnemyMedusa:_create_spawn(mode, from, x, y) local hitbox = CONFIG.ENEMY.MEDUSA.HITBOX local i = 0 -- FIXME: put spawn logic inside function if mode == 9 or mode == 11 then from.logic.even = true local odd_part = EnemyMedusa(self.game_logic, self.config) odd_part.logic.even = false odd_part.x = from.x + x odd_part.y = from.y odd_part.logic.spawn_dir = from.logic.spawn_dir odd_part.logic.start_angle = from.logic.start_angle odd_part.logic.mode = from.logic.mode odd_part:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) self.game_logic.enemy_handler:add(odd_part) table.insert(self.spawn_group, odd_part) elseif mode == 12 then from.logic.even = true local odd_part = EnemyMedusa(self.game_logic, self.config) odd_part.logic.even = false odd_part.x = CONFIG.GAME.VIEWPORT.W + (i * PART_DIST_X) odd_part.y = from.y odd_part.logic.spawn_dir = -1 odd_part.logic.start_angle = from.logic.start_angle odd_part.logic.mode = from.logic.mode odd_part:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) self.game_logic.enemy_handler:add(odd_part) table.insert(self.spawn_group, odd_part) end end end, ["entities.enemy_medusa_renderer"] = function() EnemyMedusaRenderer = lazy.class("EnemyMedusaRenderer") function EnemyMedusaRenderer:constructor(entity) self.entity = entity self.animation = ModAnimate({322, 354}, 31, 2) self.animation_alt = ModAnimate({418, 450}, 21, 2) end function EnemyMedusaRenderer:render() local camera = self.entity.game_logic.camera local pal = UTILITY.get_pal(self.entity) local anim = UTILITY.get_anim(self.entity) local dir = UTILITY.conver_direction_spr(self.entity.logic.spawn_dir) if self.entity.logic.wait > 0 then return end lazy.tic.set_palette_from_table(pal) local x = lazy.math.round(self.entity.x + camera.xs) local y = lazy.math.round(self.entity.y + camera.ys) self.entity.logic.flashing:render_function( function() anim:render(FRAME, x, y, nil, dir) end ) lazy.tic.palette_reset() end end, ["entities.enemy_seeker_logic"] = function() EnemySeekerLogic = lazy.extend("EnemySeekerLogic", EnemyBaseLogic) local PI2 = math.pi * 2 local FOLLOW_BUFFER = 14 local BASE_ANGLE = 28 function EnemySeekerLogic:constructor(entity) self.entity = entity self.flashing = Flashing() self.flashing:start() self.STATES = lazy.enum("INTRO", "ATTACK") self.state = self.STATES.MOVE self.state_count = 0 local config = self.entity.config self.hp_mod = config[1] -- self.move_speed_mod = config[2] self.use_alt_pal = config[2] self.pal_mode = config[3] self.mode = config[4] self.projectile_speed = config[5] -- 0.2 self.projectile_acc = config[6] -- 0.0038 self.attack_frame = config[7] -- 120 self.accuracy_acc = config[8] -- 0.06 self.spawn_mode = config[9] -- 2 self.accuracy = 0 self.projectile_damage = CONFIG.ENEMY.SEEKER.DAMAGE_PROJECTILE -- update logic self.hp = self.hp_mod self.animation_projectile = ModAnimate({295, 295}, 2) self.frame_offset = math.random(0, self.attack_frame) -- hard mode self.entity:setup_hardmode(self) end function EnemySeekerLogic:update(dt) self.flashing:update(dt) if (FRAME + self.frame_offset) % self.attack_frame == 0 then self:_fire_weapon() end end function EnemySeekerLogic:_fire_weapon() local normal = {} -- down if self.mode == 1 then normal = lazy.math.normalAngleDegree(90) -- aim elseif self.mode == 2 then normal = lazy.math.normal( self.entity.x + 12, self.entity.y + 4, self.entity.game_logic.spaceship.x + 8, self.entity.game_logic.spaceship.y + 8 ) -- random elseif self.mode == 3 then local angle = math.rad(270+math.random(-45, 45)) normal = lazy.math.normalAngleRad(angle) end local accuracy_acc = self.accuracy_acc local update_func = function(self) if self.accuracy then -- self.accuracy = self.accuracy + self.accuracy_acc else -- self.accuracy = 0 -- self.accuracy_acc = accuracy_acc self.accuracy = accuracy_acc end if self.entity.y < CONFIG.SPACESHIP.Y - FOLLOW_BUFFER then local ship = self.entity.game_logic.spaceship local s_norm = lazy.math.normal(self.entity.x + 2, self.entity.y + 2, ship.x + 8, ship.y + 8) local angle_ship = lazy.math.normalizeAngleRad(math.atan(s_norm.y, s_norm.x)) local angle_ent = lazy.math.normalizeAngleRad(math.atan(self.normal.y, self.normal.x)) if angle_ent > 3/4*PI2 and angle_ent < PI2 then angle_ent = angle_ent - PI2 end local angle = angle_ent + (angle_ship - angle_ent) * self.accuracy self.normal = lazy.math.normalAngleRad(angle) end self.entity.x = self.entity.x + self.speed * self.normal.x self.entity.y = self.entity.y + self.speed * self.normal.y self.speed = self.speed + self.acc end self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, self.entity.x + 10, self.entity.y + 4, 4, 4, normal, self.projectile_speed, self.projectile_acc, update_func, self.animation_projectile ) DATA.SFX[DATA.SFX.INDEX.FOLLOW_BULLET]:play() -- DATA.SFX[DATA.SFX.INDEX.EYE_BULLET]:play() end function EnemySeekerLogic:kill() self.entity.delete = true local game_logic = self.entity.game_logic game_logic:particle_explosion(self.entity.x + self.entity.w / 2, self.entity.y + self.entity.h / 2) game_logic.sfx_handler:add(SfxEntity(self.entity.x + self.entity.w / 2 - 8, self.entity.y + self.entity.h / 2 - 8, DATA.GFX.EXPLOSION)) DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() DATA.CAMERA.ENEMY_EXPLOSION:shake(game_logic.camera) end end, ["entities.enemy_bounce"] = function() EnemyBounce = lazy.extend("EnemyBounce", EnemyBase) local SPAWN_X_BUFFER = 16 function EnemyBounce:constructor(game_logic, config) EnemyBounce.super:initialize( self, game_logic, config, 0, 0, 16, 16 ) self.logic = EnemyBounceLogic(self) self.renderer = EnemyBounceRenderer(self) self.render_prio = UTILITY.set_draw_priority(6, self.logic.use_alt_pal, self.logic.pal_mode) end function EnemyBounce:update(dt) self.logic:update(dt) end function EnemyBounce:render() self.renderer:render() end function EnemyBounce:spawn() local enemy_handler = self.game_logic.enemy_handler self.logic.spawn_dir = lazy.math.sign((math.random() * 2) - 1) self.x = math.random(SPAWN_X_BUFFER, CONFIG.GAME.VIEWPORT.W - SPAWN_X_BUFFER) self.y = -16 local hitbox = CONFIG.ENEMY.BOUNCE.HITBOX self:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) enemy_handler:add(self) end end, ["entities.enemy_shooter"] = function() EnemyShooter = lazy.extend("EnemyShooter", EnemyBase) local SPAWN_DIR_TRACKER = true function EnemyShooter:constructor(game_logic, config) EnemyShooter.super:initialize( self, game_logic, config, 0, 0, 16 , 16 ) self.logic = EnemyShooterLogic(self) self.renderer = EnemyShooterRenderer(self) self.render_prio = UTILITY.set_draw_priority(2, self.logic.use_alt_pal, self.logic.pal_mode) end function EnemyShooter:update(dt) self.logic:update(dt) end function EnemyShooter:render() self.renderer:render() end function EnemyShooter:spawn() local enemy_handler = self.game_logic.enemy_handler for i = 1, 6 do -- self.logic.spawn_dir = lazy.math.sign((math.random() * 2) - 1) if SPAWN_DIR_TRACKER then self.logic.spawn_dir = 1 else self.logic.spawn_dir = -1 end SPAWN_DIR_TRACKER = not SPAWN_DIR_TRACKER local x = 0 if self.logic.spawn_dir > 0 then x = -16 else x = CONFIG.GAME.VIEWPORT.W + 16 end local y = math.random(CONFIG.ENEMY.SHOOTER.SPAWN_HEIGHT_MIN, CONFIG.ENEMY.SHOOTER.SPAWN_HEIGHT_MAX) local w = 16 local h = 16 local isColliding = false enemy_handler:loop( function(e) if not e:typeof(EnemyShooter) then return end if lazy.util.is_box_inside_box(x, y, w, h, e.x, e.y, e.w, e.h) then isColliding = true end end ) if not isColliding then self.x = x self.y = y local hitbox = CONFIG.ENEMY.SHOOTER.HITBOX self:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) enemy_handler:add(self) break end end end end, ["entities.control_ball_renderer"] = function() ControlBallEntityRenderer = lazy.extend("ControlBallEntityRenderer", Entity) function ControlBallEntityRenderer:constructor(entity) self.entity = entity self.sid = 288 self.w = 2 self.h = 2 end function ControlBallEntityRenderer:render() -- ball spr( self.sid, self.entity.x, self.entity.y, 0, 1, 0, 0, self.w , self.h ) end end, ["entities.enemy_heart"] = function() EnemyHeart = lazy.extend("EnemyHeart", EnemyBase) function EnemyHeart:constructor(game_logic, config) EnemyHeart.super:initialize( self, game_logic, config, 0, 0, 32, 32 ) self.logic = EnemyHeartLogic(self) self.renderer = EnemyHeartRenderer(self) self.render_prio = UTILITY.set_draw_priority(1, false, 1) end function EnemyHeart:update(dt) self.logic:update(dt) end function EnemyHeart:render() self.renderer:render() end function EnemyHeart:spawn() local enemy_handler = self.game_logic.enemy_handler self.x = CONFIG.GAME.VIEWPORT.W / 2 - 16 self.y = CONFIG.GAME.VIEWPORT.H / 3 - 16 local hitbox = CONFIG.ENEMY.HEART.HITBOX self:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) enemy_handler:add(self) end end, ["entities.enemy_kamikaze"] = function() EnemyKamikaze = lazy.extend("EnemyKamikaze", EnemyBase) local SPAWN_DIR_TRACKER = true function EnemyKamikaze:constructor(game_logic, config) EnemyKamikaze.super:initialize( self, game_logic, config, 0, 0, 16, 16 ) self.logic = EnemyKamikazeLogic(self) self.renderer = EnemyKamikazeRenderer(self) self.render_prio = UTILITY.set_draw_priority(4, self.logic.use_alt_pal, self.logic.pal_mode) end function EnemyKamikaze:update(dt) self.logic:update(dt) end function EnemyKamikaze:render() self.renderer:render() end function EnemyKamikaze:spawn() local enemy_handler = self.game_logic.enemy_handler self.y = math.random(CONFIG.ENEMY.KAMIKAZE.SPAWN_HEIGHT_MIN, CONFIG.ENEMY.KAMIKAZE.SPAWN_HEIGHT_MAX) -- self.logic.spawn_dir = lazy.math.sign((math.random() * 2) - 1) if SPAWN_DIR_TRACKER then self.logic.spawn_dir = 1 else self.logic.spawn_dir = -1 end SPAWN_DIR_TRACKER = not SPAWN_DIR_TRACKER if self.logic.spawn_dir > 0 then self.x = -16 else self.x = CONFIG.GAME.VIEWPORT.W end local hitbox = CONFIG.ENEMY.KAMIKAZE.HITBOX self:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, hitbox.W, hitbox.H) enemy_handler:add(self) end end, ["entities.enemy_base_logic"] = function() EnemyBaseLogic = lazy.class("EnemyBaseLogic") function EnemyBaseLogic:constructor() end function EnemyBaseLogic:update(dt) end function EnemyBaseLogic:kill() self.entity.delete = true self.entity.game_logic:particle_explosion(self.entity.x + self.entity.w / 2, self.entity.y + self.entity.h / 2) self.entity.game_logic.sfx_handler:add(SfxEntity(self.entity.x, self.entity.y, DATA.GFX.EXPLOSION)) DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() DATA.CAMERA.ENEMY_EXPLOSION:shake(self.entity.game_logic.camera) end --- ! deprecated function EnemyBaseLogic:drop_item() local type_i = UTILITY.get_weighted_value(CONFIG.GLOBAL.DROP_RATE_BASE) if self.use_alt_pal then type_i = UTILITY.get_weighted_value(CONFIG.GLOBAL.DROP_RATE_CORRUPT) end local exp_entity = ExpEnitity( type_i, self.entity.x + 8 - 4, self.entity.y + 8 - 4 ) self.entity.game_logic.exp_handler:add(exp_entity) return exp_entity end function EnemyBaseLogic:is_immune() return self.hp_mod < 0 end end, ["entities.spaceship_renderer"] = function() SpaceshipEntityRenderer = lazy.class("SpaceshipEntityRenderer") function SpaceshipEntityRenderer:constructor(entity) self.entity = entity self.id = 256 self.id_rocket = 258 self.y = 96 self.sprw = 2 self.sprh = 2 self.turning = 0 end function SpaceshipEntityRenderer:render() local camera = self.entity.game_logic.camera local id = self.entity.logic.enable_rocket and self.id_rocket or self.id self.entity.logic.flashing:render_function( function() spr( id, self.entity.x + camera.xs, self.entity.y + camera.ys, 0, 1, 0, 0, self.sprw, self.sprh ) end ) end function SpaceshipEntityRenderer:setTurning(turning) self.turning = turning end end, ["entities.enemy_shooter_renderer"] = function() EnemyShooterRenderer = lazy.class("EnemyShooterRenderer") function EnemyShooterRenderer:constructor(entity) self.entity = entity self.animation = ModAnimate({326, 358}, 24, 2) self.animation_alt = ModAnimate({422, 454}, 24, 2) self.mode1_id = 500 self.mode2_id = 484 end function EnemyShooterRenderer:render() local camera = self.entity.game_logic.camera local pal = UTILITY.get_pal(self.entity) local anim = UTILITY.get_anim(self.entity) lazy.tic.set_palette_from_table(pal) self.entity.logic.flashing:render_function( function() if self.entity.logic.state == self.entity.logic.STATES.MOVE then anim:render(FRAME, self.entity.x + camera.xs, self.entity.y + camera.ys) else anim:renderFrame(2, self.entity.x + camera.xs, self.entity.y + camera.ys) end if self.entity.logic.mode == 1 then spr( self.mode1_id, self.entity.x + camera.xs, self.entity.y + 8 + camera.ys, 0, 1, 0, 0, 2, 1 ) elseif self.entity.logic.mode == 2 then spr( self.mode2_id, self.entity.x + camera.xs, self.entity.y + 8 + camera.ys, 0, 1, 0, 0, 2, 1 ) end end ) lazy.tic.palette_reset() end end, ["entities.control_ball"] = function() ControlBallEntity = lazy.extend("ControlBallEntity", Entity) function ControlBallEntity:constructor(game_logic, x, y, w, h) self.game_logic = game_logic self.logic = ControlBallEntityLogic(self) self.renderer = ControlBallEntityRenderer(self) ControlBallEntity.super:initialize( self, x, y, w, h ) -- self.speed = CONFIG.SPACESHIP.SPEED end function ControlBallEntity:update(dt) self.logic:update(dt) end function ControlBallEntity:render() self.renderer:render() end end, ["entities.exp_entity"] = function() ExpEnitity = lazy.extend("ExpEnitity", Entity) local DEL_BUFFER = 8 function ExpEnitity:constructor(type, x, y) self.STATES = lazy.enum("FALL", "IN") self.state = self.STATES.FALL self.in_speed = 1.5 self.in_entity = nil self.speed = 1 self.animate_exp = ModAnimate({302, 303, 318, 319}, 7) self.animate_level = ModAnimate({300, 301, 316, 317}, 7) self.animate_heart = ModAnimate({296, 296, 296, 296, 296, 297, 298, 312, 313, 314}, 5) self.type = type if self.type == CONFIG.GLOBAL.DROP_TYPE.EXP then self.animate = self.animate_exp elseif self.type == CONFIG.GLOBAL.DROP_TYPE.LEVEL then self.animate = self.animate_level elseif self.type == CONFIG.GLOBAL.DROP_TYPE.HEART then self.animate = self.animate_heart else self.animate = ModAnimate({1, 1}, 7) end ExpEnitity.super:initialize( self, x, y, 8, 8 ) end function ExpEnitity:update(dt) if self.state == self.STATES.FALL then self.y = self.y + self.speed elseif self.state == self.STATES.IN then -- get change local dx = self.in_entity.x + 4 - self.x local dy = self.in_entity.y + 4 - self.y -- get normalized vector local l = math.sqrt(dx^2 + dy^2) local normal_x = dx / l local normal_y = dy / l -- apply speed local pos_x = self.in_speed * normal_x local pos_y = self.in_speed * normal_y -- apply position self.x = self.x + pos_x self.y = self.y + pos_y -- check for object destruction if lazy.util.is_box_inside_box(self.x+3, self.y+3, self.w-6, self.h-6, self.in_entity.x+5, self.in_entity.y+5, self.in_entity.w-10, self.in_entity.h-10) then self.delete = true if self.type == CONFIG.GLOBAL.DROP_TYPE.EXP then self.in_entity.logic:addExp() DATA.SFX[DATA.SFX.INDEX.EXP_COLLECT]:play() elseif self.type == CONFIG.GLOBAL.DROP_TYPE.LEVEL then self.in_entity.logic:addLevel() elseif self.type == CONFIG.GLOBAL.DROP_TYPE.HEART then self.in_entity.logic:addHealth() DATA.SFX[DATA.SFX.INDEX.HEALTH_UP]:play() end end end if self.y > CONFIG.GAME.VIEWPORT.H + DEL_BUFFER then self.delete = true end end function ExpEnitity:render() self.animate:render(FRAME, self.x, self.y) end -- TODO: replace with game_logic ref function ExpEnitity:setInEntity(entity) self.in_entity = entity end end, ["entities.enemy_medusa_logic"] = function() EnemyMedusaLogic = lazy.extend("EnemyMedusaLogic", EnemyBaseLogic) local PI2 = 2 * math.pi local PHI = 1.618033988749 function EnemyMedusaLogic:constructor(entity) self.entity = entity self.flashing = Flashing() local config = self.entity.config self.hp_mod = config[1] self.move_speed_mod = config[2] self.use_alt_pal = config[3] self.pal_mode = config[4] self.mode = config[5] self.parts = config[6] -- update logic self.hp = self.hp_mod self.move_speed = self.move_speed_mod self.damage = CONFIG.ENEMY.MEDUSA.DAMAGE_COLLISION self.wait = 0 -- self.strech = PI2 * 3.2 self.strech = CONFIG.GAME.VIEWPORT.W self.base_y = (CONFIG.GAME.VIEWPORT.H - 2) - CONFIG.ENEMY.MEDUSA.AMP self.start_angle = 0 self.acc = 0 self.jerk = 0 if self.mode == 5 then self.mode = math.random(1, 2) elseif self.mode == 6 then self.mode = math.random(3, 4) elseif self.mode == 7 then end if self.mode == 1 or self.mode == 2 then self.start_angle = PI2 * math.random() elseif self.mode == 3 then self.start_angle = 0.3 * math.random() * lazy.math.random_sign() elseif self.mode == 4 then self.start_angle = 0.3 * math.random() * lazy.math.random_sign() elseif self.mode == 7 then self.start_angle = 5/4 * math.pi self.angle = self.start_angle self.angle_step = -1/(60*6) * PI2 self.base_x = self.entity.game_logic.spaceship.x self.base_y = 0 self.base_i = 0 self.wait = 0 elseif self.mode == 8 then self.jerk = 0.0001 self.acc = 0.0 elseif self.mode == 9 then self.jerk = 0.00012 self.acc = 0.0 -- 0.0065 self.move_speed = 0.23 elseif self.mode == 10 then self.base_x = CONFIG.GAME.VIEWPORT.W / 2 - 8 elseif self.mode == 11 then self.start_angle = -math.pi + 1/16*math.pi * math.random() * lazy.math.random_sign() elseif self.mode == 12 then self.start_angle = 0 end -- hard mode self.entity:setup_hardmode(self) end function EnemyMedusaLogic:update(dt) self.flashing:update(dt) local mode = self.mode if mode == 7 then if self.wait ~= 0 then self.wait = self.wait - 1 if self.wait == 0 then self.flashing:start() self.angle = self.start_angle + 1/PHI*self.base_i end else self.angle = self.angle + self.angle_step end local th = self.angle local r = 48 + (self.wait == 0 and (self.base_i * 7/10 ) or 0) local a = 0.01 + (math.abs(self.base_x - self.entity.game_logic.spaceship.x)/64) * 0.01 a = lazy.math.bound(a, 0 , 1) self.base_x = lazy.math.lerp(a, self.base_x, self.entity.game_logic.spaceship.x) --self.base_x = lazy.math.nlerp(lazy.tween.func.easeOutCubic, self.base_x, self.entity.game_logic.spaceship.x, self.base_x) local x = self.entity.game_logic.spaceship.x local y = self.entity.game_logic.spaceship.y - 8 self.entity.x = self.base_x + r * math.cos(th) self.entity.y = y + r * math.sin(th) elseif mode == 10 then local theta = self.start_angle + (self.entity.y + 8) / CONFIG.GAME.VIEWPORT.H local th = PI2 * 1.5 * (theta - 0.5) local hsum = 104 * self.spawn_dir * math.cos(th) -- x and y self.entity.x = self.base_x + hsum self.entity.y = self.entity.y + self.move_speed else self.entity.x = self.entity.x + self.spawn_dir * self.move_speed local theta = self.start_angle + ((self.entity.x + 8) / self.strech) - 0.5 local hsum = 1 local offset = 0 -- basic sin movement if mode == 1 then local th = PI2 * 1.5 * theta local h1 = math.cos(th) hsum = h1 -- square wave movement elseif mode == 2 then local th = PI2 * 1.3 * theta local h1 = math.sin(th) local h2 = math.sin(3 * th) / 3 local h3 = math.sin(5 * th) / 5 hsum = (h1 + h2 + h3) - 0.1 --- versin x elseif mode == 3 then local th = PI2 * 3 * theta local h1 = (math.sin(th) / (th)) - 0.7 hsum = 2.5 * h1 --- 1 - versin x elseif mode == 4 then local th = PI2 * 2.3 * theta local h1 = -(math.sin(th) / (th) - 0.30) offset = 0 -- -24 hsum = 1.4 * h1 -- wall elseif mode == 8 then local th = PI2 * theta^2 self.acc = self.acc + self.jerk self.move_speed = lazy.math.upper_bound(self.move_speed + self.acc, 3.5) local amp = math.cos(0.6 * th) hsum = 1 * amp * -math.cos(1.9 * th) -- 2x sin elseif mode == 9 then local amp = 1.3 local th = PI2 * 1.25 * theta if self.even then hsum = amp * math.cos(th) else hsum = amp * math.cos(math.pi + th) end self.acc = self.acc + self.jerk self.move_speed = lazy.math.upper_bound(self.move_speed + self.acc, 1.7) -- helix elseif mode == 11 then local amp = 1.2 local th = PI2 * 1.8 * theta offset = -24 if self.even then hsum = amp * math.cos(th) else hsum = amp * math.cos(0.36*math.pi + th) end -- opposite sin elseif mode == 12 then local th = PI2 * 1.25 * theta local amp = 1.2 * math.cos(0.4 * th) if self.even then hsum = amp * math.cos(th) else hsum = amp * math.cos(math.pi + th) end end self.entity.y = self.base_y + CONFIG.ENEMY.MEDUSA.AMP * hsum + offset end -- cehck for delete if mode == 7 then -- no deletion elseif mode == 10 then if self.entity.y > CONFIG.GAME.VIEWPORT.H then self.entity.delete = true end else if self.spawn_dir > 0 and self.entity.x > CONFIG.GAME.VIEWPORT.W then self.entity.delete = true elseif self.spawn_dir < 0 and self.entity.x < -16 then self.entity.delete = true end end end end, ["entities.enemy_heart_renderer"] = function() EnemyHeartRenderer = lazy.class("EnemyHeartRenderer") local PI2 = math.pi * 2 function EnemyHeartRenderer:constructor(entity) self.entity = entity self.spr_id_mouth = 72 self.spr_id_eyes = 76 self.spr_id_eye_back = 118 self.spr_id_iris = 119 self.body_parts = 12 self.spr_id_list_body = {490, 491, 492, 493, 494, 495} self.body_small_id = 488 self.blink_timeout_max = 35 self.blink_timeout = 0 self.anim_black_hole = ModAnimate({87, 103}, 12) self.blink_frame = 180 self.strench = PI2 / 1 self.a = 0 self.t = 0 self.eyes = { lazy.util.position(13, 3), lazy.util.position(23, 8), lazy.util.position(25, 15), lazy.util.position(19, 22), lazy.util.position(11, 21), lazy.util.position(5, 19), lazy.util.position(8, 13), lazy.util.position(2, 11), lazy.util.position(5, 6), } end function EnemyHeartRenderer:render() local camera = self.entity.game_logic.camera local STATES = self.entity.logic.STATES local state = self.entity.logic.state if FRAME % self.blink_frame == 0 then self.blink_timeout = self.blink_timeout_max self.blink_frame = math.random(180, 500) end if self.blink_timeout > 0 then self.blink_timeout = self.blink_timeout - 1 end self.t = self.t + 0.0007 local pal = CONFIG.GLOBAL.PAL_DEFAULT if state == STATES.TRANS_IN or state == STATES.TRANS_OUT then pal = CONFIG.ENEMY.HEART.PAL_INTRO end lazy.tic.set_palette_from_table(pal) self.entity.logic.flashing:render_function( function() -- local pal = CONFIG.GLOBAL.PAL_DEFAULT if state == STATES.TRANS_IN then -- print("TRANS_IN", 1, 1) end -- quasar spr( 128, CONFIG.GAME.VIEWPORT.W - 20 - 16, 8 * 7 - 1, 0, 1, 0, 0, 4, 4 ) -- body part fix spr( 495, self.entity.x + 24 + 4, self.entity.y + 16, 0 ) -- draw body for i = 0, self.body_parts do for i_body, id in ipairs(self.spr_id_list_body) do local angle_offset = i * PI2 / 4 local offset = 2 * math.sin(angle_offset + PI2*self.t * self.strench) local x = self.entity.x - 4 + (i * 8) + (i_body - 1) * 8 + offset local y = self.entity.y + (i - 1) * -8 if not (i == 0 and i_body == 1) then spr( id, x, y, 0 ) end end end -- draw small body for i = 0, 8 do -- self.body_small_id local x = CONFIG.GAME.VIEWPORT.W - 20 + math.sin(i * PI2 / 4 + PI2 * FRAME * 0.0045 ) local y = i * 8 spr( self.body_small_id, x, y, 0 ) end if state == STATES.IDLE or state == STATES.TRANS_IN or state == STATES.TRANS_OUT then for _, eye in ipairs(self.eyes) do local x = self.entity.x + eye.x local y = self.entity.y + eye.y spr( self.spr_id_eye_back, x, y, 0 ) local offset = self.entity.game_logic.spaceship.x / (CONFIG.GAME.VIEWPORT.W - 16) * 4 spr( self.spr_id_iris, x + offset - 1, y + 2, 0 ) if self.blink_timeout > 0 then local offset = (x * y) % 15 local a = (self.blink_timeout - offset) / (self.blink_timeout_max / 2) a = lazy.math.bound(a, 0, 1) local h = 5 * a rect(x, y, 5, h, 6) end end spr( self.spr_id_eyes, self.entity.x, self.entity.y, 0, 1, 0, 0, 4, 4 ) elseif state == STATES.ATTACK or state == STATES.ANTICIPATION or state == STATES.COOLDOWN then spr( self.spr_id_mouth, self.entity.x, self.entity.y, 0, 1, 0, 0, 4, 4 ) self.anim_black_hole:render(FRAME, self.entity.x + 11, self.entity.y + 12) end end ) lazy.tic.palette_reset() end end, ["entities.particle"] = function() Particle = lazy.extend("Particle", ParticleBase) -- TODO: needs reference to game_logic function Particle:constructor(x, y, dir_v, init_vel, acc, maxtime, color_table) self.maxtime = maxtime self.time = 0 self.color_table = color_table self.vel = init_vel self.doubled = (math.random() < 0.2) Particle.super:initialize( self, x, y, dir_v, init_vel, acc ) end function Particle:update(dt) self.time = lazy.math.bound(self.time + 1, 0, self.maxtime) -- TODO: needs reference to camera if self.time == self.maxtime then self.delete = true end self.x = self.x + self.dir_v.x * self.vel self.y = self.y + self.dir_v.y * self.vel self.vel = self.vel + self.acc if self.x < 0 or self.x > CONFIG.GAME.VIEWPORT.W or self.y < 0 or self.y > CONFIG.GAME.VIEWPORT.H then self.delete = true end end function Particle:render() local index = 1 + lazy.math.round( self.time/self.maxtime * (#self.color_table - 1)) if self.doubled then rect(self.x, self.y, 2, 2, self.color_table[index]) else pix(self.x, self.y, self.color_table[index]) end end end, ["entities.enemy_bounce_logic"] = function() EnemyBounceLogic = lazy.extend("EnemyBounceLogic", EnemyBaseLogic) local ANGLE_RANGE_MIN = 20 local ANGLE_RANGE_MAX = 32 function EnemyBounceLogic:constructor(entity) self.entity = entity self.flashing = Flashing() self.STATES = lazy.enum("BOUNCE", "ANTICIPATION") self.state = self.STATES.BOUNCE self.state_count = 0 local config = self.entity.config self.hp_mod = config[1] self.move_speed_mod = config[2] self.use_alt_pal = config[3] self.pal_mode = config[4] self.bounces = config[5] self.inf_bounces = (self.bounces == -1) -- update logic self.hp = self.hp_mod self.move_speed = self.move_speed_mod self.damage = CONFIG.ENEMY.BOUNCE.DAMAGE_COLLISION --- self.DIRECTION = lazy.enum("DOWN", "UP", "RIGHT" ,"LEFT") self.current_direction = self.DIRECTION.DOWN self.bounce_count = -1 self.dir_normal = {} self:_set_direction(false) -- hard mode self.entity:setup_hardmode(self) end function EnemyBounceLogic:update(dt) self.flashing:update(dt) if self.state == self.STATES.BOUNCE then self.entity.x = self.entity.x + self.dir_normal.x * self.move_speed self.entity.y = self.entity.y + self.dir_normal.y * self.move_speed elseif self.state == self.STATES.ANTICIPATION then self.state_count = self.state_count - 1 if self.state_count == 0 then self.state = self.STATES.BOUNCE end end if self.bounce_count <= self.bounces or self.inf_bounces then if self.entity.y > CONFIG.GAME.VIEWPORT.H - 16 then self.entity.y = CONFIG.GAME.VIEWPORT.H - 16 self.current_direction = self.DIRECTION.UP self:_set_direction(true) elseif self.entity.y < 0 and self.bounce_count ~= 0 then self.entity.y = 0 self.current_direction = self.DIRECTION.DOWN self:_set_direction(true) elseif self.entity.x < 0 then self.entity.x = 0 self.current_direction = self.DIRECTION.RIGHT self:_set_direction(true) self.entity.game_logic.camera:shake(1, 15) DATA.SFX[DATA.SFX.INDEX.BOUNCE]:play() elseif self.entity.x > CONFIG.GAME.VIEWPORT.W - 16 then self.entity.x = CONFIG.GAME.VIEWPORT.W - 16 self.current_direction = self.DIRECTION.LEFT self:_set_direction(true) end end if FRAME % 5 == 0 then PARTICLE_FACTORY.bounce( self.entity.game_logic.particle_handler, self.entity.x + 8 - 1, self.entity.y + 8 - 1 ) end if self.entity.x < -16 or self.entity.x > CONFIG.GAME.VIEWPORT.W or self.entity.y < -16 or self.entity.y >CONFIG.GAME.VIEWPORT.H then self.entity.delete = true end end function EnemyBounceLogic:_set_direction(isNotFirstBounce) self.bounce_count = self.bounce_count + 1 local sign = lazy.util.fif(math.random() < 0.5, 1, -1) local mod_angle = sign * math.random(ANGLE_RANGE_MIN, ANGLE_RANGE_MAX) if self.current_direction == self.DIRECTION.DOWN then self.dir_normal = lazy.math.normalAngleDegree(90 + mod_angle) elseif self.current_direction == self.DIRECTION.UP then self.dir_normal = lazy.math.normalAngleDegree(270 + mod_angle) elseif self.current_direction == self.DIRECTION.RIGHT then self.dir_normal = lazy.math.normalAngleDegree(0 + mod_angle) elseif self.current_direction == self.DIRECTION.LEFT then self.dir_normal = lazy.math.normalAngleDegree(180 + mod_angle) end self.state = self.STATES.ANTICIPATION self.state_count = CONFIG.ENEMY.BOUNCE.BOUNCE_COOLDOWN if isNotFirstBounce then self.entity.game_logic.camera:shake(1, 5) DATA.SFX[DATA.SFX.INDEX.BOUNCE]:play() end end end, ["entities.enemy_base"] = function() EnemyBase = lazy.extend("EnemyBase", Entity) function EnemyBase:constructor(game_logic, config, x, y, w, h) self.game_logic = game_logic self.config = config self.render_prio = 0 self.spawn_group = {} EnemyBase.super:initialize( self, x, y, w, h ) end function EnemyBase:spawn() end function EnemyBase:setup_hardmode(logic) if logic.entity.game_logic.hard_mode then local hp_cor_inc = logic.use_alt_pal and CONFIG.HARDMODE.HP_COR_INC or 0 logic.pal_mode = CONFIG.HARDMODE.PAL logic.hp = logic.hp + CONFIG.HARDMODE.HP_INCREASE + hp_cor_inc logic.hp = lazy.math.round(logic.hp * (1 + CONFIG.HARDMODE.HP_MUL)) end end end, ["entities.enemy_zombie_ship"] = function() EnemyZombieShip = lazy.extend("EnemyZombieShip", EnemyBase) function EnemyZombieShip:constructor(game_logic, config) local size = config[5] EnemyZombieShip.super:initialize( self, game_logic, config, 0, 0, 16 * size, 16 * size ) self.logic = EnemyZombieShipLogic(self) self.renderer = EnemyZombieShipRenderer(self) local prio = config[7] and 4 or 1 self.render_prio = UTILITY.set_draw_priority(prio, self.logic.use_alt_pal, self.logic.pal_mode) end function EnemyZombieShip:update(dt) self.logic:update(dt) end function EnemyZombieShip:render() self.renderer:render() end function EnemyZombieShip:spawn() local enemy_handler = self.game_logic.enemy_handler if self.logic.spawn_mode == 1 then for i = 1, 6 do local x = math.random(1, CONFIG.GAME.VIEWPORT.W - (16 * self.logic.size) - 1) local y = -( (16+8) * self.logic.size) local w = 16 * self.logic.size local h = 16 * self.logic.size local isNotColliding = true enemy_handler:loop( function(e) if not e:typeof(EnemyZombieShip) then return end if lazy.util.is_box_inside_box(x, y, w, h, e.x, e.y, e.w, e.h) then isNotColliding = false end end ) if isNotColliding then self:_create_spawn(self, x, y) break end end elseif self.logic.spawn_mode == 2 or self.logic.spawn_mode == 3 then local w = 16 * self.logic.size + 2 local h = (16 + 8) * self.logic.size local count = math.floor(CONFIG.GAME.VIEWPORT.W / w) + 2 local total_w = w * count local offset_x = CONFIG.GAME.VIEWPORT.W / 2 - total_w / 2 self.spawn_group = {} local hole = -1 if self.logic.spawn_mode == 3 then hole = math.random(1, count - 2) end for i = 0, count - 1 do local x = i * w + offset_x local y = -h if hole ~= i then if i == 0 then self:_create_spawn(self, x, y) table.insert(self.spawn_group, self) else local enemy = EnemyZombieShip(self.game_logic, self.config) self:_create_spawn(enemy, x, y) table.insert(self.spawn_group, enemy) end end end end end function EnemyZombieShip:_create_spawn(entity, x, y) entity.x = x entity.y = y local hitbox = CONFIG.ENEMY.ZOMBIE_SHIP.HITBOX local _hitbox_w = 16 * entity.logic.size - entity.logic.size * 2 local _hitbox_h = 16 * entity.logic.size - entity.logic.size * 2 entity:addBoundingBox(CONFIG.GLOBAL.KEY_HITBOX, hitbox.X, hitbox.Y, _hitbox_w, _hitbox_h) entity.game_logic.enemy_handler:add(entity) end end, ["entities.enemy_kamikaze_logic"] = function() EnemyKamikazeLogic = lazy.extend("EnemyKamikazeLogic", EnemyBaseLogic) local WAIT_TIMEOUT = 60 local JERK = 0.01 local FIND_DIST = 55 function EnemyKamikazeLogic:constructor(entity) self.entity = entity self.flashing = Flashing() self.STATES = lazy.enum("WAIT", "FIND", "AIM", "ATTACK") self.state = self.STATES.WAIT self._state_timeout = math.random(0 , WAIT_TIMEOUT) self.isAimAttack = false local config = self.entity.config self.hp_mod = config[1] self.move_speed_mod = config[2] self.use_alt_pal = config[3] self.pal_mode = config[4] self.aim_timeout_mod = config[5] self.attack_timeout_mod = config[6] -- update logic self.hp = self.hp_mod self.move_speed = self.move_speed_mod self.aim_timer = self.aim_timeout_mod self.attack_timeout = self.attack_timeout_mod self.damage = CONFIG.ENEMY.KAMIKAZE.DAMAGE_COLLISION self.normal_direction_vec = {} -- ! depricated values self.current_acc = 0 self.current_speed = 0 self.start_pos = {} self.distance = 0 self.attack_count = 0 self.a = 0 -- hard mode self.entity:setup_hardmode(self) self.damage = self.use_alt_pal and CONFIG.ENEMY.KAMIKAZE.DAMAGE_COLLISION_CORRUPT or CONFIG.ENEMY.KAMIKAZE.DAMAGE_COLLISION end function EnemyKamikazeLogic:update(dt) self.flashing:update(dt) local spaceship = self.entity.game_logic.spaceship if self.state == self.STATES.WAIT then self._state_timeout = self._state_timeout - 1 if self._state_timeout == 0 then self.state = self.STATES.FIND end elseif self.state == self.STATES.FIND then self.entity.x = self.entity.x + self.spawn_dir * self.move_speed if math.abs( (self.entity.x + 8) - (spaceship.x + 8)) < FIND_DIST and self.entity.x > 0 and self.entity.x < CONFIG.GAME.VIEWPORT.W - 16 then self.state = self.STATES.AIM end elseif self.state == self.STATES.AIM then self.aim_timer = self.aim_timer - 1 if self.aim_timer == 0 then self.isAimAttack = true self.state = self.STATES.ATTACK self.normal_direction_vec = lazy.math.normal( self.entity.x + 8, self.entity.y + 8, spaceship.x + 8, spaceship.y + 8 ) self.start_pos = lazy.util.position(self.entity.x, self.entity.y) local offset_x = 8 + self.normal_direction_vec.x * 8 local offset_y = 8 + self.normal_direction_vec.y * 8 self.distance = lazy.math.distance( self.entity.x + 8, self.entity.y + 8, spaceship.x + offset_x, spaceship.y + offset_y ) end elseif self.state == self.STATES.ATTACK then local max_time = self.attack_timeout self.a = self.attack_count / max_time self.t = lazy.tween.func.easeInBack(self.a) self.entity.x = self.start_pos.x + self.t * self.normal_direction_vec.x * self.distance self.entity.y = self.start_pos.y + self.t * self.normal_direction_vec.y * self.distance if self.attack_count == max_time then self:kill() end self.attack_count = lazy.math.upper_bound(self.attack_count + 1, max_time) end if self.state == self.STATES.ATTACK and FRAME % 2 == 0 then PARTICLE_FACTORY.aim_attack_trail( self.entity.game_logic.particle_handler, self.entity.x + 8 - 1, self.entity.y + 8 - 1 ) end end function EnemyKamikazeLogic:kill() self.entity.delete = true self.entity.game_logic:particle_explosion(self.entity.x + self.entity.w / 2, self.entity.y + self.entity.h / 2) self.entity.game_logic.sfx_handler:add(SfxEntity(self.entity.x, self.entity.y, DATA.GFX.EXPLOSION_KAMIKAZE)) DATA.SFX[DATA.SFX.INDEX.EXPLODE]:play() DATA.CAMERA.ENEMY_EXPLOSION:shake(self.entity.game_logic.camera) end end, ["entities.enemy_shooter_logic"] = function() EnemyShooterLogic = lazy.extend("EnemyShooterLogic", EnemyBaseLogic) function EnemyShooterLogic:constructor(entity) self.STATES = lazy.enum("MOVE", "ANTICIPATION", "SHOOT", "COOLDOWN") self.state = self.STATES.MOVE self.entity = entity self.flashing = Flashing() local config = self.entity.config self.hp_mod = config[1] self.move_speed_mod = config[2] self.use_alt_pal = config[3] self.pal_mode = config[4] self.mode = config[5] self.attack_timeout_mod = config[6] self.projectile_speed_mod = config[7] self.shots_max = config[8] -- update logic self.hp = self.hp_mod self.move_speed = self.move_speed_mod self.attack_time_max = self.attack_timeout_mod self.attack_timer = math.random(1, self.attack_time_max) self.projectile_damage = CONFIG.ENEMY.SHOOTER.DAMAGE_PROJECTILE self.player = {x = -16, y = -16} self.shoots_total = 0 self.shoot_buffer_count = 0 self.normal_direction_vec = {} self.shoot_cooldown_count = 0 self.shoot_anticipation_count = 0 self.state_count = 0 -- hard mode self.entity:setup_hardmode(self) end function EnemyShooterLogic:update(dt) self.flashing:update(dt) if self.state == self.STATES.MOVE then self.entity.x = self.entity.x + self.spawn_dir * self.move_speed if self.entity.x > 0 and self.entity.x < CONFIG.GAME.VIEWPORT.W - 16 then if self.attack_timer == 0 then self.state = self.STATES.ANTICIPATION self.state_count = CONFIG.ENEMY.SHOOTER.SHOOT_ANTICIPATION else self.attack_timer = self.attack_timer - 1 end end elseif self.state == self.STATES.ANTICIPATION then self.state_count = self.state_count - 1 if self.state_count == 0 then self.state = self.STATES.SHOOT self.shoot_buffer_count = CONFIG.ENEMY.SHOOTER.SHOOT_BUFFER self.shoots_total = self.shots_max end elseif self.state == self.STATES.SHOOT then if self.shoot_buffer_count == 0 then self.shoot_buffer_count = CONFIG.ENEMY.SHOOTER.SHOOT_BUFFER self.shoots_total = self.shoots_total - 1 self:_fire_weapon() else self.shoot_buffer_count = self.shoot_buffer_count - 1 end if self.shoots_total == 0 then self.state = self.STATES.COOLDOWN self.state_count = CONFIG.ENEMY.SHOOTER.SHOOT_COOLDOWN end elseif self.state == self.STATES.COOLDOWN then self.state_count = self.state_count - 1 if self.state_count == 0 then self.state = self.STATES.MOVE self.attack_timer = self.attack_time_max end end if self.entity.x < -16 or self.entity.x > CONFIG.GAME.VIEWPORT.W + 16 then self.entity.delete = true end end function EnemyShooterLogic:_fire_weapon() local x_offset = 8 - 2 local y = 13 local pos_x, pos_y = self.entity.x + x_offset, self.entity.y + y - 2 -- handler, x, y, w, h, normal, speed, acc, update_func, mod_animate) if self.mode == 1 then self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, pos_x, pos_y, 4, 5, CONFIG.ENEMY.SHOOTER.MODE1_PROJ_NORMAL, self.projectile_speed_mod, 0, nil, self.entity.game_logic.projectile_enemy_shooter_anim ) elseif self.mode == 2 then self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, pos_x, pos_y, 4, 5, CONFIG.ENEMY.SHOOTER.MODE2_PROJ_NORMAL1, self.projectile_speed_mod, 0, nil, self.entity.game_logic.projectile_enemy_shooter_anim ) self.entity.game_logic:spawn_projectile( self.entity, self.entity.game_logic.enemy_projectile_handler, pos_x, pos_y, 4, 5, CONFIG.ENEMY.SHOOTER.MODE2_PROJ_NORMAL2, self.projectile_speed_mod, 0, nil, self.entity.game_logic.projectile_enemy_shooter_anim ) end DATA.SFX[DATA.SFX.INDEX.ENEMY_BULLET]:play() end end, ["sfx.flashing"] = function() -- Implements flashing animation to work with the rendering logic of tic-80 -- This class acts more like an psudo-interface Flashing = lazy.class("Flashing") function Flashing:constructor() self.time = 0 self.cooldown = 0 end function Flashing:update(dt) if self.time ~= 0 then self.time = self.time - 1 end if self.time == 0 and self.cooldown ~= 0 then self.cooldown = self.cooldown - 1 end end function Flashing:render_function(func) if self.time ~= 0 then lazy.tic.set_palette_from_table(CONFIG.GLOBAL.FLASH_PALATTE) func() lazy.tic.palette_reset() else func() end end function Flashing:start(time) if self.cooldown ~= 0 then return end self.time = time and time or CONFIG.GLOBAL.FLASH_TIME self.cooldown = 2 end function Flashing:is_flashing() return self.time ~= 0 end end, } if files[path] then return files[path] else return origin_seacher(path) end end end --- GEN CODE --- -- CURRENT BUILD COUNTER BUILD = 13 --- METADATA GLOBALS META = { TITLE = "Quasar Rush", VERSION = "1.4b"..BUILD, AUTHOR = "Jaded Puma", } -- BUILD SETTINGS -- Since tic-80 requires a single file for export -- this is set after combining all lua files into a single one. IS_EXPORT = true DEMO_MODE = false DEBUG_MODE = false DEBUG_HP = false DEBUG_BOUNDINGBOX = false DEBUG_DISPLAY = false DEBUG_WAVE = false DEBUG_WAVE_DISPLAY = false DEBUG_NO_MUSIC = false DEBUG_SKIP_INTRO = false DEBUG_SHOW_COLOR_FPS = false DEBUG_SLOWDOWN_DETECT = false DEBUG_ENABLE_HARDMODE = false DEBUG_START_WAVE = 0 if DEBUG_MODE then META.VERSION = META.VERSION.."-DEBUG" end -- SET LIB PATH -- this is where project is localed if not IS_EXPORT then -- !!! set your project path here local PATH = ";/home/puma/.local/share/com.nesbox.tic/TIC-80/quasar-rush/?.lua" package.path = package.path .. PATH end -- LIBRARIES require("lazylib-tic.init") -- CORE FILES require("core.config") require("core.debug") require("core.debug_fps") require("core.globals") require("core.utility") require("core.data") require("core.wave_enemy_entry_base") require("core.wave_enemy_entry_range") require("core.wave_enemy_entry_onstart") require("core.wave_enemy_entry_timed") require("core.wave_enemy_entry_trigger") require("core.wave_enemy_entry_always") require("core.wave_enemy_entry_mod_time") require("core.wave_entry_pool") require("core.wave") require("core.wave_pool") require("core.controlled_random") -- SFX require("sfx.flashing") -- STATES require("states.intro") require("states.level") require("states.level_logic") require("states.level_render") -- ENTITIES require("entities.particle") require("entities.control_ball") require("entities.control_ball_logic") require("entities.control_ball_renderer") require("entities.spaceship") require("entities.spaceship_logic") require("entities.spaceship_renderer") require("entities.projectile_entity") require("entities.projectile_entity_logic") require("entities.projectile_entity_renderer") require("entities.enemy_base") require("entities.enemy_base_logic") require("entities.enemy_zombie_ship") require("entities.enemy_zombie_ship_logic") require("entities.enemy_zombie_ship_renderer") require("entities.enemy_medusa") require("entities.enemy_medusa_logic") require("entities.enemy_medusa_renderer") require("entities.enemy_kamikaze") require("entities.enemy_kamikaze_logic") require("entities.enemy_kamikaze_renderer") require("entities.enemy_shooter") require("entities.enemy_shooter_logic") require("entities.enemy_shooter_renderer") require("entities.enemy_peek") require("entities.enemy_peek_logic") require("entities.enemy_peek_renderer") require("entities.enemy_organic") require("entities.enemy_organic_logic") require("entities.enemy_orgranic_renderer") require("entities.enemy_bounce") require("entities.enemy_bounce_logic") require("entities.enemy_bounce_renderer") require("entities.enemy_seeker") require("entities.enemy_seeker_logic") require("entities.enemy_seeker_renderer") require("entities.enemy_heart") require("entities.enemy_heart_logic") require("entities.enemy_heart_renderer") require("entities.exp_entity") require("entities.sfx_entity") -- LOAD AFTER -- avoids depency issues require("core.wave_enemy_data") require("core.wave_data") require("core.particle_factory") require("core.upgrades") -- function BOOT() trace("Booting... '"..META.TITLE.."' BUILD: "..META.VERSION) -- Garbage collector settings if DEMO_MODE then trace("Set as DEMO build.") end if IS_EXPORT then trace("Set as export build.") else trace("Custom package path set.") end -- frame counter FRAME = -1 -- cursor global CURSOR = { x = -1, y = -1, left = false, middle = false, right = false, scrollx = 0, scrolly = 0, } -- globals SCREEN = { WIDTH = 240, HEIGHT = 136 } -- set inital state local start_state = StateIntro() if DEBUG_MODE and DEBUG_SKIP_INTRO then start_state = StateLevel() end -- debug if DEBUG_MODE then debugMode = DebugMode() trace("DEBUG_MODE is set to active.") end -- setup state manager with inital state STATE_MANAGER = StateManager(start_state) -- gc control collectgarbage("collect" ) collectgarbage("stop") GC_STATES = lazy.enum("STEP_UP", "STEP_DOWN") GC_SPEEDS = lazy.enum("SLOW", "FAST", "RAPID") GC_STATE = GC_STATES.STEP_UP GC_SPEED = GC_SPEEDS.RAPID GC_STEP = 1000 GC_STEP_SLOW = 1 GC_STEP_FAST = 10 GC_STEP_RAPID = 30 GC_STEP_CHANGE_STATE = 2 GC_STEP_CHANGE_STATE_COUNT = GC_STEP_CHANGE_STATE GC_STEP_SPEED_CHECK = 3 GC_STEP_SPEED_CHECK_COUNT = GC_STEP_SPEED_CHECK GC_COLLECT = 60 end function TIC() -- DEPRECATED VAR local dt = 1 -- debug update if debugMode then debugMode:update(dt) end -- cursor update CURSOR.x, CURSOR.y, CURSOR.left, CURSOR.middle, CURSOR.right, CURSOR.scrollx, CURSOR.scrolly = mouse() -- state update STATE_MANAGER:update(dt) -- render update cls(0) STATE_MANAGER:render() if debugMode then debugMode:render() end -- Update GC every frame local sign = 1; if GC_STATE == GC_STATES.STEP_UP then sign = 1 elseif GC_STATE == GC_STATES.STEP_DOWN then sign = -1 end if GC_SPEED == GC_SPEEDS.SLOW then GC_STEP = GC_STEP + GC_STEP_SLOW * sign elseif GC_SPEED == GC_SPEEDS.FAST then GC_STEP = GC_STEP + GC_STEP_FAST * sign elseif GC_SPEED == GC_SPEEDS.RAPID then GC_STEP = GC_STEP + GC_STEP_RAPID * sign end GC_STEP = lazy.math.lower_bound(GC_STEP, 1) local completed = collectgarbage("step", GC_STEP) collectgarbage("stop") -- check for change if GC_STATE == GC_STATES.STEP_UP then if not completed then GC_STEP_SPEED_CHECK_COUNT = GC_STEP_SPEED_CHECK_COUNT - 1 else GC_STEP_SPEED_CHECK_COUNT = GC_STEP_SPEED_CHECK end if completed then GC_STEP_CHANGE_STATE_COUNT = GC_STEP_CHANGE_STATE_COUNT - 1 else GC_STEP_CHANGE_STATE_COUNT = GC_STEP_CHANGE_STATE end if GC_STEP_CHANGE_STATE_COUNT == 0 then GC_STATE = GC_STATES.STEP_DOWN GC_SPEED = GC_SPEEDS.SLOW GC_STEP_CHANGE_STATE_COUNT = GC_STEP_CHANGE_STATE GC_STEP_SPEED_CHECK_COUNT = GC_STEP_SPEED_CHECK end elseif GC_STATE == GC_STATES.STEP_DOWN then if completed then GC_STEP_SPEED_CHECK_COUNT = GC_STEP_SPEED_CHECK_COUNT - 1 else GC_STEP_SPEED_CHECK_COUNT = GC_STEP_SPEED_CHECK end if not completed then GC_STEP_CHANGE_STATE_COUNT = GC_STEP_CHANGE_STATE_COUNT - 1 else GC_STEP_CHANGE_STATE_COUNT = GC_STEP_CHANGE_STATE end if GC_STEP_CHANGE_STATE_COUNT == 0 then GC_STATE = GC_STATES.STEP_UP GC_SPEED = GC_SPEEDS.SLOW GC_STEP_CHANGE_STATE_COUNT = GC_STEP_CHANGE_STATE GC_STEP_SPEED_CHECK_COUNT = GC_STEP_SPEED_CHECK end end if GC_STEP_SPEED_CHECK_COUNT == 0 then GC_STEP_SPEED_CHECK_COUNT = GC_STEP_SPEED_CHECK if GC_SPEED == GC_SPEEDS.SLOW then GC_SPEED = GC_SPEEDS.FAST elseif GC_SPEED == GC_SPEEDS.FAST then GC_SPEED = GC_SPEEDS.RAPID end end if FRAME % GC_COLLECT == 0 then collectgarbage("collect") collectgarbage("stop") end FRAME = FRAME + 1 end function OVR() STATE_MANAGER:overlay() end function SCN(scanline) STATE_MANAGER:scanline(scanline) end --- TIC-80 DATA