1207 lines
37 KiB
Text
1207 lines
37 KiB
Text
-- Insert Coin: a demo for Singe, to "Game Is On" by poorartistt.
|
|
--
|
|
-- Forty years of video games in four minutes, an era to a section of the song: a phosphor bat and
|
|
-- ball, aliens in formation that step on the beat, a sixteen-bit runner over parallax hills and a
|
|
-- kart round a track seen in perspective, a tape loading, flat-shaded polygons with their wobble, modern glass
|
|
-- and chrome and physics, a countdown to continue, a boss in a storm of bullets, a bonus stage, the
|
|
-- high score table, and every era again, faster, at the end.
|
|
--
|
|
-- Everything moves on the music: the aliens step on the beat, the ball meets the bats on the beat,
|
|
-- the runner jumps on the snare, the boss fires on the kick, the hats, and the snare.
|
|
dofile("Singe/Framework.singe")
|
|
local K = dofile(DIR .. "../kit.lua")
|
|
|
|
local W = 640
|
|
local H = 480
|
|
local FOV = 60
|
|
local PHOSPHOR = { 70, 255, 140 }
|
|
local NEON = { { 255, 60, 90 }, { 255, 170, 40 }, { 255, 240, 70 }, { 70, 230, 120 }, { 60, 180, 255 }, { 190, 90, 255 } }
|
|
local ALIEN_COLS = 10
|
|
local ALIEN_ROWS = 5
|
|
local RING_EVERY = 14 -- World units between rings in the polygon era: one passes each beat
|
|
local BRICK_COLS = 9
|
|
local BRICK_ROWS = 6
|
|
local COIN_MAX = 150
|
|
|
|
local sync = K.load(DIR, W, H)
|
|
local beat = sync.beats[3] - sync.beats[2]
|
|
local art = {}
|
|
local video = {}
|
|
local world = {}
|
|
local lastT = 0
|
|
|
|
|
|
-- Pixel drawing ------------------------------------------------------------------------------------
|
|
|
|
-- Frame frame of a sheet of frames frameW wide, at x, y, scale pixels a pixel.
|
|
local function px(sprite, frameW, frame, x, y, scale)
|
|
local h = spriteGetHeight(sprite)
|
|
|
|
spriteDrawGrid(sprite, math.floor(x), math.floor(y), frame * frameW, 0, frameW, h, math.floor(x + frameW * scale) - 1, math.floor(y + h * scale) - 1)
|
|
end
|
|
|
|
|
|
-- Text in the 8 by 8 font, scale pixels a pixel. align "center" centres it on x.
|
|
local function ptext(x, y, text, scale, r, g, b, align)
|
|
local w = #text * 8 * scale
|
|
|
|
if align == "center" then
|
|
x = x - w / 2
|
|
end
|
|
spriteSetTint(art.font, r, g, b)
|
|
for i = 1, #text do
|
|
local c = text:byte(i) - 32
|
|
|
|
if c > 0 and c < 96 then
|
|
spriteDrawGrid(art.font, math.floor(x + (i - 1) * 8 * scale), math.floor(y), (c % 16) * 8, (c // 16) * 8, 8, 8, math.floor(x + i * 8 * scale) - 1, math.floor(y + 8 * scale) - 1)
|
|
end
|
|
end
|
|
spriteSetTint(art.font, 255, 255, 255)
|
|
end
|
|
|
|
|
|
-- The same, each letter its own colour round the wheel, cycling: the high score table's rainbow.
|
|
local function rainbow(x, y, text, scale, t, align)
|
|
local w = #text * 8 * scale
|
|
|
|
if align == "center" then
|
|
x = x - w / 2
|
|
end
|
|
for i = 1, #text do
|
|
local r, g, b = K.hue(i * 0.06 - t * 0.8)
|
|
|
|
ptext(x + (i - 1) * 8 * scale, y, text:sub(i, i), scale, r, g, b)
|
|
end
|
|
end
|
|
|
|
|
|
local function scanlines(alpha)
|
|
spriteSetAlpha(art.scanlines, alpha or 255)
|
|
spriteDraw(art.scanlines, 0, 0, W - 1, H - 1)
|
|
spriteSetAlpha(art.scanlines, 255)
|
|
end
|
|
|
|
|
|
-- Loading --------------------------------------------------------------------------------------------
|
|
|
|
local function loadArt()
|
|
for _, name in ipairs({ "invaderA", "invaderB", "cannon", "runner", "coin", "ship", "kart", "bossCore", "bossArm", "turret", "bullet", "font", "mountains", "hills", "ground", "scanlines" }) do
|
|
art[name] = spriteLoad(DIR .. "art/" .. name .. ".png")
|
|
end
|
|
spriteQuality(art.bossCore, RENDER_SMOOTH)
|
|
spriteQuality(art.bossArm, RENDER_SMOOTH)
|
|
video.tunnel = videoLoad(DIR .. "video/tunnel.mp4")
|
|
video.plasma = videoLoad(DIR .. "video/plasma.mp4")
|
|
video.tunnelFrames = videoGetFrameCount(video.tunnel)
|
|
videoSetVolume(video.tunnel, 0, 0)
|
|
videoSetVolume(video.plasma, 0, 0)
|
|
|
|
-- Explosions on the overlay: square sparks added over everything.
|
|
art.boom = emitterNew()
|
|
emitterSetBlend(art.boom, PARTICLE_ADD)
|
|
emitterSetRate(art.boom, 0)
|
|
emitterSetLife(art.boom, 0.3, 0.9)
|
|
emitterSetSpeed(art.boom, 60, 320)
|
|
emitterSetSpread(art.boom, 180)
|
|
emitterSetDrag(art.boom, 2)
|
|
emitterSetSize(art.boom, 9, 2)
|
|
emitterSetMax(art.boom, 4000)
|
|
emitterStart(art.boom)
|
|
end
|
|
|
|
|
|
local function boom(x, y, count, r, g, b)
|
|
emitterSetPosition(art.boom, x, y)
|
|
emitterSetColor(art.boom, 255, 255, 255, 255, r, g, b, 0)
|
|
emitterBurst(art.boom, count)
|
|
end
|
|
|
|
|
|
-- The world --------------------------------------------------------------------------------------------
|
|
|
|
local function flatMesh(verts, faces)
|
|
local positions = {}
|
|
local normals = {}
|
|
local indices = {}
|
|
|
|
for _, f in ipairs(faces) do
|
|
local a = verts[f[1]]
|
|
local b = verts[f[2]]
|
|
local c = verts[f[3]]
|
|
local ux = b[1] - a[1]
|
|
local uy = b[2] - a[2]
|
|
local uz = b[3] - a[3]
|
|
local vx = c[1] - a[1]
|
|
local vy = c[2] - a[2]
|
|
local vz = c[3] - a[3]
|
|
local nx = uy * vz - uz * vy
|
|
local ny = uz * vx - ux * vz
|
|
local nz = ux * vy - uy * vx
|
|
local l = math.sqrt(nx * nx + ny * ny + nz * nz) + 1e-9
|
|
|
|
for _, p in ipairs({ a, b, c }) do
|
|
positions[#positions + 1] = p[1]
|
|
positions[#positions + 1] = p[2]
|
|
positions[#positions + 1] = p[3]
|
|
normals[#normals + 1] = nx / l
|
|
normals[#normals + 1] = ny / l
|
|
normals[#normals + 1] = nz / l
|
|
indices[#indices + 1] = #positions / 3
|
|
end
|
|
end
|
|
return meshNew(positions, normals, nil, indices)
|
|
end
|
|
|
|
|
|
-- A low-poly ship: a few triangles, flat shaded, nose toward -Z.
|
|
local function shipMesh()
|
|
local v = {
|
|
{ 0, 0, -2.2 }, { -0.5, 0.15, 0.4 }, { 0.5, 0.15, 0.4 }, { 0, 0.5, 0.2 }, { 0, -0.2, 0.6 },
|
|
{ -1.8, -0.05, 0.9 }, { 1.8, -0.05, 0.9 }, { 0, 0.15, 1.0 },
|
|
}
|
|
local f = {
|
|
{ 1, 4, 2 }, { 1, 3, 4 }, { 1, 2, 5 }, { 1, 5, 3 }, { 2, 4, 8 }, { 4, 3, 8 }, { 2, 8, 5 }, { 3, 5, 8 },
|
|
{ 2, 6, 5 }, { 3, 5, 7 }, { 2, 5, 6 }, { 3, 7, 5 },
|
|
}
|
|
|
|
return flatMesh(v, f)
|
|
end
|
|
|
|
|
|
local function octahedron(r)
|
|
local v = { { r, 0, 0 }, { -r, 0, 0 }, { 0, r, 0 }, { 0, -r, 0 }, { 0, 0, r }, { 0, 0, -r } }
|
|
local f = { { 1, 3, 5 }, { 5, 3, 2 }, { 2, 3, 6 }, { 6, 3, 1 }, { 5, 4, 1 }, { 2, 4, 5 }, { 6, 4, 2 }, { 1, 4, 6 } }
|
|
|
|
return flatMesh(v, f)
|
|
end
|
|
|
|
|
|
local function buildRacer()
|
|
local ground = materialNew()
|
|
|
|
materialSetTexture(ground, spriteLoad(DIR .. "art/track.png"))
|
|
materialSetFilter(ground, FILTER_NEAREST)
|
|
materialSetUnlit(ground, true)
|
|
world.track = nodeNew()
|
|
nodeSetMesh(world.track, meshPlane(400, 400), ground)
|
|
local skyMaterial = materialNew()
|
|
|
|
materialSetTexture(skyMaterial, spriteLoad(DIR .. "art/raceSky.png"))
|
|
materialSetUnlit(skyMaterial, true)
|
|
materialSetDoubleSided(skyMaterial, true)
|
|
world.raceSky = nodeNew()
|
|
nodeSetMesh(world.raceSky, meshSphere(320, 32), skyMaterial)
|
|
world.rivals = {}
|
|
for i = 1, 5 do
|
|
local n = nodeNew()
|
|
local c = NEON[i]
|
|
|
|
nodeSetSprite(n, art.kart, 3.2, false)
|
|
nodeSetBillboard(n, BILLBOARD_Y)
|
|
world.rivals[i] = { node = n, offset = 0.12 + i * 0.09, tint = c }
|
|
end
|
|
end
|
|
|
|
|
|
local function buildPolygons()
|
|
local hull = materialNew()
|
|
|
|
materialSetColor(hull, 200, 210, 230)
|
|
materialSetRoughness(hull, 0.6)
|
|
world.ship = nodeNew()
|
|
nodeSetMesh(world.ship, shipMesh(), hull)
|
|
world.rings = {}
|
|
world.ringMaterials = {}
|
|
for i = 1, 6 do
|
|
local m = materialNew()
|
|
local c = NEON[i]
|
|
|
|
materialSetColor(m, c[1] // 2, c[2] // 2, c[3] // 2)
|
|
materialSetRoughness(m, 0.7)
|
|
world.ringMaterials[i] = m
|
|
end
|
|
local ringMesh = meshTorus(7, 0.6, 6)
|
|
|
|
for i = 1, 14 do
|
|
local n = nodeNew()
|
|
|
|
nodeSetMesh(n, ringMesh, world.ringMaterials[(i - 1) % 6 + 1])
|
|
world.rings[i] = n
|
|
end
|
|
local floorMaterial = materialNew()
|
|
|
|
materialSetTexture(floorMaterial, spriteLoad(DIR .. "art/ground.png"))
|
|
materialSetFilter(floorMaterial, FILTER_NEAREST)
|
|
materialSetTiling(floorMaterial, 30, 30)
|
|
materialSetColor(floorMaterial, 70, 55, 120)
|
|
world.polyFloor = nodeNew()
|
|
nodeSetMesh(world.polyFloor, meshPlane(300, 300, 4), floorMaterial)
|
|
world.enemies = {}
|
|
local enemyMaterial = materialNew()
|
|
|
|
materialSetColor(enemyMaterial, 255, 80, 60)
|
|
materialSetEmissive(enemyMaterial, 120, 20, 10)
|
|
local enemyMesh = octahedron(1.2)
|
|
|
|
for i = 1, 6 do
|
|
local n = nodeNew()
|
|
|
|
nodeSetMesh(n, enemyMesh, enemyMaterial)
|
|
world.enemies[i] = n
|
|
end
|
|
world.shardAnchor = nodeNew()
|
|
world.shards = emitterNew(world.shardAnchor)
|
|
emitterSetRate(world.shards, 0)
|
|
emitterSetLife(world.shards, 0.4, 1.0)
|
|
emitterSetSpeed(world.shards, 6, 16)
|
|
emitterSetSpread(world.shards, 180)
|
|
emitterSetGravity(world.shards, 0, -10, 0)
|
|
emitterSetSize(world.shards, 0.5, 0.1)
|
|
emitterSetColor(world.shards, 255, 200, 80, 255, 255, 60, 20, 0)
|
|
emitterSetBlend(world.shards, PARTICLE_ADD)
|
|
emitterStart(world.shards)
|
|
world.light = lightNew(LIGHT_DIRECTIONAL)
|
|
nodeSetPosition(world.light, 3, 8, 4)
|
|
nodeLookAt(world.light, 0, 0, 0)
|
|
lightSetIntensity(world.light, 0)
|
|
end
|
|
|
|
|
|
-- The modern era: a wall of glossy bricks on a dark floor, and chrome balls to knock it down.
|
|
local function buildModern()
|
|
local floorMaterial = materialNew()
|
|
|
|
materialSetColor(floorMaterial, 30, 30, 40)
|
|
materialSetMetallic(floorMaterial, 0.8)
|
|
materialSetRoughness(floorMaterial, 0.15)
|
|
world.modernFloor = nodeNew()
|
|
nodeSetMesh(world.modernFloor, meshBox(60, 0.4, 60), floorMaterial)
|
|
nodeSetPosition(world.modernFloor, 0, -0.2, 0)
|
|
bodyNew(world.modernFloor, BODY_STATIC, SHAPE_BOX, 60, 0.4, 60)
|
|
world.brickMaterials = {}
|
|
for i, c in ipairs(NEON) do
|
|
local m = materialNew()
|
|
|
|
materialSetColor(m, c[1], c[2], c[3])
|
|
materialSetMetallic(m, 0.3)
|
|
materialSetRoughness(m, 0.12)
|
|
world.brickMaterials[i] = m
|
|
end
|
|
world.brickMesh = meshBox(1.2, 0.6, 0.6)
|
|
world.bricks = {}
|
|
world.ballMesh = meshSphere(0.45, 24)
|
|
world.ballMaterial = K.chrome(240, 240, 250, 0.04)
|
|
world.balls = {}
|
|
world.spark = nodeNew()
|
|
world.sparks = emitterNew(world.spark)
|
|
emitterSetBlend(world.sparks, PARTICLE_ADD)
|
|
emitterSetRate(world.sparks, 0)
|
|
emitterSetLife(world.sparks, 0.3, 0.8)
|
|
emitterSetSpeed(world.sparks, 2, 7)
|
|
emitterSetSpread(world.sparks, 180)
|
|
emitterSetGravity(world.sparks, 0, -9, 0)
|
|
emitterSetSize(world.sparks, 0.08, 0.02)
|
|
emitterSetColor(world.sparks, 255, 240, 200, 255, 255, 120, 40, 0)
|
|
emitterSetTrail(world.sparks, 5, 0.03)
|
|
emitterStart(world.sparks)
|
|
world.sun = lightNew(LIGHT_DIRECTIONAL)
|
|
nodeSetPosition(world.sun, -6, 10, 8)
|
|
nodeLookAt(world.sun, 0, 0, 0)
|
|
lightSetIntensity(world.sun, 0)
|
|
lightSetShadow(world.sun, true)
|
|
end
|
|
|
|
|
|
local function buildBonus()
|
|
local tunnel = materialNew()
|
|
|
|
materialSetVideo(tunnel, video.tunnel)
|
|
materialSetUnlit(tunnel, true)
|
|
materialSetDoubleSided(tunnel, true)
|
|
world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 80, 1.8)
|
|
world.coinMesh = meshCylinder(0.6, 0.12, 20)
|
|
world.coinMaterial = K.chrome(255, 200, 60, 0.25)
|
|
materialSetEmissive(world.coinMaterial, 150, 100, 10)
|
|
world.coins = {}
|
|
world.bin = nodeNew()
|
|
nodeSetPosition(world.bin, 0, -100, 0)
|
|
world.binWalls = {}
|
|
for _, w in ipairs({ { 0, -4.6, 0, 16, 0.4, 8 }, { -8, 0, 0, 0.4, 12, 8 }, { 8, 0, 0, 0.4, 12, 8 }, { 0, 0, -4, 16, 12, 0.4 }, { 0, 0, 4, 16, 12, 0.4 } }) do
|
|
local n = nodeNew()
|
|
|
|
nodeSetPosition(n, w[1], w[2] - 100, w[3])
|
|
bodyNew(n, BODY_KINEMATIC, SHAPE_BOX, w[4], w[5], w[6])
|
|
world.binWalls[#world.binWalls + 1] = { node = n, x = w[1], y = w[2], z = w[3] }
|
|
end
|
|
end
|
|
|
|
|
|
local function buildWorld()
|
|
sceneEnable(true)
|
|
sceneSetSky(DIR .. "art/duskSky.png")
|
|
sceneSetBloom(0.8, 0.5)
|
|
sceneSetAmbient(40, 40, 50)
|
|
world.camera = nodeNew()
|
|
cameraSet(world.camera)
|
|
cameraSetPerspective(FOV, 0.1, 800)
|
|
buildRacer()
|
|
buildPolygons()
|
|
buildModern()
|
|
buildBonus()
|
|
end
|
|
|
|
|
|
local function hideAll()
|
|
for _, n in ipairs({ world.track, world.raceSky, world.ship, world.polyFloor, world.modernFloor, world.tunnel }) do
|
|
nodeSetVisible(n, false)
|
|
end
|
|
for _, r in ipairs(world.rivals) do
|
|
nodeSetVisible(r.node, false)
|
|
end
|
|
for _, n in ipairs(world.rings) do
|
|
nodeSetVisible(n, false)
|
|
end
|
|
for _, n in ipairs(world.enemies) do
|
|
nodeSetVisible(n, false)
|
|
end
|
|
lightSetIntensity(world.light, 0)
|
|
lightSetIntensity(world.sun, 0)
|
|
sceneSetFog()
|
|
sceneSetSkyIntensity(1)
|
|
-- Behind a 2D era the 3D layer is plain black.
|
|
nodeSetPosition(world.camera, 0, -500, 0)
|
|
nodeLookAt(world.camera, 0, -500, -1)
|
|
end
|
|
|
|
|
|
-- The eras ----------------------------------------------------------------------------------------------
|
|
-- Each is a scene table without its bars, so the finale can run them again: enter, draw(t, bar,
|
|
-- k), and leave. An era's own time is the music's, so whatever it plays lands on the beat wherever
|
|
-- it is placed, as long as it is placed on a whole bar.
|
|
|
|
local ERA = {}
|
|
|
|
-- Switching on: the tube warms up from a line, INSERT COIN blinks on the beat, and a coin spins
|
|
-- down into the slot on the downbeat of bar 5.
|
|
ERA.boot = {
|
|
enter = function(t)
|
|
hideAll()
|
|
end,
|
|
draw = function(t, bar, k)
|
|
K.flash(0, 0, 0, 255)
|
|
local warm = K.smooth(1, 1.6, bar)
|
|
|
|
if warm < 1 then
|
|
local h = math.max(2, H * warm * warm)
|
|
|
|
K.box(0, H / 2 - h / 2, W - 1, H / 2 + h / 2, 255, 255, 255, math.floor(255 * (1 - warm * 0.85)))
|
|
return
|
|
end
|
|
if (K.beat(t) % 1) < 0.5 then
|
|
ptext(W / 2, 200, "INSERT COIN", 5, 255, 255, 255, "center")
|
|
end
|
|
local land = K.barTime(5)
|
|
local fall = K.clamp((t - (land - beat * 4)) / (beat * 4), 0, 1)
|
|
local cy = K.lerp(-60, 320, fall * fall)
|
|
local spin = math.floor(t * 12) % 4
|
|
|
|
if fall < 1 then
|
|
px(art.coin, 8, spin, W / 2 - 32, cy, 8)
|
|
end
|
|
K.box(W / 2 - 40, 392, W / 2 + 40, 402, 90, 90, 100, 255)
|
|
ptext(W / 2, 430, (fall >= 1) and "CREDIT 1" or "CREDIT 0", 3, 255, 220, 80, "center")
|
|
scanlines(160)
|
|
end,
|
|
}
|
|
|
|
-- 1-bit: two bats and a ball in green phosphor. The ball crosses the court once a beat, so it meets
|
|
-- a bat on every beat, and its afterglow trails behind it.
|
|
ERA.pong = {
|
|
enter = function(t)
|
|
hideAll()
|
|
world.pongScore = { 0, 0 }
|
|
end,
|
|
draw = function(t, bar, k)
|
|
K.flash(0, 6, 2, 255)
|
|
local b = K.beat(t)
|
|
local left = 60
|
|
local right = W - 60
|
|
|
|
for y = 20, H - 20, 24 do
|
|
K.box(W / 2 - 3, y, W / 2 + 3, y + 12, PHOSPHOR[1], PHOSPHOR[2], PHOSPHOR[3], 160)
|
|
end
|
|
local function ballAt(beatPos)
|
|
local whole = math.floor(beatPos)
|
|
local p = beatPos - whole
|
|
local x = (whole % 2 == 0) and K.lerp(left + 12, right - 12, p) or K.lerp(right - 12, left + 12, p)
|
|
local tri = (beatPos * 0.37) % 2
|
|
|
|
tri = (tri < 1) and tri or (2 - tri)
|
|
return x, K.lerp(40, H - 40, tri)
|
|
end
|
|
for g = 6, 1, -1 do
|
|
local gx, gy = ballAt(b - g * 0.02)
|
|
|
|
K.box(gx - 7, gy - 7, gx + 7, gy + 7, PHOSPHOR[1], PHOSPHOR[2], PHOSPHOR[3], 30 + (6 - g) * 20)
|
|
end
|
|
local x, y = ballAt(b)
|
|
|
|
K.box(x - 8, y - 8, x + 8, y + 8, 220, 255, 230, 255)
|
|
local _, ly = ballAt(math.floor(b) + ((math.floor(b) % 2 == 0) and 0 or 1))
|
|
local _, ry = ballAt(math.floor(b) + ((math.floor(b) % 2 == 0) and 1 or 0))
|
|
|
|
K.box(left - 10, ly - 40, left, ly + 40, PHOSPHOR[1], PHOSPHOR[2], PHOSPHOR[3], 255)
|
|
K.box(right, ry - 40, right + 10, ry + 40, PHOSPHOR[1], PHOSPHOR[2], PHOSPHOR[3], 255)
|
|
local points = math.floor(b / 4)
|
|
|
|
ptext(W / 2 - 80, 30, string.format("%02d", points % 100), 6, PHOSPHOR[1], PHOSPHOR[2], PHOSPHOR[3], "center")
|
|
ptext(W / 2 + 80, 30, string.format("%02d", (points * 3) % 100), 6, PHOSPHOR[1], PHOSPHOR[2], PHOSPHOR[3], "center")
|
|
K.strobe(t, "kicks", 20, PHOSPHOR[1], PHOSPHOR[2], PHOSPHOR[3], 30)
|
|
scanlines(200)
|
|
end,
|
|
}
|
|
|
|
-- 8-bit: a formation of aliens that steps on every beat, the cannon below firing on every snare and
|
|
-- taking out the lowest alien above it.
|
|
local function alienPositions(step)
|
|
local cycle = step % 40
|
|
local dx = (cycle < 20) and cycle or (40 - cycle)
|
|
|
|
return 48 + dx * 8, 60 + math.floor(step / 20) * 16
|
|
end
|
|
|
|
|
|
ERA.invaders = {
|
|
enter = function(t)
|
|
hideAll()
|
|
world.alive = {}
|
|
for i = 1, ALIEN_COLS * ALIEN_ROWS do
|
|
world.alive[i] = true
|
|
end
|
|
world.shots = {}
|
|
world.alienBase = K.beat(t)
|
|
end,
|
|
draw = function(t, bar, k)
|
|
K.flash(0, 0, 8, 255)
|
|
local step = math.floor(K.beat(t) - world.alienBase)
|
|
local ox, oy = alienPositions(step)
|
|
local frame = step % 2
|
|
local cannonX = W / 2 + math.sin(t * 1.4) * 220
|
|
|
|
-- On a snare, a shot from the cannon at the lowest alien in the column above it.
|
|
if K.hitCount("snares", lastT, t) > 0 then
|
|
local col = math.floor((cannonX - ox) / 52 + 0.5)
|
|
|
|
if col >= 0 and col < ALIEN_COLS then
|
|
for row = ALIEN_ROWS - 1, 0, -1 do
|
|
local i = row * ALIEN_COLS + col + 1
|
|
|
|
if world.alive[i] then
|
|
world.shots[#world.shots + 1] = { x = cannonX, start = t, target = i }
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
for row = 0, ALIEN_ROWS - 1 do
|
|
local c = NEON[row % #NEON + 1]
|
|
local sprite = (row < 2) and art.invaderA or art.invaderB
|
|
|
|
spriteSetTint(sprite, c[1], c[2], c[3])
|
|
for col = 0, ALIEN_COLS - 1 do
|
|
if world.alive[row * ALIEN_COLS + col + 1] then
|
|
px(sprite, (row < 2) and 12 or 12, frame, ox + col * 52, oy + row * 40, 4)
|
|
end
|
|
end
|
|
spriteSetTint(sprite, 255, 255, 255)
|
|
end
|
|
for i = #world.shots, 1, -1 do
|
|
local s = world.shots[i]
|
|
local row = (s.target - 1) // ALIEN_COLS
|
|
local col = (s.target - 1) % ALIEN_COLS
|
|
local tx = ox + col * 52 + 24
|
|
local ty = oy + row * 40 + 16
|
|
local p = (t - s.start) / (beat * 0.35)
|
|
|
|
if p >= 1 then
|
|
if world.alive[s.target] then
|
|
world.alive[s.target] = false
|
|
local c = NEON[row % #NEON + 1]
|
|
|
|
boom(tx, ty, 60, c[1], c[2], c[3])
|
|
end
|
|
table.remove(world.shots, i)
|
|
else
|
|
local sx = K.lerp(s.x, tx, p)
|
|
local sy = K.lerp(H - 70, ty, p)
|
|
|
|
K.box(sx - 2, sy - 10, sx + 2, sy + 10, 255, 255, 255, 255)
|
|
end
|
|
end
|
|
spriteSetTint(art.cannon, 80, 255, 120)
|
|
px(art.cannon, 13, 0, cannonX - 26, H - 64, 4)
|
|
spriteSetTint(art.cannon, 255, 255, 255)
|
|
K.box(0, H - 28, W - 1, H - 26, 80, 255, 120, 255)
|
|
ptext(20, 14, string.format("SCORE %05d", (ALIEN_COLS * ALIEN_ROWS - (function() local n = 0 for _, a in ipairs(world.alive) do if a then n = n + 1 end end return n end)()) * 50), 2, 255, 255, 255)
|
|
emitterDraw(art.boom)
|
|
scanlines(180)
|
|
end,
|
|
}
|
|
|
|
-- 16-bit: a runner over parallax hills. The ground runs at a steady speed, the runner jumps on every
|
|
-- snare, as high as the note being played says, and the coins are placed where the jumps will take
|
|
-- him.
|
|
local RUN_SPEED = 260
|
|
local RUNNER_X = 170
|
|
|
|
ERA.runner = {
|
|
enter = function(t)
|
|
hideAll()
|
|
world.collected = {}
|
|
world.runScore = 0
|
|
end,
|
|
draw = function(t, bar, k)
|
|
for i = 0, 11 do
|
|
local r = 40 + i * 12
|
|
local g = 90 + i * 12
|
|
local b = 210 + i * 4
|
|
|
|
K.box(0, i * 30, W - 1, i * 30 + 29, r, g, math.min(255, b), 255)
|
|
end
|
|
local function layer(sprite, speed, y, scale)
|
|
local w = spriteGetWidth(sprite) * scale
|
|
local offset = (t * speed) % w
|
|
|
|
for x = -offset, W, w do
|
|
spriteDraw(sprite, math.floor(x), y, math.floor(x + w) - 1, y + spriteGetHeight(sprite) * scale - 1)
|
|
end
|
|
end
|
|
layer(art.mountains, RUN_SPEED * 0.15, 170, 2)
|
|
layer(art.hills, RUN_SPEED * 0.4, 260, 2)
|
|
layer(art.ground, RUN_SPEED, H - 96, 2)
|
|
local groundY = H - 96 - 64
|
|
local b = K.beat(t)
|
|
local jumpY = 0
|
|
|
|
-- Jumps on the backbeat: beats 2 and 4 of each bar, a beat long.
|
|
local whole = math.floor(b)
|
|
local p = b - whole
|
|
|
|
if (whole - (sync.barStart - 1)) % 2 == 1 then
|
|
local note = K.note(K.beatTime(whole))
|
|
|
|
jumpY = 4 * p * (1 - p) * (90 + note * 8)
|
|
end
|
|
-- Coins at the top of every jump, a beat's worth of ground apart.
|
|
for j = math.floor(b) - 2, math.floor(b) + 4 do
|
|
if (j - (sync.barStart - 1)) % 2 == 1 then
|
|
local apex = K.beatTime(j + 0.5)
|
|
local cx = RUNNER_X + 24 + (apex - t) * RUN_SPEED
|
|
local note = K.note(K.beatTime(j))
|
|
local cy = groundY - (90 + note * 8) + 8
|
|
|
|
if t < apex then
|
|
px(art.coin, 8, math.floor(t * 10) % 4, cx, cy, 4)
|
|
elseif not world.collected[j] then
|
|
world.collected[j] = true
|
|
world.runScore = world.runScore + 100
|
|
boom(RUNNER_X + 32, cy + 16, 30, 255, 230, 80)
|
|
end
|
|
end
|
|
end
|
|
local frame = (jumpY > 0) and 4 or (math.floor(b * 4) % 4)
|
|
|
|
px(art.runner, 16, frame, RUNNER_X, groundY - jumpY, 4)
|
|
ptext(20, 14, string.format("COINS %03d", world.runScore // 100), 2, 255, 255, 255)
|
|
ptext(W - 20 - 9 * 16, 14, string.format("WORLD 1-%d", math.floor(bar - 16) // 2 + 1), 2, 255, 255, 255)
|
|
emitterDraw(art.boom)
|
|
end,
|
|
}
|
|
|
|
-- 16-bit, the other trick: a kart on a flat track seen in perspective, the sky turning with it. It
|
|
-- laps once every four bars.
|
|
local function trackPoint(a)
|
|
return math.cos(a) * 144, math.sin(a) * 104
|
|
end
|
|
|
|
|
|
ERA.racer = {
|
|
enter = function(t)
|
|
hideAll()
|
|
nodeSetVisible(world.track, true)
|
|
nodeSetVisible(world.raceSky, true)
|
|
for _, r in ipairs(world.rivals) do
|
|
nodeSetVisible(r.node, true)
|
|
end
|
|
world.raceBase = t
|
|
end,
|
|
draw = function(t, bar, k)
|
|
local lap = (bar - 1) / 4
|
|
local a = lap * 2 * math.pi
|
|
local x, z = trackPoint(a)
|
|
local ax, az = trackPoint(a + 0.08)
|
|
|
|
nodeSetPosition(world.camera, x, 4.5, z)
|
|
nodeLookAt(world.camera, ax, 3.2, az)
|
|
nodeSetPosition(world.raceSky, x, 0, z)
|
|
for i, r in ipairs(world.rivals) do
|
|
local ra = a + r.offset + 0.02 * math.sin(t * 2 + i)
|
|
local rx, rz = trackPoint(ra)
|
|
|
|
nodeSetPosition(r.node, rx + math.sin(i * 2.1) * 6, 1.4, rz + math.cos(i * 1.7) * 6)
|
|
end
|
|
local lean = math.sin(a * 2) * 8
|
|
|
|
spriteRotateAndScale(art.kart, lean, 6)
|
|
spriteDraw(art.kart, W // 2, H - 70, true)
|
|
ptext(20, 14, string.format("LAP %d/3", math.floor(lap) % 3 + 1), 3, 255, 255, 255)
|
|
ptext(W - 20 - 6 * 24, 14, string.format("POS %d", 6 - math.floor(lap * 1.5) % 5), 3, 255, 240, 80)
|
|
local laps = (t - world.raceBase)
|
|
|
|
ptext(W - 20 - 8 * 16, 48, string.format("%02d\"%02d", math.floor(laps) % 60, math.floor(laps * 100) % 100), 2, 255, 255, 255)
|
|
end,
|
|
leave = function(t)
|
|
spriteRotateAndScale(art.kart, 0, 1)
|
|
end,
|
|
}
|
|
|
|
-- Loading: a tape loading, the border awash with stripes, a bar filling to the riser.
|
|
ERA.loading = {
|
|
enter = function(t)
|
|
hideAll()
|
|
end,
|
|
draw = function(t, bar, k)
|
|
local border = { { 20, 20, 120 }, { 255, 255, 80 }, { 60, 200, 200 }, { 200, 60, 200 }, { 255, 140, 60 }, { 255, 255, 255 }, { 40, 40, 40 } }
|
|
local seed = math.floor(t * 50)
|
|
local band = 3 + math.floor(6 * (1 - k))
|
|
|
|
for y = 0, H - 1, band do
|
|
local c = border[((seed * 31 + y * 7) // band) % #border + 1]
|
|
|
|
K.box(0, y, W - 1, y + band - 1, c[1], c[2], c[3], 255)
|
|
end
|
|
K.box(64, 56, W - 65, H - 57, 60, 60, 200, 255)
|
|
ptext(96, 90, "PRESS PLAY ON TAPE", 2, 160, 160, 255)
|
|
ptext(96, 130, "OK", 2, 160, 160, 255)
|
|
ptext(96, 170, "SEARCHING", 2, 160, 160, 255)
|
|
ptext(96, 210, "FOUND GAME ON", 2, 160, 160, 255)
|
|
ptext(96, 250, "LOADING", 2, 160, 160, 255)
|
|
if (t * 3) % 1 < 0.5 then
|
|
K.box(96 + 8 * 16, 250, 96 + 8 * 16 + 14, 265, 160, 160, 255, 255)
|
|
end
|
|
local fill = K.clamp(k, 0, 1)
|
|
|
|
K.box(96, 320, W - 97, 350, 20, 20, 90, 255)
|
|
K.box(98, 322, 98 + (W - 196 - 4) * fill, 348, 160, 160, 255, 255)
|
|
ptext(W / 2, 370, string.format("%3d%%", math.floor(fill * 100)), 2, 255, 255, 255, "center")
|
|
scanlines(120)
|
|
if bar > 40.6 then
|
|
K.flash(255, 255, 255, math.floor(255 * K.smooth(40.6, 41, bar)))
|
|
end
|
|
end,
|
|
}
|
|
|
|
-- 32-bit: a flat-shaded ship through hexagonal rings, one ring a beat, in a dense fog, every
|
|
-- position snapped to a coarse grid as the machines of the time snapped theirs. From bar 49 enemy
|
|
-- crystals come at the ship and burst on the snare.
|
|
local function snap(v)
|
|
return math.floor(v * 12 + 0.5) / 12
|
|
end
|
|
|
|
|
|
ERA.polygons = {
|
|
enter = function(t)
|
|
hideAll()
|
|
nodeSetVisible(world.ship, true)
|
|
nodeSetVisible(world.polyFloor, true)
|
|
for _, n in ipairs(world.rings) do
|
|
nodeSetVisible(n, true)
|
|
end
|
|
lightSetIntensity(world.light, 3)
|
|
sceneSetFog(30, 10, 50, 10, 120)
|
|
sceneSetSkyIntensity(0)
|
|
world.enemyBase = K.beat(t)
|
|
end,
|
|
draw = function(t, bar, k)
|
|
local b = K.beat(t)
|
|
local z = -b * RING_EVERY
|
|
local sway = math.sin(t * 0.9) * 3
|
|
|
|
nodeSetPosition(world.ship, snap(sway), snap(2 + math.sin(t * 1.3) * 0.8), snap(z - 8))
|
|
nodeSetRotation(world.ship, 0, 0, snap(math.cos(t * 0.9) * -25))
|
|
nodeSetPosition(world.camera, snap(sway * 0.6), snap(3.4), snap(z))
|
|
nodeLookAt(world.camera, sway * 0.8, 2, z - 30)
|
|
nodeSetPosition(world.polyFloor, 0, -3, z - (z % 20))
|
|
local first = math.floor(b)
|
|
|
|
for i, n in ipairs(world.rings) do
|
|
local rb = first + i - 1
|
|
|
|
nodeSetPosition(n, 0, 2, snap(-rb * RING_EVERY - 8))
|
|
nodeSetRotation(n, 90, rb * 15, 0)
|
|
end
|
|
for i, m in ipairs(world.ringMaterials) do
|
|
local v = K.decay("kicks", t, 6)
|
|
local c = NEON[i]
|
|
|
|
materialSetEmissive(m, math.floor(c[1] * v), math.floor(c[2] * v), math.floor(c[3] * v))
|
|
end
|
|
if bar >= 49 then
|
|
for i, n in ipairs(world.enemies) do
|
|
local eb = math.floor(b) + i * 2
|
|
local ez = -eb * RING_EVERY - 8
|
|
local alive = (eb - b) > 0.35
|
|
|
|
nodeSetVisible(n, alive)
|
|
nodeSetPosition(n, snap(math.sin(eb * 1.7) * 3), snap(2 + math.cos(eb * 2.3) * 1.5), snap(ez))
|
|
nodeSetRotation(n, t * 90, t * 130, 0)
|
|
end
|
|
if K.hitCount("snares", lastT, t) > 0 then
|
|
nodeSetPosition(world.shardAnchor, sway, 2, z - 20)
|
|
emitterBurst(world.shards, 80)
|
|
end
|
|
end
|
|
end,
|
|
}
|
|
|
|
-- Now: glass, chrome, and physics. A wall of glossy bricks under a dusk sky, and on every kick a
|
|
-- chrome ball thrown into it; the wall is built again at bar 65 and comes down harder.
|
|
local function buildWall()
|
|
for _, b in ipairs(world.bricks) do
|
|
bodyDelete(b)
|
|
nodeDelete(b)
|
|
end
|
|
for _, b in ipairs(world.balls) do
|
|
bodyDelete(b)
|
|
nodeDelete(b)
|
|
end
|
|
world.bricks = {}
|
|
world.balls = {}
|
|
for row = 0, BRICK_ROWS - 1 do
|
|
for col = 0, BRICK_COLS - 1 do
|
|
local n = nodeNew()
|
|
|
|
nodeSetMesh(n, world.brickMesh, world.brickMaterials[(row + col) % #world.brickMaterials + 1])
|
|
nodeSetPosition(n, (col - (BRICK_COLS - 1) / 2) * 1.25 + ((row % 2) * 0.62 - 0.31), 0.3 + row * 0.62, 0)
|
|
bodyNew(n, BODY_DYNAMIC, SHAPE_BOX, 1.2, 0.6, 0.6)
|
|
bodySetMass(n, 1)
|
|
bodySetFriction(n, 0.8)
|
|
world.bricks[#world.bricks + 1] = n
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
ERA.modern = {
|
|
enter = function(t)
|
|
hideAll()
|
|
nodeSetVisible(world.modernFloor, true)
|
|
lightSetIntensity(world.sun, 4)
|
|
sceneSetSkyIntensity(1.2)
|
|
buildWall()
|
|
world.wallBuilt = K.bar(t)
|
|
end,
|
|
draw = function(t, bar, k)
|
|
if bar >= 65 and world.wallBuilt < 65 then
|
|
buildWall()
|
|
world.wallBuilt = bar
|
|
end
|
|
local a = t * 0.12
|
|
|
|
nodeSetPosition(world.camera, math.sin(a) * 7, 2.4 + math.sin(t * 0.3) * 0.6, 7.5 + math.cos(a) * 2)
|
|
nodeLookAt(world.camera, 0, 1.6, 0)
|
|
if K.hitCount("kicks", lastT, t) > 0 and #world.balls < 40 then
|
|
local n = nodeNew()
|
|
local tx = (math.random() - 0.5) * 8
|
|
local ty = 0.5 + math.random() * 3
|
|
|
|
nodeSetMesh(n, world.ballMesh, world.ballMaterial)
|
|
nodeSetPosition(n, tx * 0.3, ty + 1.5, 14)
|
|
bodyNew(n, BODY_DYNAMIC, SHAPE_SPHERE, 0.45)
|
|
bodySetMass(n, (bar >= 65) and 8 or 4)
|
|
bodySetVelocity(n, tx * 0.9, 1.2, -32)
|
|
world.balls[#world.balls + 1] = n
|
|
end
|
|
end,
|
|
leave = function(t)
|
|
for _, b in ipairs(world.bricks) do
|
|
bodyDelete(b)
|
|
nodeDelete(b)
|
|
end
|
|
for _, b in ipairs(world.balls) do
|
|
bodyDelete(b)
|
|
nodeDelete(b)
|
|
end
|
|
world.bricks = {}
|
|
world.balls = {}
|
|
end,
|
|
}
|
|
|
|
-- Game over, and a countdown to continue, a number a bar, glitching, until the coin goes in.
|
|
ERA.continue = {
|
|
enter = function(t)
|
|
hideAll()
|
|
end,
|
|
draw = function(t, bar, k)
|
|
K.flash(0, 0, 0, 255)
|
|
local n = 8 - math.floor(bar - 73)
|
|
|
|
if bar < 74 then
|
|
ptext(W / 2, 200, "GAME OVER", 6, 255, 60, 60, "center")
|
|
else
|
|
ptext(W / 2, 80, "CONTINUE?", 4, 255, 255, 255, "center")
|
|
local pulse = 1 + 0.15 * K.decay("snares", t, 6)
|
|
local s = math.floor(18 * pulse)
|
|
local glitch = (math.random() < 0.15) and math.random(-20, 20) or 0
|
|
|
|
ptext(W / 2 + glitch, 170, tostring(math.max(1, n)), s, 255, 220, 60, "center")
|
|
for i = 1, 3 do
|
|
local y = math.random(0, H)
|
|
|
|
K.box(0, y, W - 1, y + math.random(1, 4), 255, 255, 255, math.random(20, 90))
|
|
end
|
|
end
|
|
if bar > 80.25 then
|
|
local p = K.smooth(80.25, 80.95, bar)
|
|
|
|
px(art.coin, 8, math.floor(t * 12) % 4, W / 2 - 32, K.lerp(-60, 380, p * p), 8)
|
|
end
|
|
ptext(W / 2, 430, "CREDIT 0", 3, 255, 220, 80, "center")
|
|
scanlines(160)
|
|
end,
|
|
}
|
|
|
|
-- The boss: a core and four turning arms with turrets, firing rings on the kick, spirals on the
|
|
-- hats, and aimed shots on the snare, while the ship below weaves and fires back. Its health runs
|
|
-- out over sixteen bars, and it goes up on the last.
|
|
ERA.boss = {
|
|
enter = function(t)
|
|
hideAll()
|
|
world.bullets = {}
|
|
world.bossHit = 0
|
|
videoPlay(video.plasma)
|
|
end,
|
|
draw = function(t, bar, k)
|
|
videoDraw(video.plasma, 0, 0, W - 1, H - 1)
|
|
K.flash(0, 0, 10, 200)
|
|
local dt = t - lastT
|
|
local cx = W / 2 + math.sin(t * 0.5) * 60
|
|
local cy = 150 + math.sin(t * 0.8) * 20
|
|
local spin = t * 40
|
|
local shipX = W / 2 + math.sin(t * 1.1) * 210 + math.sin(t * 2.7) * 40
|
|
local shipY = H - 70
|
|
local dying = bar >= 96
|
|
|
|
-- Stars streaming past.
|
|
for i = 1, 60 do
|
|
local sy = (i * 97 + t * (60 + (i % 5) * 50)) % H
|
|
local sx = (i * 173) % W
|
|
|
|
K.box(sx, sy, sx + 1, sy + 1 + (i % 3), 255, 255, 255, 120 + (i % 4) * 30)
|
|
end
|
|
if not dying then
|
|
if K.hitCount("kicks", lastT, t) > 0 then
|
|
for i = 1, 20 do
|
|
local a = i / 20 * 2 * math.pi + t
|
|
|
|
world.bullets[#world.bullets + 1] = { x = cx, y = cy, vx = math.cos(a) * 150, vy = math.sin(a) * 150, c = 1 }
|
|
end
|
|
end
|
|
if K.hitCount("hats", lastT, t) > 0 then
|
|
for arm = 0, 3 do
|
|
local a = math.rad(spin) + arm * math.pi / 2
|
|
|
|
world.bullets[#world.bullets + 1] = { x = cx + math.cos(a) * 110, y = cy + math.sin(a) * 110, vx = math.cos(a + 1.2) * 180, vy = math.sin(a + 1.2) * 180, c = 2 }
|
|
end
|
|
end
|
|
if K.hitCount("snares", lastT, t) > 0 then
|
|
for i = -1, 1 do
|
|
local a = math.atan(shipY - cy, shipX - cx) + i * 0.18
|
|
|
|
world.bullets[#world.bullets + 1] = { x = cx, y = cy, vx = math.cos(a) * 240, vy = math.sin(a) * 240, c = 3 }
|
|
end
|
|
end
|
|
end
|
|
-- The arms and turrets, then the core over them, flashing white while it is being hit.
|
|
local hitFlash = (math.floor(t * 30) % 2 == 0) and not dying
|
|
|
|
for arm = 0, 3 do
|
|
local a = spin + arm * 90
|
|
local ax = cx + math.cos(math.rad(a)) * 60
|
|
local ay = cy + math.sin(math.rad(a)) * 60
|
|
|
|
spriteRotate(art.bossArm, a)
|
|
spriteDraw(art.bossArm, math.floor(ax), math.floor(ay), true)
|
|
spriteDraw(art.turret, math.floor(cx + math.cos(math.rad(a)) * 110) - 14, math.floor(cy + math.sin(math.rad(a)) * 110) - 14)
|
|
end
|
|
spriteSetTint(art.bossCore, 255, hitFlash and 200 or 255, hitFlash and 200 or 255)
|
|
spriteRotateAndScale(art.bossCore, -spin * 0.5, 1.4 + 0.1 * K.decay("kicks", t, 8))
|
|
spriteDraw(art.bossCore, math.floor(cx), math.floor(cy), true)
|
|
spriteSetTint(art.bossCore, 255, 255, 255)
|
|
-- Bullets.
|
|
local tints = { { 255, 80, 220 }, { 80, 230, 255 }, { 255, 230, 80 } }
|
|
|
|
for i = #world.bullets, 1, -1 do
|
|
local bl = world.bullets[i]
|
|
|
|
bl.x = bl.x + bl.vx * dt
|
|
bl.y = bl.y + bl.vy * dt
|
|
if bl.x < -20 or bl.x > W + 20 or bl.y < -20 or bl.y > H + 20 then
|
|
table.remove(world.bullets, i)
|
|
else
|
|
local c = tints[bl.c]
|
|
|
|
spriteSetTint(art.bullet, c[1], c[2], c[3])
|
|
spriteDraw(art.bullet, math.floor(bl.x) - 6, math.floor(bl.y) - 6)
|
|
end
|
|
end
|
|
spriteSetTint(art.bullet, 255, 255, 255)
|
|
-- The ship and its fire.
|
|
for i = 0, 6 do
|
|
local fy = shipY - ((t * 900 + i * 70) % 420)
|
|
|
|
K.box(shipX - 2, fy, shipX + 2, fy + 12, 255, 255, 150, 255)
|
|
end
|
|
px(art.ship, 16, 0, shipX - 32, shipY - 32, 4)
|
|
-- Health.
|
|
local health = 1 - K.clamp((bar - 81) / 15, 0, 1)
|
|
|
|
K.box(120, 18, W - 121, 30, 60, 0, 0, 255)
|
|
K.box(122, 20, 122 + (W - 244 - 4) * health, 28, 255, 60, 60, 255)
|
|
ptext(W / 2, 36, "BOSS", 2, 255, 255, 255, "center")
|
|
if dying and K.hitCount("hats", lastT, t) > 0 then
|
|
boom(cx + math.random(-90, 90), cy + math.random(-90, 90), 90, NEON[math.random(1, 6)][1], NEON[math.random(1, 6)][2], 60)
|
|
end
|
|
emitterDraw(art.boom)
|
|
if bar > 96.85 then
|
|
K.flash(255, 255, 255, math.floor(255 * K.smooth(96.85, 97, bar)))
|
|
end
|
|
end,
|
|
leave = function(t)
|
|
videoPause(video.plasma)
|
|
spriteRotate(art.bossArm, 0)
|
|
spriteRotateAndScale(art.bossCore, 0, 1)
|
|
end,
|
|
}
|
|
|
|
-- The bonus stage: down the tunnel, the coins pour in on the beat and pile up, and the bonus counts.
|
|
ERA.bonus = {
|
|
enter = function(t)
|
|
hideAll()
|
|
nodeSetVisible(world.tunnel, true)
|
|
for _, w in ipairs(world.binWalls) do
|
|
nodeSetPosition(w.node, w.x, w.y, w.z)
|
|
end
|
|
world.phase = 0
|
|
world.phaseT = t
|
|
world.bonusBase = t
|
|
sceneSetSkyIntensity(1.4)
|
|
lightSetIntensity(world.sun, 3)
|
|
end,
|
|
draw = function(t, bar, k)
|
|
nodeSetPosition(world.camera, 0, 6, 12)
|
|
nodeLookAt(world.camera, 0, -2.5, 0)
|
|
world.phase = (world.phase + (t - world.phaseT) * (0.6 + 1.5 * K.decay("kicks", t, 5))) % 1
|
|
world.phaseT = t
|
|
videoSeek(video.tunnel, math.floor(world.phase * video.tunnelFrames))
|
|
nodeSetRotation(world.tunnel, 0, 0, t * 30)
|
|
local beats = math.floor((K.beat(t) - K.beat(world.bonusBase)) * 2)
|
|
|
|
while #world.coins < math.min(COIN_MAX, beats) do
|
|
local n = nodeNew()
|
|
|
|
nodeSetMesh(n, world.coinMesh, world.coinMaterial)
|
|
nodeSetPosition(n, (math.random() - 0.5) * 12, 9, (math.random() - 0.5) * 5)
|
|
nodeSetRotation(n, math.random(0, 360), math.random(0, 360), 0)
|
|
bodyNew(n, BODY_DYNAMIC, SHAPE_CYLINDER, 0.6, 0.12)
|
|
bodySetMass(n, 0.3)
|
|
bodySetBounce(n, 0.4)
|
|
bodySetAngularVelocity(n, (math.random() - 0.5) * 12, (math.random() - 0.5) * 12, 0)
|
|
world.coins[#world.coins + 1] = n
|
|
end
|
|
ptext(W / 2, 30, "BONUS STAGE", 4, 255, 230, 80, "center")
|
|
ptext(W / 2, 80, string.format("%07d", #world.coins * 500), 4, 255, 255, 255, "center")
|
|
end,
|
|
leave = function(t)
|
|
for _, n in ipairs(world.coins) do
|
|
bodyDelete(n)
|
|
nodeDelete(n)
|
|
end
|
|
world.coins = {}
|
|
for _, w in ipairs(world.binWalls) do
|
|
nodeSetPosition(w.node, w.x, w.y - 100, w.z)
|
|
end
|
|
end,
|
|
}
|
|
|
|
-- The high score table, which is also the credits.
|
|
ERA.scores = {
|
|
enter = function(t)
|
|
hideAll()
|
|
videoPlay(video.plasma)
|
|
end,
|
|
draw = function(t, bar, k)
|
|
videoDraw(video.plasma, 0, 0, W - 1, H - 1)
|
|
K.flash(0, 0, 20, 190)
|
|
rainbow(W / 2, 40, "HIGH SCORES", 4, t, "center")
|
|
local rows = {
|
|
{ "1ST", "999990", "CODE CLAUDE" },
|
|
{ "2ND", "888880", "MUSIC POORARTISTT" },
|
|
{ "3RD", "777770", "ENGINE SINGE" },
|
|
{ "4TH", "666660", "PIXELS CLAUDE" },
|
|
{ "5TH", "555550", "YOU THANKS!" },
|
|
}
|
|
|
|
for i, r in ipairs(rows) do
|
|
if bar >= 113 + (i - 1) * 1.2 then
|
|
local y = 120 + i * 52
|
|
|
|
ptext(40, y, r[1], 3, 255, 255, 255)
|
|
ptext(140, y, r[2], 3, 255, 230, 80)
|
|
rainbow(330, y, r[3], 2, t + i * 0.3)
|
|
end
|
|
end
|
|
scanlines(120)
|
|
end,
|
|
leave = function(t)
|
|
videoPause(video.plasma)
|
|
end,
|
|
}
|
|
|
|
-- The end: game over, thanks, and the set switched off on the last hit.
|
|
ERA.outro = {
|
|
enter = function(t)
|
|
hideAll()
|
|
end,
|
|
draw = function(t, bar, k)
|
|
K.flash(0, 0, 0, 255)
|
|
local p = t - K.barTime(137)
|
|
local last = K.duration - 0.9
|
|
|
|
if t < last then
|
|
ptext(W / 2, 140, "GAME OVER", 7, 255, 60, 60, "center")
|
|
if p > 2 then
|
|
rainbow(W / 2, 260, "THANKS FOR PLAYING", 3, t, "center")
|
|
end
|
|
if p > 4 and (t * 2) % 1 < 0.5 then
|
|
ptext(W / 2, 340, "PRESS START", 3, 255, 255, 255, "center")
|
|
end
|
|
scanlines(160)
|
|
else
|
|
local q = (t - last) / 0.35
|
|
|
|
if q < 1 then
|
|
local h = math.max(2, H * (1 - q) ^ 3)
|
|
local w = (q < 0.6) and W or W * (1 - (q - 0.6) / 0.4)
|
|
|
|
K.box(W / 2 - w / 2, H / 2 - h / 2, W / 2 + w / 2, H / 2 + h / 2, 255, 255, 255, 255)
|
|
end
|
|
end
|
|
end,
|
|
}
|
|
|
|
-- The finale runs the eras again, two bars each, every one on its own beat; its time is moved by
|
|
-- whole bars, so the beat lands where it did.
|
|
local MONTAGE = {
|
|
{ era = "pong", bar = 5, label = "1-BIT" }, { era = "invaders", bar = 9, label = "8-BIT" }, { era = "runner", bar = 17, label = "16-BIT" },
|
|
{ era = "racer", bar = 25, label = "16-BIT" }, { era = "polygons", bar = 49, label = "32-BIT" }, { era = "boss", bar = 85, label = "BOSS" },
|
|
{ era = "bonus", bar = 97, label = "BONUS" },
|
|
}
|
|
|
|
ERA.montage = {
|
|
enter = function(t)
|
|
world.montageCut = 0
|
|
end,
|
|
draw = function(t, bar, k)
|
|
local cut = math.floor((bar - 121) / 2) + 1
|
|
local final = cut > #MONTAGE
|
|
local m = MONTAGE[math.min(cut, #MONTAGE)]
|
|
|
|
if final then
|
|
-- The last two bars: the name, slammed in on the downbeat, over everything flashing.
|
|
if world.montageCut ~= cut then
|
|
if world.montageEra and ERA[world.montageEra].leave then
|
|
ERA[world.montageEra].leave(t)
|
|
end
|
|
world.montageEra = nil
|
|
world.montageCut = cut
|
|
hideAll()
|
|
end
|
|
local r, g, b = K.hue(t * 0.7)
|
|
|
|
K.flash(r // 3, g // 3, b // 3, 255)
|
|
local s = 10 + 4 * math.exp(-(bar - 135) * 6)
|
|
|
|
rainbow(W / 2, 170, "GAME ON", math.floor(s), t, "center")
|
|
K.strobe(t, "kicks", 12, 255, 255, 255, 120)
|
|
scanlines(160)
|
|
return
|
|
end
|
|
local shift = K.barTime(m.bar) - K.barTime(121 + (cut - 1) * 2)
|
|
local et = t + shift
|
|
|
|
if world.montageCut ~= cut then
|
|
if world.montageEra and ERA[world.montageEra].leave then
|
|
ERA[world.montageEra].leave(t)
|
|
end
|
|
world.montageEra = m.era
|
|
world.montageCut = cut
|
|
ERA[m.era].enter(et)
|
|
end
|
|
local saved = lastT
|
|
|
|
lastT = saved + shift
|
|
ERA[m.era].draw(et, K.bar(et), 0)
|
|
lastT = saved
|
|
ptext(W - 20 - #m.label * 24, H - 40, m.label, 3, 255, 255, 255)
|
|
K.flash(255, 255, 255, math.floor(220 * math.exp(-((bar - 121) % 2) * 2 * 1.7778 * 10)))
|
|
end,
|
|
leave = function(t)
|
|
if world.montageEra and ERA[world.montageEra].leave then
|
|
ERA[world.montageEra].leave(t)
|
|
end
|
|
end,
|
|
}
|
|
|
|
|
|
local function place(era, from, to)
|
|
return { from = from, to = to, enter = ERA[era].enter, draw = ERA[era].draw, leave = ERA[era].leave }
|
|
end
|
|
|
|
|
|
local SCENES = {
|
|
place("boot", 0, 5),
|
|
place("pong", 5, 9),
|
|
place("invaders", 9, 17),
|
|
place("runner", 17, 25),
|
|
place("racer", 25, 33),
|
|
place("loading", 33, 41),
|
|
place("polygons", 41, 57),
|
|
place("modern", 57, 73),
|
|
place("continue", 73, 81),
|
|
place("boss", 81, 97),
|
|
place("bonus", 97, 113),
|
|
place("scores", 113, 121),
|
|
place("montage", 121, 137),
|
|
place("outro", 137, 200),
|
|
-- The time at the end of each frame, for "what happened since last frame".
|
|
{
|
|
from = -10, to = 1000,
|
|
draw = function(t, bar, k)
|
|
lastT = t
|
|
end,
|
|
},
|
|
}
|
|
|
|
-- A ball meeting a brick throws sparks where they met.
|
|
function onCollision(a, b, x, y, z, speed)
|
|
if speed > 4 then
|
|
nodeSetPosition(world.spark, x, y, z)
|
|
emitterBurst(world.sparks, math.min(60, math.floor(speed * 3)))
|
|
end
|
|
end
|
|
|
|
loadArt()
|
|
buildWorld()
|
|
hideAll()
|
|
K.run(SCENES)
|