123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218 |
- --[========================================================================[--
- Game logic for Thrust II Reloaded.
- Copyright © 2015-2018 Pedro Gimeno Fortea
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- --]========================================================================]--
- local game = {}
- local la,le,lfs,lf,lg,li,lj,lk,lm,lmo,lp,ls,lsys,lth,lt,lw = require'ns'()
- local json = require '3rdparty.dkjson'
- local garbagetimer = 0
- local collision_canvas, no_collision
- local enemies, ship, cable, orbs, counters
- local tileset, tilecoll, spriteset, spritecoll, crashset, shotimg
- local tilebatch, orbbatch
- local tilequads, spritequads, crashquads
- local orb_sprite
- local lastxtile, lastytile
- local fineangle
- local was_turning, shooting, extending, thrusting
- local thrustvol = 0
- -- cached namespaces
- local map, enemytypes, decoys, targets, respawn, agents, keys
- local tgt_tile_start = 122
- local tgt_frames = 2 -- how many frames in animation
- local tgt_tile_current = tgt_tile_start
- local tgt_fps = 8/25
- local tgt_timer = 0
- local tgt_index -- index of current target in the targets table
- -- squared velocity threshold for being able to drop an orb into the target
- -- (can't drop if the orb crosses the target too fast)
- local tgt_vel_threshold2 = 10000 -- 100 px/s
- local cable_maxlen = 80
- local cable_maxlen2 = cable_maxlen * cable_maxlen
- local cable_extend_speed = 5
- local max_shoot_radius = 224
- local shoot_radius = 0
- local total_shoot_time = 4
- local latchsnd, orbdangersnd, crashsnd, cablesnd, enemykillsnd
- local getagentsnd, shootsnd, orbpickupsnd, orbdropsnd, thrustsnd, music
- local DIV = love_version >= 11000000 and 255 or 1
- function game.load()
- -- Game viewport
- game.vx, game.vy, game.vw, game.vh = 0, 0, main.ww, main.wh
- -- Cache some namespaces
- map, enemytypes, decoys, targets, respawn, agents, keys = main.map, main.enemytypes,
- main.decoys, main.targets, main.respawn, main.agents, main.keys
- tileset = lg.newImage("img/Tiles32x32.png")
- tilecoll = lg.newImage("img/Tiles32x32_collision.png")
- spriteset = lg.newImage("img/Sprites32x32.png")
- spritecoll = lg.newImage("img/Sprites32x32_collision.png")
- crashset = lg.newImage("img/Explosion32x32.png")
- shotimg = lg.newImage("img/Shot.png")
- -- Don't use nearest for the shot, as it's blurred to start with.
- shotimg:setFilter("linear", "linear")
- local tx, ty = tileset:getDimensions()
- local sx, sy = spriteset:getDimensions()
- -- Tiles are easy
- tilequads = {} -- used for both normal tiles and collision tiles
- -- which means the images must have the same size
- -- (same applies to spritequads below)
- for y = 0, (ty-1)/32 do
- for x = 0, (tx-1)/32 do
- tilequads[#tilequads + 1] = lg.newQuad(x*32, y*32, 32, 32, tx, ty)
- end
- end
- -- Sprites
- spritequads = {}
- -- Ship quads
- for y = 0, 1 do
- for x = 0, 15 do
- spritequads[#spritequads + 1] = lg.newQuad(x*32, y*32, 32, 32, sx, sy)
- end
- end
- -- Enemies quads
- -- Assumes that if there is a higher sprite in a row, then the space
- -- in the next rows until completing that height is empty.
- -- E.g. if X is 32x32 (1x1 cells) and Y is 64x64 (2x2 cells):
- -- X YY YY X X correct
- -- YY YY
- --
- -- X YY YY X X incorrect - don't reuse the empty spaces
- -- X YY YY (our algorithm isn't that clever)
- local idx = 32 -- 32x32 cell number - we start after the ship sprites
- local height = 1 -- height of the highest cell in the row
- for k, v in ipairs(enemytypes) do
- -- Cache the starting quad. The animation spans nframes from here.
- if v.nframes >= 1 then v.initsprite = #spritequads + 1 end
- for enemynum = 1, v.nframes do
- spritequads[#spritequads + 1] =
- lg.newQuad(idx%16*32, (idx-idx%16)/16*32, v.width*32, v.height*32, sx, sy)
- if v.height > height then height = v.height end
- idx = idx + v.width
- if idx % 16 == 0 then
- idx = idx + 32 * (height-1) -- skip row height
- height = 1
- end
- end
- end
- -- Finally, the orb
- orb_sprite = #spritequads + 1
- spritequads[orb_sprite] =
- lg.newQuad(idx%16*32, (idx-idx%16)/16*32, 32, 32, sx, sy)
- -- and its glow
- idx = idx + 1
- spritequads[orb_sprite+1] =
- lg.newQuad(idx%16*32, (idx-idx%16)/16*32, 32, 32, sx, sy)
- -- Ship crash animation
- sx, sy = crashset:getDimensions()
- crashquads = {}
- for idx = 0, 31 do
- crashquads[idx+1] = lg.newQuad(idx%4*32, (idx-idx%4)/4*32, 32, 32, sx, sy)
- end
- tilebatch = lg.newSpriteBatch(tileset,
- -- In 1D, a 33-pixel window can see up to two 32-pixel tiles
- -- simultaneously. One needs a 34-pixel window to be able
- -- to see three. So in 1D it would be: floor((widht+62)/32)
- -- which equals 2 for width=33 and 3 for width=34. In 2D the
- -- natural extension is the following.
- math.floor((game.vw + 62)*(game.vh + 62)/(32*32))
- -- For 640x480, that's 336 tiles. Less than the default 1000,
- -- so quite bearable.
- )
- collision_canvas = lg.newCanvas(32, 64) -- enough for the ship and orb sprite
- -- 0.10.0 compatibility
- if collision_canvas.newImageData then
- no_collision = string.rep("\0", #collision_canvas:newImageData():getString())
- else
- no_collision = string.rep("\0", #collision_canvas:getImageData():getString())
- end
- -- How many orbs we're going to draw max. It's pretty static, it only changes
- -- when an orb is picked up.
- orbbatch = lg.newSpriteBatch(spriteset, #main.orbs + #main.decoys)
- -- Load sounds
- latchsnd = la.newSource("snd/latch.wav", "static")
- latchsnd:setVolume(0.4) -- make it subtler
- getagentsnd = la.newSource("snd/LoadAgent.wav", "static")
- shootsnd = la.newSource("snd/AgentShot.wav", "static")
- orbdangersnd = la.newSource("snd/orbdanger.wav", "static")
- orbdangersnd:setLooping(true)
- orbpickupsnd = la.newSource("snd/click_one_22khz.wav", "static")
- orbdropsnd = la.newSource("snd/click_two_22khz.wav", "static")
- thrustsnd = la.newSource("snd/jet_lp.ogg", "static")
- thrustsnd:setLooping(true)
- -- Crash sound
- crashsnd = la.newSource("snd/Grenade-SoundBible.com-1777900486.ogg", "static")
- -- Enemy killed
- enemykillsnd = la.newSource("snd/supertank_plazma_fireball_22khz.mp3", "static")
- -- Make a duplicate so we can ply two at the same time
- -- Only 0.9.1+ has Source:clone(); 0.9.0 needs to load a new instance.
- local enemykillsnd2 = enemykillsnd.clone and enemykillsnd:clone()
- or la.newSource("snd/supertank_plazma_fireball_22khz.mp3", "static")
- -- Turn it into a table
- enemykillsnd = { enemykillsnd, enemykillsnd2 }
- -- Cable extend/retract sound
- cablesnd = la.newSource("snd/Cable_Sound.wav", "static")
- -- Music
- music = la.newSource("snd/Thrust II - in game - Loop 909924-6942804.ogg", "stream")
- music:setVolume(0.2)
- end
- local indent_true = {indent=true}
- function game.savegame()
- main.savedelay = 2.5
- local f = json.encode(game.state, indent_true)
- lfs.write("saved.txt", f, #f)
- end
- function game.activate()
- if cable.m ~= 0 then
- orbdangersnd:setVolume(0)
- orbdangersnd:play()
- end
- if main.music then
- music:play()
- end
- thrustvol = 0
- thrustsnd:setVolume(0)
- thrustsnd:play()
- end
- function game.deactivate()
- orbdangersnd:stop()
- thrustsnd:stop()
- music:stop()
- end
- function game.pause(pause)
- if pause then
- orbdangersnd:stop()
- music:pause()
- thrustsnd:setVolume(0)
- thrustvol = 0
- else
- if cable.m ~= 0 then
- orbdangersnd:play()
- end
- if main.music then
- music:play()
- end
- thrusting = lk.isDown(keys.thrust)
- if thrusting then
- thrustsnd:setVolume(1)
- thrustvol = 1
- end
- end
- end
- local function new_or_load_game(load)
- -- restore saved game or start new game
- if load and main.isFile("saved.txt") then
- local f, s = lfs.read("saved.txt")
- local tmp, err
- game.state, tmp, err = json.decode(f, 1, json.null, nil)
- if err then
- game.state = false
- else
- -- validate
- if #game.state.orbs > #main.orbs
- or #game.state.enemies > #main.enemies
- or game.state.counters.shields > 99
- then game.state = false end
- end
- else
- game.state = false
- end
- if not game.state then
- -- New game - copy initial state
- game.state = { enemies = main.deepcopy(main.enemies),
- dyingenemies = {},
- ship = { angle = 0 },
- orbs = main.deepcopy(main.orbs),
- cable = { length = 0, m = 0, latched = false },
- counters = { shields = 10, score = 0, respawn = 1, clock = 0,
- deadtimer = false, },
- }
- game.state.ship.x = main.respawn[game.state.counters.respawn].x
- game.state.ship.y = main.respawn[game.state.counters.respawn].y
- game.state.ship.oldx = game.state.ship.x -- initialize integrator with vel 0
- game.state.ship.oldy = game.state.ship.y
- game.state.cable.x = game.state.ship.x
- game.state.cable.y = game.state.ship.y
- game.state.cable.oldx = game.state.ship.x
- game.state.cable.oldy = game.state.ship.y
- -- Can't find a good place for this at the moment.
- game.state.counters.shields = game.state.counters.shields - 1
- end
- -- Compatibility fixup
- if not game.state.dyingenemies then
- game.state.dyingenemies = {}
- end
- if game.state.counters.agent and not game.state.counters.shoot_agent then
- game.state.counters.shoot_agent = game.state.counters.agent
- end
- -- shortcuts
- enemies, ship, cable, orbs, counters =
- game.state.enemies, game.state.ship, game.state.cable, game.state.orbs,
- game.state.counters
- -- Clean up to start again
- orbbatch:clear()
- -- Add decoys
- for k, v in ipairs(decoys) do
- orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
- end
- -- Add normal orbs
- for k, v in ipairs(orbs) do
- v.sprite = orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
- end
- -- Update map with current progress
- tgt_index = #orbs + (cable.m ~= 0 and 1 or 0)
- for k, v in ipairs(targets) do
- if k <= tgt_index then
- map[v.y*128+v.x+1] = v.empty
- else
- map[v.y*128+v.x+1] = v.tile
- end
- end
- -- force refresh
- lastxtile = false
- -- assume no key pressed
- was_turning = false
- shooting = false
- extending = false
- -- initialize first frame
- game.update(0)
- end
- local function update_ship(dt)
- if dt == 0 then return end
- --[[ update angle ]]
- local angvel = 16
- local kleft, kright = lk.isDown(keys.left), lk.isDown(keys.right)
- if kleft and not kright then
- if was_turning then
- fineangle = (fineangle - angvel*dt) % 32
- ship.angle = math.ceil(fineangle) % 32
- else
- was_turning = true -- sharp reaction upon pressing
- fineangle = (ship.angle - 1) % 32
- ship.angle = fineangle
- end
- elseif kright and not kleft then
- if was_turning then
- fineangle = (fineangle + angvel*dt) % 32
- ship.angle = math.floor(fineangle)
- else
- was_turning = true
- fineangle = (ship.angle + 1) % 32
- ship.angle = fineangle
- end
- else
- was_turning = false
- fineangle = ship.angle
- end
- --[[ update ship (Verlet integrator) ]]
- -- flight parameters
- local thrust = 600
- local gravity = 45
- local dragx = 0.2
- local dragy = 0.166
- local shipmass = 1
- local dt2 = dt*dt
- local shipforcex, shipforcey = 0, gravity * shipmass
- local was_thrusting = thrusting
- thrusting = lk.isDown(keys.thrust)
- if thrusting then
- thrustsnd:setVolume(1)
- thrustvol = 1
- shipforcex = shipforcex + math.sin(fineangle*math.pi/16) * thrust
- shipforcey = shipforcey - math.cos(fineangle*math.pi/16) * thrust
- --else
- -- sound stopping handled by game.update, to ramp down volume
- end
- local orbaccelx = 0
- local orbaccely = 0
- local newcablex
- local newcabley
- local newshipx = ship.x
- local newshipy = ship.y
- local orbmass
- orbmass = cable.m * shipmass -- orb mass is how heavier it is than the ship
- newcablex = cable.x
- newcabley = cable.y
- orbaccelx = 0
- orbaccely = gravity
- newcablex = newcablex + (cable.x - cable.oldx) * (1-dragx*dt) + orbaccelx * dt2
- newcabley = newcabley + (cable.y - cable.oldy) * (1-dragy*dt) + orbaccely * dt2
- local accelx = shipforcex / shipmass
- local accely = shipforcey / shipmass
- newshipx = newshipx + (newshipx - ship.oldx) * (1-dragx*dt) + accelx * dt2
- newshipy = newshipy + (newshipy - ship.oldy) * (1-dragy*dt) + accely * dt2
- -- Apply length constraint
- local cablex = newshipx - newcablex
- local cabley = newshipy - newcabley
- local actuallen = math.sqrt(cablex*cablex + cabley*cabley)
- if actuallen < 0.00001 then actuallen = 0.00001 end -- should never happen
- -- the distance to adjust for is cable_maxlen - actuallen
- -- ours is a rope, not a stick - remove this condition if original behavior wanted
- if actuallen > cable_maxlen then
- newshipx = newshipx + (cable_maxlen - actuallen) * cablex / actuallen
- * (orbmass / (orbmass + shipmass))
- newshipy = newshipy + (cable_maxlen - actuallen) * cabley / actuallen
- * (orbmass / (orbmass + shipmass))
- newcablex = newcablex + (cable_maxlen - actuallen) * cablex / actuallen
- * -(shipmass / (orbmass + shipmass))
- newcabley = newcabley + (cable_maxlen - actuallen) * cabley / actuallen
- * -(shipmass / (orbmass + shipmass))
- end
- -- "Scroll" orb position
- cable.oldx = cable.x
- cable.oldy = cable.y
- cable.x = newcablex
- cable.y = newcabley
- -- "Scroll" ship position
- ship.oldx = ship.x
- ship.oldy = ship.y
- ship.x = newshipx
- ship.y = newshipy
- -- Normalize if both orb and ship are equal modulo 4096
- if not cable or math.floor(ship.x / 4096) == math.floor(cable.x / 4096) then
- local k = math.floor(ship.x / 4096) * 4096
- ship.x = ship.x - k
- ship.oldx = ship.oldx - k
- if cable then
- cable.x = cable.x - k
- cable.oldx = cable.oldx - k
- end
- end
- end
- function game.newgame()
- new_or_load_game(main.restore)
- end
- local function collided()
- -- Collision test - paint the collision tiles/sprites to a canvas
- -- and multiply by ship's collision sprite to see if there are
- -- common pixels (all zeros = no)
- lg.setCanvas(collision_canvas)
- -- Clear collision canvas
- if collision_canvas.clear then
- collision_canvas:clear()
- else
- lg.clear()
- end
- -- In order to only do getImageData once, our canvas is
- -- divided into two halves vertically. The top part is for
- -- the ship, the bottom part is for the orb.
- for i = 0, (cable.m ~= 0 and 32 or 0), 32 do -- Draw twice if an orb is carried, else once.
- lg.setScissor(0, i, 32, 32) -- select which half to be drawn
- local topleftx = i == 0 and math.floor(ship.x-15.5)%4096 or math.floor(cable.x-16)%4096
- local toplefty = i == 0 and math.floor(ship.y-15.5) or math.floor(cable.y-16)
- -- Colision with tile
- local localx = -(topleftx%32)
- local localy = -(toplefty%32)
- local tilex = (topleftx+localx)/32
- local tiley = (toplefty+localy)/32
- if tiley > 62 then tiley = 62 end
- if tiley < 0 then tiley = 0 end
- lg.draw(tilecoll, tilequads[map[tilex%128 + tiley*128 + 1]], localx, localy+i)
- if localx + 32 < 32 then
- lg.draw(tilecoll, tilequads[map[(tilex+1)%128 + tiley*128 + 1]], localx+32, localy+i)
- end
- if localy + 32 < 32 then
- lg.draw(tilecoll, tilequads[map[tilex%128 + (tiley+1)*128 + 1]], localx, localy+32+i)
- end
- if localx + 32 < 32 and localy + 32 < 32 then
- lg.draw(tilecoll, tilequads[map[(tilex+1)%128 + (tiley+1)*128 + 1]], localx+32, localy+32+i)
- end
- -- Collision with decoys
- local vx, vy
- for k, v in ipairs(decoys) do
- vx = v.x-16 - topleftx - (topleftx < 32 and v.x >= 4064 and 4096 or 0)
- vy = v.y-16 - toplefty
- if vx > -32 and vx < 32 and vy > -32 and vy < 32 then -- only draw if in range
- lg.draw(spritecoll, spritequads[orb_sprite], vx, vy + i)
- end
- end
- -- Same for orbs
- for k, v in ipairs(orbs) do
- vx = v.x-16 - topleftx - (topleftx < 32 and v.x >= 4064 and 4096 or 0)
- vy = v.y-16 - toplefty
- if vx > -32 and vx < 32 and vy > -32 and vy < 32 then -- only draw if in range
- lg.draw(spritecoll, spritequads[orb_sprite], vx, vy + i)
- end
- end
- if i ~= 0 then
- -- only collide the orb with the ship, not with itself
- -- they are guaranteed to be in the same multiple of 4096
- vx = math.floor(ship.x-15.5) - topleftx
- vy = math.floor(ship.y-15.5) - toplefty
- lg.draw(spritecoll, spritequads[ship.angle + 1], vx, vy + i)
- end
- -- Collision with enemies
- local et
- for k, v in ipairs(enemies) do
- et = enemytypes[v.type]
- vx = math.floor(v.x/2)*2 - topleftx - (topleftx < et.width*32 and v.x >= 4096-et.width*32 and 4096 or 0)
- vy = math.floor(v.y/2)*2 - toplefty
- if vx > -(et.width*32) and vx < 32 and vy > -(et.height*32) and vy < 32 then
- -- visible
- lg.draw(spriteset, spritequads[v.f], vx, vy + i)
- end
- end
- -- Draw ship/orb in multiplicative mode
- if love_version < 0010000 then
- lg.setBlendMode("multiplicative")
- else
- lg.setBlendMode("multiply", "premultiplied")
- end
- lg.draw(spritecoll, spritequads[i == 0 and ship.angle+1 or orb_sprite], 0, i)
- lg.setBlendMode("alpha", "alphamultiply") -- return blend mode to normal
- end
- lg.setScissor()
- lg.setCanvas()
- if collision_canvas.newImageData then
- return collision_canvas:newImageData():getString() ~= no_collision
- else
- return collision_canvas:getImageData():getString() ~= no_collision
- end
- end
- function game.update(dt)
- garbagetimer = garbagetimer + dt
- if garbagetimer >= 5 then
- -- playing with Canvas:getImageData tends to generate garbage that isn't
- -- collected, so we help Lua a bit here
- collectgarbage()
- garbagetimer = 0
- end
- if not counters.deadtimer and dt ~= 0 and collided() then
- -- Player died - start death timer
- counters.deadtimer = 2.5
- ship.crashframe = 1
- ship.crashinterval = 0
- thrusting = false
- crashsnd:play()
- end
- if not thrusting then
- thrustvol = (thrustvol * 0.33) - 0.00001
- if thrustvol < 0 then
- thrustvol = 0
- end
- thrustsnd:setVolume(thrustvol)
- end
- if counters.deadtimer then
- -- refresh ship crash animation
- if ship.crashframe then
- -- when crashframe exists, crashinterval must exist too
- ship.crashinterval = ship.crashinterval + dt
- if ship.crashinterval >= 0.1 then
- ship.crashinterval = ship.crashinterval - 0.1
- ship.crashframe = ship.crashframe + 1
- if ship.crashframe > 16 then
- ship.crashframe = nil
- ship.crashinterval = nil
- end
- end
- end
- counters.deadtimer = counters.deadtimer - dt
- if counters.deadtimer <= 0 then
- counters.deadtimer = false
- ship.crashframe = nil
- ship.crashinterval = nil
- -- Respawn point for the player
- ship.x = respawn[counters.respawn].x
- ship.y = respawn[counters.respawn].y
- ship.oldx = ship.x
- ship.oldy = ship.y
- ship.angle = 0
- was_turning = false
- if cable then
- cable.x = ship.x
- cable.y = ship.y + cable_maxlen
- cable.oldx = cable.x
- cable.oldy = cable.y
- end
- if counters.shields == 0 then
- main.activate(screens.gameover)
- return
- else
- counters.shields = counters.shields - 1
- screens.getready.fromstart = false
- main.activate(screens.getready)
- return
- end
- end
- end
- -- Update dead enemies' animations
- do
- local l = #game.state.dyingenemies
- for i = l, 1, -1 do
- local v = game.state.dyingenemies[i]
- v.t = v.t + dt
- if v.t >= 0.1 then
- v.t = v.t - 0.1
- v.f = v.f + 1
- if v.f > 8 then
- game.state.dyingenemies[i] = game.state.dyingenemies[l]
- game.state.dyingenemies[l] = nil
- l = l - 1
- end
- end
- end
- end
- if dt > 0.05 then dt = 0.05 end -- smoothen the movement
- -- read keys
- local was_shooting = shooting
- shooting = lk.isDown(keys.fire)
- local pickup = lk.isDown(keys.pickup)
- if not pickup and cable.latched then cable.latched = false end
- -- Update orb timer
- if counters.orbinterval and not counters.deadtimer then
- counters.orbinterval = counters.orbinterval + dt
- if counters.orbinterval >= 0.625 then
- counters.orbinterval = counters.orbinterval - 0.625
- counters.orbtimer = counters.orbtimer - 1
- end
- if counters.orbtimer <= 0 then
- -- Kaboom!
- main.activate(screens.orbexplode)
- return
- end
- end
- -- Check if an orb was picked up
- if cable.latched then
- cable.x = orbs[cable.latched].x
- cable.y = orbs[cable.latched].y
- cable.oldx = cable.x
- cable.oldy = cable.y
- if (orbs[cable.latched].x-ship.x)^2 + (orbs[cable.latched].y-ship.y)^2 >= cable_maxlen2 then
- -- Latched to orb
- cable.m = orbs[cable.latched].m
- orbpickupsnd:play()
- orbdangersnd:setVolume(0)
- orbdangersnd:play()
- -- Start the unstable orb timer
- counters.orbinterval = 0
- counters.orbtimer = 500
- -- Remove the orb. The orbs table is unsorted, so for performance, to
- -- avoid scrolling (if removing a middle element) or creating a hole
- -- (if setting it to nil), we move the last element to this place.
- --table.remove(orbs, cable.latched) -- works, but this is presumably faster:
- orbs[cable.latched] = orbs[#orbs]
- orbs[#orbs] = nil
- cable.oldx = cable.x
- cable.oldy = cable.y
- -- Sprites can't be deleted, so regenerate the batch.
- orbbatch:clear()
- -- Add decoys
- for k, v in ipairs(decoys) do
- orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
- end
- -- Add normal orbs
- for k, v in ipairs(orbs) do
- v.sprite = orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
- end
- cable.latched = false
- end
- end
- -- Update player
- if not counters.deadtimer then
- update_ship(dt)
- end
- -- Clamp vertically
- if ship.y < 64 then ship.y = 64 ship.oldy = 64 - dt*100 end -- make a bouncing effect
- if ship.y > 2038 then ship.y = 2038 end
- -- Assign respawn zone
- for k, v in ipairs(respawn) do
- if ship.x%4096 >= v.topleftx and ship.x%4096 < v.topleftx + v.w
- and ship.y >= v.toplefty and ship.y < v.toplefty + v.h
- then
- counters.respawn = k
- break
- end
- end
- -- Target animation timer
- tgt_timer = tgt_timer + dt
- if tgt_timer / tgt_fps >= 1 then
- tgt_timer = tgt_timer % tgt_fps
- tgt_tile_current = (tgt_tile_current - tgt_tile_start + 1) % tgt_frames + tgt_tile_start
- end
- -- Deposit orb in target
- local tgt = targets[tgt_index]
- if cable.m ~= 0 and tgt and dt > 0 then
- -- Check if orb is in target
- local orbvelx = (cable.x - cable.oldx) / dt
- local orbvely = (cable.y - cable.oldy) / dt
- if orbvelx*orbvelx + orbvely*orbvely < tgt_vel_threshold2 then
- if cable.x >= tgt.x*32 and cable.x < tgt.x*32+32
- and cable.y >= tgt.y*32 and cable.y < tgt.y*32+32
- then
- orbdropsnd:play()
- counters.score = counters.score + counters.orbtimer * 10
- cable.m = 0
- map[tgt.y*128+tgt.x+1] = tgt.tile
- tgt_index = tgt_index - 1
- if tgt_index == 0 then
- -- Last target achieved!
- main.activate(screens.gamewon)
- return
- end
- -- Force refresh of map
- lastxtile = false
- -- extra shield every 4 orbs deposited
- if (#main.orbs - #orbs) % 4 == 0 then
- counters.shields = counters.shields + 1
- end
- counters.orbinterval = nil
- counters.orbtimer = nil
- orbdangersnd:stop()
- end
- end
- end
- -- Agent timer
- if counters.agent then
- counters.agentinterval = counters.agentinterval + dt
- if counters.agentinterval >= 0.625 then
- counters.agentinterval = counters.agentinterval - 0.625
- counters.agenttime = counters.agenttime - 1
- if counters.agenttime <= 0 then
- counters.agenttime = nil
- counters.agent = nil
- counters.agentinterval = nil
- end
- end
- end
- -- Check if agent picked up.
- if pickup and (not counters.agenttime or counters.agenttime < 290) then
- local x, y = ship.x % 4096, ship.y + 64
- local t
- if y >= 0 and y < 2048 then
- t = agents[map[math.floor(x/32) + math.floor(y/32)*128 + 1]]
- if t then
- counters.agent = t
- counters.agenttime = 300
- counters.agentinterval = 0
- getagentsnd:play()
- end
- end
- end
- -- Shoot timer
- if counters.shoot_timer then
- counters.shoot_timer = counters.shoot_timer - dt
- if counters.shoot_timer <= 0 then
- counters.shoot_timer = nil
- counters.shoot_x = nil
- counters.shoot_y = nil
- counters.shoot_agent = nil
- end
- end
- -- Check if shooting
- if shooting and not was_shooting and counters.agent and not counters.shoot_timer and not counters.deadtimer then
- counters.shoot_timer = total_shoot_time
- counters.shoot_x = ship.x
- counters.shoot_y = ship.y
- counters.shoot_agent = counters.agent
- shootsnd:play()
- end
- if counters.shoot_timer then
- shoot_radius = counters.shoot_timer / total_shoot_time
- shoot_radius = (1 - shoot_radius ^ 5) * max_shoot_radius
- if counters.shoot_timer > 1.5 then -- past 2.5 seconds it's too diluted
- local x1, y1, x2, y2
- local radius2 = shoot_radius*shoot_radius*(0.85*0.85) -- (the .85 compensates the diffuse image radius)
- -- did it hit any enemies?
- local k, v, l
- k = 1
- l = #enemies
- while k <= l do -- iterate manually to safely delete elements
- v = enemies[k]
- if v.type == counters.shoot_agent then
- -- the centre of the bounding box must be within the radius
- -- (not too realistic, but a collision analysis for this sounds
- -- overkill)
- x1 = v.x - counters.shoot_x
- y1 = v.y - counters.shoot_y
- x2 = x1 + enemytypes[v.type].width*32
- y2 = y1 + enemytypes[v.type].height*32
- -- Consider wraparound if the distance is too large
- if x1 < -2048 then
- x1 = x1 + 4096
- x2 = x2 + 4096
- elseif x1 > 2048 then
- x1 = x1 - 4096
- x2 = x2 - 4096
- end
- x1 = (x1 + x2) * 0.5
- y1 = (y1 + y2) * 0.5
- x1 = x1*x1
- y1 = y1*y1
- if x1 + y1 < radius2 then
- -- Killed enemy
- counters.score = counters.score + counters.agenttime * 10
- enemies[k].f = 1 -- explosion frame
- enemies[k].t = 0 -- timer to advance frame
- game.state.dyingenemies[#game.state.dyingenemies + 1] = enemies[k]
- enemies[k] = enemies[l]
- enemies[l] = nil
- l = l - 1
- -- Play kill enemy sound
- if enemykillsnd[1]:isPlaying() then
- enemykillsnd[2]:play()
- else
- enemykillsnd[1]:play()
- end
- else
- k = k + 1
- end
- else
- k = k + 1
- end
- end
- end
- end
- if cable.m == 0 then
- local was_extending = extending
- extending = (pickup and 1 or -1)
- if cable.length > 0 or extending == 1 then
- cable.length = cable.length + extending * dt * cable_extend_speed
- else
- extending = false
- end
- if extending ~= was_extending and not counters.deadtimer then
- cablesnd:play()
- end
- if cable.length > 1 then cable.length = 1 end
- if cable.length <= 0 then
- cable.length = 0
- -- fixup position x and y
- cable.x = ship.x
- cable.y = ship.y + cable_maxlen
- -- equate speed to ship's speed
- cable.oldx = ship.oldx - ship.x + cable.x
- cable.oldy = ship.oldy - ship.y + cable.y
- end
- end
- -- Check if cable reaches orb
- if cable.m == 0 and cable.length == 1 and not cable.latched then
- -- Window for consideration
- local sx1, sy1, sx2, sy2 = ship.x-cable_maxlen, ship.y-cable_maxlen,
- ship.x+cable_maxlen, ship.y+cable_maxlen
- for k, v in ipairs(orbs) do
- if v.x >= sx1 and v.x <= sx2 and v.y >= sy1 and v.y <= sy2 then
- -- *Might* be in range - do the more expensive Euclidean check
- if (v.x-ship.x)^2 + (v.y-ship.y)^2 <= cable_maxlen2 then
- -- In range - latch to orb
- cable.latched = k
- cable.x = v.x
- cable.y = v.y
- latchsnd:play()
- break
- end
- end
- end
- end
- -- Update enemies' sprites and positions
- counters.clock = counters.clock + dt
- local t, tpos, et
- for k, v in ipairs(enemies) do
- -- Position
- tpos = (v.t + counters.clock) % v.period
- if tpos * 2 >= v.period then
- -- going back
- t = (v.period - tpos)*2 / v.period
- else
- t = tpos * 2 / v.period
- end
- -- Hack because our horizontal positions are broken
- local vx1
- if v.x0 == v.x1 then vx1 = v.x1 else vx1 = v.x1+2 end
- v.x = t < 0.5 and v.x0 + ( vx1 - v.x0) * t or vx1 - ( vx1 - v.x0) * (1 - t)
- v.y = t < 0.5 and v.y0 + (v.y1 - v.y0) * t or v.y1 - (v.y1 - v.y0) * (1 - t)
- -- Frame
- et = enemytypes[v.type]
- -- t = (v.frame + counters.clock * v.fps) % et.nframes
- if vx1 ~= v.x0 then
- t = (tpos/(v.period*16)*(vx1-v.x0)*v.fps) % et.nframes
- else
- t = (tpos/(v.period*16)*(v.y1-v.y0)*v.fps) % et.nframes
- end
- if et.pingpong then
- if tpos * 2 >= v.period then -- going backwards
- t = (et.nframes-t) % et.nframes
- end
- end
- v.f = et.initsprite + math.floor(t)
- end
- -- Debug string
- -- game.DEBUG=tostring(collided()).. " " .. 1/dt
- -- Loop music
- if music:isPlaying() then
- local pos = music:tell("samples")
- -- Loop it back to a sensible position
- if pos >= 6942804 then music:seek(pos - (6942804 - 909924), "samples") end
- end
- end
- function game.tiles_draw(x, y)
- -- Clamp coordinates to acceptable values
- --if y < 0 then y = 0 end
- --if y > 2048 - game.vh then y = 2048 - game.vh end
- x = x % 4096
- local xtile = math.floor(x/32)
- local ytile = math.floor(y/32)
- local xtiles = math.floor(game.vw+62)/32 -- max visible tiles
- local ytiles = math.floor(game.vh+62)/32
- if ytile + ytiles > 64 then
- -- clamp vertically
- ytiles = 64 - ytile
- end
- if xtile ~= lastxtile or ytile ~= lastytile then
- -- update required
- lastxtile, lastytile = xtile, ytile
- tilebatch:clear()
- for yt = ytile, ytile + ytiles - 1 do
- for xt = xtile, xtile + xtiles - 1 do
- tilebatch:add(tilequads[map[yt*128 + xt%128 + 1]],
- (xt-xtile)*32, (yt-ytile)*32)
- end
- end
- end
- lg.draw(tilebatch, -(x%32), -(y%32))
- end
- function game.orbs_draw(x, y)
- x = x % 4096
- lg.draw(orbbatch, -x, -y)
- -- Draw 1 screen to the left and/or 1 screen to the right if necessary
- if x < 32 then
- lg.draw(orbbatch, -x-4096, -y)
- end
- if x + game.vh >= 4064 then
- lg.draw(orbbatch, -x+4096, -y)
- end
- end
- function game.draw()
- local vpx = math.floor(ship.x+0.5-game.vw/2)
- local vpy = math.floor(ship.y+0.5-game.vh/2)
- if vpy < 0 then vpy = 0 end
- if vpy > 2048-game.vh then vpy = 2048-game.vh end
- lg.setScissor(game.vx, game.vy, game.vw, game.vh)
- -- draw tiles
- game.tiles_draw(vpx, vpy)
- -- HACK: draw target in white first (we will colorize it later using multiplicative)
- local tgt = targets[tgt_index]
- if tgt then
- lg.draw(tileset, tilequads[75], tgt.x*32-vpx, tgt.y*32-vpy)
- end
- -- draw enemies
- local et
- for k, v in ipairs(enemies) do
- et = enemytypes[v.type]
- for x = v.x - 4096, vpx + game.vw, 4096 do
- if not (x >= vpx + game.vw
- or x+et.width*32 < vpx
- or v.y >= vpy + game.vh
- or v.y+et.height*32 < vpy)
- then
- -- visible
- lg.draw(spriteset, spritequads[v.f],
- (math.floor(x/2)*2 - vpx), (math.floor(v.y/2)*2 - vpy)
- )
- end
- end
- end
- -- Draw enemy crash animations
- for k, v in ipairs(game.state.dyingenemies) do
- lg.draw(crashset, crashquads[v.f], v.x - vpx, v.y - vpy)
- lg.draw(crashset, crashquads[v.f], v.x - 4096 - vpx, v.y - vpy)
- end
- -- draw cable line
- if (cable.m ~= 0 or cable.length ~= 0) and not counters.deadtimer then
- lg.setLineStyle("smooth")
- -- draw cable with length cable.length
- local cx = (cable.x-ship.x)*cable.length + ship.x
- local cy = (cable.y-ship.y)*cable.length + ship.y
- if not counters.deadtimer then
- lg.line(cx-vpx, cy-vpy, ship.x-vpx, ship.y-vpy)
- end
- end
- -- draw orbs/decoys
- game.orbs_draw(vpx, vpy)
- -- draw carried orb, if any
- if cable.m ~= 0 then
- -- draw orb
- if counters.deadtimer then
- if ship.crashframe then
- lg.draw(crashset, crashquads[ship.crashframe+16], cable.x-vpx-15, cable.y-vpy-18)
- end
- else
- local orbx = math.floor(cable.x-vpx-16)
- local orby = math.floor(cable.y-vpy-16)
- lg.draw(spriteset, spritequads[orb_sprite], orbx, orby)
- -- draw glow
- --lg.setBlendMode("additive")
- local orbtime = (500 - counters.orbtimer)*0.625 + counters.orbinterval
- local freq = .01/(1.01-orbtime*0.0032)
- if freq > 10 then freq = 10 end
- local amp = (1-math.cos(freq*orbtime))*freq*0.5
- lg.setColor(255/DIV,255/DIV,255/DIV, amp^0.7*(255/DIV))
- orbdangersnd:setVolume(amp^2)
- lg.draw(spriteset, spritequads[orb_sprite+1], orbx, orby)
- lg.setColor(255/DIV,255/DIV,255/DIV,255/DIV)
- lg.setBlendMode("alpha", "alphamultiply")
- end
- end
- -- draw ship
- if counters.deadtimer then
- if ship.crashframe then
- lg.draw(crashset, crashquads[ship.crashframe], ship.x-vpx-16, ship.y-vpy-16)
- end
- else
- lg.draw(spriteset, spritequads[ship.angle+1], math.floor(ship.x-vpx-15.5), math.floor(ship.y-vpy-15.5))
- end
- -- HACK: draw target in multiplicative mode (colorizes other sprites)
- if tgt then
- if love_version < 0010000 then
- lg.setBlendMode("multiplicative")
- else
- lg.setBlendMode("multiply", "premultiplied")
- end
- lg.draw(tileset, tilequads[tgt_tile_current], tgt.x*32-vpx, tgt.y*32-vpy)
- lg.setBlendMode("alpha", "alphamultiply")
- end
- -- draw shooting explosion
- if counters.shoot_timer then
- -- colorize explosion - red stays at 255,
- -- green diminishes slow and blue diminishes quick
- local red, green, blue
- local t = counters.shoot_timer/total_shoot_time -- from 1 to 0
- red = 255
- green = 255*t^1.5
- blue = 255*t^6
- lg.setColor(red/DIV, green/DIV, blue/DIV, 255/DIV*(counters.shoot_timer/total_shoot_time))
- local drawx = counters.shoot_x - vpx
- if drawx < -2048 then drawx = drawx + 4096 end
- if drawx > 2048 then drawx = drawx - 4096 end
- lg.draw(shotimg, drawx, counters.shoot_y-vpy, 0, shoot_radius/256, shoot_radius/256, 256, 256)
- lg.setColor(255/DIV, 255/DIV, 255/DIV, 255/DIV)
- end
- -- FIXME: draw current agent
- if counters.agent then
- for k, v in pairs(agents) do
- if v == counters.agent then
- lg.draw(tileset, tilequads[k], 0, main.wh-32)
- lg.print(counters.agenttime, 35, main.wh-24)
- local num = 0
- for i = 1, #game.state.enemies do
- if game.state.enemies[i].type == counters.agent then
- num = num + 1
- end
- end
- lg.print(num, 10, main.wh-48)
- end
- end
- end
- -- FIXME: draw current shields
- lg.print(string.format("%02d", counters.shields), 96, main.wh-24)
- -- FIXME: draw current orb timer
- if counters.orbtimer then
- lg.print(string.format("%03d", counters.orbtimer), 144, main.wh-24)
- end
- lg.print(string.format("%8d", counters.score), 160, main.wh - 24)
- lg.setScissor()
- --[[ debug
- lg.print(game.DEBUG, 0, 0)
- -- draw collision canvas
- lg.setColor(100/DIV,100/DIV,100/DIV)
- lg.rectangle("fill", 100, 100, 32, 64)
- lg.setColor(255/DIV,255/DIV,255/DIV)
- lg.draw(collision_canvas, 100, 100)
- ]]
- end
- function game.keypressed(k, r)
- if r then return end
- if k == "escape" then
- return main.dialog("EXIT TO MENU?", main.tomenu)
- end
- if k == "f10" then game.savegame() end
- if k == "f3" then
- new_or_load_game(true)
- screens.getready.fromstart = false
- return main.activate(screens.getready)
- end
- end
- function game.resize(neww, newh)
- game.vx = 0
- game.vy = 0
- game.vw = main.ww
- game.vh = main.wh
- tilebatch = lg.newSpriteBatch(tileset,
- -- In 1D, a 33-pixel window can see up to two 32-pixel tiles
- -- simultaneously. One needs a 34-pixel window to be able
- -- to see three. So in 1D it would be: floor((widht+62)/32)
- -- which equals 2 for width=33 and 3 for width=34. In 2D the
- -- natural extension is the following.
- math.floor((game.vw + 62)*(game.vh + 62)/(32*32))
- -- For 640x480, that's 336 tiles. Less than the default 1000,
- -- so quite bearable.
- )
- -- force refresh
- lastxtile = false
- end
- return game
|