mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 09:10:49 +02:00
add third person mode
This commit is contained in:
@@ -49,14 +49,16 @@ T.eq(defs._owners and defs._owners.voxel, "DRAMATIC_SHAPE",
|
||||
|
||||
-- ------- the ladders the engine drives
|
||||
|
||||
T.eq(#defs.voxel.levels, 7, "voxel exposes a seven-rung ladder")
|
||||
T.eq(#defs.voxel.levels, 8, "voxel exposes an eight-rung ladder")
|
||||
T.eq(defs.voxel.levels[1], "OFF", "rung 0 is OFF")
|
||||
T.eq(defs.voxel.levels[2], "FULL",
|
||||
"FULL is the first rung after OFF -- the order those two get used in")
|
||||
T.eq(defs.voxel.levels[6], "75", "rung 5 is the 75-degree camera")
|
||||
T.eq(defs.voxel.levels[7], "1ST (EXPERIMENTAL)",
|
||||
"the top rung is the first-person camera, labelled as the experiment it is")
|
||||
T.eq(Pipelines.maxLevel("voxel"), 6, "the engine reads the ladder height")
|
||||
"rung 6 is the first-person camera, labelled as the experiment it is")
|
||||
T.eq(defs.voxel.levels[8], "3RD (EXPERIMENTAL)",
|
||||
"and the top rung is the third-person one, labelled the same way")
|
||||
T.eq(Pipelines.maxLevel("voxel"), 7, "the engine reads the ladder height")
|
||||
T.eq(Pipelines.levelLabel("voxel", 3), "35", "the engine reads the rung labels")
|
||||
|
||||
-- ------- gating: inert until switched on, and inert without a GPU
|
||||
@@ -1084,12 +1086,14 @@ local Curve = run.loader.exports.DRAMATIC_SHAPE.lib.require("WorldCurve")
|
||||
-- with nothing on screen saying a keypress had done it.
|
||||
Pipelines.setLevel("voxel", 0)
|
||||
local walk = {}
|
||||
for _ = 1, 7 do
|
||||
for _ = 1, 8 do
|
||||
Game.keypressed(keyGame, "3")
|
||||
walk[#walk + 1] = Pipelines.levelLabel("voxel")
|
||||
end
|
||||
T.eq(table.concat(walk, ","), "15,35,50,75,1ST (EXPERIMENTAL),OFF,15",
|
||||
"3 walks OFF -> 15 -> 35 -> 50 -> 75 -> 1ST and wraps, never touching FULL")
|
||||
T.eq(table.concat(walk, ","),
|
||||
"15,35,50,75,1ST (EXPERIMENTAL),3RD (EXPERIMENTAL),OFF,15",
|
||||
"3 walks OFF -> 15 -> 35 -> 50 -> 75 -> 1ST -> 3RD and wraps, never "
|
||||
.. "touching FULL")
|
||||
|
||||
-- FULL is 35 degrees, so a press from it goes ON to 50 rather than back to
|
||||
-- the rung that shows the same camera -- the key never appears to do nothing.
|
||||
@@ -1176,8 +1180,8 @@ T.eq(GBCFX.level, 0, "and on the live renderer")
|
||||
-- TILT with or without us. Park the ladder on its top rung and turn both
|
||||
-- back on, so the single press under test is the one that wraps to OFF --
|
||||
-- where nothing else is going to clear them.
|
||||
-- 6 is the "1ST" rung, the last one the key walks before it wraps to OFF
|
||||
Pipelines.setLevel("voxel", 6)
|
||||
-- 3RD is the last rung the key walks before it wraps to OFF
|
||||
Pipelines.setLevel("voxel", VoxelState.TP_LEVEL)
|
||||
Tilt.setLevel(3)
|
||||
GBCFX.setLevel(4)
|
||||
keyGame.save.options.tilt = 3
|
||||
@@ -3674,6 +3678,213 @@ FirstPerson.blend = 0
|
||||
VoxelState.reset()
|
||||
end
|
||||
|
||||
-- ------- the third-person rung
|
||||
--
|
||||
-- 3RD is 1ST with the eye on a boom, so what the suite has to hold still is
|
||||
-- the boom: where it stands the eye behind a pivot, the march through the
|
||||
-- world that shortens it when something is in the way, and the two things
|
||||
-- its extension decides that 1ST decides the other way -- the player's own
|
||||
-- card being drawn, and the body turning to face where it walks.
|
||||
|
||||
do
|
||||
local FirstPerson =
|
||||
run.loader.exports.DRAMATIC_SHAPE.lib.require("FirstPerson")
|
||||
local ThirdPerson =
|
||||
run.loader.exports.DRAMATIC_SHAPE.lib.require("ThirdPerson")
|
||||
local VoxelState = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelState")
|
||||
local Voxel3D = run.loader.exports.DRAMATIC_SHAPE.lib.require("Voxel3D")
|
||||
|
||||
T.eq(VoxelState.TP_LEVEL, 7, "3RD is the eighth rung")
|
||||
T.check(VoxelState.isThirdPerson(7), "and isThirdPerson answers for it")
|
||||
T.check(not VoxelState.isThirdPerson(6), "but not for 1ST")
|
||||
T.check(VoxelState.isFreeCam(6) and VoxelState.isFreeCam(7),
|
||||
"both rungs that stand the camera with the player answer isFreeCam")
|
||||
T.check(not VoxelState.isFreeCam(5), "the 75-degree orbit does not")
|
||||
T.eq(VoxelState.ANGLE_LABELS[VoxelState.TP_LEVEL + 1], "3RD (EXPERIMENTAL)",
|
||||
"the rung wears the experimental label")
|
||||
T.eq(VoxelState.ANGLES_DEG[VoxelState.TP_LEVEL + 1], 75,
|
||||
"and hands the blend the same 75-degree orbit 1ST does")
|
||||
T.eq(VoxelState.HOTKEY_ORDER[#VoxelState.HOTKEY_ORDER], VoxelState.TP_LEVEL,
|
||||
"the 3 key walks onto it, and off it back to OFF")
|
||||
|
||||
-- the boom's own tween: picked from an orbit rung (blend fully out) the
|
||||
-- extension SNAPS, so the dive into the world is one motion rather than a
|
||||
-- dive followed by a slide; picked from inside the head it eases
|
||||
VoxelState.setLevel(VoxelState.TP_LEVEL)
|
||||
ThirdPerson.out, ThirdPerson.len = 0, 0
|
||||
ThirdPerson.update(1 / 60, 0)
|
||||
T.eq(ThirdPerson.out, 1, "picked from the diorama, the boom starts extended")
|
||||
ThirdPerson.out, ThirdPerson.len = 0, 0
|
||||
ThirdPerson.update(1 / 60, 1)
|
||||
T.check(ThirdPerson.out > 0 and ThirdPerson.out < 1,
|
||||
"picked from inside the head, it slides out over the boom's own time")
|
||||
|
||||
-- ------- the world the march asks
|
||||
--
|
||||
-- A stub map that refuses everything from x = 4 rightward, with no tileset
|
||||
-- for the height field to read -- so the ground answers 0 (the lookup is
|
||||
-- guarded for exactly this) and only the walkability speaks.
|
||||
local wall = { map = {
|
||||
inBounds = function(_, cx, cy) return cx >= 0 and cy >= 0 end,
|
||||
isWalkableCell = function(_, cx) return cx < 4 end,
|
||||
cellTile = function() return 0 end,
|
||||
} }
|
||||
|
||||
T.check(ThirdPerson._occupied(wall, 70, 10, 8),
|
||||
"an unwalkable cell refuses the eye at head height")
|
||||
T.check(not ThirdPerson._occupied(wall, 70, 30, 8),
|
||||
"but not one the eye stands well above -- a fence is not a wall")
|
||||
T.check(ThirdPerson._occupied(wall, 70, 2, 8),
|
||||
"and the ground refuses it from below")
|
||||
T.check(ThirdPerson._occupied(wall, -20, 30, 8),
|
||||
"off the world entirely there is nowhere to stand: the border ring")
|
||||
|
||||
-- the march itself: the wall's face is at x = 64, the pivot at x = 40, so
|
||||
-- the eye may travel 24 east of it less the clearance pad -- the FACE's own
|
||||
-- position rather than the four-pixel sampling grid's
|
||||
local room = ThirdPerson.reach(wall, { 40, 10, 8 }, 1, 0, 0, ThirdPerson.BOOM)
|
||||
T.check(math.abs(room - (24 - ThirdPerson.PAD)) < 0.5,
|
||||
"the boom stops a pad short of the face that blocked it")
|
||||
T.eq(ThirdPerson.reach(wall, { 40, 30, 8 }, 1, 0, 0, ThirdPerson.BOOM),
|
||||
ThirdPerson.BOOM, "with nothing tall enough in the way it runs out full")
|
||||
T.eq(ThirdPerson.reach(nil, { 40, 10, 8 }, 1, 0, 0, ThirdPerson.BOOM),
|
||||
ThirdPerson.BOOM, "and with no world to ask at all -- headless, mid-warp")
|
||||
|
||||
-- ------- the eye
|
||||
--
|
||||
-- place() is pure arithmetic over the pivot and the look, given a world
|
||||
-- with nothing in it: the suite lends the overworld away for the length of
|
||||
-- the check so the march has nothing to shorten against.
|
||||
local Game = require("src.core.Game")
|
||||
local hadOw = Game.overworld
|
||||
Game.overworld = nil
|
||||
|
||||
ThirdPerson.out, ThirdPerson.len = 1, ThirdPerson.BOOM
|
||||
local eye, aim = ThirdPerson.place({ 100, 20, 200 }, 0, 0, 1,
|
||||
{ 100, 20, 224 })
|
||||
T.check(math.abs(eye[3] - (200 - ThirdPerson.BOOM)) < 1e-6,
|
||||
"looking south, the eye stands a full boom north of the pivot")
|
||||
T.eq(eye[2], 20 + ThirdPerson.PIVOT_LIFT,
|
||||
"raised to the orbit point above the head")
|
||||
T.check(math.abs(eye[1] - (100 - ThirdPerson.SHOULDER)) < 1e-6,
|
||||
"and slid along the rail to the camera's own right -- which is west "
|
||||
.. "looking south, and puts the player left of centre")
|
||||
T.check(math.abs(aim[1] - eye[1]) < 1e-6
|
||||
and math.abs(aim[3] - eye[3] - (ThirdPerson.BOOM + 24)) < 1e-6,
|
||||
"the focus slides with it, so the rail moves the frame and not the look")
|
||||
|
||||
ThirdPerson.out, ThirdPerson.len = 0, 0
|
||||
local head = { 100, 20, 200 }
|
||||
local focus = { 100, 20, 224 }
|
||||
T.eq(ThirdPerson.place(head, 0, 0, 1, focus), head,
|
||||
"with the boom fully in, the eye IS the head -- 1ST to the pixel")
|
||||
|
||||
-- ------- what the extension decides
|
||||
--
|
||||
-- The rig is built through FirstPerson exactly as 1ST's is; the boom moves
|
||||
-- the eye, and everything keyed to where the eye stands follows it.
|
||||
ThirdPerson.out, ThirdPerson.len = 1, ThirdPerson.BOOM
|
||||
FirstPerson.yaw, FirstPerson.pitch = 0, 0
|
||||
FirstPerson.blend = 1
|
||||
FirstPerson.frame({ px = 100, py = 200, gh = 0, lift = 0 }, 500, 600, 320, 288)
|
||||
T.check(FirstPerson.cardBlend() == 1,
|
||||
"the boomed rig turns the cards to face it, exactly as the head does")
|
||||
T.check(not FirstPerson.hidePlayer(),
|
||||
"but the player's own card is DRAWN -- it is what the camera is watching")
|
||||
T.eq(FirstPerson.apparentFacing("down", 108, 208), "up",
|
||||
"and it shows the camera behind it its back")
|
||||
|
||||
-- and a boom a wall has squeezed back into the head takes the card out
|
||||
-- again: at that range it is the first-person problem word for word
|
||||
ThirdPerson.len = ThirdPerson.SHOW_AT - 1
|
||||
T.check(FirstPerson.hidePlayer(),
|
||||
"backed into a fence, the collapsed boom stops drawing the card")
|
||||
ThirdPerson.len = ThirdPerson.SHOW_AT
|
||||
T.check(not FirstPerson.hidePlayer(), "and draws it again the moment it clears")
|
||||
ThirdPerson.len = ThirdPerson.BOOM
|
||||
|
||||
T.eq(FirstPerson.bodyFacing(1, 0), "right",
|
||||
"a body walking east turns east, whichever way the camera is pointed")
|
||||
T.eq(FirstPerson.bodyFacing(0, -1), "up", "and north walking north")
|
||||
T.eq(FirstPerson.bodyFacing(0, 0), FirstPerson.compassFacing(),
|
||||
"standing still it comes back round to the camera's bearing, which is "
|
||||
.. "the one A talks along")
|
||||
|
||||
-- ------- the spin-flicker guard
|
||||
--
|
||||
-- The player's card is the one whose body the camera is derived FROM: the
|
||||
-- body is pointed along the camera's own yaw, so the angle between them is
|
||||
-- a flat 180 degrees and the card should show its back and nothing else,
|
||||
-- at every bearing.
|
||||
--
|
||||
-- Quantise the body to a compass point first and that stops being true.
|
||||
-- The shoulder rail stands the eye a few degrees off the exact rear axis,
|
||||
-- and the round trip through four directions has no margin to spare for
|
||||
-- it: in a band just short of each 45-degree boundary the pair measures as
|
||||
-- 135 degrees and picks the mirrored PROFILE frame. Standing perfectly
|
||||
-- still. Spin the camera and you sweep four of those bands a revolution --
|
||||
-- the character flicking sideways for a split second, which is the bug
|
||||
-- this pair of checks exists to hold shut.
|
||||
--
|
||||
-- 44 degrees is inside the first band. No lag anywhere: the body is
|
||||
-- pointed exactly where the camera looks, which is what standing still IS.
|
||||
FirstPerson.yaw, FirstPerson.pitch = math.rad(44), 0
|
||||
FirstPerson.frame({ px = 100, py = 200, gh = 0, lift = 0 }, 500, 600, 320, 288)
|
||||
FirstPerson.bodyYaw = FirstPerson.yaw
|
||||
T.eq(FirstPerson.apparentFacing("down", 108, 208), "right",
|
||||
"measured off the compass point, a standing body picks the profile -- "
|
||||
.. "the flicker, reproduced with the camera perfectly still")
|
||||
T.eq(FirstPerson.playerFacing("down", 108, 208), "up",
|
||||
"measured off the body's own bearing, the card keeps its back turned")
|
||||
|
||||
-- and the whole revolution, which is the assertion that actually matters:
|
||||
-- there is no bearing at all where a standing body shows anything but its
|
||||
-- back
|
||||
;(function()
|
||||
local wrong = {}
|
||||
for deg = 0, 359 do
|
||||
FirstPerson.yaw = math.rad(deg)
|
||||
FirstPerson.frame({ px = 100, py = 200, gh = 0, lift = 0 },
|
||||
500, 600, 320, 288)
|
||||
FirstPerson.bodyYaw = FirstPerson.yaw
|
||||
if FirstPerson.playerFacing(FirstPerson.compassFacing(), 108, 208)
|
||||
~= "up" then
|
||||
wrong[#wrong + 1] = deg
|
||||
end
|
||||
end
|
||||
T.eq(#wrong, 0,
|
||||
"a standing body shows its back at every one of 360 bearings (bad: "
|
||||
.. table.concat(wrong, ",") .. ")")
|
||||
end)()
|
||||
|
||||
-- and with nothing holding a bearing -- a scripted walk, a cutscene, the
|
||||
-- grid walk -- the player falls back to the four-direction answer with
|
||||
-- everybody else
|
||||
FirstPerson.releaseBody()
|
||||
T.eq(FirstPerson.bodyYaw, nil, "releasing the body drops the bearing")
|
||||
T.eq(FirstPerson.playerFacing("down", 108, 208),
|
||||
FirstPerson.apparentFacing("down", 108, 208),
|
||||
"and the card reads exactly as an NPC's would")
|
||||
T.eq(FirstPerson.pointBody(0, 0), FirstPerson.compassFacing(),
|
||||
"pointing it again hands back the compass facing p.facing wants")
|
||||
T.eq(FirstPerson.bodyYaw, FirstPerson.yaw, "and records the bearing behind it")
|
||||
FirstPerson.yaw, FirstPerson.pitch = 0, 0
|
||||
FirstPerson.frame({ px = 100, py = 200, gh = 0, lift = 0 }, 500, 600, 320, 288)
|
||||
|
||||
ThirdPerson.out, ThirdPerson.len = 0, 0
|
||||
T.eq(FirstPerson.bodyFacing(1, 0), "down",
|
||||
"in the head the body is the head, whichever way it walks")
|
||||
T.check(FirstPerson.hidePlayer(),
|
||||
"and the card the camera stands inside is left out of the frame again")
|
||||
|
||||
-- everything the section borrowed, put back
|
||||
Game.overworld = hadOw
|
||||
FirstPerson.blend = 0
|
||||
ThirdPerson.out, ThirdPerson.len, ThirdPerson.want = 0, 0, 0
|
||||
Voxel3D.camera = nil
|
||||
VoxelState.reset()
|
||||
end
|
||||
|
||||
-- ------- the VR rig's arithmetic
|
||||
--
|
||||
-- VRRig is the deliberately pure half of the VR stack: headset poses in,
|
||||
|
||||
Reference in New Issue
Block a user