404 lines
9.8 KiB
Lua
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
|