-- AURORA -- Cadenza HBlank tech demo. -- A still scene of a polar sky. The horizon is a per-scanline palette ramp; -- the aurora bands are palette-cycled; the mountains are flat polygons; the -- Aria SPEAK chip whispers a few slogans on a loop. No interaction beyond -- pressing A to toggle the title overlay. local W, H = gfx.W, gfx.H local PAL = { -- Sky: a single index whose RGB is rewritten per scanline by hpal. sky = 96, -- Mountain silhouettes: dark indigo and a slightly lighter haze. mtn_dark = 64 + 1, mtn_haze = 64 + 4, -- Aurora bands: 8 entries cycled in place. aurora_0 = 100, aurora_n = 8, -- Stars: white-ish ramp. star = 15, } local horizonY = 130 -- screen Y where mountains begin local frame = 0 local showTitle = true local nextSpeakAt = 90 -- frames until the next whisper local SLOGANS = { "AH-OW R-OW R-AH", -- "aurora" "D-AE M-AH K-L-IH Z", -- "damocles" "M-AE EH S-T-R-OW", -- "maestro" "AH B-IH G IH N-IH NG", -- "a beginning" "AH S-EH K-AH N-D CH-AE-N S", -- "a second chance" "K-L-IH R W-AO T-ER F-L-AO R-IH D-AH", -- "Clearwater Florida" "N-AY N T-EE N N-AY N T-EE F-AY V", -- "nineteen ninety five" } -- ---------------- palette setup -------------------------------------------- local function pal_setup() -- Aurora ramp: cool greens through cyan into a magenta tail. -- Components are 0..63 (native 18-bit palette). gfx.pal(PAL.aurora_0 + 0, 6, 30, 14) gfx.pal(PAL.aurora_0 + 1, 8, 42, 22) gfx.pal(PAL.aurora_0 + 2, 10, 50, 30) gfx.pal(PAL.aurora_0 + 3, 12, 56, 44) gfx.pal(PAL.aurora_0 + 4, 16, 50, 56) gfx.pal(PAL.aurora_0 + 5, 28, 36, 56) gfx.pal(PAL.aurora_0 + 6, 40, 22, 50) gfx.pal(PAL.aurora_0 + 7, 30, 14, 38) end -- ---------------- the sky -------------------------------------------------- -- Schedule a per-scanline sky gradient that breathes slightly with the demo -- clock. The sky never reaches the horizon; mountains overdraw. local function schedule_sky() local t = frame / 60 local breathe = (math.sin(t * 0.4) + 1) * 0.5 -- 0..1 -- Top of the sky: deep indigo with a very faint magenta tint that grows. local r0 = math.floor( 4 + breathe * 6) local g0 = math.floor( 2 + breathe * 4) local b0 = math.floor(18 + breathe * 6) -- Toward the horizon: pull warmer (the aurora "afterglow"). local r1 = math.floor(20 + breathe * 10) local g1 = math.floor( 8 + breathe * 6) local b1 = math.floor(20 + breathe * 6) gfx.hpal_ramp(PAL.sky, 0, horizonY - 1, r0, g0, b0, r1, g1, b1) end -- ---------------- the aurora ----------------------------------------------- -- Three soft horizontal bands, each composed of stacked rectangles in the -- 8-entry aurora ramp. Per-frame palette cycling rotates the ramp, which -- makes the colour run through each band. local bandY = { 30, 56, 80 } local function draw_aurora() local t = frame / 60 for bi = 1, #bandY do local cy = bandY[bi] -- Sway: each band shifts horizontally with a different phase and speed. local sway = math.floor(math.sin(t * (0.5 + bi * 0.2) + bi) * 18) local thickness = 12 -- Translucent feel: draw multiple thin rects with palette-ramp indices. for k = 0, thickness - 1 do local idx = PAL.aurora_0 + ((k + bi) % PAL.aurora_n) local y = cy + k - thickness / 2 -- Width tapers with sway so the band feels like it moves. local x0 = 30 + sway + k * 2 local w = W - 60 - k * 4 gfx.rect(x0, y, w, 1, idx) end end -- Cycle the aurora palette range a fraction of a slot per frame. if frame % 5 == 0 then gfx.pal_cycle(PAL.aurora_0, PAL.aurora_n, 1) end end -- ---------------- stars ---------------------------------------------------- -- A fixed grid of star positions, twinkled by a per-star sin phase. local stars = {} local function build_stars() for i = 1, 60 do stars[i] = { x = math.random(0, W - 1), y = math.random(0, horizonY - 6), ph = math.random() * math.pi * 2, sp = 0.06 + math.random() * 0.05, } end end local function draw_stars() local t = frame / 60 for _, s in ipairs(stars) do local v = math.sin(t * 12 * s.sp + s.ph) if v > 0.55 then gfx.pset(s.x, s.y, PAL.star) if v > 0.85 then gfx.pset(s.x + 1, s.y, PAL.star) gfx.pset(s.x - 1, s.y, PAL.star) gfx.pset(s.x, s.y + 1, PAL.star) gfx.pset(s.x, s.y - 1, PAL.star) end end end end -- ---------------- mountains ------------------------------------------------ local function draw_mountains() -- Two layers: a hazy back ridge, then a dark front ridge. -- Back layer: gentle, lower amplitude. for x = 0, W - 1 do local h = math.floor(18 + math.sin(x * 0.045) * 6 + math.sin(x * 0.11) * 4) gfx.rect(x, horizonY - h, 1, H - (horizonY - h), PAL.mtn_haze) end -- Front layer: jagged peaks via flat triangles. local peaks = { 0, 40, 90, 150, 210, 270, 330, 384 } local heights = { 12, 36, 22, 44, 18, 38, 26, 10 } -- Floor. gfx.rect(0, horizonY + 14, W, H - (horizonY + 14), PAL.mtn_dark) for i = 1, #peaks - 1 do local x0 = peaks[i] local x1 = peaks[i + 1] local h0 = heights[i] local h1 = heights[i + 1] -- Triangle from (x0, horizon) up to peak between, down to (x1, horizon). local mid = (x0 + x1) / 2 local mh = math.max(h0, h1) + 4 gfx.tri(x0, horizonY + 14, mid, horizonY + 14 - mh, x1, horizonY + 14, PAL.mtn_dark) end end -- ---------------- title overlay -------------------------------------------- local function draw_title() if not showTitle then return end local function centred(s, y, c) gfx.text(s, math.floor(W / 2 - (#s * 8) / 2), y, c) end -- Title fades in over the first 3 seconds, holds, fades out. local t = frame / 60 local cycle = (t % 16) if cycle > 12 then -- "presented by" centred("PRESENTED BY", H - 32, 80 + 10) centred("DAMOCLES INTERACTIVE", H - 18, 80 + 14) else centred("AURORA", H - 32, 15) centred("CADENZA HBLANK TECH DEMO", H - 18, 80 + 10) end centred("PRESS A TO HIDE TITLE - SELECT TO EJECT", 8, 80 + 8) end -- ---------------- entry points -------------------------------------------- function _init() pal_setup() build_stars() -- Set the cleared sky to a known indigo so the rect path that draws -- nothing per pixel still has a sensible background; the hpal stream -- overrides this on the visible scanlines. gfx.pal(PAL.sky, 4, 2, 18) end function _update() frame = frame + 1 gfx.fade(0) gfx.mosaic(0) if inp.btnp("a") then showTitle = not showTitle end nextSpeakAt = nextSpeakAt - 1 if nextSpeakAt <= 0 then snd.say(SLOGANS[math.random(1, #SLOGANS)], 90 + math.random(0, 40), 0.45) nextSpeakAt = 60 * (5 + math.random(0, 6)) -- 5..11 seconds end end function _draw() -- Fill the sky region with the single sky index; HBlank rewrites its -- RGB per scanline so the gradient appears. gfx.rect(0, 0, W, horizonY, PAL.sky) schedule_sky() draw_stars() draw_aurora() draw_mountains() draw_title() end