sindicate 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -2
- package/docs/api/README.md +28 -0
- package/docs/api/anim.md +125 -0
- package/docs/api/assets.md +208 -0
- package/docs/api/collision.md +101 -0
- package/docs/api/fbxloader.md +50 -0
- package/docs/api/grass.md +103 -0
- package/docs/api/input.md +164 -0
- package/docs/api/quality.md +159 -0
- package/docs/api/renderer.md +115 -0
- package/docs/api/retarget.md +149 -0
- package/docs/api/shatter.md +121 -0
- package/docs/api/utils-decals.md +188 -0
- package/docs/api/water.md +143 -0
- package/docs/api/wind-weather-uniforms.md +105 -0
- package/docs/contracts/render.md +32 -0
- package/docs/contracts/time-feel.md +52 -0
- package/package.json +1 -1
- package/src/core/anim.js +184 -0
- package/src/core/assets.js +596 -0
- package/src/core/engine.js +284 -0
- package/src/core/input/bindings.js +30 -0
- package/src/core/input/gamepad.js +92 -0
- package/src/core/input/index.js +62 -0
- package/src/core/input/keyboard.js +16 -0
- package/src/core/input/mouse.js +48 -0
- package/src/core/quality.js +5 -1
- package/src/core/renderer.js +104 -0
- package/src/core/retarget.js +282 -0
- package/src/index.js +19 -2
- package/src/systems/aim.js +114 -0
- package/src/systems/campfire.js +150 -0
- package/src/systems/climbing.js +136 -0
- package/src/systems/deadeye.js +332 -0
- package/src/systems/encounters.js +123 -0
- package/src/systems/entity.js +524 -0
- package/src/systems/fistCurl.js +38 -0
- package/src/systems/footsteps.js +161 -0
- package/src/systems/glass.js +67 -0
- package/src/systems/herd.js +277 -0
- package/src/systems/horse.js +361 -0
- package/src/systems/mountedTraveller.js +518 -0
- package/src/systems/nav.js +343 -0
- package/src/systems/npc.js +880 -0
- package/src/systems/pathFollow.js +107 -0
- package/src/systems/platform.js +484 -0
- package/src/systems/riding.js +580 -0
- package/src/systems/seatedRider.js +396 -0
- package/src/systems/skybirds.js +129 -0
- package/src/systems/trainCamera.js +414 -0
- package/src/systems/traveller.js +254 -0
- package/src/systems/tumbleweeds.js +92 -0
- package/src/systems/vat.js +327 -0
- package/src/systems/vfx.js +472 -0
- package/src/world/blobShadows.js +109 -0
- package/src/world/clouds.js +61 -0
- package/src/world/flora.js +87 -0
- package/src/world/footprints.js +117 -0
- package/src/world/lampField.js +58 -0
- package/src/world/lampGlow.js +92 -0
- package/src/world/scatter.js +1077 -0
- package/src/world/sky.js +363 -0
- package/src/world/tileManager.js +197 -0
- package/src/world/walkHumps.js +74 -0
- package/src/world/weather.js +312 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// THE TRAIN CAMERA — what riding a train is actually FOR.
|
|
2
|
+
//
|
|
3
|
+
// A passenger is not driving. He cannot steer it, he cannot stop it, and the whole value of the
|
|
4
|
+
// journey is the one thing a third-person game normally refuses to show you: the train itself, from
|
|
5
|
+
// outside, moving. So while he rides, the camera stops being a follow camera and becomes a CUTTING
|
|
6
|
+
// ROOM — six shots on a loop, about five seconds each, which is GTA V's train and the right reference.
|
|
7
|
+
//
|
|
8
|
+
// THREE RULES. Every bug in here came from breaking one of them.
|
|
9
|
+
//
|
|
10
|
+
// 1. CUT, DO NOT EASE. The shots are sixty metres apart. Interpolating between two of them flies the
|
|
11
|
+
// lens through the middle of a boxcar at 40 m/s: it cannot be kept out of the geometry, and it
|
|
12
|
+
// does not read as an edit, it reads as a drone. So the cuts are HARD and the movement lives
|
|
13
|
+
// INSIDE each shot — a dolly in, a slow crane, a pan that lags its subject. That is what film does.
|
|
14
|
+
//
|
|
15
|
+
// 2. PLANT IT, DO NOT CHASE IT. Every lens position here is written EXACTLY — set(), never a lerp
|
|
16
|
+
// toward a moving target. Measured on the live game at 16 m/s, sampling 1800 frames of the whole
|
|
17
|
+
// cycle: the lens sits still in the loco's own frame to within 11 mm a frame (that 11 mm IS the
|
|
18
|
+
// dolly), the vertical never changes sign twice, and the look direction moves 0.02° a frame.
|
|
19
|
+
// A smoother chasing a body doing 16 m/s sits v/k metres behind it and every wobble in the frame
|
|
20
|
+
// time wobbles the lag — THAT is what "bouncing like mad" was. There was never anything to chase:
|
|
21
|
+
// the train's own transform is smooth, so a camera written straight off it is smooth too. The only
|
|
22
|
+
// thing low-passed in this file is the TERRAIN, because the ground under a lens doing 16 m/s is a
|
|
23
|
+
// lattice and sampling it raw is its own chatter.
|
|
24
|
+
//
|
|
25
|
+
// 3. IT IS FILMED AFTER THE IRON HAS MOVED. main.js updates the player at :673 and the trains at
|
|
26
|
+
// :692 — so any car transform read from inside player.update() is a frame old, which at 16 m/s is
|
|
27
|
+
// 27 cm, resampled every frame against a frame time that jitters. That is a camera bolted to a
|
|
28
|
+
// carriage that is shivering. This rig runs from Player.aboardPost, at the END of the tick, once
|
|
29
|
+
// the train has actually moved. (platform.js's postUpdate exists for precisely this reason.)
|
|
30
|
+
// ...and it refreshes the matrices of the two cars it reads (see update): three only rebuilds
|
|
31
|
+
// matrixWorld at RENDER, and the loco's is otherwise only fresh by accident — the crew's IK solve
|
|
32
|
+
// walks updateWorldMatrix up through the loco root as a side effect. Shoot the driver and the
|
|
33
|
+
// fireman and that accident stops happening, which would put a frame of lag into every exterior
|
|
34
|
+
// shot in the middle of a train robbery.
|
|
35
|
+
//
|
|
36
|
+
// AND IT GETS OUT OF THE WAY. Aim, draw, take a round, stop at a platform, or press V, and it hands the
|
|
37
|
+
// lens back to the ordinary follow camera in the SAME FRAME, with a snap and not a swoop. A cinematic
|
|
38
|
+
// camera that traps a man who is being shot at is not a feature.
|
|
39
|
+
import * as THREE from 'three/webgpu';
|
|
40
|
+
|
|
41
|
+
// Module temporaries. Nothing in this file allocates after construction — it runs every frame at 120.
|
|
42
|
+
const _p = new THREE.Vector3(); // where the lens goes
|
|
43
|
+
const _t = new THREE.Vector3(); // what it looks at
|
|
44
|
+
const _a = new THREE.Vector3();
|
|
45
|
+
const _b = new THREE.Vector3();
|
|
46
|
+
|
|
47
|
+
const GRACE = 1.1; // seconds of ordinary follow camera after she starts to move, before the first
|
|
48
|
+
// cut — you get to watch her pull out of the platform from your own seat.
|
|
49
|
+
const ROLLING = 1.2; // m/s. Below this she is standing at a call and the film is over: he wants his
|
|
50
|
+
// own camera and his mouse back, to look about and to find the way off.
|
|
51
|
+
const PASS_SPEED = 6; // below this the trackside pass-by has nothing to pass, so it is skipped
|
|
52
|
+
const BLOCK_CUT = 0.45; // seconds of a lost sightline before a shot gives up and cuts early. Long
|
|
53
|
+
// enough that a lineside rock flicking through frame is a wipe, not a bug.
|
|
54
|
+
|
|
55
|
+
// ── THE SHOTS ─────────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
// `dur` is per shot and deliberately not shared. A tight shot on a set of coupling rods outstays its
|
|
57
|
+
// welcome in four seconds; a trackside pass-by needs seven, because a 55 m consist doing 16 m/s takes
|
|
58
|
+
// three and a half of them just to go by. Average 5.4 s, which is the "about five" that was asked for.
|
|
59
|
+
//
|
|
60
|
+
// Each shot writes _p and _t and returns false if it cannot be taken right now (nothing to film, or a
|
|
61
|
+
// wall in the way), in which case the rig moves on to the next one. `guard` means the shot's sightline
|
|
62
|
+
// is worth testing against the world; the interior shots do not need it, because they are written in
|
|
63
|
+
// the car's own frame and are inside it by construction.
|
|
64
|
+
const SHOTS = [
|
|
65
|
+
// 1 ── THE SEAT. Establish that you are ON a train before you show him the train. Him on his bench in
|
|
66
|
+
// the right of frame with the county tearing past the glass at his shoulder, the saloon running away
|
|
67
|
+
// down the left, and the lens creeping in on him over the shot.
|
|
68
|
+
//
|
|
69
|
+
// The numbers are the carriage's, not a guess (train.js:145): bench pan 2.109, BACKREST TOP 2.653,
|
|
70
|
+
// roof 4.10, glass 2.319–3.640, seat.y 1.367 and his head bone seat.y + 1.556. The first version of
|
|
71
|
+
// this shot sat at seat.y + 1.30 = 2.667 — fourteen millimetres above the backrest — so a bench back
|
|
72
|
+
// filled the bottom half of the frame and you could not see the man or the window. It is at head
|
|
73
|
+
// height plus a shoulder now, which clears the backs by 0.66 m and looks down the car.
|
|
74
|
+
{ id: 'seat', dur: 5.0, shot(r, k) {
|
|
75
|
+
const car = r.host.car, s = r.host.seat, f = r.fore();
|
|
76
|
+
if (car.kind === 'carriage') {
|
|
77
|
+
// Across the aisle, behind his shoulder, creeping in 0.75 m. x 0.75 and |z − his bench| < 2.6
|
|
78
|
+
// are INSIDE the saloon box (hx 1.30, y 1.93…4.10, z −6.5…7.3) by construction — this lens
|
|
79
|
+
// cannot clip the planking, because it is never outside it.
|
|
80
|
+
_p.set(0.75, s.y + 1.95, s.z - f * (2.55 - k * 0.75));
|
|
81
|
+
_t.set(s.x - 0.25, s.y + 1.52, s.z + f * 1.60);
|
|
82
|
+
} else {
|
|
83
|
+
// The roof rider. Outboard of his shoulder and well above the clerestory (4.747), looking
|
|
84
|
+
// along the train past him. Everything under this lens is roof; there is nothing to clip.
|
|
85
|
+
_p.set(3.05, 6.40, f * (-5.6 + k * 1.3));
|
|
86
|
+
_t.set(1.30, 4.80, f * 3.0);
|
|
87
|
+
}
|
|
88
|
+
car.root.localToWorld(_p);
|
|
89
|
+
car.root.localToWorld(_t);
|
|
90
|
+
return true;
|
|
91
|
+
} },
|
|
92
|
+
|
|
93
|
+
// 2 ── THE TRACKSIDE PASS-BY. The best one, and the reason the file exists. A camera is PLANTED
|
|
94
|
+
// beside the rails ahead of the train — on the line's own geometry, from the route it is actually
|
|
95
|
+
// running, not a guess off the loco's heading — and then it does not move an inch. The train comes
|
|
96
|
+
// to IT. It is the only shot here with no camera motion at all, and it is the one that sells the
|
|
97
|
+
// weight of the thing, because the only thing moving in frame is eighty tons of iron.
|
|
98
|
+
//
|
|
99
|
+
// The pan LAGS its subject. A real operator on a tripod cannot whip-pan an express through ninety
|
|
100
|
+
// degrees at twelve metres either: the train slides out of frame at the abeam point and he catches it
|
|
101
|
+
// up going away. That lag is the shot.
|
|
102
|
+
{ id: 'trackside', dur: 7.0, guard: true, plant(r) {
|
|
103
|
+
const T = r.train;
|
|
104
|
+
if (T.speed < PASS_SPEED || !T.route) return false;
|
|
105
|
+
const R = T.route;
|
|
106
|
+
// 44 m of run-up: the loco reaches the lens at 2.7 s and the tail clears it at about 6.1 s, so
|
|
107
|
+
// the whole consist passes inside the shot instead of the shot ending halfway down the train.
|
|
108
|
+
const u = T.u + 44;
|
|
109
|
+
if (u > R.len + 30) return false; // she is about to berth: there is no pass-by
|
|
110
|
+
R.pos(u, _a); _a.y = R.height(u);
|
|
111
|
+
R.pos(u + 4, _b); // the LINE's own tangent here, not the loco's
|
|
112
|
+
const dx = _b.x - _a.x, dz = _b.z - _a.z;
|
|
113
|
+
const L = Math.hypot(dx, dz) || 1;
|
|
114
|
+
const nx = dz / L, nz = -dx / L; // the perpendicular, in the six-foot's own frame
|
|
115
|
+
// Try both sides of the road. 12 m of standoff and 2.4 m of tripod: near enough that the loco
|
|
116
|
+
// fills the frame, far enough that it does not become a blur of planking.
|
|
117
|
+
for (let i = 0; i < 2; i++) {
|
|
118
|
+
const side = i ? -1 : 1;
|
|
119
|
+
const x = _a.x + nx * 12 * side, z = _a.z + nz * 12 * side;
|
|
120
|
+
if (r.world.waterDepthAt?.(x, z) > 0.25) continue; // not in the creek
|
|
121
|
+
const y = Math.max(r.world.groundAt(x, z) + 2.4, _a.y + 1.5);
|
|
122
|
+
_p.set(x, y, z);
|
|
123
|
+
r.aimCar(_t, r.host.car, 1.6);
|
|
124
|
+
// A camera INSIDE the station or the water tower cannot see the train it is pointing at, so
|
|
125
|
+
// the sightline test throws that plant out for us — no special case for the buildings.
|
|
126
|
+
if (!r.world.segmentBlocked(_p, _t)) { r.px = x; r.py = y; r.pz = z; return true; }
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
},
|
|
130
|
+
shot(r) {
|
|
131
|
+
_p.set(r.px, r.py, r.pz);
|
|
132
|
+
r.aimCar(_a, r.host.car, 1.6);
|
|
133
|
+
// ...and the shot is over the moment its subject is gone: his car forty metres past the lens is
|
|
134
|
+
// a camera filming an empty desert. Cut early rather than sit through it.
|
|
135
|
+
_b.set(_a.x - r.px, 0, _a.z - r.pz);
|
|
136
|
+
if (_b.x * r.fx + _b.z * r.fz > 40) return false;
|
|
137
|
+
_t.copy(_a);
|
|
138
|
+
return true;
|
|
139
|
+
} },
|
|
140
|
+
|
|
141
|
+
// 3 ── THE FOOTPLATE. Out on the running board ahead of the cab, looking BACK along the boiler at the
|
|
142
|
+
// driver in his window, with the smoke coming over the top of frame and the train trailing away
|
|
143
|
+
// behind him. It is outboard of the body (|x| 2.55 against a 1.64 half-width) and above the running
|
|
144
|
+
// board, so it is in fresh air on every axis.
|
|
145
|
+
//
|
|
146
|
+
// TWO SHOTS WERE THROWN AWAY TO GET THIS ONE, and both were photographed before the vote. The lens
|
|
147
|
+
// INSIDE the cab does frame the backhead, the gauges and both men — and the cab interior is unlit, so
|
|
148
|
+
// the top third of the frame is a black slab and the two of them are hard-cropped silhouettes. And
|
|
149
|
+
// the version looking FORWARD off the running board is a handsome shot of a boiler with the crew
|
|
150
|
+
// squarely behind the lens, which is a footplate shot with no footplate in it. This one has the man.
|
|
151
|
+
{ id: 'footplate', dur: 5.0, guard: true, shot(r, k) {
|
|
152
|
+
const s = r.locoSide();
|
|
153
|
+
_p.set(s * 2.55, 3.45, 3.2 - k * 1.1); // creeping back down the board toward him
|
|
154
|
+
_t.set(s * 0.95, 3.15, -5.4); // the cab window, and the driver in it (crew z −4.95)
|
|
155
|
+
r.loco.root.localToWorld(_p);
|
|
156
|
+
r.loco.root.localToWorld(_t);
|
|
157
|
+
return true;
|
|
158
|
+
} },
|
|
159
|
+
|
|
160
|
+
// 4 ── THE WIDE. High and behind, far enough back to hold all 55 m of her at once with the smoke
|
|
161
|
+
// trailing off the stack — the shot that says "this is a train" and not "this is a carriage". It
|
|
162
|
+
// cranes down and in over the shot (24 m → 17 m up, 27 m → 16 m out), which is the only movement it
|
|
163
|
+
// needs: at this range the train's own progress does all the work.
|
|
164
|
+
{ id: 'wide', dur: 5.5, guard: true, shot(r, k) {
|
|
165
|
+
const mid = r.train.len * 0.5;
|
|
166
|
+
r.along(_a, -mid, 0, 0); // the middle of the consist
|
|
167
|
+
const back = 46 - k * 4, out = 27 - k * 11, up = 24 - k * 7;
|
|
168
|
+
r.along(_p, -mid - back, r.side * out, up);
|
|
169
|
+
_p.y = Math.max(_p.y, r.floorAt(_p.x, _p.z, 5)); // never behind a ridge
|
|
170
|
+
_t.set(_a.x, _a.y + 2.2, _a.z);
|
|
171
|
+
return true;
|
|
172
|
+
} },
|
|
173
|
+
|
|
174
|
+
// 5 ── THE RODS. Mine, and it earns its place because a steam locomotive is the only vehicle in this
|
|
175
|
+
// game whose MECHANISM is worth filming: the drivers turn on distance rolled and the coupling rods
|
|
176
|
+
// reciprocate off the same number (train.js: radians = distance / radius), so they can never be out
|
|
177
|
+
// of step with the ground. Wheel height, four metres out, tracking forward along the running board.
|
|
178
|
+
// Ballast blurring, rods hammering. It is the shortest shot in the cycle on purpose — a tight
|
|
179
|
+
// mechanical hold is over in four seconds.
|
|
180
|
+
{ id: 'rods', dur: 4.5, guard: true, shot(r, k) {
|
|
181
|
+
const s = r.locoSide();
|
|
182
|
+
_p.set(s * (4.8 - k * 1.2), 1.95, -3.6 + k * 3.6);
|
|
183
|
+
_t.set(s * 0.6, 1.35, -1.8); // the drivers (hub 0.93) and the rods above them
|
|
184
|
+
r.loco.root.localToWorld(_p);
|
|
185
|
+
r.loco.root.localToWorld(_t);
|
|
186
|
+
_p.y = Math.max(_p.y, r.floorAt(_p.x, _p.z, 0.7)); // a cutting must not swallow the lens
|
|
187
|
+
return true;
|
|
188
|
+
} },
|
|
189
|
+
|
|
190
|
+
// 6 ── THE LOW THREE-QUARTER. The lens runs ahead of her, low, off the shoulder of the track, and she
|
|
191
|
+
// comes ON to it — 24 m of clear air in front of the smokebox closing to 7, so the loco GROWS in
|
|
192
|
+
// frame while the camera holds its line. The only shot that gets the cowcatcher in your face.
|
|
193
|
+
//
|
|
194
|
+
// MEASURED FROM HER NOSE, and that is the whole of the fix. along() is written off the LEAD CAR'S
|
|
195
|
+
// CENTRE, and the first version of this shot asked for `train.len + d` — which is 85 m in front of
|
|
196
|
+
// the loco's middle — and then aimed at a point 50 m ahead of her. The result was a speck of a train
|
|
197
|
+
// on the horizon and four hundred square metres of sand, held for five and a half seconds. The lead
|
|
198
|
+
// car's half-length is the number that turns "forward of the lead car" into "clear of her buffers".
|
|
199
|
+
{ id: 'front', dur: 5.5, guard: true, shot(r, k) {
|
|
200
|
+
if (r.train.reversed) return false; // she is being propelled: what is coming at the lens is the
|
|
201
|
+
// arse end of a carriage, and that is not this shot
|
|
202
|
+
const nose = r.train.cars[0].len * 0.5;
|
|
203
|
+
const d = 24 - k * 17, out = r.side * (9.0 - k * 4.2);
|
|
204
|
+
r.along(_p, nose + d, out, 1.3);
|
|
205
|
+
_p.y = Math.max(_p.y, r.floorAt(_p.x, _p.z, 1.5));
|
|
206
|
+
r.along(_t, 3.0, 0, 2.3); // the smokebox, not the sky above it
|
|
207
|
+
return true;
|
|
208
|
+
} },
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
// ── THE RIG ───────────────────────────────────────────────────────────────────────────────────
|
|
212
|
+
class TrainCamera {
|
|
213
|
+
constructor(train, host) {
|
|
214
|
+
this.train = train;
|
|
215
|
+
this.host = host;
|
|
216
|
+
this.game = train.game;
|
|
217
|
+
this.world = train.game.world;
|
|
218
|
+
this.i = 0;
|
|
219
|
+
this.t = 0;
|
|
220
|
+
this.grace = GRACE;
|
|
221
|
+
this.off = false; // V: he wants his own camera back
|
|
222
|
+
this.side = 1; // which flank the exterior shots stand on — chosen at each cut
|
|
223
|
+
this.blockT = 0;
|
|
224
|
+
this.floor = null; // a LOW-PASSED ground height under the lens (see floorAt)
|
|
225
|
+
this.px = 0; this.py = 0; this.pz = 0; // the trackside plant, in plain numbers: no vector to alias
|
|
226
|
+
this.fx = 0; this.fz = 1; // the way she is going
|
|
227
|
+
this.was = null; // the rider we set up for, so a re-board resets the cycle
|
|
228
|
+
this._planted = null; // the shot whose plant() has been run for this take
|
|
229
|
+
this._held = false;
|
|
230
|
+
this._k = 0;
|
|
231
|
+
this._f = 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
get loco() { return this.train.cars[0]; }
|
|
235
|
+
|
|
236
|
+
// +1 if the car the player is in has its own +Z pointing the way the train is travelling. Every
|
|
237
|
+
// "behind him" in the seat shot is written in car-local z and needs this, because a train that has
|
|
238
|
+
// reversed out of Bonebreak is running with its cars facing backwards.
|
|
239
|
+
fore() {
|
|
240
|
+
return Math.cos(this.host.car.root.rotation.y - this.train.travelYaw) >= 0 ? 1 : -1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// `side` is chosen in the WORLD's terms (see cut). A locomotive that is propelling from the back of
|
|
244
|
+
// the train is turned end for end, so its own +X is the other flank — and a shot written in loco
|
|
245
|
+
// coordinates would stand on the town side of the line and film the back of the station.
|
|
246
|
+
locoSide() { return this.train.reversed ? -this.side : this.side; }
|
|
247
|
+
|
|
248
|
+
// A point in the TRAIN's frame: `f` metres forward of the LEAD CAR'S CENTRE, `s` to its right, `u`
|
|
249
|
+
// up. Taken off the leading car's own position, so it inherits the curve the train is actually on.
|
|
250
|
+
along(out, f, s, u) {
|
|
251
|
+
const lead = this.train.reversed ? this.train.cars[this.train.cars.length - 1] : this.train.cars[0];
|
|
252
|
+
const p = lead.root.position;
|
|
253
|
+
return out.set(p.x + this.fx * f + this.fz * s, p.y + u, p.z + this.fz * f - this.fx * s);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Where a car IS, plus `up` — the aim point for anything filming the player's own coach.
|
|
257
|
+
aimCar(out, car, up) {
|
|
258
|
+
const p = car.root.position;
|
|
259
|
+
return out.set(p.x, p.y + up, p.z);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// THE GROUND, LOW-PASSED. A raw groundAt() under a lens doing 16 m/s samples the terrain lattice and
|
|
263
|
+
// steps with it, and a max() against a stepping floor is chatter — the exact thing this rig exists
|
|
264
|
+
// not to do. So the floor is a first-order lag (4/s): it still lifts the camera out of a rise, it
|
|
265
|
+
// just cannot buzz. Re-latched on every cut, because a cut may move the lens sixty metres.
|
|
266
|
+
floorAt(x, z, clear) {
|
|
267
|
+
const g = this.world.groundAt(x, z) + clear;
|
|
268
|
+
this.floor = this.floor == null ? g : this.floor + (g - this.floor) * this._k;
|
|
269
|
+
return this.floor;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
cut(to) {
|
|
273
|
+
this.i = ((to ?? this.i + 1) % SHOTS.length + SHOTS.length) % SHOTS.length;
|
|
274
|
+
this.t = 0;
|
|
275
|
+
this.blockT = 0;
|
|
276
|
+
this.floor = null;
|
|
277
|
+
this._planted = null;
|
|
278
|
+
// WHICH FLANK THE EXTERIOR SHOTS STAND ON. The town is on the +z side of the line — that is the
|
|
279
|
+
// platform, the station building and the water tower, and it is the side train.js already steps
|
|
280
|
+
// you down onto (makeSeat: of the two sides, take the one nearer the town). So the lens takes the
|
|
281
|
+
// OTHER one: open desert, no building to bury it in, and the town in the background of the frame
|
|
282
|
+
// instead of pressed against the glass. The right-of-travel vector is (fz, −fx), so its z is
|
|
283
|
+
// −fx·side, and away from the town means driving that negative.
|
|
284
|
+
this.side = this.fx >= 0 ? 1 : -1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── the one call player.js makes ────────────────────────────────────────────────────────────
|
|
288
|
+
// Returns TRUE when it owns the lens (and the mouse is dead), FALSE when it has handed it back.
|
|
289
|
+
update(dt, camera, player, input) {
|
|
290
|
+
const T = this.train;
|
|
291
|
+
// A NEW RIDE IS A NEW CYCLE. The rig lives on the seat host for the life of the game (no
|
|
292
|
+
// allocation on boarding, and none on alighting either), so it notices a change of rider itself
|
|
293
|
+
// and needs no hook in train.js's board/alight to do it.
|
|
294
|
+
if (this.was !== this.host.rider) {
|
|
295
|
+
this.was = this.host.rider;
|
|
296
|
+
this.i = 0; this.t = 0; this.grace = GRACE; this.off = false;
|
|
297
|
+
this.floor = null; this.blockT = 0; this._planted = null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const ui = this.game.ui;
|
|
301
|
+
const menu = ui?.menuOpen || ui?.dialogueOpen;
|
|
302
|
+
if (!menu && input) {
|
|
303
|
+
// TRIANGLE works the lens from the pad (Nick): the pad synthesizes Triangle as KeyQ, and Q
|
|
304
|
+
// is free aboard — the weapon cycle lives in combatInputs, which the aboard path never runs.
|
|
305
|
+
if (input.hit('KeyV') || input.hit('KeyQ')) { this.off = !this.off; this.grace = 0; input.consume('KeyV'); input.consume('KeyQ'); }
|
|
306
|
+
if (input.hit('KeyC') && !this.off) { this.cut(); this.grace = 0; input.consume('KeyC'); }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// HAND IT BACK, INSTANTLY. He reached for the gun, he is reloading, he took a round, a fight
|
|
310
|
+
// started, he died, she has stopped at a platform, or he asked for it back.
|
|
311
|
+
//
|
|
312
|
+
// gunInputs does not run at all on the aboard path (player.js returns above it at :582), so
|
|
313
|
+
// `aiming` and `drawn` can never LATCH while he is sitting down — the RAW right button is the only
|
|
314
|
+
// honest test of "he is trying to bring the gun up", and it is the one that matters. (`drawn` is
|
|
315
|
+
// still tested, because a man can board with it already in his hand — and train.js's board() takes
|
|
316
|
+
// it off him for exactly that reason, so this is the belt to that pair of braces.)
|
|
317
|
+
const armed = player.aiming || player.drawn || player.reloading || player._drawT > 0
|
|
318
|
+
|| (input?.acting && (input.held(2) || input.held(0)));
|
|
319
|
+
const fight = player._combatT > 0 || this.game._inCombat || !player.alive;
|
|
320
|
+
// ...and STANDING AT A CALL is not a journey. Cutting between six angles of a train that is not
|
|
321
|
+
// moving is a slideshow, and the man in it is trying to find the door: give him back his own
|
|
322
|
+
// camera and his mouse the moment she comes to a stand.
|
|
323
|
+
if (this.off || armed || fight || T.stabled || T.speed < ROLLING) {
|
|
324
|
+
this.hold(player);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (this.grace > 0) { this.grace -= dt; this.hold(player); return false; }
|
|
329
|
+
|
|
330
|
+
// THE MATRICES OF THE TWO CARS THIS RIG READS. See rule 3 at the top: three rebuilds matrixWorld at
|
|
331
|
+
// render, and a shot written in a car's own frame off last frame's matrix is 27 cm of lag at line
|
|
332
|
+
// speed, jittering with the frame time. host.sync() has already refreshed the player's car; the
|
|
333
|
+
// LOCO's is only fresh as a side effect of its crew's IK solve, and the crew can be shot.
|
|
334
|
+
this.loco.root.updateWorldMatrix(false, false);
|
|
335
|
+
|
|
336
|
+
this._k = Math.min(1, dt * 4);
|
|
337
|
+
this.fx = Math.sin(T.travelYaw);
|
|
338
|
+
this.fz = Math.cos(T.travelYaw);
|
|
339
|
+
|
|
340
|
+
this.t += dt;
|
|
341
|
+
let S = SHOTS[this.i];
|
|
342
|
+
|
|
343
|
+
// Take the shot. A shot that cannot be taken (nothing to pass, no clear line, a loco on the wrong
|
|
344
|
+
// end of the train) is SKIPPED rather than shown — a camera pointing at sand for five seconds is
|
|
345
|
+
// worse than a shorter cycle. The bounded loop is the whole safety argument: it can never spin,
|
|
346
|
+
// and if every shot in the list refuses, the follow camera takes over.
|
|
347
|
+
for (let tries = 0; tries < SHOTS.length; tries++) {
|
|
348
|
+
S = SHOTS[this.i];
|
|
349
|
+
if (this._planted !== S) {
|
|
350
|
+
if (S.plant && !S.plant(this)) { this.cut(); continue; }
|
|
351
|
+
this._planted = S;
|
|
352
|
+
}
|
|
353
|
+
if (S.shot(this, Math.min(1, this.t / S.dur)) !== false) break;
|
|
354
|
+
this.cut();
|
|
355
|
+
}
|
|
356
|
+
if (this._planted !== SHOTS[this.i]) { this.hold(player); return false; }
|
|
357
|
+
|
|
358
|
+
// THE SIGHTLINE. A rock, a shed or the water tower between the lens and the train is a black frame,
|
|
359
|
+
// and the user WILL see it. Tested every third frame (it is a BVH raycast, and 25 ms of latency on
|
|
360
|
+
// a cut-away is invisible), and only after BLOCK_CUT of it, so a fence post flicking through frame
|
|
361
|
+
// is a wipe and not a bug.
|
|
362
|
+
if (S.guard) {
|
|
363
|
+
this._f++;
|
|
364
|
+
if (this._f % 3 === 0 && this.world.segmentBlocked(_p, _t)) this.blockT += dt * 3;
|
|
365
|
+
else this.blockT = Math.max(0, this.blockT - dt);
|
|
366
|
+
if (this.blockT > BLOCK_CUT) this.cut();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
camera.position.copy(_p);
|
|
370
|
+
camera.lookAt(_t);
|
|
371
|
+
if (this.t >= S.dur) this.cut();
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Handing the lens back. The follow camera EASES toward its target (player.js: lerp on rdt*10), so
|
|
376
|
+
// without a snap it would swoop in from sixty metres away over half a second — with a gun in his hand
|
|
377
|
+
// and someone shooting at him. So it CUTS, and it cuts to a camera that is already behind his head:
|
|
378
|
+
// camYaw is re-derived from the car's CURRENT heading, because the carriage has been going round
|
|
379
|
+
// curves since he sat down and the yaw board() gave him is a minute stale.
|
|
380
|
+
//
|
|
381
|
+
// ...and the shot that was up is ABANDONED, not paused. Come back to it after ten seconds of a
|
|
382
|
+
// gunfight and its trackside tripod is a hundred and sixty metres behind the train, filming the
|
|
383
|
+
// desert. The take restarts from the top when the film does.
|
|
384
|
+
hold(player) {
|
|
385
|
+
if (this._held) return;
|
|
386
|
+
this._held = true;
|
|
387
|
+
this.t = 0;
|
|
388
|
+
this._planted = null;
|
|
389
|
+
this.floor = null;
|
|
390
|
+
player._camSnap = true;
|
|
391
|
+
player.camYaw = this.host.car.root.rotation.y + (this.host.seat?.ry ?? 0) + Math.PI;
|
|
392
|
+
player.camPitch = 0.06;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// The prompt, so the keys are discoverable without a manual. main.js renders whatever the interactable
|
|
397
|
+
// hands it and splits on the first dash, so the leading "E —" is what lights up as the key cap.
|
|
398
|
+
const PROMPT = 'E — Get off · C next shot · V free camera';
|
|
399
|
+
|
|
400
|
+
// Hang a rig on a seat host. Built ONCE, on the loading screen, with the train — nothing here is
|
|
401
|
+
// allocated when he boards. player.js calls host.camera() while he is aboard and falls back to the
|
|
402
|
+
// ordinary follow camera the moment it answers false. A stagecoach host has no `camera` field at all,
|
|
403
|
+
// which is exactly why the coach is untouched by any of this.
|
|
404
|
+
export function attachTrainCamera(train, host) {
|
|
405
|
+
const rig = new TrainCamera(train, host);
|
|
406
|
+
host.cam = rig;
|
|
407
|
+
host.camPrompt = PROMPT;
|
|
408
|
+
host.camera = (dt, camera, player, input) => {
|
|
409
|
+
const on = rig.update(dt, camera, player, input);
|
|
410
|
+
if (on) rig._held = false;
|
|
411
|
+
return on;
|
|
412
|
+
};
|
|
413
|
+
return rig;
|
|
414
|
+
}
|