sindicate 0.16.0 → 0.17.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/CHANGELOG.md +80 -0
- package/package.json +1 -1
- package/src/audio/synth.js +31 -13
- package/src/core/anim.js +22 -0
- package/src/core/engine.js +50 -5
- package/src/core/overrides.js +84 -0
- package/src/core/quality.js +6 -2
- package/src/systems/climbing.js +18 -2
- package/src/systems/coach.js +3 -3
- package/src/systems/combat.js +17 -17
- package/src/systems/crime.js +5 -5
- package/src/systems/deadeye.js +4 -4
- package/src/systems/encounters.js +1 -1
- package/src/systems/enemy.js +9 -9
- package/src/systems/footsteps.js +1 -1
- package/src/systems/loot.js +2 -2
- package/src/systems/npc.js +1 -1
- package/src/systems/player.js +32 -15
- package/src/systems/riding.js +26 -7
- package/src/systems/shop.js +2 -2
- package/src/systems/train.js +16 -3
- package/src/vehicle/car.js +4 -1
- package/src/world/cityPlan.js +500 -0
- package/src/world/mapWorld.js +120 -0
- package/src/world/roadAssert.js +28 -1
- package/src/world/roadLink.js +88 -15
- package/src/world/roadTerrain.js +36 -8
- package/src/world/roadWorld.js +5 -0
- package/src/world/settlement.js +254 -0
- package/src/world/walkHumps.js +22 -1
- package/tools/modelCatalogue.mjs +59 -0
- package/tools/unitySceneToBuildings.mjs +273 -0
- package/tools/wholeBuildings.mjs +233 -0
package/src/systems/enemy.js
CHANGED
|
@@ -175,11 +175,11 @@ export class Enemy extends Character {
|
|
|
175
175
|
// A hostile is not: killing an Enemy reports nothing, which is the self-defence rule.
|
|
176
176
|
const murder = this.civilian && (this._killer == null || this._killer === this.game.player);
|
|
177
177
|
super.die();
|
|
178
|
-
if (murder) this.game.crime?.report('murder', this.position.clone());
|
|
178
|
+
if (murder) this.game.crime?.report?.('murder', this.position.clone());
|
|
179
179
|
if (this.faction === 'ally') return;
|
|
180
180
|
// outlaws bleed where they fall
|
|
181
181
|
const gy = this.game.world.groundAt(this.position.x, this.position.z);
|
|
182
|
-
this.game.vfx?.bloodDecal({ x: this.position.x, y: gy, z: this.position.z });
|
|
182
|
+
this.game.vfx?.bloodDecal?.({ x: this.position.x, y: gy, z: this.position.z });
|
|
183
183
|
// tell the mission engine — a kill stage tallies hostiles by the mission tag stamped on them
|
|
184
184
|
// when they spawned (game/missions.js), so it advances only on the RIGHT deaths.
|
|
185
185
|
this.game.missions?.enemyKilled?.(this);
|
|
@@ -221,16 +221,16 @@ export class Enemy extends Character {
|
|
|
221
221
|
speed: rg.projSpeed, range: rg.max + 12, friendly: false, shooter: this,
|
|
222
222
|
});
|
|
223
223
|
this.game.vfx?.muzzleFlash?.(from.addScaledVector(dir, 0.35), dir);
|
|
224
|
-
this.game.audio?.play('gunshot');
|
|
224
|
+
this.game.audio?.play?.('gunshot');
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
227
|
const dd = dist2D(this.position.x, this.position.z, player.position.x, player.position.z);
|
|
228
228
|
if (dd < 2.3 && !this.game.world.losBlocked(this.position, player.position)) { // not through a wall
|
|
229
229
|
player.takeDamage(Math.round(this.def.damage * (this._dmgMul ?? 1)), this.position, { attacker: this });
|
|
230
|
-
if (player === this.game.player) this.game.audio?.play('hurt'); // YOU took it — the full body thud
|
|
230
|
+
if (player === this.game.player) this.game.audio?.play?.('hurt'); // YOU took it — the full body thud
|
|
231
231
|
else { // a brawl you're only near — impact, distance-faded
|
|
232
232
|
const dL = dist2D(this.position.x, this.position.z, this.game.player.position.x, this.game.player.position.z);
|
|
233
|
-
if (dL < 20) this.game.audio?.play('hit', 1 - dL / 20);
|
|
233
|
+
if (dL < 20) this.game.audio?.play?.('hit', 1 - dL / 20);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
}
|
|
@@ -271,9 +271,9 @@ export class Enemy extends Character {
|
|
|
271
271
|
this._enraged = true;
|
|
272
272
|
this.speed *= 1.3;
|
|
273
273
|
this._dmgMul = 1.3;
|
|
274
|
-
this.game.ui?.toast(`${this.name} is enraged!`);
|
|
274
|
+
this.game.ui?.toast?.(`${this.name} is enraged!`);
|
|
275
275
|
this.game.shake?.(0.25);
|
|
276
|
-
this.game.audio?.play('aggro');
|
|
276
|
+
this.game.audio?.play?.('aggro');
|
|
277
277
|
this.game.vfx?.spawn?.({ pos: this.position.clone().setY(this.position.y + 1.2), count: 16, size: 0.5, life: 0.7, color: 0xff3020, spread: 2.0 });
|
|
278
278
|
}
|
|
279
279
|
|
|
@@ -284,7 +284,7 @@ export class Enemy extends Character {
|
|
|
284
284
|
if (!this.passive && d < this.def.aggroR) {
|
|
285
285
|
this.state = 'chase';
|
|
286
286
|
if (player === this.game.player) { // don't announce a scrap the player isn't in
|
|
287
|
-
this.game.audio?.play('aggro');
|
|
287
|
+
this.game.audio?.play?.('aggro');
|
|
288
288
|
this.game.hint?.('combat', 'Tab locks on. Ctrl dodge-rolls — time it as the blow lands.');
|
|
289
289
|
}
|
|
290
290
|
} else {
|
|
@@ -366,7 +366,7 @@ export class Enemy extends Character {
|
|
|
366
366
|
// the WHIFF of the blow — attenuated by the listener's (player's) distance so a brawl across
|
|
367
367
|
// the street is faint, not full-blast. The uppercut gets the beefier swing.
|
|
368
368
|
const dL = dist2D(this.position.x, this.position.z, this.game.player.position.x, this.game.player.position.z);
|
|
369
|
-
if (dL < 20) this.game.audio?.play(swing === 'punchUpper' ? 'swingHeavy' : 'swing', 1 - dL / 20);
|
|
369
|
+
if (dL < 20) this.game.audio?.play?.(swing === 'punchUpper' ? 'swingHeavy' : 'swing', 1 - dL / 20);
|
|
370
370
|
// damage lands mid-swing — stored so a hit on this enemy can cancel it (interrupt)
|
|
371
371
|
this._swing = { t: 0.42, kind: 'melee', target: player }; // lands via _landSwing (game-time)
|
|
372
372
|
} else if (!this.busy) {
|
package/src/systems/footsteps.js
CHANGED
|
@@ -50,7 +50,7 @@ export class Footsteps {
|
|
|
50
50
|
const F = '/assets/audio/footsteps';
|
|
51
51
|
const map = {};
|
|
52
52
|
for (const s of SURFACES) for (let i = 1; i <= VARIANTS; i++) map[`${s}${i}`] = `${F}/${s}${i}.wav`;
|
|
53
|
-
this.game.audio?.loadSamples(map);
|
|
53
|
+
this.game.audio?.loadSamples?.(map);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// What is the hero standing on? A desert plain: wading water (the creek) > packed
|
package/src/systems/loot.js
CHANGED
|
@@ -67,7 +67,7 @@ export class Loot {
|
|
|
67
67
|
n = Math.round(n);
|
|
68
68
|
if (!n) return this.money;
|
|
69
69
|
this.money = Math.max(0, this.money + n);
|
|
70
|
-
if (n > 0) { this._bump = true; this.game.audio?.play('coin'); } // synth.js has a 'coin' case
|
|
70
|
+
if (n > 0) { this._bump = true; this.game.audio?.play?.('coin'); } // synth.js has a 'coin' case
|
|
71
71
|
return this.money;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -153,7 +153,7 @@ export class Loot {
|
|
|
153
153
|
// TAKEN. The float reads like a damage number because that's the beat it lands on —
|
|
154
154
|
// showDamage() takes any {position} and any string (combat.js passes 'HEADSHOT').
|
|
155
155
|
this.add(p.amount);
|
|
156
|
-
this.game.ui?.showDamage({ position: p.root.position }, `$${p.amount}`);
|
|
156
|
+
this.game.ui?.showDamage?.({ position: p.root.position }, `$${p.amount}`);
|
|
157
157
|
this._giveItems(p.items); // BEFORE _park clears the slot — the money floats, the things get a toast
|
|
158
158
|
this._park(p);
|
|
159
159
|
}
|
package/src/systems/npc.js
CHANGED
|
@@ -566,7 +566,7 @@ export class NPC extends Character {
|
|
|
566
566
|
this.animator?.play?.('gunFire', { once: true, fade: 0.08, onDone: () => {} });
|
|
567
567
|
const mz = tmpV1.set(this.position.x, this.position.y + 1.35, this.position.z);
|
|
568
568
|
this.game.vfx?.muzzleFlash?.(mz, null);
|
|
569
|
-
this.game.audio?.play('gunshot_far');
|
|
569
|
+
this.game.audio?.play?.('gunshot_far');
|
|
570
570
|
if (Math.random() < 0.38) t.takeDamage(12, this.position, { attacker: this });
|
|
571
571
|
} else if (!this.animator?.actions?.gunFire?.isRunning?.()) this.play('gunAim');
|
|
572
572
|
return; // standing his ground — no follow-move this frame
|
package/src/systems/player.js
CHANGED
|
@@ -135,15 +135,20 @@ export class Player extends Character {
|
|
|
135
135
|
this.capsuleSteps = 5; // smoother capsule stepping for the player (stairs)
|
|
136
136
|
|
|
137
137
|
// ---- gun state (HUD contract: weapon/aiming/ammo/ammoMax/reloading/deadEye/drawn)
|
|
138
|
-
|
|
138
|
+
// WHATEVER THE GAME REGISTERED, not western's two names. Everything downstream already
|
|
139
|
+
// reads WEAPONS[this.weapon] generically; only these few lines named `revolver` and
|
|
140
|
+
// `rifle` — and naming them meant a game without both could not construct a Player at
|
|
141
|
+
// all. The starting weapon is the first registered, or 'fists' if none are.
|
|
142
|
+
this.weapon = Object.keys(WEAPONS)[0] ?? 'fists';
|
|
139
143
|
this.aiming = false; // RMB held — ADS. NOT required to fire.
|
|
140
144
|
this.drawn = false; // gun in hand vs in leather (HUD contract)
|
|
141
145
|
this.reloading = false;
|
|
142
146
|
this.deadEye = 1; // 0..1 meter — OWNED by game/deadeye.js (Ctrl). HUD contract.
|
|
143
147
|
this.deadEyeActive = false; // slow-mo + tagging running — set by deadeye.js. HUD contract.
|
|
144
|
-
|
|
145
|
-
this.
|
|
146
|
-
this.
|
|
148
|
+
// per-weapon mag persists across Q cycling — one entry per weapon the game registered
|
|
149
|
+
this._mags = Object.fromEntries(Object.entries(WEAPONS).map(([k, w]) => [k, w.mag]));
|
|
150
|
+
this.ammo = this._mags[this.weapon] ?? 0;
|
|
151
|
+
this.ammoMax = WEAPONS[this.weapon]?.mag ?? 0;
|
|
147
152
|
this._drawT = 0; // gunDraw one-shot input gate
|
|
148
153
|
this._holsterT = 0; // unaimed seconds until the gun goes back to leather
|
|
149
154
|
this._holstering = false; // holster clip in flight — the gun is still in hand
|
|
@@ -927,7 +932,19 @@ export class Player extends Character {
|
|
|
927
932
|
const gunPose = !!gunW && (this.aiming || this.reloading || this._drawT > 0 || this._gunRecoilT > 0);
|
|
928
933
|
if (!this.busy && !this.airborne) {
|
|
929
934
|
if (this.animator._overlay) this.animator.setOverlay(null, { fade: BASE_FADE });
|
|
930
|
-
|
|
935
|
+
// WHICH CLIP IS THE BODY PLAYING? A decision point (core/overrides.js), because the
|
|
936
|
+
// answer below is right for a pack that has a full-body clip for every gun state and
|
|
937
|
+
// wrong for one that does not: Kubold's rifle set has a single standing reload, so
|
|
938
|
+
// `gunReloadWalk` maps onto it and walking into a reload stops the legs dead. A game
|
|
939
|
+
// whose pack differs answers for itself; one that says nothing gets exactly this.
|
|
940
|
+
const chosen = this.game?.engine?.overrides?.ask('locomotionClip', {
|
|
941
|
+
moving, sprinting, drawn: this.drawn, aiming: this.aiming,
|
|
942
|
+
reloading: this.reloading, holstering: this._holstering,
|
|
943
|
+
weapon: this.weapon, animator: this.animator,
|
|
944
|
+
has: (n) => !!this.animator.actions[n],
|
|
945
|
+
});
|
|
946
|
+
if (chosen) this.animator.play(chosen);
|
|
947
|
+
else if (this.drawn && gunW) {
|
|
931
948
|
if (this.reloading) this.animator.play(moving ? 'gunReloadWalk' : 'gunReload');
|
|
932
949
|
else if (this._holstering) this.animator.play(moving ? 'gunWalkHolster' : 'gunHolster');
|
|
933
950
|
else if (this.aiming) this.animator.play(moving ? 'gunWalkAimed' : 'gunAim');
|
|
@@ -1153,7 +1170,7 @@ export class Player extends Character {
|
|
|
1153
1170
|
this.drawn = false; // fresh weapon starts holstered — first fire/aim plays the draw
|
|
1154
1171
|
this._holstering = false;
|
|
1155
1172
|
this.placeWeapons();
|
|
1156
|
-
this.game.audio?.play('ui');
|
|
1173
|
+
this.game.audio?.play?.('ui');
|
|
1157
1174
|
}
|
|
1158
1175
|
// WHISTLE (H) — call the companion horse over. On foot only (this whole block never runs mounted).
|
|
1159
1176
|
if (input.hit('KeyH') && !this.busy) this.whistle();
|
|
@@ -1252,11 +1269,11 @@ export class Player extends Character {
|
|
|
1252
1269
|
this._fireBuf = 0; // consumed — one click, one shot
|
|
1253
1270
|
this._holsterT = HOLSTER_IDLE;
|
|
1254
1271
|
if (this.ammo > 0) this._fire(w, movin);
|
|
1255
|
-
else { this.game.audio?.play('dryfire'); this._gunFireT = 0.25; }
|
|
1272
|
+
else { this.game.audio?.play?.('dryfire'); this._gunFireT = 0.25; }
|
|
1256
1273
|
}
|
|
1257
1274
|
}
|
|
1258
1275
|
|
|
1259
|
-
if (this.drawn) this.game.ui?.aimTarget(this._aimingAtBody());
|
|
1276
|
+
if (this.drawn) this.game.ui?.aimTarget?.(this._aimingAtBody());
|
|
1260
1277
|
if (this.drawn) this._menaceCheck(dt);
|
|
1261
1278
|
if (this.aiming || this.reloading || this._drawT > 0) this._holsterT = HOLSTER_IDLE;
|
|
1262
1279
|
if (this.drawn) this.game.hint?.('gun', 'Left-click to fire — hold right-click to aim down the sights. R reloads.');
|
|
@@ -1302,7 +1319,7 @@ export class Player extends Character {
|
|
|
1302
1319
|
if (!this.drawn) { this.drawn = true; this.placeWeapons(); }
|
|
1303
1320
|
if (this._holstering) this._abortHolster();
|
|
1304
1321
|
this._holsterT = HOLSTER_IDLE;
|
|
1305
|
-
this.game.audio?.play('reload');
|
|
1322
|
+
this.game.audio?.play?.('reload');
|
|
1306
1323
|
const done = () => {
|
|
1307
1324
|
if (!this.reloading) return; // cancelled — the old ammo stands
|
|
1308
1325
|
this.reloading = false;
|
|
@@ -1379,7 +1396,7 @@ export class Player extends Character {
|
|
|
1379
1396
|
// at 5 Hz meant walking all 150 souls 750 times a second to re-panic the same dozen men.
|
|
1380
1397
|
if (this._menaceCool <= 0) {
|
|
1381
1398
|
this._menaceCool = 4;
|
|
1382
|
-
this.game.crime?.report('menace', best.position.clone());
|
|
1399
|
+
this.game.crime?.report?.('menace', best.position.clone());
|
|
1383
1400
|
}
|
|
1384
1401
|
}
|
|
1385
1402
|
|
|
@@ -1424,7 +1441,7 @@ export class Player extends Character {
|
|
|
1424
1441
|
this.ammo = this._mags[this.weapon] = Math.max(0, this.ammo - 1);
|
|
1425
1442
|
this.game.combat?.fireBullet?.({ from, dir, damage: w.damage, speed: 90, range: w.range, friendly: true, shooter: this });
|
|
1426
1443
|
this.game.vfx?.muzzleFlash?.(from, dir);
|
|
1427
|
-
this.game.audio?.play('gunshot');
|
|
1444
|
+
this.game.audio?.play?.('gunshot');
|
|
1428
1445
|
// camera kick (RECOIL_KICK × recoil, eased settle — see update)
|
|
1429
1446
|
this._recoilAmp = RECOIL_KICK * w.recoil;
|
|
1430
1447
|
this._recoilT = RECOIL_TIME;
|
|
@@ -1527,7 +1544,7 @@ export class Player extends Character {
|
|
|
1527
1544
|
const run = moving ? Math.min(1, (this._hspeed ?? 0) / top) : 0;
|
|
1528
1545
|
this.vy = 7.2 * (1 + 0.33 * run);
|
|
1529
1546
|
this.busy = true; // suppress locomotion overrides; cleared on landing
|
|
1530
|
-
this.game.audio?.play('whoosh');
|
|
1547
|
+
this.game.audio?.play?.('whoosh');
|
|
1531
1548
|
// start/speed/end come from CLIP_TIMING (tuned in /anim.html)
|
|
1532
1549
|
this.animator.play(moving ? 'jumpRun' : 'jumpIdle', { once: true, fade: 0.03 });
|
|
1533
1550
|
this.busy = false; // allow air control; anim continues as one-shot
|
|
@@ -1559,7 +1576,7 @@ export class Player extends Character {
|
|
|
1559
1576
|
}
|
|
1560
1577
|
|
|
1561
1578
|
dodge(input) {
|
|
1562
|
-
if (this.stamina < 20) { this.game.audio?.play('whoosh'); return; } // too winded to roll
|
|
1579
|
+
if (this.stamina < 20) { this.game.audio?.play?.('whoosh'); return; } // too winded to roll
|
|
1563
1580
|
this.stamina -= 20;
|
|
1564
1581
|
this._stamPause = 1;
|
|
1565
1582
|
let anim = 'dodgeF';
|
|
@@ -1568,7 +1585,7 @@ export class Player extends Character {
|
|
|
1568
1585
|
else if (input.down('KeyD')) anim = 'dodgeR';
|
|
1569
1586
|
this.busy = true;
|
|
1570
1587
|
this.dodgeT = 0.7;
|
|
1571
|
-
this.game.audio?.play('whoosh');
|
|
1588
|
+
this.game.audio?.play?.('whoosh');
|
|
1572
1589
|
this.animator.play(anim, { once: true, fade: 0.08, timeScale: 1.25, onDone: () => { this.busy = false; } });
|
|
1573
1590
|
// BURST OF MOVEMENT — WHERE YOU ARE RUNNING, not where the lens is pointed.
|
|
1574
1591
|
//
|
|
@@ -1607,7 +1624,7 @@ export class Player extends Character {
|
|
|
1607
1624
|
super.takeDamage(amount, fromPos, opts);
|
|
1608
1625
|
this._combatT = Math.max(this._combatT, 4); // struck → stay in combat
|
|
1609
1626
|
this.game.noteDamageTaken?.();
|
|
1610
|
-
this.game.ui?.flashDamage();
|
|
1627
|
+
this.game.ui?.flashDamage?.();
|
|
1611
1628
|
if (!this.alive) {
|
|
1612
1629
|
// dying mid-aim/mid-reload: cancel the gun so the corpse doesn't fall with the aim
|
|
1613
1630
|
// overlay blended over the death clip, and a cancelled reload can't refill on respawn
|
package/src/systems/riding.js
CHANGED
|
@@ -9,12 +9,31 @@ import { clone as skeletonClone } from 'three/addons/utils/SkeletonUtils.js';
|
|
|
9
9
|
import { loadModel, loadClips } from '../core/assets.js';
|
|
10
10
|
import { clamp, angleLerp } from '../core/utils.js';
|
|
11
11
|
|
|
12
|
+
// WHERE THE SEATED SOURCE POSE COMES FROM. Hardcoded, these are western's horse assets, and a
|
|
13
|
+
// seated pose is not a horse thing: a soldier in a tank hatch, a driver in a car and a rider on
|
|
14
|
+
// a horse all want the same retarget from the same kind of source clip. The paths are content,
|
|
15
|
+
// so they are injected — `setSeatedContent()` — and default to exactly what was here.
|
|
16
|
+
//
|
|
17
|
+
// This is the pattern createCar() already follows and riding.js did not: createCar takes
|
|
18
|
+
// surfaceAt/obstacleAt and knows nothing about any game, which is why it needed no changes to
|
|
19
|
+
// serve a third one.
|
|
20
|
+
let SEAT_RIG = '/assets/horse/Rider.fbx';
|
|
21
|
+
let SEAT_AUDIO = '/assets/audio/horse';
|
|
12
22
|
const HA = '/assets/horse/anims';
|
|
13
|
-
|
|
23
|
+
let RIDER_CLIPS = {
|
|
14
24
|
rideIdle: `${HA}/Rider_Idle_01.fbx`,
|
|
15
25
|
mountL: `${HA}/Rider_Mount_Dismount_Left.fbx`,
|
|
16
26
|
mountR: `${HA}/Rider_Mount_Dismount_Right.fbx`,
|
|
17
27
|
};
|
|
28
|
+
|
|
29
|
+
// A game supplies its own seated source: the rig the pose is read FROM, the clips on it, and
|
|
30
|
+
// where the mount/dismount audio lives. Anything omitted keeps western's.
|
|
31
|
+
// setSeatedContent({ rig: '/assets/mil/Soldier.fbx', clips: { rideIdle: '…/Sit_Turret.fbx' } })
|
|
32
|
+
export function setSeatedContent({ rig, clips, audioDir } = {}) {
|
|
33
|
+
if (rig) { SEAT_RIG = rig; _riderRigPromise = null; } // drop the cache — it holds the old rig
|
|
34
|
+
if (clips) { RIDER_CLIPS = { ...RIDER_CLIPS, ...clips }; _riderRigPromise = null; }
|
|
35
|
+
if (audioDir) SEAT_AUDIO = audioDir;
|
|
36
|
+
}
|
|
18
37
|
// Rider Biped → player-rig bone map, from Unity's Humanoid avatar (R_CG is the Hips). The
|
|
19
38
|
// player is the Modular Fantasy Hero rig (Hips/Shoulder/Elbow/UpperLeg naming), NOT the old
|
|
20
39
|
// peasant (Pelvis/UpperArm/Thigh). Foot→Ball, Toe→Toes (modular has no separate ankle bone).
|
|
@@ -112,7 +131,7 @@ const _poseCache = new Map(); // clip url -> the loaded clip (the rig master i
|
|
|
112
131
|
export async function makeSeatedPose(targetRoot, boneMap = BIPED_TO_PEASANT, clipUrl = null) {
|
|
113
132
|
// FRESH FBX load (not the shared loadModel master — the player's Riding setup poses that and
|
|
114
133
|
// never restores it, so captureBind on it would read a posed rig, not bind).
|
|
115
|
-
if (!_riderRigPromise) _riderRigPromise = Promise.all([new FBXLoader().loadAsync(
|
|
134
|
+
if (!_riderRigPromise) _riderRigPromise = Promise.all([new FBXLoader().loadAsync(SEAT_RIG), loadClips({ rideIdle: RIDER_CLIPS.rideIdle })]);
|
|
116
135
|
const [riderMaster, rClips] = await _riderRigPromise;
|
|
117
136
|
// Pick the clip into a LOCAL — never write it back into rClips. That shared object is the
|
|
118
137
|
// cache: overwriting rideIdle with the chair sit meant the very next call that asked for the
|
|
@@ -244,8 +263,8 @@ export class Riding {
|
|
|
244
263
|
_loadSfx() {
|
|
245
264
|
if (this._sfxLoaded) return;
|
|
246
265
|
this._sfxLoaded = true;
|
|
247
|
-
const H =
|
|
248
|
-
this.game.audio?.loadSamples({
|
|
266
|
+
const H = SEAT_AUDIO;
|
|
267
|
+
this.game.audio?.loadSamples?.({
|
|
249
268
|
step1: `${H}/step1.m4a`, step2: `${H}/step2.m4a`, step3: `${H}/step3.m4a`, step4: `${H}/step4.m4a`, step5: `${H}/step5.m4a`,
|
|
250
269
|
whinny1: `${H}/whinny1.m4a`, whinny2: `${H}/whinny2.m4a`, whinny3: `${H}/whinny3.m4a`,
|
|
251
270
|
neigh: `${H}/neigh.m4a`, neigh2: `${H}/neigh2.m4a`, neigh3: `${H}/neigh3.m4a`,
|
|
@@ -288,7 +307,7 @@ export class Riding {
|
|
|
288
307
|
async setup() {
|
|
289
308
|
const player = this.player.model;
|
|
290
309
|
const [riderRig, rClips] = await Promise.all([
|
|
291
|
-
loadModel(
|
|
310
|
+
loadModel(SEAT_RIG),
|
|
292
311
|
loadClips(RIDER_CLIPS),
|
|
293
312
|
]);
|
|
294
313
|
for (const b of realBonesOf(player)) this.pbone[b.name] = b; // first-occurrence bones (modular dupes)
|
|
@@ -395,7 +414,7 @@ export class Riding {
|
|
|
395
414
|
if (this.game.ownHorse && this.game.ownHorse !== horse) this.game.ownHorse.owner = null;
|
|
396
415
|
this.game.ownHorse = horse; horse.owner = this.player;
|
|
397
416
|
this.mode = 'mounting'; this.mountT = 0; this.speed = 0;
|
|
398
|
-
this._loadSfx(); this.game.audio?.sample('snort', { gain: 0.5 }); // silent the very first mount (samples still decoding)
|
|
417
|
+
this._loadSfx(); this.game.audio?.sample?.('snort', { gain: 0.5 }); // silent the very first mount (samples still decoding)
|
|
399
418
|
this.startClimb(this.climbSide());
|
|
400
419
|
this.besidePos(this.player.position);
|
|
401
420
|
this.player.root.rotation.y = horse.heading;
|
|
@@ -521,7 +540,7 @@ export class Riding {
|
|
|
521
540
|
this.besidePos(this.player.root.position);
|
|
522
541
|
this.player.heading = horse.heading; this.player.root.rotation.set(0, horse.heading, 0); // clear saddle tilt
|
|
523
542
|
this._seatQuat = null; this._reinLoc = null; // recapture seat + rein hands on the next mount
|
|
524
|
-
this.game.audio?.sample('snort', { gain: 0.45 });
|
|
543
|
+
this.game.audio?.sample?.('snort', { gain: 0.45 });
|
|
525
544
|
horse.rider = null; horse.speed = 0;
|
|
526
545
|
this.horse = null; this.mode = 'foot';
|
|
527
546
|
this.player.vy = 0; this.player._capGrounded = false;
|
package/src/systems/shop.js
CHANGED
|
@@ -213,7 +213,7 @@ export class Shop {
|
|
|
213
213
|
_buy(id) {
|
|
214
214
|
if (this._curStock(id) <= 0) { this.ui.toast?.(`He's clean out of that.`); this.game.audio?.play?.('ui'); return; }
|
|
215
215
|
const price = SH.buyPrice(id);
|
|
216
|
-
if (!this.game.loot?.spend(price)) { // spend() returns false and takes nothing if he's short
|
|
216
|
+
if (!this.game.loot?.spend?.(price)) { // spend() returns false and takes nothing if he's short
|
|
217
217
|
this.ui.toast?.(`You're short the coin for that.`);
|
|
218
218
|
this.game.audio?.play?.('ui');
|
|
219
219
|
return;
|
|
@@ -229,7 +229,7 @@ export class Shop {
|
|
|
229
229
|
if (!this.ui.satchel.has(id, 1)) return;
|
|
230
230
|
const price = SH.sellPrice(id);
|
|
231
231
|
this.ui.satchel.remove(id, 1);
|
|
232
|
-
this.game.loot?.add(price); // add() plays the coin chime
|
|
232
|
+
this.game.loot?.add?.(price); // add() plays the coin chime
|
|
233
233
|
this.ui.toast?.(`Sold ${SH.items[id].name} — $${price}.`);
|
|
234
234
|
this._rebuild();
|
|
235
235
|
}
|
package/src/systems/train.js
CHANGED
|
@@ -34,10 +34,23 @@ import { SeatedRider, SEAT, passengerPosture, driverPosture } from './seatedRide
|
|
|
34
34
|
// setTrainContent({ colliders, dropItemsFor, railPaths, railStops, crossing, halts })
|
|
35
35
|
let colliders = [], dropItemsFor = () => null;
|
|
36
36
|
let SURVEY_PATHS = [], SURVEY_STOPS = [], SURVEY_CROSSING = null, RAIL_HALTS = [];
|
|
37
|
+
// KEYS THIS ACCEPTS AND NEVER READS. `railPaths`, `railStops` and `crossing` are assigned to
|
|
38
|
+
// module state that nothing consults — a game supplying them gets silence, and no way to tell
|
|
39
|
+
// that from working. They are kept (removing them would break callers that pass them today)
|
|
40
|
+
// and now say so once, so the next game to reach for one finds out at the call rather than by
|
|
41
|
+
// wondering why its level crossings do nothing.
|
|
42
|
+
const DEAD = new Set();
|
|
43
|
+
const dead = (k) => {
|
|
44
|
+
if (DEAD.has(k)) return;
|
|
45
|
+
DEAD.add(k);
|
|
46
|
+
console.warn(`[train] setTrainContent({ ${k} }) is accepted but never read — it has no effect. `
|
|
47
|
+
+ 'Use railLines / railLineStops / levelCrossing / junction / railTop / halts.');
|
|
48
|
+
};
|
|
49
|
+
|
|
37
50
|
export function setTrainContent(c = {}) {
|
|
38
51
|
if (c.colliders) colliders = c.colliders;
|
|
39
52
|
if (c.dropItemsFor) dropItemsFor = c.dropItemsFor;
|
|
40
|
-
if (c.railPaths) SURVEY_PATHS = c.railPaths;
|
|
53
|
+
if (c.railPaths) { SURVEY_PATHS = c.railPaths; dead('railPaths'); }
|
|
41
54
|
// THE TRACK AUTHOR'S OWN SURVEY. Without it the fleet grades its own polyline and runs
|
|
42
55
|
// beside the rails the county actually laid.
|
|
43
56
|
if (c.railLines) LINES_IN = c.railLines;
|
|
@@ -45,8 +58,8 @@ export function setTrainContent(c = {}) {
|
|
|
45
58
|
if (c.levelCrossing) XING_IN = c.levelCrossing;
|
|
46
59
|
if (c.junction) JUNC_IN = c.junction;
|
|
47
60
|
if (c.railTop != null) { RAIL_TOP = c.railTop; RAIL_Y = RAIL_TOP - FLANGE; }
|
|
48
|
-
if (c.railStops) SURVEY_STOPS = c.railStops;
|
|
49
|
-
if (c.crossing !== undefined) SURVEY_CROSSING = c.crossing;
|
|
61
|
+
if (c.railStops) { SURVEY_STOPS = c.railStops; dead('railStops'); }
|
|
62
|
+
if (c.crossing !== undefined) { SURVEY_CROSSING = c.crossing; dead('crossing'); }
|
|
50
63
|
if (c.halts) RAIL_HALTS = c.halts;
|
|
51
64
|
}
|
|
52
65
|
import { dist2D } from '../core/utils.js';
|
package/src/vehicle/car.js
CHANGED
|
@@ -164,7 +164,10 @@ export function createCar({ surfaceAt, obstacleAt = null, x = 0, y = 0, z = 0, h
|
|
|
164
164
|
const seatY = surfaceAt?.(x, z, y);
|
|
165
165
|
const car = {
|
|
166
166
|
cfg,
|
|
167
|
-
|
|
167
|
+
// cfg, NOT CAR_DEFAULTS. Every later frame uses cfg.rideHeight, so spawning from the
|
|
168
|
+
// default drops a vehicle with a custom one at the wrong height and pops it up on the
|
|
169
|
+
// first update — a tank at 0.9 starts 0.56 m low and jumps.
|
|
170
|
+
pos: { x, y: (seatY ?? y) + cfg.rideHeight, z },
|
|
168
171
|
heading, // where the car POINTS
|
|
169
172
|
travel: heading, // where it is actually GOING — the two differ when it slides
|
|
170
173
|
speed: 0,
|