minimojs 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +82 -285
- package/dist/internal/AnimationSystem.js +345 -0
- package/dist/internal/AssetSystem.d.ts +1 -0
- package/dist/internal/AssetSystem.js +101 -0
- package/dist/internal/BackgroundSystem.d.ts +1 -0
- package/dist/internal/BackgroundSystem.js +26 -0
- package/dist/internal/CanvasSystem.d.ts +1 -0
- package/dist/internal/CanvasSystem.js +51 -0
- package/dist/internal/ExplosionSystem.d.ts +1 -0
- package/dist/internal/ExplosionSystem.js +540 -0
- package/dist/internal/InputSystem.d.ts +1 -0
- package/dist/internal/InputSystem.js +265 -0
- package/dist/internal/LoopSystem.d.ts +1 -0
- package/dist/internal/LoopSystem.js +61 -0
- package/dist/internal/PhysicsSystem.d.ts +1 -0
- package/dist/internal/PhysicsSystem.js +174 -0
- package/dist/internal/RenderSystem.d.ts +1 -0
- package/dist/internal/RenderSystem.js +910 -0
- package/dist/internal/SoundSystem.d.ts +1 -0
- package/dist/internal/SoundSystem.js +55 -0
- package/dist/internal/SpriteSystem.d.ts +1 -0
- package/dist/internal/SpriteSystem.js +32 -0
- package/dist/internal/TextSystem.d.ts +1 -0
- package/dist/internal/TextSystem.js +16 -0
- package/dist/internal/TimerSystem.d.ts +1 -0
- package/dist/internal/TimerSystem.js +43 -0
- package/dist/internal/TrailSystem.d.ts +1 -0
- package/dist/internal/TrailSystem.js +116 -0
- package/dist/internal/TransitionSystem.d.ts +1 -0
- package/dist/internal/TransitionSystem.js +74 -0
- package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.js +198 -0
- package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.js +120 -0
- package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.js +599 -0
- package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.js +13 -0
- package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.js +447 -0
- package/dist/minimo-arcaderacer.d.ts +1431 -0
- package/dist/minimo-arcaderacer.js +2060 -0
- package/dist/minimo.d.ts +1412 -162
- package/dist/minimo.js +2083 -816
- package/package.json +3 -2
- package/dist/animations.js +0 -30
- package/dist/audio.js +0 -17
- package/dist/game.js +0 -1105
- package/dist/input.js +0 -185
- package/dist/internal-types.js +0 -4
- package/dist/physics.js +0 -10
- package/dist/render.js +0 -75
- package/dist/sprite.js +0 -149
- package/dist/timers.js +0 -23
- /package/dist/{pointer-info.js → internal/AnimationSystem.d.ts} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export class SoundSystem {
|
|
3
|
+
constructor() {
|
|
4
|
+
/** @internal */
|
|
5
|
+
this._audioCtx = null;
|
|
6
|
+
}
|
|
7
|
+
sound(freq, durationMs) {
|
|
8
|
+
const ctx = this.getAudioContext();
|
|
9
|
+
const oscillator = ctx.createOscillator();
|
|
10
|
+
const gain = ctx.createGain();
|
|
11
|
+
oscillator.type = "square";
|
|
12
|
+
oscillator.frequency.setValueAtTime(freq, ctx.currentTime);
|
|
13
|
+
gain.gain.setValueAtTime(0.08, ctx.currentTime);
|
|
14
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + durationMs / 1000);
|
|
15
|
+
oscillator.connect(gain);
|
|
16
|
+
gain.connect(ctx.destination);
|
|
17
|
+
oscillator.start(ctx.currentTime);
|
|
18
|
+
oscillator.stop(ctx.currentTime + durationMs / 1000);
|
|
19
|
+
}
|
|
20
|
+
soundSequence(...tones) {
|
|
21
|
+
if (tones.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
const ctx = this.getAudioContext();
|
|
24
|
+
let startTime = ctx.currentTime;
|
|
25
|
+
for (const [freq, durationMs] of tones) {
|
|
26
|
+
if (!Number.isFinite(freq) || !Number.isFinite(durationMs))
|
|
27
|
+
continue;
|
|
28
|
+
if (freq <= 0 || durationMs <= 0)
|
|
29
|
+
continue;
|
|
30
|
+
const durationSec = durationMs / 1000;
|
|
31
|
+
const oscillator = ctx.createOscillator();
|
|
32
|
+
const gain = ctx.createGain();
|
|
33
|
+
oscillator.type = "square";
|
|
34
|
+
oscillator.frequency.setValueAtTime(freq, startTime);
|
|
35
|
+
gain.gain.setValueAtTime(0.08, startTime);
|
|
36
|
+
gain.gain.exponentialRampToValueAtTime(0.001, startTime + durationSec);
|
|
37
|
+
oscillator.connect(gain);
|
|
38
|
+
gain.connect(ctx.destination);
|
|
39
|
+
oscillator.start(startTime);
|
|
40
|
+
oscillator.stop(startTime + durationSec);
|
|
41
|
+
startTime += durationSec;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** @internal */
|
|
45
|
+
getAudioContext() {
|
|
46
|
+
if (!this._audioCtx) {
|
|
47
|
+
this._audioCtx = new AudioContext();
|
|
48
|
+
}
|
|
49
|
+
const ctx = this._audioCtx;
|
|
50
|
+
if (ctx.state === "suspended") {
|
|
51
|
+
ctx.resume();
|
|
52
|
+
}
|
|
53
|
+
return ctx;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export class SpriteSystem {
|
|
3
|
+
constructor(
|
|
4
|
+
/** @internal */ animationSystem,
|
|
5
|
+
/** @internal */ game) {
|
|
6
|
+
this.animationSystem = animationSystem;
|
|
7
|
+
this.game = game;
|
|
8
|
+
/** @internal */
|
|
9
|
+
this._sprites = [];
|
|
10
|
+
}
|
|
11
|
+
add(sprite) {
|
|
12
|
+
sprite._setGame(this.game);
|
|
13
|
+
this._sprites.push(sprite);
|
|
14
|
+
return sprite;
|
|
15
|
+
}
|
|
16
|
+
destroySprite(sprite) {
|
|
17
|
+
const idx = this._sprites.indexOf(sprite);
|
|
18
|
+
if (idx !== -1) {
|
|
19
|
+
this._sprites.splice(idx, 1);
|
|
20
|
+
}
|
|
21
|
+
this.animationSystem.clearSpriteAnimations(sprite);
|
|
22
|
+
}
|
|
23
|
+
getSprites() {
|
|
24
|
+
return [...this._sprites];
|
|
25
|
+
}
|
|
26
|
+
getMutableSprites() {
|
|
27
|
+
return this._sprites;
|
|
28
|
+
}
|
|
29
|
+
clearAll() {
|
|
30
|
+
this._sprites = [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export class TextSystem {
|
|
3
|
+
constructor() {
|
|
4
|
+
/** @internal */
|
|
5
|
+
this._entries = [];
|
|
6
|
+
}
|
|
7
|
+
drawText(text, x, y, fontSize, color = "#ffffff", centered = false) {
|
|
8
|
+
this._entries.push({ text, x, y, fontSize, color, centered });
|
|
9
|
+
}
|
|
10
|
+
getEntries() {
|
|
11
|
+
return this._entries;
|
|
12
|
+
}
|
|
13
|
+
clear() {
|
|
14
|
+
this._entries = [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export class TimerSystem {
|
|
3
|
+
constructor() {
|
|
4
|
+
/** @internal */
|
|
5
|
+
this._timers = [];
|
|
6
|
+
/** @internal */
|
|
7
|
+
this._timerIdCounter = 0;
|
|
8
|
+
}
|
|
9
|
+
update(dtMs) {
|
|
10
|
+
const timers = this._timers;
|
|
11
|
+
const toRemove = new Set();
|
|
12
|
+
const toFire = [];
|
|
13
|
+
for (const timer of timers) {
|
|
14
|
+
timer.elapsed += dtMs;
|
|
15
|
+
if (timer.elapsed >= timer.delayMs) {
|
|
16
|
+
toFire.push(timer);
|
|
17
|
+
if (timer.repeat) {
|
|
18
|
+
timer.elapsed -= timer.delayMs;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
toRemove.add(timer.id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (toRemove.size > 0) {
|
|
26
|
+
this._timers = timers.filter((t) => !toRemove.has(t.id));
|
|
27
|
+
}
|
|
28
|
+
for (const timer of toFire) {
|
|
29
|
+
timer.callback();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
addTimer(delayMs, repeat, callback) {
|
|
33
|
+
const id = ++this._timerIdCounter;
|
|
34
|
+
this._timers.push({ id, delayMs, elapsed: 0, repeat, callback });
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
clearTimer(id) {
|
|
38
|
+
this._timers = this._timers.filter((t) => t.id !== id);
|
|
39
|
+
}
|
|
40
|
+
clearAll() {
|
|
41
|
+
this._timers = [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const DEFAULT_OPTIONS = {
|
|
2
|
+
durationMs: 300,
|
|
3
|
+
spacingMs: 40,
|
|
4
|
+
fadeMs: 180,
|
|
5
|
+
alpha: 0.45,
|
|
6
|
+
scaleTo: 0.8,
|
|
7
|
+
};
|
|
8
|
+
/** @internal */
|
|
9
|
+
export class TrailSystem {
|
|
10
|
+
constructor() {
|
|
11
|
+
/** @internal */
|
|
12
|
+
this._emitters = [];
|
|
13
|
+
/** @internal */
|
|
14
|
+
this._ghosts = [];
|
|
15
|
+
}
|
|
16
|
+
animateTrail(sprite, options, onComplete) {
|
|
17
|
+
const durationMs = this.sanitizeDuration(options?.durationMs, DEFAULT_OPTIONS.durationMs);
|
|
18
|
+
const spacingMs = this.sanitizeDuration(options?.spacingMs, DEFAULT_OPTIONS.spacingMs);
|
|
19
|
+
const fadeMs = this.sanitizeDuration(options?.fadeMs, DEFAULT_OPTIONS.fadeMs);
|
|
20
|
+
const alpha = this.sanitizeAlpha(options?.alpha, DEFAULT_OPTIONS.alpha);
|
|
21
|
+
const scaleTo = this.sanitizeScale(options?.scaleTo, DEFAULT_OPTIONS.scaleTo);
|
|
22
|
+
const emitters = this._emitters.filter((entry) => entry.sprite !== sprite);
|
|
23
|
+
emitters.push({
|
|
24
|
+
sprite,
|
|
25
|
+
elapsedMs: 0,
|
|
26
|
+
durationMs,
|
|
27
|
+
timeUntilNextEmitMs: 0,
|
|
28
|
+
spacingMs,
|
|
29
|
+
fadeMs,
|
|
30
|
+
alpha,
|
|
31
|
+
scaleTo,
|
|
32
|
+
onComplete,
|
|
33
|
+
});
|
|
34
|
+
this._emitters = emitters;
|
|
35
|
+
}
|
|
36
|
+
update(dtMs, getSnapshot) {
|
|
37
|
+
const emitters = this._emitters;
|
|
38
|
+
const toRemove = [];
|
|
39
|
+
for (let i = 0; i < emitters.length; i++) {
|
|
40
|
+
const emitter = emitters[i];
|
|
41
|
+
emitter.elapsedMs += dtMs;
|
|
42
|
+
emitter.timeUntilNextEmitMs -= dtMs;
|
|
43
|
+
while (emitter.timeUntilNextEmitMs <= 0 && emitter.elapsedMs <= emitter.durationMs) {
|
|
44
|
+
const snapshot = getSnapshot(emitter.sprite);
|
|
45
|
+
this._ghosts.push({
|
|
46
|
+
canvas: snapshot.canvas,
|
|
47
|
+
layer: snapshot.layer,
|
|
48
|
+
ignoreScroll: snapshot.ignoreScroll,
|
|
49
|
+
x: snapshot.x,
|
|
50
|
+
y: snapshot.y,
|
|
51
|
+
rotation: snapshot.rotation,
|
|
52
|
+
alpha: snapshot.alpha * emitter.alpha,
|
|
53
|
+
flipX: snapshot.flipX,
|
|
54
|
+
flipY: snapshot.flipY,
|
|
55
|
+
drawWidth: snapshot.drawWidth,
|
|
56
|
+
drawHeight: snapshot.drawHeight,
|
|
57
|
+
ageMs: 0,
|
|
58
|
+
fadeMs: emitter.fadeMs,
|
|
59
|
+
startAlpha: snapshot.alpha * emitter.alpha,
|
|
60
|
+
baseDrawWidth: snapshot.drawWidth,
|
|
61
|
+
baseDrawHeight: snapshot.drawHeight,
|
|
62
|
+
scaleTo: emitter.scaleTo,
|
|
63
|
+
});
|
|
64
|
+
emitter.timeUntilNextEmitMs += emitter.spacingMs;
|
|
65
|
+
}
|
|
66
|
+
if (emitter.elapsedMs >= emitter.durationMs) {
|
|
67
|
+
toRemove.push(i);
|
|
68
|
+
emitter.onComplete?.();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (let i = toRemove.length - 1; i >= 0; i--) {
|
|
72
|
+
emitters.splice(toRemove[i], 1);
|
|
73
|
+
}
|
|
74
|
+
const ghosts = this._ghosts;
|
|
75
|
+
for (let i = ghosts.length - 1; i >= 0; i--) {
|
|
76
|
+
const ghost = ghosts[i];
|
|
77
|
+
ghost.ageMs += dtMs;
|
|
78
|
+
const t = Math.min(ghost.ageMs / ghost.fadeMs, 1);
|
|
79
|
+
const scale = 1 + (ghost.scaleTo - 1) * t;
|
|
80
|
+
ghost.alpha = ghost.startAlpha * (1 - t);
|
|
81
|
+
ghost.drawWidth = ghost.baseDrawWidth * scale;
|
|
82
|
+
ghost.drawHeight = ghost.baseDrawHeight * scale;
|
|
83
|
+
if (t >= 1) {
|
|
84
|
+
ghosts.splice(i, 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
getRenderEntries() {
|
|
89
|
+
return this._ghosts;
|
|
90
|
+
}
|
|
91
|
+
clearSpriteTrails(sprite) {
|
|
92
|
+
this._emitters = this._emitters.filter((entry) => entry.sprite !== sprite);
|
|
93
|
+
}
|
|
94
|
+
clearAll() {
|
|
95
|
+
this._emitters = [];
|
|
96
|
+
this._ghosts = [];
|
|
97
|
+
}
|
|
98
|
+
/** @internal */
|
|
99
|
+
sanitizeDuration(value, fallback) {
|
|
100
|
+
if (!Number.isFinite(value))
|
|
101
|
+
return fallback;
|
|
102
|
+
return Math.max(1, value);
|
|
103
|
+
}
|
|
104
|
+
/** @internal */
|
|
105
|
+
sanitizeAlpha(value, fallback) {
|
|
106
|
+
if (!Number.isFinite(value))
|
|
107
|
+
return fallback;
|
|
108
|
+
return Math.max(0, Math.min(1, value));
|
|
109
|
+
}
|
|
110
|
+
/** @internal */
|
|
111
|
+
sanitizeScale(value, fallback) {
|
|
112
|
+
if (!Number.isFinite(value))
|
|
113
|
+
return fallback;
|
|
114
|
+
return Math.max(0, value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const DEFAULT_DURATION_MS = 420;
|
|
2
|
+
const DEFAULT_DIRECTION = "left-to-right";
|
|
3
|
+
const DEFAULT_PIXEL_SIZE = 28;
|
|
4
|
+
/** @internal */
|
|
5
|
+
export class TransitionSystem {
|
|
6
|
+
constructor() {
|
|
7
|
+
/** @internal */
|
|
8
|
+
this._active = null;
|
|
9
|
+
}
|
|
10
|
+
get isActive() {
|
|
11
|
+
return this._active !== null;
|
|
12
|
+
}
|
|
13
|
+
start(fromCanvas, toCanvas, width, height, options, onComplete) {
|
|
14
|
+
const type = options.type;
|
|
15
|
+
const durationMs = this.sanitizeDuration(options.durationMs, DEFAULT_DURATION_MS);
|
|
16
|
+
const color = typeof options.color === "string" && options.color.trim()
|
|
17
|
+
? options.color
|
|
18
|
+
: type === "flash"
|
|
19
|
+
? "#ffffff"
|
|
20
|
+
: "#000000";
|
|
21
|
+
const centerX = this.sanitizeCoordinate(options.centerX, width / 2);
|
|
22
|
+
const centerY = this.sanitizeCoordinate(options.centerY, height / 2);
|
|
23
|
+
const pixelSize = this.sanitizePixelSize(options.pixelSize, DEFAULT_PIXEL_SIZE);
|
|
24
|
+
this._active = {
|
|
25
|
+
fromCanvas,
|
|
26
|
+
toCanvas,
|
|
27
|
+
type,
|
|
28
|
+
progress: 0,
|
|
29
|
+
color,
|
|
30
|
+
direction: options.direction ?? DEFAULT_DIRECTION,
|
|
31
|
+
centerX,
|
|
32
|
+
centerY,
|
|
33
|
+
pixelSize,
|
|
34
|
+
elapsedMs: 0,
|
|
35
|
+
durationMs,
|
|
36
|
+
onComplete,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
update(dtMs) {
|
|
40
|
+
if (!this._active)
|
|
41
|
+
return;
|
|
42
|
+
this._active.elapsedMs += dtMs;
|
|
43
|
+
this._active.progress = Math.max(0, Math.min(1, this._active.elapsedMs / this._active.durationMs));
|
|
44
|
+
if (this._active.progress >= 1) {
|
|
45
|
+
const onComplete = this._active.onComplete;
|
|
46
|
+
this._active = null;
|
|
47
|
+
onComplete?.();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
getRenderEntry() {
|
|
51
|
+
return this._active;
|
|
52
|
+
}
|
|
53
|
+
clear() {
|
|
54
|
+
this._active = null;
|
|
55
|
+
}
|
|
56
|
+
/** @internal */
|
|
57
|
+
sanitizeDuration(value, fallback) {
|
|
58
|
+
if (!Number.isFinite(value))
|
|
59
|
+
return fallback;
|
|
60
|
+
return Math.max(1, value);
|
|
61
|
+
}
|
|
62
|
+
/** @internal */
|
|
63
|
+
sanitizeCoordinate(value, fallback) {
|
|
64
|
+
if (!Number.isFinite(value))
|
|
65
|
+
return fallback;
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
/** @internal */
|
|
69
|
+
sanitizePixelSize(value, fallback) {
|
|
70
|
+
if (!Number.isFinite(value))
|
|
71
|
+
return fallback;
|
|
72
|
+
return Math.max(2, Math.round(value));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const AUTO_BODY_WIDTH_VISUAL_RATIO = 0.76;
|
|
2
|
+
const AUTO_BODY_LENGTH_PER_PIXEL = 1.92;
|
|
3
|
+
/** @internal */
|
|
4
|
+
export class ArcadeRacerCollisionSystem {
|
|
5
|
+
constructor(
|
|
6
|
+
/** @internal */ engine) {
|
|
7
|
+
this.engine = engine;
|
|
8
|
+
}
|
|
9
|
+
resolveAutoBodyFromVisual(visual, _scale) {
|
|
10
|
+
try {
|
|
11
|
+
const surface = this.engine.resolveVisualSurface(visual);
|
|
12
|
+
return {
|
|
13
|
+
width: Math.max(1, surface.width * AUTO_BODY_WIDTH_VISUAL_RATIO),
|
|
14
|
+
length: Math.max(1, surface.height),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
resolveCollisionBodyMetrics(visual, scale, bodyWidth, bodyLength, bodyOffsetX, bodyOffsetY) {
|
|
22
|
+
return {
|
|
23
|
+
laneWidth: this.getBodyLaneWidthFromVisual(scale, bodyWidth),
|
|
24
|
+
distanceLength: this.getBodyLengthFromVisual(scale, bodyLength),
|
|
25
|
+
laneOffset: this.getBodyLaneOffsetFromVisual(visual, scale, bodyOffsetX),
|
|
26
|
+
distanceOffset: this.getBodyDistanceOffsetFromVisual(visual, scale, bodyOffsetY),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
getDebugCollisionRectFromVisual(visualRect, visual, bodyWidth, bodyLength, bodyOffsetX, bodyOffsetY) {
|
|
30
|
+
const surface = this.engine.resolveVisualSurface(visual);
|
|
31
|
+
const projectedScale = Math.max(0, Math.min(visualRect.width / Math.max(1, surface.width), visualRect.height / Math.max(1, surface.height)));
|
|
32
|
+
const rectWidth = Math.max(4, bodyWidth * projectedScale);
|
|
33
|
+
const rectHeight = Math.max(8, bodyLength * projectedScale);
|
|
34
|
+
const offsetX = bodyOffsetX * projectedScale;
|
|
35
|
+
const offsetY = bodyOffsetY * projectedScale;
|
|
36
|
+
return {
|
|
37
|
+
x: visualRect.x + (visualRect.width - rectWidth) / 2 + offsetX,
|
|
38
|
+
y: visualRect.y + visualRect.height - rectHeight + offsetY,
|
|
39
|
+
width: rectWidth,
|
|
40
|
+
height: rectHeight,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
getRealCollisionRect(current, width, height, ahead, lane, visual, referenceScale, bodyWidth, bodyLength, bodyOffsetX, bodyOffsetY, resolvedBody, anchorCenterX, anchorBottomY) {
|
|
44
|
+
const metrics = resolvedBody ??
|
|
45
|
+
this.resolveCollisionBodyMetrics(visual, referenceScale, bodyWidth, bodyLength, bodyOffsetX, bodyOffsetY);
|
|
46
|
+
const bodyLaneWidth = metrics.laneWidth;
|
|
47
|
+
const bodyDistanceLength = metrics.distanceLength;
|
|
48
|
+
const laneCenter = lane + metrics.laneOffset;
|
|
49
|
+
const bodyDistanceOffset = metrics.distanceOffset;
|
|
50
|
+
const depthCenter = this.getBodyDistanceCenterFromAnchor(ahead, bodyDistanceLength, bodyDistanceOffset);
|
|
51
|
+
const center = this.engine.projectTrafficPoint(current, depthCenter, width, height);
|
|
52
|
+
const far = this.engine.projectTrafficPoint(current, depthCenter + bodyDistanceLength * 0.5, width, height);
|
|
53
|
+
const near = this.engine.projectTrafficPoint(current, depthCenter - bodyDistanceLength * 0.5, width, height);
|
|
54
|
+
const laneUnit = center.roadWidth * 0.38;
|
|
55
|
+
const centerX = Number.isFinite(anchorCenterX)
|
|
56
|
+
? anchorCenterX +
|
|
57
|
+
this.getBodyOffsetXInScreenPixels(visual, referenceScale, bodyOffsetX)
|
|
58
|
+
: center.x + laneCenter * laneUnit;
|
|
59
|
+
const rectWidth = Math.max(4, bodyLaneWidth * laneUnit);
|
|
60
|
+
const rawTop = Math.min(far.y, near.y);
|
|
61
|
+
const rawBottom = Math.max(far.y, near.y);
|
|
62
|
+
const rawHeight = Math.max(8, rawBottom - rawTop);
|
|
63
|
+
const bottom = Number.isFinite(anchorBottomY)
|
|
64
|
+
? rawBottom +
|
|
65
|
+
(anchorBottomY - rawBottom) *
|
|
66
|
+
this.engine.realCollisionVerticalShiftBlend
|
|
67
|
+
: rawBottom;
|
|
68
|
+
const top = bottom - rawHeight;
|
|
69
|
+
return {
|
|
70
|
+
x: centerX - rectWidth / 2,
|
|
71
|
+
y: top,
|
|
72
|
+
width: rectWidth,
|
|
73
|
+
height: rawHeight,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
resolvePlayerTrafficCollisions() {
|
|
77
|
+
const nextCollisionIds = new Set();
|
|
78
|
+
const current = this.engine.trackSystem.sampleTrack(this.engine.distanceState);
|
|
79
|
+
const playerBodyWidth = this.engine.playerResolvedBodyState.laneWidth;
|
|
80
|
+
const playerBodyLength = this.engine.playerResolvedBodyState.distanceLength;
|
|
81
|
+
const playerLaneCenter = this.engine.playerLaneState + this.engine.playerResolvedBodyState.laneOffset;
|
|
82
|
+
const playerBodyDistanceOffset = this.engine.playerResolvedBodyState.distanceOffset;
|
|
83
|
+
const playerDistanceCenter = this.engine.distanceState +
|
|
84
|
+
this.getBodyDistanceCenterFromAnchor(this.engine.playerProjectionAheadDistance, playerBodyLength, playerBodyDistanceOffset);
|
|
85
|
+
const playerRect = this.getRealCollisionRect(current, this.engine.width, this.engine.height, this.engine.playerProjectionAheadDistance, this.engine.playerLaneState, this.engine.playerVisualState, this.engine.playerBaseScaleState, this.engine.playerBodyWidthState, this.engine.playerBodyLengthState, this.engine.playerBodyOffsetXState, this.engine.playerBodyOffsetYState, this.engine.playerResolvedBodyState, this.engine.getPlayerScreenX(), this.engine.playerScreenYState);
|
|
86
|
+
for (const traffic of this.engine.trafficState) {
|
|
87
|
+
const trafficBodyWidth = traffic.resolvedBody.laneWidth;
|
|
88
|
+
const trafficBodyLength = traffic.resolvedBody.distanceLength;
|
|
89
|
+
const trafficBodyDistanceOffset = traffic.resolvedBody.distanceOffset;
|
|
90
|
+
const trafficLaneCenter = traffic.lane + traffic.resolvedBody.laneOffset;
|
|
91
|
+
const trafficDistanceCenter = this.getBodyDistanceCenterFromAnchor(traffic.distance, trafficBodyLength, trafficBodyDistanceOffset);
|
|
92
|
+
const distanceDelta = trafficDistanceCenter - playerDistanceCenter;
|
|
93
|
+
const laneDelta = trafficLaneCenter - playerLaneCenter;
|
|
94
|
+
const hitDepth = Math.abs(distanceDelta) < (playerBodyLength + trafficBodyLength) * 0.5;
|
|
95
|
+
const hitLane = Math.abs(laneDelta) < (playerBodyWidth + trafficBodyWidth) * 0.5;
|
|
96
|
+
if (!hitDepth || !hitLane)
|
|
97
|
+
continue;
|
|
98
|
+
const trafficVisualBottom = this.engine.projectTrafficPoint(current, traffic.distance - this.engine.distanceState, this.engine.width, this.engine.height).y;
|
|
99
|
+
const trafficRect = this.getRealCollisionRect(current, this.engine.width, this.engine.height, traffic.distance - this.engine.distanceState, traffic.lane, traffic.visual, traffic.baseScale, traffic.bodyWidth, traffic.bodyLength, traffic.bodyOffsetX, traffic.bodyOffsetY, traffic.resolvedBody, undefined, trafficVisualBottom);
|
|
100
|
+
if (playerRect &&
|
|
101
|
+
trafficRect &&
|
|
102
|
+
!this.rectsOverlap(playerRect, trafficRect)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
nextCollisionIds.add(traffic.id);
|
|
106
|
+
if (this.engine.activeCollisionIds.has(traffic.id))
|
|
107
|
+
continue;
|
|
108
|
+
const collision = {
|
|
109
|
+
trafficId: traffic.id,
|
|
110
|
+
distanceDelta,
|
|
111
|
+
laneDelta,
|
|
112
|
+
trafficDistance: traffic.distance,
|
|
113
|
+
trafficLane: traffic.lane,
|
|
114
|
+
};
|
|
115
|
+
for (const handler of this.engine.collisionHandlers) {
|
|
116
|
+
handler(collision);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
this.engine.activeCollisionIds = nextCollisionIds;
|
|
120
|
+
}
|
|
121
|
+
getPlayerScreenLaneUnit() {
|
|
122
|
+
return this.engine.roadNearWidth * 0.38;
|
|
123
|
+
}
|
|
124
|
+
getBodyDistanceCenterFromAnchor(anchorDistance, bodyLength, bodyDistanceOffset) {
|
|
125
|
+
return anchorDistance + bodyLength * 0.5 - bodyDistanceOffset;
|
|
126
|
+
}
|
|
127
|
+
/** @internal */
|
|
128
|
+
getBodyOffsetXInScreenPixels(visual, scale, bodyOffsetX) {
|
|
129
|
+
if (!Number.isFinite(bodyOffsetX) || bodyOffsetX === 0)
|
|
130
|
+
return 0;
|
|
131
|
+
try {
|
|
132
|
+
const surface = this.engine.resolveVisualSurface(visual);
|
|
133
|
+
const safeScale = Number.isFinite(scale) ? Math.max(0, scale) : 1;
|
|
134
|
+
const visualWidth = Math.max(1, surface.width * safeScale);
|
|
135
|
+
return Math.max(-visualWidth * 0.5, Math.min(visualWidth * 0.5, bodyOffsetX * safeScale));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** @internal */
|
|
142
|
+
getBodyLaneWidthFromVisual(scale, bodyWidth) {
|
|
143
|
+
if (!Number.isFinite(bodyWidth) || bodyWidth <= 0)
|
|
144
|
+
return 0;
|
|
145
|
+
const safeScale = Number.isFinite(scale) ? Math.max(0, scale) : 1;
|
|
146
|
+
const projectedWidth = Math.max(1, bodyWidth * safeScale);
|
|
147
|
+
const laneUnit = Math.max(1, this.getPlayerScreenLaneUnit());
|
|
148
|
+
return Math.max(0.01, projectedWidth / laneUnit);
|
|
149
|
+
}
|
|
150
|
+
/** @internal */
|
|
151
|
+
getBodyLengthFromVisual(scale, bodyLength) {
|
|
152
|
+
if (!Number.isFinite(bodyLength) || bodyLength <= 0)
|
|
153
|
+
return 0;
|
|
154
|
+
const safeScale = Number.isFinite(scale) ? Math.max(0, scale) : 1;
|
|
155
|
+
const projectedLength = Math.max(1, bodyLength * safeScale);
|
|
156
|
+
return Math.max(1, projectedLength * AUTO_BODY_LENGTH_PER_PIXEL);
|
|
157
|
+
}
|
|
158
|
+
/** @internal */
|
|
159
|
+
getBodyLaneOffsetFromVisual(visual, scale, bodyOffsetX) {
|
|
160
|
+
if (!Number.isFinite(bodyOffsetX) || bodyOffsetX === 0)
|
|
161
|
+
return 0;
|
|
162
|
+
try {
|
|
163
|
+
const surface = this.engine.resolveVisualSurface(visual);
|
|
164
|
+
const safeScale = Number.isFinite(scale) ? Math.max(0, scale) : 1;
|
|
165
|
+
const projectedOffsetX = bodyOffsetX * safeScale;
|
|
166
|
+
const laneUnit = Math.max(1, this.getPlayerScreenLaneUnit());
|
|
167
|
+
const visualWidth = Math.max(1, surface.width * safeScale);
|
|
168
|
+
const clampedOffsetX = Math.max(-visualWidth * 0.5, Math.min(visualWidth * 0.5, projectedOffsetX));
|
|
169
|
+
return clampedOffsetX / laneUnit;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** @internal */
|
|
176
|
+
getBodyDistanceOffsetFromVisual(visual, scale, bodyOffsetY) {
|
|
177
|
+
if (!Number.isFinite(bodyOffsetY) || bodyOffsetY === 0)
|
|
178
|
+
return 0;
|
|
179
|
+
try {
|
|
180
|
+
const surface = this.engine.resolveVisualSurface(visual);
|
|
181
|
+
const safeScale = Number.isFinite(scale) ? Math.max(0, scale) : 1;
|
|
182
|
+
const projectedOffsetY = bodyOffsetY * safeScale;
|
|
183
|
+
const visualHeight = Math.max(1, surface.height * safeScale);
|
|
184
|
+
const clampedOffsetY = Math.max(-visualHeight * 0.5, Math.min(visualHeight * 0.5, projectedOffsetY));
|
|
185
|
+
return clampedOffsetY * AUTO_BODY_LENGTH_PER_PIXEL;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** @internal */
|
|
192
|
+
rectsOverlap(a, b) {
|
|
193
|
+
return !(a.x + a.width <= b.x ||
|
|
194
|
+
b.x + b.width <= a.x ||
|
|
195
|
+
a.y + a.height <= b.y ||
|
|
196
|
+
b.y + b.height <= a.y);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export function sanitizeLaneSpacePositions(values) {
|
|
3
|
+
if (!Array.isArray(values))
|
|
4
|
+
return null;
|
|
5
|
+
const sanitized = [];
|
|
6
|
+
for (const value of values) {
|
|
7
|
+
if (!Number.isFinite(value))
|
|
8
|
+
continue;
|
|
9
|
+
sanitized.push(Math.max(-1, Math.min(1, value)));
|
|
10
|
+
}
|
|
11
|
+
return sanitized;
|
|
12
|
+
}
|
|
13
|
+
/** @internal */
|
|
14
|
+
export class ArcadeRacerLaneSystem {
|
|
15
|
+
constructor(requestedLaneCount, laneDirections) {
|
|
16
|
+
const highestConfiguredLaneIndex = this.getHighestConfiguredLaneIndex(laneDirections);
|
|
17
|
+
this.laneCountState = Math.max(1, Math.round(requestedLaneCount), highestConfiguredLaneIndex + 1);
|
|
18
|
+
this.lanePositionsState = this.buildLanePositions(this.laneCountState);
|
|
19
|
+
const laneDirectionGroups = this.resolveLaneDirectionIndices(laneDirections, this.laneCountState);
|
|
20
|
+
this.sameDirectionLaneIndicesState = laneDirectionGroups.same;
|
|
21
|
+
this.oncomingLaneIndicesState = laneDirectionGroups.oncoming;
|
|
22
|
+
}
|
|
23
|
+
get laneCount() {
|
|
24
|
+
return this.laneCountState;
|
|
25
|
+
}
|
|
26
|
+
getLanePosition(laneIndex) {
|
|
27
|
+
if (this.lanePositionsState.length === 0)
|
|
28
|
+
return 0;
|
|
29
|
+
const safeIndex = Number.isFinite(laneIndex)
|
|
30
|
+
? Math.max(0, Math.min(this.laneCountState - 1, Math.round(laneIndex)))
|
|
31
|
+
: 0;
|
|
32
|
+
return this.lanePositionsState[safeIndex] ?? 0;
|
|
33
|
+
}
|
|
34
|
+
getLanePositions(direction = "all") {
|
|
35
|
+
if (direction === "all") {
|
|
36
|
+
return [...this.lanePositionsState];
|
|
37
|
+
}
|
|
38
|
+
const indices = direction === "same"
|
|
39
|
+
? this.sameDirectionLaneIndicesState
|
|
40
|
+
: this.oncomingLaneIndicesState;
|
|
41
|
+
if (indices === null) {
|
|
42
|
+
return [...this.lanePositionsState];
|
|
43
|
+
}
|
|
44
|
+
return indices.map((index) => this.getLanePosition(index));
|
|
45
|
+
}
|
|
46
|
+
clampRoadLane(value) {
|
|
47
|
+
if (!Number.isFinite(value))
|
|
48
|
+
return 0;
|
|
49
|
+
return Math.max(-1, Math.min(1, value));
|
|
50
|
+
}
|
|
51
|
+
/** @internal */
|
|
52
|
+
buildLanePositions(laneCount) {
|
|
53
|
+
const positions = [];
|
|
54
|
+
for (let index = 0; index < laneCount; index++) {
|
|
55
|
+
positions.push(((index + 0.5) / laneCount) * 2 - 1);
|
|
56
|
+
}
|
|
57
|
+
return positions;
|
|
58
|
+
}
|
|
59
|
+
/** @internal */
|
|
60
|
+
sanitizeLaneIndexList(values, laneCount) {
|
|
61
|
+
if (!Array.isArray(values))
|
|
62
|
+
return [];
|
|
63
|
+
const result = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const value of values) {
|
|
66
|
+
if (!Number.isFinite(value))
|
|
67
|
+
continue;
|
|
68
|
+
const index = Math.round(value);
|
|
69
|
+
if (index < 0 || index >= laneCount || seen.has(index))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(index);
|
|
72
|
+
result.push(index);
|
|
73
|
+
}
|
|
74
|
+
return result.sort((a, b) => a - b);
|
|
75
|
+
}
|
|
76
|
+
/** @internal */
|
|
77
|
+
getHighestConfiguredLaneIndex(config) {
|
|
78
|
+
let highest = -1;
|
|
79
|
+
for (const list of [config?.same, config?.oncoming]) {
|
|
80
|
+
if (!Array.isArray(list))
|
|
81
|
+
continue;
|
|
82
|
+
for (const value of list) {
|
|
83
|
+
if (!Number.isFinite(value))
|
|
84
|
+
continue;
|
|
85
|
+
highest = Math.max(highest, Math.round(value));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return highest;
|
|
89
|
+
}
|
|
90
|
+
/** @internal */
|
|
91
|
+
resolveLaneDirectionIndices(config, laneCount) {
|
|
92
|
+
const hasSame = Array.isArray(config?.same);
|
|
93
|
+
const hasOncoming = Array.isArray(config?.oncoming);
|
|
94
|
+
if (!hasSame && !hasOncoming) {
|
|
95
|
+
return { same: null, oncoming: null };
|
|
96
|
+
}
|
|
97
|
+
const allLaneIndices = Array.from({ length: laneCount }, (_, index) => index);
|
|
98
|
+
const same = this.sanitizeLaneIndexList(config?.same, laneCount);
|
|
99
|
+
const oncoming = this.sanitizeLaneIndexList(config?.oncoming, laneCount);
|
|
100
|
+
if (hasSame && hasOncoming) {
|
|
101
|
+
const sameSet = new Set(same);
|
|
102
|
+
return {
|
|
103
|
+
same,
|
|
104
|
+
oncoming: oncoming.filter((index) => !sameSet.has(index)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (hasSame) {
|
|
108
|
+
const sameSet = new Set(same);
|
|
109
|
+
return {
|
|
110
|
+
same,
|
|
111
|
+
oncoming: allLaneIndices.filter((index) => !sameSet.has(index)),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const oncomingSet = new Set(oncoming);
|
|
115
|
+
return {
|
|
116
|
+
same: allLaneIndices.filter((index) => !oncomingSet.has(index)),
|
|
117
|
+
oncoming,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|