Files

404 lines
9.8 KiB
Lua

local app = {}
local C = wada.ui.colors
local fl = math.floor
local ROWS, COLS = 4, 8
local TOP_H = 16
local SY, AH = 14, 10 -- alien row spacing, alien height
local PW, PH = 20, 7 -- player size
local SC, SR, CW, CH = 5, 3, 6, 5 -- shield cols/rows, cell size
local ROWC = { 0xff3355, 0xffaa22, 0x33dd77, 0x33aaff }
local PTS = { 30, 20, 10, 10 }
local cv, W, H
local SX, AW, STEP
local py, shield_y, ufo_y
local px
local alive, n_alive
local fx, fy, fdir, anim, last_step
local pb, abul, ufo
local shields
local score, hi, lives, level
local state = "ready"
local last, resume_at, next_fire, next_ufo, over_at = 0, 0, 0, 0, 0
local msg = nil
local function clamp(v, lo, hi_)
if v < lo then return lo end
if v > hi_ then return hi_ end
return v
end
-- drawing helpers: the canvas wants whole numbers
local function rect(x, y, w, h, c)
cv:rect(fl(x), fl(y), fl(w), fl(h), c, true)
end
local function text(x, y, s, c, size)
cv:text(fl(x), fl(y), s, c, size)
end
local function center(s, y, size, c)
text((W - wada.ui.text_w(s, size)) / 2, y, s, c, size)
end
local function make_shields()
shields = {}
for i = 1, 4 do
local s = { x = fl((i - 0.5) * W / 4 - SC * CW / 2), cells = {} }
for r = 1, SR do
s.cells[r] = {}
for c = 1, SC do s.cells[r][c] = true end
end
shields[i] = s
end
end
local function new_wave()
alive = {}
for r = 1, ROWS do
alive[r] = {}
for c = 1, COLS do alive[r][c] = true end
end
n_alive = ROWS * COLS
fx = 8
fy = TOP_H + 18 + math.min(level - 1, 4) * 8
fdir = 1
anim = 0
last_step = wada.sys.millis()
pb = nil
abul = {}
make_shields()
end
local function init_game()
score, lives, level = 0, 3, 1
px = fl((W - PW) / 2)
ufo = nil
new_wave()
local now = wada.sys.millis()
next_ufo = now + 15000
next_fire = now + 1000
resume_at = 0
end
local function start()
init_game()
state = "play"
last = wada.sys.millis()
resume_at = last + 500
end
local function game_over()
state = "over"
over_at = wada.sys.millis()
if score > hi then
hi = score
wada.store.set("hi", tostring(hi))
end
wada.sys.beep()
end
-- Returns true if the bullet chewed a shield cell.
-- dir -1: bullet moving up (check bottom row first); 1: moving down.
local function shield_hit(bx, by, bh, dir)
for i = 1, #shields do
local s = shields[i]
if bx >= s.x and bx < s.x + SC * CW and by < shield_y + SR * CH and by + bh > shield_y then
local c = fl((bx - s.x) / CW) + 1
local r0, r1, st = 1, SR, 1
if dir < 0 then r0, r1, st = SR, 1, -1 end
for r = r0, r1, st do
if s.cells[r][c] then
local cy0 = shield_y + (r - 1) * CH
if by < cy0 + CH and by + bh > cy0 then
s.cells[r][c] = false
return true
end
end
end
end
end
return false
end
local function kill_alien(r, c)
alive[r][c] = false
n_alive = n_alive - 1
score = score + PTS[r]
wada.sys.beep()
end
local function step_formation(now)
local minc, maxc, low = COLS + 1, 0, 0
for r = 1, ROWS do
for c = 1, COLS do
if alive[r][c] then
if c < minc then minc = c end
if c > maxc then maxc = c end
if r > low then low = r end
end
end
end
if maxc == 0 then return end
local left = fx + (minc - 1) * SX
local right = fx + (maxc - 1) * SX + AW
if (fdir > 0 and right + STEP > W - 2) or (fdir < 0 and left - STEP < 2) then
fy = fy + 8
fdir = -fdir
else
fx = fx + fdir * STEP
end
anim = 1 - anim
local bottom = fy + (low - 1) * SY + AH
if bottom >= shield_y then
for i = 1, #shields do
for r = 1, SR do
for c = 1, SC do shields[i].cells[r][c] = false end
end
end
end
if bottom >= py then game_over() end
end
local function alien_fire(now)
local cols = {}
for c = 1, COLS do
for r = 1, ROWS do
if alive[r][c] then cols[#cols + 1] = c; break end
end
end
if #cols == 0 then return end
local c = cols[wada.sys.random(1, #cols)]
local row = 1
for r = ROWS, 1, -1 do
if alive[r][c] then row = r; break end
end
abul[#abul + 1] = {
x = fx + (c - 1) * SX + AW // 2,
y = fy + (row - 1) * SY + AH,
}
end
local function update(dt, now)
-- player bullet
if pb then
pb.y = pb.y - 220 * dt
if pb.y + 8 < TOP_H then
pb = nil
else
local tx, ty = pb.x + 1, pb.y
local c = fl((tx - fx) / SX) + 1
local r = fl((ty - fy) / SY) + 1
if r >= 1 and r <= ROWS and c >= 1 and c <= COLS and alive[r][c] then
local ox = tx - (fx + (c - 1) * SX)
local oy = ty - (fy + (r - 1) * SY)
if ox <= AW and oy <= AH then
kill_alien(r, c)
pb = nil
end
end
if pb and ufo and tx >= ufo.x and tx <= ufo.x + 20 and ty < ufo_y + 7 then
score = score + ({ 50, 100, 150 })[wada.sys.random(1, 3)]
ufo = nil
pb = nil
wada.sys.beep()
end
if pb and shield_hit(pb.x, pb.y, 8, -1) then pb = nil end
end
end
-- wave cleared
if n_alive == 0 then
level = level + 1
new_wave()
resume_at = now + 800
return
end
-- formation movement
local iv = math.max(35, 50 + n_alive * (14 - math.min(level - 1, 6)))
if now - last_step >= iv then
last_step = now
step_formation(now)
if state ~= "play" then return end
end
-- alien fire
if now >= next_fire and #abul < 3 then
alien_fire(now)
next_fire = now + math.max(250, wada.sys.random(500, 1300) - level * 40)
end
-- alien bullets
local bspd = 110 + math.min(level, 8) * 6
for i = #abul, 1, -1 do
local b = abul[i]
b.y = b.y + bspd * dt
if b.y > H then
table.remove(abul, i)
elseif shield_hit(b.x, b.y, 8, 1) then
table.remove(abul, i)
elseif b.x + 2 > px and b.x < px + PW and b.y + 8 > py and b.y < py + PH then
lives = lives - 1
abul = {}
pb = nil
resume_at = now + 1200
wada.sys.beep()
if lives <= 0 then game_over() end
return
end
end
-- UFO
if ufo then
ufo.x = ufo.x + ufo.dir * 80 * dt
if ufo.x < -24 or ufo.x > W + 4 then ufo = nil end
elseif now >= next_ufo and fy > TOP_H + 12 then
local d = wada.sys.random(1, 2) == 1 and 1 or -1
ufo = { x = d > 0 and -20 or W, dir = d }
next_ufo = now + wada.sys.random(12000, 25000)
end
end
local function draw()
cv:fill(C.bg)
text(4, 2, "Score " .. score .. " Hi " .. hi .. " Lv " .. level, C.text, 12)
for i = 1, lives do
rect(W - 10 - (i - 1) * 12, 5, 9, 5, C.good)
end
-- shields (one rect per run of cells)
for i = 1, #shields do
local s = shields[i]
for r = 1, SR do
local c = 1
while c <= SC do
if s.cells[r][c] then
local e = c
while e < SC and s.cells[r][e + 1] do e = e + 1 end
rect(s.x + (c - 1) * CW, shield_y + (r - 1) * CH, (e - c + 1) * CW, CH, C.good)
c = e + 1
else
c = c + 1
end
end
end
end
-- aliens
for r = 1, ROWS do
for c = 1, COLS do
if alive[r][c] then
local x = fx + (c - 1) * SX
local y = fy + (r - 1) * SY
rect(x, y, AW, 7, ROWC[r])
if anim == 0 then
rect(x + 1, y + 7, AW - 2, 3, ROWC[r])
else
rect(x + 3, y + 7, AW - 6, 3, ROWC[r])
end
end
end
end
if ufo then
rect(ufo.x, ufo_y, 20, 7, C.bad)
end
-- player (blinks while recovering)
local now = wada.sys.millis()
if now >= resume_at or (now // 100) % 2 == 0 then
rect(px, py, PW, PH, C.accent)
rect(px + PW // 2 - 1, py - 3, 2, 3, C.accent)
end
if pb then rect(pb.x, pb.y, 2, 8, C.text) end
for i = 1, #abul do rect(abul[i].x, abul[i].y, 2, 8, C.warn or 0xE8A33D) end
if state == "ready" then
center("SPACE INVADERS", H * 0.18, 16, C.text)
center("Left/right to move", H * 0.40, 12, C.sub)
center("Up / Enter / tap middle to fire", H * 0.47, 12, C.sub)
center("Tap or Enter to start", H * 0.60, 12, C.sub)
elseif state == "over" then
center("GAME OVER", H * 0.30, 24, C.bad)
center("Score " .. score .. " Hi " .. hi, H * 0.46, 14, C.sub)
center("Tap or Enter to play again", H * 0.58, 12, C.sub)
end
end
local function move(d)
px = clamp(px + d, 2, W - PW - 2)
end
local function fire()
if state == "play" and not pb and wada.sys.millis() >= resume_at then
pb = { x = px + PW // 2 - 1, y = py - 8 }
end
end
local function try_start()
if wada.sys.millis() - over_at > 800 then start() end
end
function app.on_open(w, h)
W, H = w, h
SX = fl(W / 11)
AW = fl(SX * 0.6)
STEP = math.max(2, fl(W / 80))
py = H - 14
shield_y = py - 36
ufo_y = TOP_H + 1
hi = tonumber(wada.store.get("hi")) or 0
cv = wada.ui.canvas(W, H)
cv:pos(0, 0)
init_game()
last = wada.sys.millis()
wada.timer.every(40)
draw()
end
function app.on_tick()
local now = wada.sys.millis()
local dt = clamp((now - last) / 1000, 0, 0.045)
last = now
if state == "play" and now >= resume_at then update(dt, now) end
draw()
end
function app.on_input(ev)
if ev.type == "key" then
local k = ev.key
if k == "left" or k == "a" then
move(-14)
elseif k == "right" or k == "d" then
move(14)
elseif k == "up" or k == "w" or k == "enter" or k == " " then
if state == "play" then fire() else try_start() end
end
elseif ev.type == "swipe" then
if ev.dir == "left" then
move(-22)
elseif ev.dir == "right" then
move(22)
elseif ev.dir == "up" then
if state == "play" then fire() else try_start() end
end
elseif ev.type == "down" then
if state ~= "play" then
try_start()
elseif ev.x < W // 3 then
move(-22)
elseif ev.x > (W * 2) // 3 then
move(22)
else
fire()
end
end
end
function app.on_close()
if score and score > hi then wada.store.set("hi", tostring(score)) end
end
return app