187 lines
6.6 KiB
Lua
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
|