OMA-COMMAND — Missile Command arcade clone in Love2D with Omarchy theme integration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
255 lines
7.7 KiB
Lua
255 lines
7.7 KiB
Lua
local HighScores = {}
|
|
|
|
local MAX_SCORES = 10
|
|
local SAVE_FILE = "high_scores.dat"
|
|
|
|
local scores = {}
|
|
|
|
-- Entry state
|
|
local entry = {
|
|
active = false,
|
|
score = 0,
|
|
letters = {"A", "A", "A"},
|
|
position = 1, -- 1, 2, or 3
|
|
blink = 0,
|
|
}
|
|
|
|
-- ── Data Management ──
|
|
|
|
function HighScores.init()
|
|
HighScores.load()
|
|
end
|
|
|
|
function HighScores.load()
|
|
scores = {}
|
|
if love.filesystem.getInfo(SAVE_FILE) then
|
|
local data = love.filesystem.read(SAVE_FILE)
|
|
if data then
|
|
for line in data:gmatch("[^\n]+") do
|
|
local initials, score = line:match("^(%a%a%a)%s+(%d+)$")
|
|
if initials and score then
|
|
table.insert(scores, {initials = initials:upper(), score = tonumber(score)})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
table.sort(scores, function(a, b) return a.score > b.score end)
|
|
-- Trim to max
|
|
while #scores > MAX_SCORES do
|
|
table.remove(scores)
|
|
end
|
|
end
|
|
|
|
function HighScores.save()
|
|
local lines = {}
|
|
for _, entry in ipairs(scores) do
|
|
table.insert(lines, string.format("%s %d", entry.initials, entry.score))
|
|
end
|
|
love.filesystem.write(SAVE_FILE, table.concat(lines, "\n") .. "\n")
|
|
end
|
|
|
|
function HighScores.isHighScore(score)
|
|
if score <= 0 then return false end
|
|
if #scores < MAX_SCORES then return true end
|
|
return score > scores[#scores].score
|
|
end
|
|
|
|
function HighScores.addScore(initials, score)
|
|
table.insert(scores, {initials = initials:upper(), score = score})
|
|
table.sort(scores, function(a, b) return a.score > b.score end)
|
|
while #scores > MAX_SCORES do
|
|
table.remove(scores)
|
|
end
|
|
HighScores.save()
|
|
end
|
|
|
|
function HighScores.getScores()
|
|
local result = {}
|
|
for _, s in ipairs(scores) do
|
|
table.insert(result, {initials = s.initials, score = s.score})
|
|
end
|
|
return result
|
|
end
|
|
|
|
function HighScores.getHighest()
|
|
if #scores > 0 then
|
|
return scores[1].score
|
|
end
|
|
return 0
|
|
end
|
|
|
|
-- ── Entry Screen ──
|
|
|
|
function HighScores.startEntry(score)
|
|
entry.active = true
|
|
entry.score = score
|
|
entry.letters = {"A", "A", "A"}
|
|
entry.position = 1
|
|
entry.blink = 0
|
|
end
|
|
|
|
function HighScores.isEntryActive()
|
|
return entry.active
|
|
end
|
|
|
|
function HighScores.updateEntry(dt)
|
|
entry.blink = entry.blink + dt
|
|
end
|
|
|
|
function HighScores.keypressedEntry(key)
|
|
if not entry.active then return nil end
|
|
|
|
if key == "left" then
|
|
entry.position = math.max(1, entry.position - 1)
|
|
elseif key == "right" then
|
|
entry.position = math.min(3, entry.position + 1)
|
|
elseif key == "up" then
|
|
local b = entry.letters[entry.position]:byte()
|
|
b = b + 1
|
|
if b > 90 then b = 65 end -- wrap Z -> A
|
|
entry.letters[entry.position] = string.char(b)
|
|
elseif key == "down" then
|
|
local b = entry.letters[entry.position]:byte()
|
|
b = b - 1
|
|
if b < 65 then b = 90 end -- wrap A -> Z
|
|
entry.letters[entry.position] = string.char(b)
|
|
elseif key == "return" or key == "kpenter" then
|
|
local initials = table.concat(entry.letters)
|
|
local score = entry.score
|
|
entry.active = false
|
|
HighScores.addScore(initials, score)
|
|
return "done", {initials = initials, score = score}
|
|
elseif key:match("^%a$") and #key == 1 then
|
|
-- Direct letter input
|
|
entry.letters[entry.position] = key:upper()
|
|
if entry.position < 3 then
|
|
entry.position = entry.position + 1
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function HighScores.drawEntry(screenW, screenH, palette, fonts)
|
|
local midX = screenW / 2
|
|
local midY = screenH * 0.25
|
|
|
|
-- "NEW HIGH SCORE" header
|
|
love.graphics.setFont(fonts.large)
|
|
love.graphics.setColor(palette.bright)
|
|
local t = love.timer.getTime()
|
|
local pulse = 0.7 + math.sin(t * 4) * 0.3
|
|
love.graphics.setColor(palette.bright[1], palette.bright[2], palette.bright[3], pulse)
|
|
love.graphics.printf("NEW HIGH SCORE", 0, midY, screenW, "center")
|
|
|
|
-- Score display
|
|
midY = midY + fonts.large:getHeight() + 16
|
|
love.graphics.setFont(fonts.medium)
|
|
love.graphics.setColor(palette.fg)
|
|
love.graphics.printf(string.format("%d", entry.score), 0, midY, screenW, "center")
|
|
|
|
-- Letter entry boxes
|
|
midY = midY + fonts.medium:getHeight() + 32
|
|
local boxW = math.floor(screenW * 0.06)
|
|
local boxH = math.floor(boxW * 1.3)
|
|
local gap = math.floor(boxW * 0.4)
|
|
local totalW = boxW * 3 + gap * 2
|
|
local startX = midX - totalW / 2
|
|
|
|
love.graphics.setFont(fonts.large)
|
|
local letterH = fonts.large:getHeight()
|
|
|
|
for i = 1, 3 do
|
|
local bx = startX + (i - 1) * (boxW + gap)
|
|
local by = midY
|
|
|
|
-- Box outline
|
|
local isSelected = (i == entry.position)
|
|
if isSelected then
|
|
local blinkOn = math.floor(entry.blink * 3) % 2 == 0
|
|
if blinkOn then
|
|
love.graphics.setColor(palette.bright)
|
|
else
|
|
love.graphics.setColor(palette.dim)
|
|
end
|
|
love.graphics.setLineWidth(3)
|
|
else
|
|
love.graphics.setColor(palette.dim)
|
|
love.graphics.setLineWidth(2)
|
|
end
|
|
love.graphics.rectangle("line", bx, by, boxW, boxH)
|
|
|
|
-- Up/down arrows for selected position
|
|
if isSelected then
|
|
love.graphics.setColor(palette.bright[1], palette.bright[2], palette.bright[3], 0.5)
|
|
local arrowX = bx + boxW / 2
|
|
-- Up arrow
|
|
local arrowTop = by - 12
|
|
love.graphics.polygon("fill", arrowX - 5, arrowTop + 8, arrowX + 5, arrowTop + 8, arrowX, arrowTop)
|
|
-- Down arrow
|
|
local arrowBot = by + boxH + 4
|
|
love.graphics.polygon("fill", arrowX - 5, arrowBot, arrowX + 5, arrowBot, arrowX, arrowBot + 8)
|
|
end
|
|
|
|
-- Letter
|
|
love.graphics.setColor(palette.fg)
|
|
local lw = fonts.large:getWidth(entry.letters[i])
|
|
local lx = bx + (boxW - lw) / 2
|
|
local ly = by + (boxH - letterH) / 2
|
|
love.graphics.print(entry.letters[i], lx, ly)
|
|
end
|
|
|
|
-- Instructions
|
|
local instrY = midY + boxH + 32
|
|
love.graphics.setFont(fonts.small)
|
|
love.graphics.setColor(palette.dim)
|
|
love.graphics.printf("TYPE LETTERS / ARROWS TO SELECT / ENTER TO CONFIRM", 0, instrY, screenW, "center")
|
|
end
|
|
|
|
function HighScores.drawTable(screenW, screenH, palette, fonts)
|
|
local topY = screenH * 0.68
|
|
local lineH = fonts.medium:getHeight() + 4
|
|
|
|
love.graphics.setFont(fonts.medium)
|
|
love.graphics.setColor(palette.bright)
|
|
love.graphics.printf("HIGH SCORES", 0, topY, screenW, "center")
|
|
|
|
topY = topY + lineH + 8
|
|
|
|
love.graphics.setFont(fonts.small)
|
|
local entryH = fonts.small:getHeight() + 3
|
|
|
|
if #scores == 0 then
|
|
love.graphics.setColor(palette.dim)
|
|
love.graphics.printf("NO SCORES YET", 0, topY, screenW, "center")
|
|
return
|
|
end
|
|
|
|
local colW = math.floor(screenW * 0.4)
|
|
local startX = (screenW - colW) / 2
|
|
|
|
for i, s in ipairs(scores) do
|
|
local y = topY + (i - 1) * entryH
|
|
local rankStr = string.format("%2d.", i)
|
|
local scoreStr = string.format("%d", s.score)
|
|
|
|
-- Rank and initials
|
|
if i == 1 then
|
|
love.graphics.setColor(palette.bright)
|
|
else
|
|
love.graphics.setColor(palette.fg[1], palette.fg[2], palette.fg[3], 0.8)
|
|
end
|
|
love.graphics.print(rankStr .. " " .. s.initials, startX, y)
|
|
|
|
-- Score right-aligned
|
|
local sw = fonts.small:getWidth(scoreStr)
|
|
love.graphics.print(scoreStr, startX + colW - sw, y)
|
|
|
|
-- Subtle separator line
|
|
love.graphics.setColor(palette.dim[1], palette.dim[2], palette.dim[3], 0.2)
|
|
love.graphics.setLineWidth(1)
|
|
love.graphics.line(startX, y + entryH - 1, startX + colW, y + entryH - 1)
|
|
end
|
|
end
|
|
|
|
return HighScores
|