singe/testScripts/skyStrike/skyStrike.singe

322 lines
11 KiB
Text

-- Sky Strike: a sprite-scaling flight game, the arcade kind where the world is flat pictures drawn
-- bigger as they come closer. There is no 3D here at all. Every object has a position in a simple
-- world (across, up, and ahead), each frame projects it to the screen with one division, and the
-- sprite is drawn stretched to the size that distance gives it.
--
-- Turning does the rest, and saves drawing pictures: the jet is one picture, turned for its bank
-- and spun for a barrel roll; the horizon tilts by turning the whole sky and ground, one picture;
-- and everything in the world turns with the horizon, the same picture at the same angle.
--
-- Arrows steer, button 1 fires, button 2 rolls (and nothing touches you mid-roll).
dofile("Singe/Framework.singe")
local W = 320
local H = 240
local FOCAL = 160 -- Screen offset = world offset * FOCAL / distance ahead
local EYE_HEIGHT = 30 -- The camera's height over the ground; the ground art assumes it
local HORIZON_Y = 100 -- Where the horizon sits when the jet flies level
local NEAR = 8 -- Closer than this ahead, a thing has gone past
local FAR = 1200 -- Scenery appears this far ahead
local SPEED = 260 -- World units a second
local STRIPE = 40 -- The ground art's stripe, in world units
local GROUND_FRAMES = 6
local BANK_MAX = 35 -- Degrees the jet banks at full turn
local HORIZON_SHARE = 0.6 -- How much of the bank tilts the horizon rather than the jet
local STEER = 120 -- World units a second across at full turn
local CLIMB = 60 -- And up or down
local ALTITUDE_MIN = 12
local ALTITUDE_MAX = 80
local ROLL_SECONDS = 0.7
local FIRE_GAP = 0.08
local SHOT_SPEED = 900
local SHOT_RANGE = 900
local ENEMY_SPEED = 160 -- On top of the jet's own
local SAFE_SECONDS = 2 -- Untouchable after being hit
local START_LIVES = 3
local JET_X = W / 2
local JET_Y = 190
local JET_DROP = 7 -- The jet flies this far below the camera: where it is hit, and its guns
local ENEMY_SPREAD = 12 -- Enemies arrive within this of the jet's height, to come at it
-- What each kind of thing is: its picture, and its height in the world, which with the distance
-- gives its height on screen.
local KINDS = {}
local held = {}
local scenery = {}
local enemies = {}
local shots = {}
local explosions = {}
local camX = 0
local camY = EYE_HEIGHT
local bank = 0
local pitch = 0
local roll = nil -- Seconds into a barrel roll, or nil
local rollWay = 1
local travelled = 0
local fireWait = 0
local spawnWait = 0
local safeUntil = 0
local score = 0
local lives = START_LIVES
local clock = 0
local lastTicks = nil
local ground = nil
local function loadKind(name, height)
local sprite = spriteLoad(DIR .. "art/" .. name .. ".png")
spriteQuality(sprite, RENDER_SMOOTH)
return { sprite = sprite, height = height, natural = spriteGetHeight(sprite), angle = nil }
end
local function reset()
scenery = {}
enemies = {}
shots = {}
explosions = {}
camX = 0
camY = EYE_HEIGHT
bank = 0
pitch = 0
roll = nil
score = 0
lives = START_LIVES
safeUntil = 0
for _ = 1, 60 do
local thing = { kind = (math.random() < 0.7) and KINDS.tree or KINDS.rock, x = math.random(-600, 600), z = math.random(NEAR, FAR) }
thing.y = thing.kind.height / 2
scenery[#scenery + 1] = thing
end
for _ = 1, 12 do
scenery[#scenery + 1] = { kind = KINDS.cloud, x = math.random(-900, 900), y = math.random(110, 220), z = math.random(NEAR, FAR) }
end
end
-- Where a point of the world lands on the screen, and how many pixels a world unit is there: the
-- one division, then the turn of the horizon about its own point. nil when it is behind the camera.
local function project(x, y, z, horizonX, horizonY, cosA, sinA)
if z < NEAR then
return nil
end
local perUnit = FOCAL / z
local dx = (x - camX) * perUnit
local dy = (camY - y) * perUnit
return horizonX + dx * cosA - dy * sinA, horizonY + dx * sinA + dy * cosA, perUnit
end
-- A thing drawn at its distance: its kind's picture, turned to the angle (once a frame for all of a
-- kind, since they share it), stretched to its height there.
local function drawThing(kind, sx, sy, perUnit, angle)
if kind.angle ~= angle then
spriteRotate(kind.sprite, angle)
kind.angle = angle
end
local scale = kind.height * perUnit / kind.natural
local w = spriteGetWidth(kind.sprite) * scale
local h = spriteGetHeight(kind.sprite) * scale
if (sx + w / 2 < 0) or (sx - w / 2 > W) or (sy + h / 2 < 0) or (sy - h / 2 > H) or (w < 1) then
return
end
spriteDraw(kind.sprite, sx - w / 2, sy - h / 2, sx + w / 2, sy + h / 2)
end
local function explode(x, y, z)
explosions[#explosions + 1] = { x = x, y = y, z = z, age = 0, spin = math.random(0, 359) }
end
local function step(dt)
local across = (held[SWITCH_RIGHT] and 1 or 0) - (held[SWITCH_LEFT] and 1 or 0)
local climb = (held[SWITCH_UP] and 1 or 0) - (held[SWITCH_DOWN] and 1 or 0)
clock = clock + dt
travelled = travelled + SPEED * dt
camX = camX + across * STEER * dt
camY = math.max(ALTITUDE_MIN, math.min(ALTITUDE_MAX, camY + climb * CLIMB * dt))
-- The bank and the nose ease toward the stick rather than snapping.
bank = bank + (across * BANK_MAX - bank) * math.min(1, dt * 6)
pitch = pitch + (climb * 18 - pitch) * math.min(1, dt * 5)
if roll then
roll = roll + dt
if roll >= ROLL_SECONDS then
roll = nil
end
end
-- The world comes toward the jet; what passes goes round to the far end again.
for _, thing in ipairs(scenery) do
thing.z = thing.z - SPEED * dt
if thing.z < NEAR then
thing.z = thing.z + FAR
thing.x = camX + math.random(-600, 600) + ((thing.kind == KINDS.cloud) and math.random(-300, 300) or 0)
end
end
-- Enemies come in from ahead, weaving, and bank into their weave.
spawnWait = spawnWait - dt
if spawnWait <= 0 and lives > 0 then
spawnWait = 0.8 + math.random() * 1.2
enemies[#enemies + 1] = { x = camX + math.random(-60, 60), y = math.max(5, camY - JET_DROP + math.random(-ENEMY_SPREAD, ENEMY_SPREAD)), z = SHOT_RANGE, phase = math.random() * 6.28, weave = 20 + math.random() * 30 }
end
for i = #enemies, 1, -1 do
local enemy = enemies[i]
local sway = math.cos(clock * 2 + enemy.phase)
enemy.z = enemy.z - (SPEED + ENEMY_SPEED) * dt
enemy.x = enemy.x + sway * enemy.weave * dt
enemy.lean = sway * 25
if enemy.z < NEAR then
-- It reached the jet: a hit unless it is off to one side, or the jet is rolling or safe.
if math.abs(enemy.x - camX) < 14 and math.abs(enemy.y - (camY - JET_DROP)) < 12 and not roll and clock > safeUntil and lives > 0 then
lives = lives - 1
safeUntil = clock + SAFE_SECONDS
explode(camX, camY - JET_DROP, NEAR + 20)
end
table.remove(enemies, i)
end
end
-- Shots fly ahead from the jet's guns, and what they meet blows up.
fireWait = fireWait - dt
if held[SWITCH_BUTTON1] and fireWait <= 0 and lives > 0 then
fireWait = FIRE_GAP
shots[#shots + 1] = { x = camX, y = camY - JET_DROP, z = NEAR + 4 }
end
for i = #shots, 1, -1 do
local shot = shots[i]
local hit = false
shot.z = shot.z + SHOT_SPEED * dt
for j = #enemies, 1, -1 do
local enemy = enemies[j]
if math.abs(enemy.x - shot.x) < 10 and math.abs(enemy.y - shot.y) < 8 and math.abs(enemy.z - shot.z) < 30 then
explode(enemy.x, enemy.y, enemy.z)
table.remove(enemies, j)
score = score + 1000
hit = true
break
end
end
if hit or shot.z > SHOT_RANGE then
table.remove(shots, i)
end
end
for i = #explosions, 1, -1 do
local blast = explosions[i]
blast.age = blast.age + dt
blast.z = blast.z - SPEED * dt * 0.5
blast.spin = blast.spin + 400 * dt
if blast.age > 0.6 or blast.z < NEAR then
table.remove(explosions, i)
end
end
end
function onInputPressed(what)
held[what] = true
if what == SWITCH_BUTTON2 and not roll and lives > 0 then
roll = 0
rollWay = (held[SWITCH_LEFT] and -1) or 1
elseif what == SWITCH_BUTTON1 and lives <= 0 then
reset()
end
end
function onInputReleased(what)
held[what] = false
end
function onOverlayUpdate()
local now = singeGetTicks()
local dt = lastTicks and math.min(0.05, (now - lastTicks) / 1000) or 0
lastTicks = now
step(dt)
-- The horizon tilts against the bank and drops as the nose comes up. The sky and the ground are
-- one picture turned about the horizon point; the frame is chosen by how far the ground has
-- moved through a stripe, which is what makes it rush past.
local tilt = -bank * HORIZON_SHARE
local radians = math.rad(tilt)
local cosA = math.cos(radians)
local sinA = math.sin(radians)
local horizonX = W / 2
local horizonY = HORIZON_Y + pitch
local frame = math.floor((travelled % STRIPE) / STRIPE * GROUND_FRAMES) + 1
overlayClear()
spriteRotateFrame(ground, tilt, frame)
spriteDrawRotatedFrame(ground, horizonX, horizonY)
-- Everything in the world, farthest first, so the near hides the far.
local drawn = {}
for _, thing in ipairs(scenery) do
drawn[#drawn + 1] = { kind = thing.kind, x = thing.x, y = thing.y, z = thing.z, angle = tilt }
end
for _, enemy in ipairs(enemies) do
drawn[#drawn + 1] = { kind = KINDS.enemy, x = enemy.x, y = enemy.y, z = enemy.z, angle = tilt + enemy.lean }
end
for _, shot in ipairs(shots) do
drawn[#drawn + 1] = { kind = KINDS.shot, x = shot.x, y = shot.y, z = shot.z, angle = 0 }
end
for _, blast in ipairs(explosions) do
drawn[#drawn + 1] = { kind = KINDS.fireball, x = blast.x, y = blast.y, z = blast.z, angle = math.floor(blast.spin) % 360, grow = 0.5 + blast.age * 2.5 }
end
table.sort(drawn, function(a, b) return a.z > b.z end)
for _, item in ipairs(drawn) do
local sx, sy, perUnit = project(item.x, item.y, item.z, horizonX, horizonY, cosA, sinA)
if sx then
drawThing(item.kind, sx, sy, perUnit * (item.grow or 1), item.angle)
end
end
-- The jet: one picture, turned for its share of the bank, and spun right round for a roll.
-- It blinks while it cannot be hit.
if lives > 0 and (clock > safeUntil or math.floor(clock * 10) % 2 == 0) then
local angle = bank * (1 - HORIZON_SHARE)
if roll then
angle = angle + rollWay * 360 * (roll / ROLL_SECONDS)
end
spriteRotate(KINDS.jet.sprite, angle)
spriteDraw(KINDS.jet.sprite, JET_X + bank * 0.8, JET_Y - pitch * 0.5, true)
end
overlayPrint(1, 0, string.format("SCORE %06d", score))
overlayPrint(1, 1, "LIVES " .. lives)
if lives <= 0 then
overlayPrint(14, 8, "GAME OVER")
overlayPrint(9, 10, "Button 1 flies again")
end
return OVERLAY_UPDATED
end
overlaySetResolution(W, H)
KINDS.tree = loadKind("tree", 22)
KINDS.rock = loadKind("rock", 8)
KINDS.cloud = loadKind("cloud", 40)
KINDS.enemy = loadKind("enemy", 10)
KINDS.shot = loadKind("shot", 3)
KINDS.fireball = loadKind("fireball", 18)
KINDS.jet = loadKind("jet", 40)
ground = spriteLoadFrames(GROUND_FRAMES, DIR .. "art/ground.png")
reset()