mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 08:11:35 +02:00
171 lines
4.8 KiB
Lua
171 lines
4.8 KiB
Lua
-- Overworld tilt mode: a cycleable, purely presentational perspective
|
|
-- tilt for the free-roam overworld. The flat world canvas is treated as
|
|
-- a ground plane, rotated about the horizontal axis through the viewport
|
|
-- centre and viewed through a perspective camera, so rows above centre
|
|
-- recede/shrink and rows below come closer (the HD-2D "diorama" look).
|
|
-- Like survey zoom this lives entirely in the draw path -- zero effect
|
|
-- on collision, movement, triggers, scripts -- and is persisted via
|
|
-- save.options.tilt (OFF / 15 / 35 / 50).
|
|
--
|
|
-- Spec: docs/new-features.md (tilt mode)
|
|
|
|
local Zoom = require("src.render.Zoom")
|
|
|
|
local Tilt = {}
|
|
|
|
-- Discrete tilt angles in degrees (index 0 is off). Cycle: off→15→35→50→off.
|
|
Tilt.ANGLES_DEG = { 0, 15, 35, 50 }
|
|
Tilt.ANGLE_LABELS = { "OFF", "15", "35", "50" }
|
|
|
|
-- Runtime state. `level` is the discrete option (0=off .. 3=50°);
|
|
-- `angle` is the live tweened tilt in radians; `from`/`goal`/`t` drive
|
|
-- the ease between any two levels (including off).
|
|
Tilt.level = 0
|
|
Tilt.angle = 0
|
|
Tilt.from = 0
|
|
Tilt.goal = 0
|
|
Tilt.t = 1
|
|
-- Compatibility: TARGET_ANGLE is the current goal; enabled mirrors level > 0.
|
|
Tilt.TARGET_ANGLE = 0
|
|
Tilt.enabled = false
|
|
|
|
Tilt.TWEEN_TIME = 0.25
|
|
Tilt.FOCAL = 1.0
|
|
Tilt.VIEW_MARGIN = 0.35
|
|
|
|
local function ease(t)
|
|
return t * t * (3 - 2 * t)
|
|
end
|
|
|
|
local function goalFor(level)
|
|
return math.rad(Tilt.ANGLES_DEG[level + 1] or 0)
|
|
end
|
|
|
|
function Tilt.setLevel(level)
|
|
level = math.floor(tonumber(level) or 0)
|
|
if level < 0 then level = 0 end
|
|
if level > 3 then level = 3 end
|
|
local goal = goalFor(level)
|
|
if goal ~= Tilt.goal or level ~= Tilt.level then
|
|
Tilt.from = Tilt.angle
|
|
Tilt.goal = goal
|
|
Tilt.t = 0
|
|
end
|
|
Tilt.level = level
|
|
Tilt.TARGET_ANGLE = goal
|
|
Tilt.enabled = level > 0
|
|
end
|
|
|
|
-- Advance OFF → 15 → 35 → 50 → OFF. Returns the new level.
|
|
function Tilt.cycle()
|
|
Tilt.setLevel((Tilt.level + 1) % 4)
|
|
return Tilt.level
|
|
end
|
|
|
|
-- Legacy name: one cycle step (same as cycle).
|
|
function Tilt.toggle()
|
|
return Tilt.cycle()
|
|
end
|
|
|
|
function Tilt.reset()
|
|
Tilt.level = 0
|
|
Tilt.angle = 0
|
|
Tilt.from = 0
|
|
Tilt.goal = 0
|
|
Tilt.t = 1
|
|
Tilt.TARGET_ANGLE = 0
|
|
Tilt.enabled = false
|
|
end
|
|
|
|
function Tilt.applyOptions(opts)
|
|
local level = math.floor(tonumber(opts and opts.tilt) or 0)
|
|
if level < 0 then level = 0 end
|
|
if level > 3 then level = 3 end
|
|
Tilt.level = level
|
|
Tilt.goal = goalFor(level)
|
|
Tilt.from = Tilt.goal
|
|
Tilt.angle = Tilt.goal
|
|
Tilt.t = 1
|
|
Tilt.TARGET_ANGLE = Tilt.goal
|
|
Tilt.enabled = level > 0
|
|
end
|
|
|
|
function Tilt.levelLabel(level)
|
|
return Tilt.ANGLE_LABELS[(level or Tilt.level) + 1] or "OFF"
|
|
end
|
|
|
|
-- Ease angle from `from` toward `goal` over TWEEN_TIME.
|
|
function Tilt.update(dt)
|
|
if Tilt.t < 1 then
|
|
Tilt.t = math.min(1, Tilt.t + dt / Tilt.TWEEN_TIME)
|
|
local e = ease(Tilt.t)
|
|
Tilt.angle = Tilt.from + (Tilt.goal - Tilt.from) * e
|
|
else
|
|
Tilt.angle = Tilt.goal
|
|
end
|
|
Tilt.TARGET_ANGLE = Tilt.goal
|
|
Tilt.enabled = Tilt.level > 0
|
|
end
|
|
|
|
-- true while tilt is on *or* still tweening -- i.e. whenever the renderer
|
|
-- must take the perspective path rather than the flat blit
|
|
function Tilt.active()
|
|
return Tilt.level > 0 or Tilt.angle > 0
|
|
end
|
|
|
|
function Tilt.gateOK(top, overworld)
|
|
return Zoom.gateOK(top, overworld)
|
|
end
|
|
|
|
function Tilt.groundPoint(cx, cy, vw, vh)
|
|
local a = Tilt.angle
|
|
if a <= 0 then return cx, cy, 1 end
|
|
local u = cx - vw * 0.5
|
|
local w = cy - vh * 0.5
|
|
local d = Tilt.FOCAL * vh
|
|
local scale = d / (d - w * math.sin(a))
|
|
local sx = vw * 0.5 + u * scale
|
|
local sy = vh * 0.5 + w * math.cos(a) * scale
|
|
return sx, sy, scale
|
|
end
|
|
|
|
-- Is a flat foot point on the ground quad at all?
|
|
--
|
|
-- The ground is the flat world canvas warped onto the perspective plane, so it
|
|
-- STOPS at that canvas. groundPoint has no such edge: it happily projects a
|
|
-- point far above the viewport, and perspective pulls it back down toward the
|
|
-- horizon, which is how an NPC two screens away ended up standing over the
|
|
-- border fill past where the map is drawn at all. Billboards ask this first
|
|
-- and skip anything the ground does not reach. `margin` is the sprite's own
|
|
-- size, so someone half off the edge still draws.
|
|
function Tilt.onGround(fx, fy, vw, vh, margin)
|
|
margin = margin or 0
|
|
return fx >= -margin and fx <= (vw or 0) + margin
|
|
and fy >= -margin and fy <= (vh or 0) + margin
|
|
end
|
|
|
|
function Tilt.viewGrowth()
|
|
local a = Tilt.angle
|
|
if a <= 0 then return 1 end
|
|
local topScale = 1 / (1 + 0.5 * math.sin(a) / Tilt.FOCAL)
|
|
local base = 1 / (math.cos(a) * topScale)
|
|
return base + Tilt.VIEW_MARGIN * (base - 1)
|
|
end
|
|
|
|
function Tilt.meshCorners(vw, vh)
|
|
local corners = {
|
|
{ 0, 0, 0, 0 },
|
|
{ vw, 0, 1, 0 },
|
|
{ vw, vh, 1, 1 },
|
|
{ 0, vh, 0, 1 },
|
|
}
|
|
local out = {}
|
|
for i, c in ipairs(corners) do
|
|
local sx, sy, scale = Tilt.groundPoint(c[1], c[2], vw, vh)
|
|
out[i] = { sx, sy, c[3], c[4], scale }
|
|
end
|
|
return out
|
|
end
|
|
|
|
return Tilt
|