sindicate 0.17.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 +36 -0
- package/package.json +1 -1
- package/src/core/anim.js +5 -0
- package/src/core/overrides.js +5 -1
- package/src/systems/player.js +25 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0 — two regressions, caught from outside
|
|
4
|
+
|
|
5
|
+
Everything here came from a third game running against 0.17.0 and reporting back. One fix I
|
|
6
|
+
shipped broken, one fix I shipped incomplete, and both were found by a consumer rather than by
|
|
7
|
+
me. Nothing else changed, and western and fable do not move.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`RETRIGGER_AT` was undeclared** (`core/anim.js`), so 0.17.0's own one-shot guard threw a
|
|
12
|
+
`ReferenceError` on the exact path it was written to protect — re-triggering a one-shot that is
|
|
13
|
+
still playing. Western was affected too: a revolver re-triggering its fire clip does that on
|
|
14
|
+
every second shot. Frame-hook isolation caught it rather than killing the frame, which is the
|
|
15
|
+
only reason it was not immediately fatal, but the one-shot silently did not play.
|
|
16
|
+
- **A split answer's overlay ran as an infinite loop.** `setOverlay` defaults to
|
|
17
|
+
`LoopRepeat/Infinity`, and the `{ base, overlay }` shape did not say otherwise, so a reload
|
|
18
|
+
overlay played for ever. The answer is now `{ base, overlay, once }`. It cannot be inferred: a
|
|
19
|
+
reload or holster overlay is a one-shot, a held aim pose must loop, and both are legitimate
|
|
20
|
+
answers to the same question.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `locomotionClip` may answer `{ base, overlay, once }` as well as a clip name — a body split is
|
|
25
|
+
two simultaneous answers and one name cannot express both — and the engine now asks BEFORE
|
|
26
|
+
dropping the overlay rather than after. Dropping it first rewound the overlay sixty times a
|
|
27
|
+
second, which a game could only stop by patching `setOverlay`. A string answer behaves exactly
|
|
28
|
+
as in 0.17.0; `null`/`undefined` still mean the engine owns the slot.
|
|
29
|
+
|
|
30
|
+
### The lesson, recorded because it caused both
|
|
31
|
+
|
|
32
|
+
Both regressions survived because I verified the wrong layer. `node --check` passes on an
|
|
33
|
+
undeclared identifier — it is a runtime `ReferenceError`, not a syntax error — and a test that
|
|
34
|
+
watches what was *asked for* is blind to what the ask *did*. The tests now drive the real
|
|
35
|
+
per-frame sequence and watch the animation across more than one clip length. An edit anchored on
|
|
36
|
+
`import * as THREE from 'three';` also silently matches nothing in a file that imports from
|
|
37
|
+
`'three/webgpu'`, which is how the constant went missing in the first place.
|
|
38
|
+
|
|
3
39
|
## 0.17.0 — towns, and a third game's report card
|
|
4
40
|
|
|
5
41
|
Two halves. Cities — the thing the road system was always for. And the first pass through
|
package/package.json
CHANGED
package/src/core/anim.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// with automatic retargeting per character rig.
|
|
3
3
|
import * as THREE from 'three/webgpu';
|
|
4
4
|
import { buildRetargetContext, retargetClip } from './retarget.js';
|
|
5
|
+
|
|
6
|
+
// How far a one-shot must have played before another play() will restart it, as a fraction of
|
|
7
|
+
// its duration. Below this the call is ignored and the clip runs on — see play(). A caller may
|
|
8
|
+
// override per call with `retrigger`.
|
|
9
|
+
const RETRIGGER_AT = 0.5;
|
|
5
10
|
// Per-clip timing (start/speed/end trims) and the clip->source-rig table are GAME
|
|
6
11
|
// DATA, not engine code — a game registers them once at boot:
|
|
7
12
|
// setClipDefaults({ timing: CLIP_TIMING, rigs: CLIP_RIGS })
|
package/src/core/overrides.js
CHANGED
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
export const POINTS = {
|
|
40
40
|
locomotionClip: {
|
|
41
41
|
args: '{ moving, sprinting, aiming, reloading, holstering, drawn, weapon, animator, has }',
|
|
42
|
-
returns: 'a clip name
|
|
42
|
+
returns: 'a clip name, or { base, overlay, once } when the body is split, or null to let '
|
|
43
|
+
+ 'the engine choose. A split needs TWO simultaneous answers — a legs-only base and '
|
|
44
|
+
+ 'an upper-body overlay — which a single name cannot express. `once` says whether '
|
|
45
|
+
+ 'that overlay is a one-shot (a reload, a holster) or a held pose that loops (an '
|
|
46
|
+
+ 'aim): it cannot be inferred, and both are legitimate answers to this question.',
|
|
43
47
|
why: 'western\'s gunslinger pack has a full-body clip for every gun state. Kubold\'s rifle '
|
|
44
48
|
+ 'set has one standing reload and no walking reload, so a game on it needs the legs to '
|
|
45
49
|
+ 'keep moving through a reload the engine would otherwise stop dead.',
|
package/src/systems/player.js
CHANGED
|
@@ -931,18 +931,41 @@ export class Player extends Character {
|
|
|
931
931
|
// full-body clips — the holster timer still reads it, so it must still exist.)
|
|
932
932
|
const gunPose = !!gunW && (this.aiming || this.reloading || this._drawT > 0 || this._gunRecoilT > 0);
|
|
933
933
|
if (!this.busy && !this.airborne) {
|
|
934
|
-
if (this.animator._overlay) this.animator.setOverlay(null, { fade: BASE_FADE });
|
|
935
934
|
// WHICH CLIP IS THE BODY PLAYING? A decision point (core/overrides.js), because the
|
|
936
935
|
// answer below is right for a pack that has a full-body clip for every gun state and
|
|
937
936
|
// wrong for one that does not: Kubold's rifle set has a single standing reload, so
|
|
938
937
|
// `gunReloadWalk` maps onto it and walking into a reload stops the legs dead. A game
|
|
939
938
|
// whose pack differs answers for itself; one that says nothing gets exactly this.
|
|
940
|
-
|
|
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', {
|
|
941
946
|
moving, sprinting, drawn: this.drawn, aiming: this.aiming,
|
|
942
947
|
reloading: this.reloading, holstering: this._holstering,
|
|
943
948
|
weapon: this.weapon, animator: this.animator,
|
|
944
949
|
has: (n) => !!this.animator.actions[n],
|
|
945
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
|
+
|
|
946
969
|
if (chosen) this.animator.play(chosen);
|
|
947
970
|
else if (this.drawn && gunW) {
|
|
948
971
|
if (this.reloading) this.animator.play(moving ? 'gunReloadWalk' : 'gunReload');
|