oma-tank/game/horizon.lua
2026-04-18 11:35:06 +01:00

187 lines
6.6 KiB
Lua

local World = require("game.world")
local Palette = require("rendering.palette")
local Projection = require("rendering.projection")
local Horizon = {}
-- Mountain profile: angular peaks as straight line segments
-- Each entry is {angle_degrees, height} — connected by straight lines
local mountainPoints = {}
local smokeParticles = {}
local VOLCANO_ANGLE = 230
-- Eruption phase: alternates between "quiet" and "active" periods so smoke reads as an eruption cycle
local eruption = { active = false, timer = 0, nextCycle = 8 }
-- Deterministic small PRNG for the mountain profile so the horizon stays the
-- same across a single session without touching math.random's global state.
local function lcg(seed)
local state = seed
return function()
state = (state * 1103515245 + 12345) % 2147483648
return state / 2147483648
end
end
function Horizon.init()
mountainPoints = {}
local rnd = lcg(12345)
local angle = 0
while angle < 360 do
local h = rnd() < 0.45
and (25 + rnd() * 45) -- peak
or (2 + rnd() * 10) -- valley / flat
table.insert(mountainPoints, {angle = angle, height = h})
-- Variable spacing — bigger jumps for angular look
angle = angle + 4 + rnd() * 8
end
-- Close the loop
table.insert(mountainPoints, {angle = 360, height = mountainPoints[1].height})
-- Tall narrow volcano
local volcanoPts = {
{VOLCANO_ANGLE - 15, 8},
{VOLCANO_ANGLE - 8, 25},
{VOLCANO_ANGLE - 3, 55},
{VOLCANO_ANGLE, 75},
{VOLCANO_ANGLE + 3, 55},
{VOLCANO_ANGLE + 8, 25},
{VOLCANO_ANGLE + 15, 8},
}
for _, vp in ipairs(volcanoPts) do
table.insert(mountainPoints, {angle = vp[1], height = vp[2]})
end
table.sort(mountainPoints, function(a, b) return a.angle < b.angle end)
smokeParticles = {}
eruption.active = false
eruption.timer = 0
eruption.nextCycle = 8 + math.random() * 6
end
-- Get mountain height at a specific angle by interpolating between nearest points
local function getMountainHeight(deg)
deg = deg % 360
-- Find the two points bracketing this angle
local prev = mountainPoints[#mountainPoints - 1]
local nxt = mountainPoints[1]
for i = 1, #mountainPoints - 1 do
if mountainPoints[i].angle <= deg and mountainPoints[i + 1].angle > deg then
prev = mountainPoints[i]
nxt = mountainPoints[i + 1]
break
end
end
-- Linear interpolation between the two points
local range = nxt.angle - prev.angle
if range <= 0 then return prev.height end
local t = (deg - prev.angle) / range
return prev.height + (nxt.height - prev.height) * t
end
function Horizon.update(dt)
-- Eruption cycle: quiet stretches then 3-5s active bursts
eruption.timer = eruption.timer + dt
if eruption.timer >= eruption.nextCycle then
eruption.timer = 0
eruption.active = not eruption.active
eruption.nextCycle = eruption.active
and (3 + math.random() * 2) -- active burst duration
or (10 + math.random() * 10) -- quiet interval between eruptions
end
if eruption.active and math.random() < 0.5 then
table.insert(smokeParticles, {
angle = VOLCANO_ANGLE + (math.random() - 0.5) * 6,
height = 78 + math.random() * 5,
vy = 18 + math.random() * 14,
vangle = (math.random() - 0.5) * 1.5,
life = 2.0 + math.random(),
maxLife = 3.0,
size = 1 + math.random() * 1.5,
})
end
for i = #smokeParticles, 1, -1 do
local sp = smokeParticles[i]
sp.height = sp.height + sp.vy * dt
sp.angle = sp.angle + sp.vangle * dt
sp.life = sp.life - dt
if sp.life <= 0 then table.remove(smokeParticles, i) end
end
end
function Horizon.draw(cameraAngle)
local p = Palette.get()
local vp = Projection.getViewport()
-- Procedural horizon "distance": smaller divisor → mountains appear closer/bigger.
-- 700 gives mountains roughly double the apparent size of our previous 1500 value
-- so the horizon reads as a near-ish backdrop rather than vanishingly far away.
local FAR_SCALE = (vp.w / 2 * math.sqrt(3)) / 700
-- === STRONG HORIZON LINE ===
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.8)
love.graphics.setLineWidth(2)
love.graphics.line(vp.x, vp.horizonY, vp.x + vp.w, vp.horizonY)
-- === MOUNTAINS: angular connected peaks ===
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.7)
love.graphics.setLineWidth(1.5)
-- Walk across screen columns in larger steps for angular look
local pts = {}
local step = 3
for sx = vp.x, vp.x + vp.w, step do
local relX = (sx - vp.x - vp.w / 2) / (vp.w / 2)
local worldAngle = cameraAngle + math.atan(relX)
local deg = math.deg(worldAngle) % 360
local height = getMountainHeight(deg)
local sy = vp.horizonY - height * FAR_SCALE
table.insert(pts, sx)
table.insert(pts, sy)
end
if #pts >= 4 then
love.graphics.line(pts)
end
-- === MOON ===
local moonWorldAngle = math.rad(10)
local moonRelAngle = moonWorldAngle - cameraAngle
while moonRelAngle > math.pi do moonRelAngle = moonRelAngle - math.pi * 2 end
while moonRelAngle < -math.pi do moonRelAngle = moonRelAngle + math.pi * 2 end
if math.abs(moonRelAngle) < math.pi / 4 then
local moonSx = vp.x + vp.w/2 + math.tan(moonRelAngle) * (vp.w/2)
local moonSy = vp.horizonY - vp.h * 0.28
local moonR = vp.h * 0.035
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.5)
love.graphics.setLineWidth(1.5)
love.graphics.circle("line", moonSx, moonSy, moonR, 16)
-- Crescent shadow
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.2)
love.graphics.arc("line", "open", moonSx + moonR*0.3, moonSy, moonR*0.7, -math.pi/2, math.pi/2, 10)
end
-- === VOLCANO SMOKE ===
for _, sp in ipairs(smokeParticles) do
local spRel = math.rad(sp.angle) - cameraAngle
while spRel > math.pi do spRel = spRel - math.pi * 2 end
while spRel < -math.pi do spRel = spRel + math.pi * 2 end
if math.abs(spRel) < math.pi / 4 then
local spSx = vp.x + vp.w/2 + math.tan(spRel) * (vp.w/2)
local spSy = vp.horizonY - sp.height * FAR_SCALE
local alpha = 0.35 * (sp.life / sp.maxLife)
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], alpha)
love.graphics.circle("fill", spSx, spSy, sp.size)
end
end
end
return Horizon