sindicate 0.16.0 → 0.18.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 +116 -0
- package/package.json +1 -1
- package/src/audio/synth.js +31 -13
- package/src/core/anim.js +27 -0
- package/src/core/engine.js +50 -5
- package/src/core/overrides.js +88 -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 +56 -16
- 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/deadeye.js
CHANGED
|
@@ -96,7 +96,7 @@ export class DeadEye {
|
|
|
96
96
|
if (!p.drawn && p._drawT <= 0) p._startDraw();
|
|
97
97
|
}
|
|
98
98
|
if (!this._canUse()) return false;
|
|
99
|
-
if ((p.deadEye ?? 0) < DEAD_EYE.minCharge) { this.game.audio?.play('dryfire'); return false; }
|
|
99
|
+
if ((p.deadEye ?? 0) < DEAD_EYE.minCharge) { this.game.audio?.play?.('dryfire'); return false; }
|
|
100
100
|
this.active = true;
|
|
101
101
|
this._tagT = 0;
|
|
102
102
|
this.tags.length = 0;
|
|
@@ -240,8 +240,8 @@ export class DeadEye {
|
|
|
240
240
|
const cap = Math.min(DEAD_EYE.maxTags, p.ammo ?? 0);
|
|
241
241
|
// A refused mark must SAY it was refused — a click that silently does nothing reads as a broken
|
|
242
242
|
// button, and you will keep clicking it. Out of chambers and out of meter are the two caps.
|
|
243
|
-
if (this.tags.length >= cap) { this.game.audio?.play('dryfire'); return; }
|
|
244
|
-
if ((p.deadEye ?? 0) < DEAD_EYE.tagCost) { this.game.audio?.play('dryfire'); return; }
|
|
243
|
+
if (this.tags.length >= cap) { this.game.audio?.play?.('dryfire'); return; }
|
|
244
|
+
if ((p.deadEye ?? 0) < DEAD_EYE.tagCost) { this.game.audio?.play?.('dryfire'); return; }
|
|
245
245
|
if (this._tagT > 0) return; // ...but a double-click inside 0.16s is just a hand
|
|
246
246
|
|
|
247
247
|
_o.copy(this.game.camera.position);
|
|
@@ -274,7 +274,7 @@ export class DeadEye {
|
|
|
274
274
|
});
|
|
275
275
|
p.deadEye = clamp(p.deadEye - DEAD_EYE.tagCost, 0, DEAD_EYE.meterMax);
|
|
276
276
|
this._tagT = DEAD_EYE.tagCooldown;
|
|
277
|
-
this.game.audio?.play('ui');
|
|
277
|
+
this.game.audio?.play?.('ui');
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
// Closest approach of ray (o,dir — dir normalized) to segment [a,b]. Returns the ray
|
|
@@ -118,6 +118,6 @@ export class Encounters {
|
|
|
118
118
|
gun.leashR = 40; // he will follow you a way for this — but not across the county
|
|
119
119
|
}
|
|
120
120
|
this.game.ui?.toast?.(`${grp.name} — they saw that.`, 3000);
|
|
121
|
-
this.game.audio?.play('aggro');
|
|
121
|
+
this.game.audio?.play?.('aggro');
|
|
122
122
|
}
|
|
123
123
|
}
|
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
|
|
@@ -926,8 +931,43 @@ export class Player extends Character {
|
|
|
926
931
|
// full-body clips — the holster timer still reads it, so it must still exist.)
|
|
927
932
|
const gunPose = !!gunW && (this.aiming || this.reloading || this._drawT > 0 || this._gunRecoilT > 0);
|
|
928
933
|
if (!this.busy && !this.airborne) {
|
|
929
|
-
|
|
930
|
-
|
|
934
|
+
// WHICH CLIP IS THE BODY PLAYING? A decision point (core/overrides.js), because the
|
|
935
|
+
// answer below is right for a pack that has a full-body clip for every gun state and
|
|
936
|
+
// wrong for one that does not: Kubold's rifle set has a single standing reload, so
|
|
937
|
+
// `gunReloadWalk` maps onto it and walking into a reload stops the legs dead. A game
|
|
938
|
+
// whose pack differs answers for itself; one that says nothing gets exactly this.
|
|
939
|
+
//
|
|
940
|
+
// IT MAY ANSWER WITH BOTH HALVES. A body split is two simultaneous answers — a legs-only
|
|
941
|
+
// base AND an upper-body overlay — so a single clip name cannot express one. Returning
|
|
942
|
+
// `{ base, overlay }` says both. And the ask now happens BEFORE the overlay is dropped:
|
|
943
|
+
// dropping it first rewound the overlay sixty times a second, which a game could only
|
|
944
|
+
// stop by patching setOverlay. That patch was the last one Blacksand still carried.
|
|
945
|
+
const answer = this.game?.engine?.overrides?.ask('locomotionClip', {
|
|
946
|
+
moving, sprinting, drawn: this.drawn, aiming: this.aiming,
|
|
947
|
+
reloading: this.reloading, holstering: this._holstering,
|
|
948
|
+
weapon: this.weapon, animator: this.animator,
|
|
949
|
+
has: (n) => !!this.animator.actions[n],
|
|
950
|
+
});
|
|
951
|
+
const chosen = typeof answer === 'string' ? answer : answer?.base ?? null;
|
|
952
|
+
const wantOverlay = typeof answer === 'object' && answer !== null ? answer.overlay ?? null : null;
|
|
953
|
+
// ONE-SHOT OR HELD? It cannot be inferred and the two are both legitimate answers to the
|
|
954
|
+
// same question: a reload or holster overlay plays once and stops, `gunAimUpper` is a
|
|
955
|
+
// held pose that must loop. setOverlay defaults to LoopRepeat/Infinity, so an unstated
|
|
956
|
+
// reload overlay ran for ever — the answer has to say.
|
|
957
|
+
const overlayOnce = typeof answer === 'object' && answer !== null ? !!answer.once : false;
|
|
958
|
+
|
|
959
|
+
// Only clear the overlay when nothing asked to keep one. `undefined` means the game did
|
|
960
|
+
// not answer at all, so the engine owns the slot exactly as before.
|
|
961
|
+
// setOverlay already returns early when the action is unchanged, so re-asserting the
|
|
962
|
+
// same overlay every frame costs nothing and cannot rewind it.
|
|
963
|
+
if (wantOverlay) {
|
|
964
|
+
this.animator.setOverlay(wantOverlay, { fade: BASE_FADE, once: overlayOnce });
|
|
965
|
+
} else if (this.animator._overlay) {
|
|
966
|
+
this.animator.setOverlay(null, { fade: BASE_FADE });
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (chosen) this.animator.play(chosen);
|
|
970
|
+
else if (this.drawn && gunW) {
|
|
931
971
|
if (this.reloading) this.animator.play(moving ? 'gunReloadWalk' : 'gunReload');
|
|
932
972
|
else if (this._holstering) this.animator.play(moving ? 'gunWalkHolster' : 'gunHolster');
|
|
933
973
|
else if (this.aiming) this.animator.play(moving ? 'gunWalkAimed' : 'gunAim');
|
|
@@ -1153,7 +1193,7 @@ export class Player extends Character {
|
|
|
1153
1193
|
this.drawn = false; // fresh weapon starts holstered — first fire/aim plays the draw
|
|
1154
1194
|
this._holstering = false;
|
|
1155
1195
|
this.placeWeapons();
|
|
1156
|
-
this.game.audio?.play('ui');
|
|
1196
|
+
this.game.audio?.play?.('ui');
|
|
1157
1197
|
}
|
|
1158
1198
|
// WHISTLE (H) — call the companion horse over. On foot only (this whole block never runs mounted).
|
|
1159
1199
|
if (input.hit('KeyH') && !this.busy) this.whistle();
|
|
@@ -1252,11 +1292,11 @@ export class Player extends Character {
|
|
|
1252
1292
|
this._fireBuf = 0; // consumed — one click, one shot
|
|
1253
1293
|
this._holsterT = HOLSTER_IDLE;
|
|
1254
1294
|
if (this.ammo > 0) this._fire(w, movin);
|
|
1255
|
-
else { this.game.audio?.play('dryfire'); this._gunFireT = 0.25; }
|
|
1295
|
+
else { this.game.audio?.play?.('dryfire'); this._gunFireT = 0.25; }
|
|
1256
1296
|
}
|
|
1257
1297
|
}
|
|
1258
1298
|
|
|
1259
|
-
if (this.drawn) this.game.ui?.aimTarget(this._aimingAtBody());
|
|
1299
|
+
if (this.drawn) this.game.ui?.aimTarget?.(this._aimingAtBody());
|
|
1260
1300
|
if (this.drawn) this._menaceCheck(dt);
|
|
1261
1301
|
if (this.aiming || this.reloading || this._drawT > 0) this._holsterT = HOLSTER_IDLE;
|
|
1262
1302
|
if (this.drawn) this.game.hint?.('gun', 'Left-click to fire — hold right-click to aim down the sights. R reloads.');
|
|
@@ -1302,7 +1342,7 @@ export class Player extends Character {
|
|
|
1302
1342
|
if (!this.drawn) { this.drawn = true; this.placeWeapons(); }
|
|
1303
1343
|
if (this._holstering) this._abortHolster();
|
|
1304
1344
|
this._holsterT = HOLSTER_IDLE;
|
|
1305
|
-
this.game.audio?.play('reload');
|
|
1345
|
+
this.game.audio?.play?.('reload');
|
|
1306
1346
|
const done = () => {
|
|
1307
1347
|
if (!this.reloading) return; // cancelled — the old ammo stands
|
|
1308
1348
|
this.reloading = false;
|
|
@@ -1379,7 +1419,7 @@ export class Player extends Character {
|
|
|
1379
1419
|
// at 5 Hz meant walking all 150 souls 750 times a second to re-panic the same dozen men.
|
|
1380
1420
|
if (this._menaceCool <= 0) {
|
|
1381
1421
|
this._menaceCool = 4;
|
|
1382
|
-
this.game.crime?.report('menace', best.position.clone());
|
|
1422
|
+
this.game.crime?.report?.('menace', best.position.clone());
|
|
1383
1423
|
}
|
|
1384
1424
|
}
|
|
1385
1425
|
|
|
@@ -1424,7 +1464,7 @@ export class Player extends Character {
|
|
|
1424
1464
|
this.ammo = this._mags[this.weapon] = Math.max(0, this.ammo - 1);
|
|
1425
1465
|
this.game.combat?.fireBullet?.({ from, dir, damage: w.damage, speed: 90, range: w.range, friendly: true, shooter: this });
|
|
1426
1466
|
this.game.vfx?.muzzleFlash?.(from, dir);
|
|
1427
|
-
this.game.audio?.play('gunshot');
|
|
1467
|
+
this.game.audio?.play?.('gunshot');
|
|
1428
1468
|
// camera kick (RECOIL_KICK × recoil, eased settle — see update)
|
|
1429
1469
|
this._recoilAmp = RECOIL_KICK * w.recoil;
|
|
1430
1470
|
this._recoilT = RECOIL_TIME;
|
|
@@ -1527,7 +1567,7 @@ export class Player extends Character {
|
|
|
1527
1567
|
const run = moving ? Math.min(1, (this._hspeed ?? 0) / top) : 0;
|
|
1528
1568
|
this.vy = 7.2 * (1 + 0.33 * run);
|
|
1529
1569
|
this.busy = true; // suppress locomotion overrides; cleared on landing
|
|
1530
|
-
this.game.audio?.play('whoosh');
|
|
1570
|
+
this.game.audio?.play?.('whoosh');
|
|
1531
1571
|
// start/speed/end come from CLIP_TIMING (tuned in /anim.html)
|
|
1532
1572
|
this.animator.play(moving ? 'jumpRun' : 'jumpIdle', { once: true, fade: 0.03 });
|
|
1533
1573
|
this.busy = false; // allow air control; anim continues as one-shot
|
|
@@ -1559,7 +1599,7 @@ export class Player extends Character {
|
|
|
1559
1599
|
}
|
|
1560
1600
|
|
|
1561
1601
|
dodge(input) {
|
|
1562
|
-
if (this.stamina < 20) { this.game.audio?.play('whoosh'); return; } // too winded to roll
|
|
1602
|
+
if (this.stamina < 20) { this.game.audio?.play?.('whoosh'); return; } // too winded to roll
|
|
1563
1603
|
this.stamina -= 20;
|
|
1564
1604
|
this._stamPause = 1;
|
|
1565
1605
|
let anim = 'dodgeF';
|
|
@@ -1568,7 +1608,7 @@ export class Player extends Character {
|
|
|
1568
1608
|
else if (input.down('KeyD')) anim = 'dodgeR';
|
|
1569
1609
|
this.busy = true;
|
|
1570
1610
|
this.dodgeT = 0.7;
|
|
1571
|
-
this.game.audio?.play('whoosh');
|
|
1611
|
+
this.game.audio?.play?.('whoosh');
|
|
1572
1612
|
this.animator.play(anim, { once: true, fade: 0.08, timeScale: 1.25, onDone: () => { this.busy = false; } });
|
|
1573
1613
|
// BURST OF MOVEMENT — WHERE YOU ARE RUNNING, not where the lens is pointed.
|
|
1574
1614
|
//
|
|
@@ -1607,7 +1647,7 @@ export class Player extends Character {
|
|
|
1607
1647
|
super.takeDamage(amount, fromPos, opts);
|
|
1608
1648
|
this._combatT = Math.max(this._combatT, 4); // struck → stay in combat
|
|
1609
1649
|
this.game.noteDamageTaken?.();
|
|
1610
|
-
this.game.ui?.flashDamage();
|
|
1650
|
+
this.game.ui?.flashDamage?.();
|
|
1611
1651
|
if (!this.alive) {
|
|
1612
1652
|
// dying mid-aim/mid-reload: cancel the gun so the corpse doesn't fall with the aim
|
|
1613
1653
|
// 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,
|