mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 07:00:51 +02:00
add camera zoom controls in overworld
This commit is contained in:
+4
-1
@@ -242,7 +242,10 @@ end
|
||||
-- Whether both mons would be in plain view from the battle camera.
|
||||
function BattleArena.clearance(map, arena)
|
||||
local BattleCam = V.require("BattleCam")
|
||||
local ok, rig = pcall(BattleCam.rig, arena, 0)
|
||||
-- the CANONICAL shot: whether a fight fits somewhere is a fact about the
|
||||
-- ground, so it must not depend on the drift's phase or on where the
|
||||
-- player last swung the camera (see BattleCam.rig's third argument)
|
||||
local ok, rig = pcall(BattleCam.rig, arena, 0, true)
|
||||
if not (ok and rig and rig.eye) then return true end
|
||||
local eye = rig.eye
|
||||
local H = BattleArena.MON_H
|
||||
|
||||
+148
-5
@@ -124,6 +124,50 @@ BattleCam.PAN_PERIOD = 26 -- seconds for one there-and-back
|
||||
BattleCam.PAN_DOLLY = 0.02 -- how far the eye breathes, as a fraction
|
||||
BattleCam.DOLLY_PERIOD = 37
|
||||
|
||||
-- ------- the player's own orbit
|
||||
--
|
||||
-- The drift above is the shot breathing. THIS is the player steering it:
|
||||
-- a right stick, a drag across the screen or the mouse walks the eye
|
||||
-- around the arena's axis, and it stops at both ends.
|
||||
--
|
||||
-- 0 is the shot the rig was solved for and the LEFT stop, because there is
|
||||
-- nothing to the left of it -- the composition below is what the whole
|
||||
-- module exists to land, and past it the two mons start swapping sides.
|
||||
--
|
||||
-- 1 is SIDE-ON: the eye swung round until it is square to the arena's
|
||||
-- north-south axis, where the two mons stand at the same distance instead
|
||||
-- of one behind the other. That is as far as the picture stays a battle
|
||||
-- rather than a diorama with two Pokemon in it, and it is a different angle
|
||||
-- for each rig -- the tele lens starts 28 degrees off the axis and the wide
|
||||
-- one 45 -- so the stop is COMPUTED from the rig rather than written down,
|
||||
-- and retuning either moves its own stop with it.
|
||||
--
|
||||
-- The input is deliberately not 1:1 with the pixels: it accumulates into
|
||||
-- `orbitGoal` and the live angle eases after it, so a flick reads as the
|
||||
-- camera being pushed rather than as the camera being dragged.
|
||||
BattleCam.ORBIT_TIME = 0.22 -- seconds for the eye to catch its goal
|
||||
BattleCam.ORBIT_DRAG = 1.15 -- fraction of the range per screen width
|
||||
BattleCam.ORBIT_STICK = 0.9 -- fraction of the range per second, full tilt
|
||||
BattleCam.ORBIT_MOUSE = 0.0011 -- fraction of the range per mouse count
|
||||
BattleCam.STICK_DEAD = 0.2
|
||||
|
||||
-- ------- and the player's own zoom
|
||||
--
|
||||
-- How much world the frame holds, as a multiple of the rig's own frameH:
|
||||
-- BELOW one is zoomed in. It has to be the LENS rather than the distance,
|
||||
-- because the rig derives its field of view from frameH and the distance
|
||||
-- together -- so moving the eye alone changes the perspective and not the
|
||||
-- framing, which is exactly what the dolly breath above is for.
|
||||
BattleCam.ZOOM_MIN = 0.45 -- the pair filling the frame
|
||||
BattleCam.ZOOM_MAX = 2.0 -- the fight in its own landscape
|
||||
BattleCam.ZOOM_STEP = 1.15
|
||||
BattleCam.ZOOM_TIME = 0.18
|
||||
|
||||
BattleCam.orbit = 0
|
||||
BattleCam.orbitGoal = 0
|
||||
BattleCam.zoom = 1
|
||||
BattleCam.zoomGoal = 1
|
||||
|
||||
-- Hold the rig perfectly still (VR sets this while a session runs). The
|
||||
-- drift exists to give a FLAT screen the depth cue the picture cannot
|
||||
-- have; a headset gets real parallax from the player's own head, and a
|
||||
@@ -134,8 +178,79 @@ BattleCam.still = false
|
||||
|
||||
BattleCam.t = 0
|
||||
|
||||
-- Every fight opens on the shot the rig was solved for: the orbit and the
|
||||
-- zoom are a way of LOOKING at this battle, not a preference carried into
|
||||
-- the next one, and a player who left the camera side-on an hour ago should
|
||||
-- not have the next encounter open there.
|
||||
function BattleCam.reset()
|
||||
BattleCam.t = 0
|
||||
BattleCam.orbit, BattleCam.orbitGoal = 0, 0
|
||||
BattleCam.zoom, BattleCam.zoomGoal = 1, 1
|
||||
end
|
||||
|
||||
-- How far the eye may swing, in radians, before it is square to the arena's
|
||||
-- axis. The rig's own stance decides it: `side` and `back` are the offset
|
||||
-- it starts at, so the bearing it starts on is atan2(side, back) and what
|
||||
-- is left to a quarter turn is the room the player has.
|
||||
function BattleCam.orbitRange(arena)
|
||||
local R = BattleCam.rigFor(arena)
|
||||
return math.max(0, math.pi / 2 - math.atan2(R.side, R.back))
|
||||
end
|
||||
|
||||
-- ------- what the player's inputs reach
|
||||
--
|
||||
-- All four take a signed amount and clamp; positive is RIGHTWARD, toward
|
||||
-- the side-on stop. Returning whether the goal actually moved lets a
|
||||
-- caller tell "steered" from "already against the stop".
|
||||
|
||||
local function setOrbit(goal)
|
||||
local was = BattleCam.orbitGoal
|
||||
BattleCam.orbitGoal = math.max(0, math.min(1, goal))
|
||||
return BattleCam.orbitGoal ~= was
|
||||
end
|
||||
|
||||
-- A drag, in fractions of the screen's width.
|
||||
function BattleCam.dragOrbit(fraction)
|
||||
return setOrbit(BattleCam.orbitGoal + (fraction or 0) * BattleCam.ORBIT_DRAG)
|
||||
end
|
||||
|
||||
-- Relative mouse motion, in counts.
|
||||
function BattleCam.mouseOrbit(dx)
|
||||
return setOrbit(BattleCam.orbitGoal + (dx or 0) * BattleCam.ORBIT_MOUSE)
|
||||
end
|
||||
|
||||
-- A stick held for `dt` seconds, as a rate with a squared response -- the
|
||||
-- first half of the throw aims and the rest travels, the same curve the
|
||||
-- free-roam look uses.
|
||||
function BattleCam.stickOrbit(x, dt)
|
||||
local a = math.abs(x or 0)
|
||||
if a < BattleCam.STICK_DEAD then return false end
|
||||
a = (a - BattleCam.STICK_DEAD) / (1 - BattleCam.STICK_DEAD)
|
||||
local v = ((x < 0) and -1 or 1) * a * a
|
||||
return setOrbit(BattleCam.orbitGoal
|
||||
+ v * BattleCam.ORBIT_STICK * (dt or 0))
|
||||
end
|
||||
|
||||
-- The zoom, in notches (positive pulls OUT, like every other zoom here).
|
||||
function BattleCam.stepZoom(notches)
|
||||
local was = BattleCam.zoomGoal
|
||||
BattleCam.zoomGoal = math.max(BattleCam.ZOOM_MIN,
|
||||
math.min(BattleCam.ZOOM_MAX,
|
||||
was * (BattleCam.ZOOM_STEP ^ (notches or 0))))
|
||||
return BattleCam.zoomGoal ~= was
|
||||
end
|
||||
|
||||
-- How much world the frame holds right now: the rig's own reach at the
|
||||
-- player's zoom. The sun's box is fitted to this too, so a zoomed shot
|
||||
-- lights exactly the ground it shows.
|
||||
function BattleCam.frameH(arena)
|
||||
return BattleCam.rigFor(arena).frameH * BattleCam.zoom
|
||||
end
|
||||
|
||||
local function chase(now, goal, dt, time)
|
||||
if now == goal then return goal end
|
||||
local v = now + (goal - now) * math.min(1, (dt or 0) / time)
|
||||
return (math.abs(goal - v) < 1e-4) and goal or v
|
||||
end
|
||||
|
||||
-- Real frame time, like every other presentational tween in this mod: a
|
||||
@@ -146,6 +261,12 @@ function BattleCam.update(dt)
|
||||
-- float precision in the sines below
|
||||
local wrap = BattleCam.PAN_PERIOD * BattleCam.DOLLY_PERIOD
|
||||
if BattleCam.t > wrap then BattleCam.t = BattleCam.t - wrap end
|
||||
-- and the steered pair easing after whatever the player last asked for,
|
||||
-- which is what keeps a flick of the stick from being a cut
|
||||
BattleCam.orbit = chase(BattleCam.orbit, BattleCam.orbitGoal, dt,
|
||||
BattleCam.ORBIT_TIME)
|
||||
BattleCam.zoom = chase(BattleCam.zoom, BattleCam.zoomGoal, dt,
|
||||
BattleCam.ZOOM_TIME)
|
||||
end
|
||||
|
||||
local function phase(t, period)
|
||||
@@ -163,17 +284,32 @@ end
|
||||
--
|
||||
-- `groundY` is the height of the arena floor, so a fight staged on a ledge
|
||||
-- or a raised walkway is shot from above THAT rather than from inside it.
|
||||
function BattleCam.rig(arena, groundY)
|
||||
-- `canonical` asks for the shot the rig was SOLVED for -- no drift, no
|
||||
-- breath, no steer, no zoom -- from a caller that is reasoning about the
|
||||
-- arena rather than drawing it. BattleArena's clearance test is the one
|
||||
-- that needs it: whether a fight can be staged somewhere is a fact about
|
||||
-- the ground, and answering it through whatever angle the player happened
|
||||
-- to leave the last battle on would pick a different arena depending on
|
||||
-- where they had swung the camera an hour ago.
|
||||
function BattleCam.rig(arena, groundY, canonical)
|
||||
groundY = groundY or 0
|
||||
local R = BattleCam.rigFor(arena)
|
||||
local mx, mz = arena.mid[1], arena.mid[2]
|
||||
-- VR asks for the same stillness for its own reason (see BattleCam.still)
|
||||
local fixed = BattleCam.still or canonical
|
||||
|
||||
local yaw = BattleCam.still and 0
|
||||
or BattleCam.PAN_YAW * phase(BattleCam.t, BattleCam.PAN_PERIOD)
|
||||
-- The drift, plus wherever the player has steered to. The steer is
|
||||
-- NEGATIVE because the rotation below runs the other way from the bearing
|
||||
-- it turns: rotating (side, back) by +yaw carries the eye back toward the
|
||||
-- arena's own axis, and the room the player has is all on the far side of
|
||||
-- that -- out toward square-on. (orbitRange measures exactly that room.)
|
||||
local steer = fixed and 0 or -BattleCam.orbit * BattleCam.orbitRange(arena)
|
||||
local yaw = steer + (fixed and 0
|
||||
or BattleCam.PAN_YAW * phase(BattleCam.t, BattleCam.PAN_PERIOD))
|
||||
local c, s = math.cos(yaw), math.sin(yaw)
|
||||
-- the breath scales the whole offset, height included, so the eye moves
|
||||
-- along its own line to the arena and the pitch of the shot never changes
|
||||
local k = BattleCam.still and 1
|
||||
local k = fixed and 1
|
||||
or 1 + BattleCam.PAN_DOLLY
|
||||
* phase(BattleCam.t, BattleCam.DOLLY_PERIOD)
|
||||
local dx = (R.side * c - R.back * s) * k
|
||||
@@ -188,10 +324,17 @@ function BattleCam.rig(arena, groundY)
|
||||
local dist = math.max(1, math.sqrt(ex * ex + ey * ey + ez * ez))
|
||||
local horiz = math.sqrt(ex * ex + ez * ez)
|
||||
|
||||
-- The lens carries the player's zoom: how much world the frame holds is
|
||||
-- the one thing that actually changes the framing here, because the field
|
||||
-- of view is DERIVED from that reach and the distance. Moving the eye
|
||||
-- instead would leave the picture the same size and only change its
|
||||
-- perspective -- which is what the dolly breath above is deliberately
|
||||
-- for, and is not what "zoom" means to anyone holding a wheel.
|
||||
local frameH = fixed and R.frameH or BattleCam.frameH(arena)
|
||||
return {
|
||||
eye = eye,
|
||||
focus = focus,
|
||||
fov = 2 * math.atan((R.frameH / 2) / dist),
|
||||
fov = 2 * math.atan((frameH / 2) / dist),
|
||||
-- the world curve is a free-roam flourish that bends the horizon away
|
||||
-- from the player; a fixed camera on a staged shot has no player to bend
|
||||
-- around, and the bend would tip the arena floor out from under the mons
|
||||
|
||||
+4
-1
@@ -472,7 +472,10 @@ function BattleScene.render(state, arena, textures, token)
|
||||
local cx, cy = arena.mid[1], arena.mid[2]
|
||||
-- the world extents the sun frustum is fitted to; the camera itself is
|
||||
-- framed by cam.fov, so these only have to describe the ground in shot
|
||||
local vh = BattleCam.rigFor(arena).frameH * ph / (BattleScene.GB_H * s)
|
||||
-- the player's zoom is part of this: the sun's box is fitted to what the
|
||||
-- frame holds, so a shot pulled wide has to light the ground it just
|
||||
-- brought into view rather than the ground the rig alone would have
|
||||
local vh = BattleCam.frameH(arena) * ph / (BattleScene.GB_H * s)
|
||||
local vw = vh * pw / ph
|
||||
|
||||
-- the cards need the camera's eye to face it, so the rig has to be live
|
||||
|
||||
+39
-5
@@ -149,11 +149,11 @@ function FirstPerson.engaged()
|
||||
return Voxel.isFreeCam(Voxel.level) and Voxel3D.available()
|
||||
end
|
||||
|
||||
-- Whether first person should be READING the player's inputs right now:
|
||||
-- engaged, with the overworld on top of the stack (a menu, a dialog or a
|
||||
-- battle above it owns the buttons, exactly as it does for grid walking).
|
||||
function FirstPerson.driving()
|
||||
if not FirstPerson.engaged() then return false end
|
||||
-- Whether the overworld is what the player is looking at: nothing pushed
|
||||
-- over it, so the buttons are free-roam's. Shared with everything else in
|
||||
-- the mod that asks the same question of the same stack (CamControl's
|
||||
-- zooms above all), rather than each restating the pcall.
|
||||
function FirstPerson.onTop()
|
||||
local ok, top, ow = pcall(function()
|
||||
local Game = require("src.core.Game")
|
||||
return Game.stack and Game.stack:top(), Game.overworld
|
||||
@@ -161,6 +161,40 @@ function FirstPerson.driving()
|
||||
return ok and top ~= nil and top == ow
|
||||
end
|
||||
|
||||
-- Whether first person should be READING the player's inputs right now:
|
||||
-- engaged, with the overworld on top of the stack (a menu, a dialog or a
|
||||
-- battle above it owns the buttons, exactly as it does for grid walking).
|
||||
function FirstPerson.driving()
|
||||
return FirstPerson.engaged() and FirstPerson.onTop()
|
||||
end
|
||||
|
||||
-- The right stick's live X, for a camera that is not this one: while a
|
||||
-- battle is staged the free-roam look is not driving, but the axes are
|
||||
-- still arriving on the wrap below (which records whatever the rung), and
|
||||
-- the battle's orbit wants them. Reading them here rather than wrapping
|
||||
-- the same seam twice.
|
||||
function FirstPerson.stickX()
|
||||
return stick.x or 0
|
||||
end
|
||||
|
||||
-- ------- lending the look finger out
|
||||
--
|
||||
-- A pinch needs both fingers on the screen, and one of them is very likely
|
||||
-- the finger this module claimed as the look drag. Rather than have the
|
||||
-- pinch fight for it, CamControl asks for it: dropLook while the gesture
|
||||
-- runs, reseatLook on whichever finger survives it. Re-seating rather than
|
||||
-- simply releasing is what stops the view snapping by however far the
|
||||
-- pinch travelled -- the finger carries on as the look drag from where it
|
||||
-- now is, which is what it looks like it should do.
|
||||
function FirstPerson.dropLook()
|
||||
lookTouch = nil
|
||||
end
|
||||
|
||||
function FirstPerson.reseatLook(id, x, y)
|
||||
if id == nil then lookTouch = nil return end
|
||||
lookTouch = { id = id, x = x, y = y }
|
||||
end
|
||||
|
||||
-- The eased blend, 0 at the orbit and 1 in the head.
|
||||
function FirstPerson.blendEased()
|
||||
return ease(FirstPerson.blend)
|
||||
|
||||
+53
-3
@@ -249,6 +249,31 @@ OverworldBattle.TEXT_RECT = {
|
||||
mimic = { 0, 56, 128, 40 },
|
||||
}
|
||||
|
||||
-- How far apart the two anchors are: the spacing every move animation was
|
||||
-- authored against, and so the yardstick the live pair is measured with.
|
||||
OverworldBattle.ANCHOR_SPAN = math.sqrt(
|
||||
(OverworldBattle.ANCHOR.enemy[1] - OverworldBattle.ANCHOR.player[1]) ^ 2
|
||||
+ (OverworldBattle.ANCHOR.enemy[2] - OverworldBattle.ANCHOR.player[2]) ^ 2)
|
||||
|
||||
-- The effects layer's scale for this shot: how far apart the two mons
|
||||
-- actually are on screen, over how far apart the slots they were authored
|
||||
-- for were. Clamped hard at both ends -- an effect is pixel art and a wild
|
||||
-- factor is worse than a slightly wrong one -- and held at exactly 1 when
|
||||
-- the marks coincide, which is a projection about to degenerate rather
|
||||
-- than a pair that has genuinely closed up.
|
||||
OverworldBattle.ANIM_SCALE_MIN = 0.5
|
||||
OverworldBattle.ANIM_SCALE_MAX = 2.0
|
||||
|
||||
function OverworldBattle.animScale(shot, px, py)
|
||||
if not (shot and shot.enemy and px and py) then return 1 end
|
||||
local dx, dy = shot.enemy[1] - px, shot.enemy[2] - py
|
||||
local span = math.sqrt(dx * dx + dy * dy)
|
||||
if not (span > 1) then return 1 end
|
||||
local k = span / OverworldBattle.ANCHOR_SPAN
|
||||
return math.max(OverworldBattle.ANIM_SCALE_MIN,
|
||||
math.min(OverworldBattle.ANIM_SCALE_MAX, k))
|
||||
end
|
||||
|
||||
function OverworldBattle.textRects(battle)
|
||||
if not battle or battle.blankForAskName then return {} end
|
||||
local r = OverworldBattle.TEXT_RECT
|
||||
@@ -478,6 +503,11 @@ function OverworldBattle.update(dt)
|
||||
return
|
||||
end
|
||||
|
||||
-- the right stick, read as a rate before the rig is built from it: the
|
||||
-- wheel, the keys, the mouse and a drag all arrive as events and have
|
||||
-- already landed, but a stick is a HELD position and only a tick can
|
||||
-- turn it into travel (CamControl, which owns every one of those inputs)
|
||||
pcall(V.require("CamControl").tick, dt)
|
||||
BattleCam.update(dt)
|
||||
-- the battle only exists once it has been pushed; a session opened at
|
||||
-- pushBattle time has it, one opened from battle.started was handed it
|
||||
@@ -1120,16 +1150,36 @@ function OverworldBattle.install()
|
||||
-- give them. They ride to where the PAIR went: the midpoint of the two
|
||||
-- mons' projected positions, less the midpoint of the slots they used to
|
||||
-- sit in. A hit still lands on the mon it is aimed at.
|
||||
--
|
||||
-- And they ride the pair's SEPARATION as well, because the mons
|
||||
-- themselves do. Both are geometry standing on the map, so the camera
|
||||
-- sizes them: zoom in and they grow, swing round to side-on and the two
|
||||
-- marks close up as the axis foreshortens. A layer that only slid would
|
||||
-- have held the authored 106-pixel spacing through all of it -- a beam
|
||||
-- fired between two mons that are no longer that far apart, ending in
|
||||
-- the air beside the one it was aimed at. Scaling about the same
|
||||
-- midpoint keeps every authored offset the same fraction of the gap it
|
||||
-- was authored as.
|
||||
local a = OverworldBattle.ANCHOR
|
||||
-- BACK SPRITES leaves the player's mon exactly where the GB put it, so that side
|
||||
-- contributes no movement at all and the pair's centre has gone half as
|
||||
-- far as the foe's mark did.
|
||||
local px, py = shot.player[1], shot.player[2]
|
||||
if OverworldBattle.backPinned() then px, py = a.player[1], a.player[2] end
|
||||
local dx = (shot.enemy[1] + px) / 2 - (a.enemy[1] + a.player[1]) / 2
|
||||
local dy = (shot.enemy[2] + py) / 2 - (a.enemy[2] + a.player[2]) / 2
|
||||
local cx, cy = (shot.enemy[1] + px) / 2, (shot.enemy[2] + py) / 2
|
||||
local ax = (a.enemy[1] + a.player[1]) / 2
|
||||
local ay = (a.enemy[2] + a.player[2]) / 2
|
||||
love.graphics.push()
|
||||
love.graphics.translate(math.floor(dx + 0.5), math.floor(dy + 0.5))
|
||||
love.graphics.translate(cx - ax, cy - ay)
|
||||
-- Clamped, and skipped outright if the marks ever coincide: a
|
||||
-- degenerate projection must leave the effects the size they were
|
||||
-- rather than collapse them to nothing or blow them across the screen.
|
||||
local k = OverworldBattle.animScale(shot, px, py)
|
||||
if k ~= 1 then
|
||||
love.graphics.translate(ax, ay)
|
||||
love.graphics.scale(k, k)
|
||||
love.graphics.translate(-ax, -ay)
|
||||
end
|
||||
local ok, err = pcall(innerAnim, self, colorized)
|
||||
love.graphics.pop()
|
||||
if not ok then error(err, 0) end
|
||||
|
||||
+61
-3
@@ -67,6 +67,43 @@ ThirdPerson.SHOULDER = 4
|
||||
-- dive so stepping 75 -> 1ST -> 3RD reads as one continuous camera
|
||||
ThirdPerson.BOOM_TIME = 0.35
|
||||
|
||||
-- ------- the player's own zoom
|
||||
--
|
||||
-- A multiplier on BOOM, stepped by the wheel, Q/E or a pinch (see
|
||||
-- CamControl, which owns every one of those and decides which camera a
|
||||
-- given input is aimed at). The range is deliberately wider IN than OUT:
|
||||
-- close is the shot people reach for, and far enough out the character is
|
||||
-- a few pixels and the rung may as well be an orbit rung.
|
||||
--
|
||||
-- Stepped in fractions rather than world pixels so a notch feels the same
|
||||
-- at both ends -- the near end of a linear step would crawl and the far
|
||||
-- end would leap.
|
||||
ThirdPerson.ZOOM_MIN = 0.45 -- ~22px: over the shoulder, close
|
||||
ThirdPerson.ZOOM_MAX = 2.4 -- ~115px: the character in a landscape
|
||||
ThirdPerson.ZOOM_STEP = 1.18 -- one wheel notch / key press
|
||||
ThirdPerson.ZOOM_TIME = 0.18 -- how fast the eye eases to a new one
|
||||
|
||||
ThirdPerson.zoom = 1 -- eased, what place() actually uses
|
||||
ThirdPerson.zoomGoal = 1 -- what the input asked for
|
||||
|
||||
-- Step the zoom by `notches` (positive pulls the camera OUT). Returns true
|
||||
-- when the goal actually moved, so a caller can tell "zoomed" from "already
|
||||
-- at the stop" and let the input fall through.
|
||||
function ThirdPerson.stepZoom(notches)
|
||||
local was = ThirdPerson.zoomGoal
|
||||
local goal = was * (ThirdPerson.ZOOM_STEP ^ (notches or 0))
|
||||
ThirdPerson.zoomGoal = math.max(ThirdPerson.ZOOM_MIN,
|
||||
math.min(ThirdPerson.ZOOM_MAX, goal))
|
||||
return ThirdPerson.zoomGoal ~= was
|
||||
end
|
||||
|
||||
-- Scale the zoom by a continuous factor -- what a pinch hands over, where
|
||||
-- the gesture's own scale IS the answer and there are no notches.
|
||||
function ThirdPerson.scaleZoom(factor)
|
||||
if not (factor and factor > 0) then return false end
|
||||
return ThirdPerson.stepZoom(math.log(factor) / math.log(ThirdPerson.ZOOM_STEP))
|
||||
end
|
||||
|
||||
-- ------- the collision's numbers
|
||||
--
|
||||
-- STEP is how far apart the samples along the boom line are, in world
|
||||
@@ -263,10 +300,20 @@ end
|
||||
-- target rather than easing, so picking 3RD from an orbit rung is one
|
||||
-- motion (the dive) rather than two (a dive, then a slide backwards).
|
||||
function ThirdPerson.update(dt, blend)
|
||||
-- the player's own zoom FIRST, so everything below measures itself
|
||||
-- against the boom length this frame actually wants. A step is a request
|
||||
-- rather than a jump: three notches of wheel should read as one glide.
|
||||
local zg = ThirdPerson.zoomGoal
|
||||
if ThirdPerson.zoom ~= zg then
|
||||
local k = math.min(1, dt / ThirdPerson.ZOOM_TIME)
|
||||
local z = ThirdPerson.zoom + (zg - ThirdPerson.zoom) * k
|
||||
ThirdPerson.zoom = (math.abs(zg - z) < 1e-4) and zg or z
|
||||
end
|
||||
|
||||
local target = ThirdPerson.selected() and 1 or 0
|
||||
if (blend or 0) <= 0 then
|
||||
ThirdPerson.out = target
|
||||
ThirdPerson.len = ThirdPerson.BOOM * target
|
||||
ThirdPerson.len = ThirdPerson.reachFor() * target
|
||||
-- and the wanted length with it: place() is what normally maintains it
|
||||
-- and it does not run at all while the rig is out of the frame, so a
|
||||
-- stale want left here would have the recovery below creeping the boom
|
||||
@@ -290,6 +337,13 @@ function ThirdPerson.update(dt, blend)
|
||||
end
|
||||
end
|
||||
|
||||
-- The boom's full length right now, before the world has its say: BOOM at
|
||||
-- the player's own zoom. Named so the collision march and the shoulder
|
||||
-- fade measure themselves against the same number.
|
||||
function ThirdPerson.reachFor()
|
||||
return ThirdPerson.BOOM * ThirdPerson.zoom
|
||||
end
|
||||
|
||||
-- ------- the eye
|
||||
--
|
||||
-- Where the camera stands, given the pivot the first-person rig would have
|
||||
@@ -310,7 +364,7 @@ function ThirdPerson.place(pivot, lx, ly, lz, focus)
|
||||
local up = ThirdPerson.PIVOT_LIFT * e
|
||||
local orbit = { pivot[1], pivot[2] + up, pivot[3] }
|
||||
|
||||
local want = ThirdPerson.BOOM * e
|
||||
local want = ThirdPerson.reachFor() * e
|
||||
ThirdPerson.want = want
|
||||
local room = ThirdPerson.reach(overworld(), orbit, -lx, -ly, -lz, want)
|
||||
-- in instantly, out only as fast as update() allows
|
||||
@@ -322,10 +376,14 @@ function ThirdPerson.place(pivot, lx, ly, lz, focus)
|
||||
-- distance. Right of the look, flat: cross(look, worldUp) normalized,
|
||||
-- which for a look of (sin y, *, cos y) is (-cos y, 0, sin y) -- the same
|
||||
-- right hand FirstPerson.moveWorld strafes along.
|
||||
-- The rail rides the ZOOM as well, so it stays the same fraction of the
|
||||
-- frame at every distance: a fixed four pixels would swamp the close shot
|
||||
-- and vanish from the wide one.
|
||||
local flat = math.sqrt(lx * lx + lz * lz)
|
||||
local sx, sz = 0, 0
|
||||
if flat > 1e-6 then
|
||||
local s = ThirdPerson.SHOULDER * e * (len / math.max(want, 1e-6))
|
||||
local s = ThirdPerson.SHOULDER * ThirdPerson.zoom * e
|
||||
* (len / math.max(want, 1e-6))
|
||||
sx, sz = -lz / flat * s, lx / flat * s
|
||||
end
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ local Water = V.require("Water")
|
||||
local AntiAlias = V.require("AntiAlias")
|
||||
local FirstPerson = V.require("FirstPerson")
|
||||
local FreeMove = V.require("FreeMove")
|
||||
local CamControl = V.require("CamControl")
|
||||
local VR = V.require("VR")
|
||||
|
||||
-- Forward declaration: the voxel pipeline's update hook (registered below)
|
||||
@@ -538,6 +539,17 @@ do
|
||||
function Game:keypressed(key)
|
||||
local claim = HOTKEYS[key]
|
||||
local top = self.stack and self.stack:top()
|
||||
-- Q and E work whichever camera is in front of the player -- the
|
||||
-- battle's lens, the third-person boom, or the engine's own survey
|
||||
-- zoom on an orbit rung. CamControl answers which, and answers "none"
|
||||
-- for 1ST and for every screen with no camera of ours behind it, in
|
||||
-- which case the key falls through untouched. Ahead of the hotkey
|
||||
-- table because unlike those it is NOT free-roam only: a staged battle
|
||||
-- is exactly where the zoom is most wanted.
|
||||
if (key == "q" or key == "e")
|
||||
and not (top and top.onKeyPressed) then
|
||||
if CamControl.zoomBy(key == "q" and 1 or -1) then return end
|
||||
end
|
||||
-- A screen with its own key handler gets the key first, exactly as the
|
||||
-- engine's first branch does: typing a nickname must not toggle a
|
||||
-- render mode. Only free-roam presses are ours to take.
|
||||
@@ -889,6 +901,17 @@ OverworldBattle.install()
|
||||
FirstPerson.install()
|
||||
FreeMove.install()
|
||||
|
||||
-- ------- the zooms, and the battle camera the player can steer
|
||||
--
|
||||
-- CamControl claims the wheel, Q/E, the mouse and the touch screen for
|
||||
-- whichever camera is actually in front of the player -- the staged
|
||||
-- battle's, the third-person boom, or the engine's own survey zoom -- and
|
||||
-- forwards everything else. Installed AFTER the two above deliberately: a
|
||||
-- wrap installed later is the OUTER one, so a fight gets first refusal on
|
||||
-- the mouse and the fingers, which is right, because while one is staged
|
||||
-- the free-roam look is not driving.
|
||||
CamControl.install()
|
||||
|
||||
-- ------- SELECT walks the angle ladder
|
||||
--
|
||||
-- The same step the "3" key makes, on the pad's own button: a phone (and
|
||||
|
||||
Reference in New Issue
Block a user