@energy8platform/game-engine 0.18.0 → 0.19.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/dist/audio.cjs.js +15 -5
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +5 -0
- package/dist/audio.esm.js +15 -5
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +46 -18
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +13 -1
- package/dist/core.esm.js +46 -18
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +333 -67
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +111 -26
- package/dist/host.esm.js +333 -67
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +46 -18
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.esm.js +46 -18
- package/dist/index.esm.js.map +1 -1
- package/dist/react.d.ts +13 -1
- package/package.json +3 -2
- package/src/audio/AudioManager.ts +17 -5
- package/src/core/GameApplication.ts +22 -5
- package/src/host/createSlotGame.ts +143 -23
- package/src/host/index.ts +4 -1
- package/src/host/overlayController.ts +81 -0
- package/src/host/pauseController.ts +21 -0
- package/src/host/runRound.ts +35 -43
- package/src/host/sceneAudio.ts +14 -0
- package/src/host/sceneController.ts +84 -19
- package/src/host/shellConfig.ts +11 -6
- package/src/host/skipGesture.ts +24 -0
- package/src/host/types.ts +5 -2
- package/src/viewport/ViewportManager.ts +19 -9
package/src/host/runRound.ts
CHANGED
|
@@ -1,63 +1,55 @@
|
|
|
1
1
|
import type { SlotSpinResultBase } from '@energy8platform/platform-core/slot-result';
|
|
2
2
|
import type { RenderContext, SlotSceneController } from './sceneController';
|
|
3
3
|
|
|
4
|
-
/** Injected dependencies for one round. All host-agnostic + unit-testable. */
|
|
5
4
|
export interface RunRoundDeps<T extends SlotSpinResultBase> {
|
|
6
|
-
/** play → normalize → enrich (roundId/nextActions/complete). From createSlotPlay. */
|
|
7
5
|
play(action: string, bet: number, roundId?: string): Promise<T>;
|
|
8
|
-
/** Settle the most recent result (post-animation). From createSlotPlay. */
|
|
9
6
|
ack(): void;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
context(action: string): RenderContext;
|
|
14
|
-
/** Role of an action from the spec ('base'|'buy'|'feature'|'free'); drives bonus detection. */
|
|
7
|
+
scene: Pick<SlotSceneController<T>, 'onSpin'>;
|
|
8
|
+
/** Build the per-round render context (without signal — runRound injects it per segment). */
|
|
9
|
+
context(action: string): Omit<RenderContext, 'signal'> & { signal?: AbortSignal };
|
|
15
10
|
roleOf(action: string): string | undefined;
|
|
16
|
-
/** Fires after each segment is presented + acked. The host updates HUD readouts (win/balance)
|
|
17
|
-
* here so they change WITH the animation, never eagerly when the play result arrives. */
|
|
18
11
|
afterPresent?(result: T): void;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
/** Once, before the first segment is played (player pressed spin). */
|
|
13
|
+
onSpinStart?(): void;
|
|
14
|
+
/** Once, after the full drain. */
|
|
15
|
+
onSpinEnd?(last: T, ctx: RenderContext): void;
|
|
16
|
+
/** Fires when entering a non-BASE mode (first free segment). */
|
|
17
|
+
onEnterMode?(trigger: T, ctx: RenderContext): Promise<void>;
|
|
18
|
+
/** Fires after the last segment of a mode. */
|
|
19
|
+
onExitMode?(last: T, ctx: RenderContext): Promise<void>;
|
|
20
|
+
/** Hands the host the AbortController for the segment about to present (for skip). */
|
|
21
|
+
beforeSegment?(ac: AbortController): void;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
/**
|
|
28
|
-
* Drive ONE round end-to-end: play the trigger, present it, ack; then drain the remaining segments
|
|
29
|
-
* (a bonus's free spins) by replaying nextActions[0] with the SAME roundId until the round reports
|
|
30
|
-
* `complete`. Fires `onBonusEnter` EXACTLY before the first free-role segment and `onBonusExit`
|
|
31
|
-
* after the last. A plain spin with no bonus is already `complete`, so the while-loop is a no-op.
|
|
32
|
-
*
|
|
33
|
-
* `ctx.bet` is captured once (bet can't change mid-round); `ctx.turbo` is a live getter so a
|
|
34
|
-
* mid-round toggle is honoured on the next segment.
|
|
35
|
-
*/
|
|
36
24
|
export async function runRound<T extends SlotSpinResultBase>(
|
|
37
25
|
deps: RunRoundDeps<T>,
|
|
38
26
|
action: string,
|
|
39
27
|
): Promise<void> {
|
|
40
|
-
|
|
41
|
-
let r = await deps.play(action, ctx.bet);
|
|
42
|
-
await deps.scene.present(r, ctx);
|
|
43
|
-
deps.ack();
|
|
44
|
-
deps.afterPresent?.(r); // HUD readouts update AFTER the animation, not before
|
|
28
|
+
deps.onSpinStart?.();
|
|
45
29
|
|
|
46
|
-
|
|
30
|
+
const ctxBet = deps.context(action).bet;
|
|
31
|
+
|
|
32
|
+
const segment = async (a: string, roundId: string | undefined): Promise<{ r: T; ctx: RenderContext }> => {
|
|
33
|
+
const ac = new AbortController();
|
|
34
|
+
deps.beforeSegment?.(ac);
|
|
35
|
+
const r = await deps.play(a, ctxBet, roundId);
|
|
36
|
+
const ctx = { ...deps.context(action), signal: ac.signal } as RenderContext;
|
|
37
|
+
await deps.scene.onSpin(r, ctx);
|
|
38
|
+
deps.ack();
|
|
39
|
+
deps.afterPresent?.(r);
|
|
40
|
+
return { r, ctx };
|
|
41
|
+
};
|
|
42
|
+
let { r, ctx } = await segment(action, undefined);
|
|
43
|
+
|
|
44
|
+
let inMode = false;
|
|
47
45
|
while (!r.complete && r.nextActions && r.nextActions.length > 0) {
|
|
48
46
|
const next = r.nextActions[0];
|
|
49
|
-
if (!
|
|
50
|
-
|
|
51
|
-
await deps.
|
|
47
|
+
if (!inMode && deps.roleOf(next) === 'free') {
|
|
48
|
+
inMode = true;
|
|
49
|
+
await deps.onEnterMode?.(r, ctx);
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
// property (so a mid-round toggle is reflected on the NEXT segment), while action/mode/bet stay
|
|
55
|
-
// the round's (the trigger's) identity — a scene must see the same bonus identity all round.
|
|
56
|
-
const segCtx = { ...deps.context(action) } as RenderContext;
|
|
57
|
-
r = await deps.play(next, ctx.bet, r.roundId);
|
|
58
|
-
await deps.scene.present(r, segCtx);
|
|
59
|
-
deps.ack();
|
|
60
|
-
deps.afterPresent?.(r);
|
|
51
|
+
({ r, ctx } = await segment(next, r.roundId));
|
|
61
52
|
}
|
|
62
|
-
if (
|
|
53
|
+
if (inMode) await deps.onExitMode?.(r, ctx);
|
|
54
|
+
deps.onSpinEnd?.(r, ctx);
|
|
63
55
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AudioManager } from '../audio/AudioManager';
|
|
2
|
+
import type { SceneAudio } from './sceneController';
|
|
3
|
+
|
|
4
|
+
/** Wrap the engine's AudioManager into the playback-only handle a scene receives. Volume/mute are
|
|
5
|
+
* deliberately omitted — those are driven by the shell's settingChange → host. */
|
|
6
|
+
export function createSceneAudio(audio: AudioManager): SceneAudio {
|
|
7
|
+
return {
|
|
8
|
+
play: (alias, opts) => audio.play(alias, 'sfx', opts),
|
|
9
|
+
playMusic: (alias, fadeMs) => audio.playMusic(alias, fadeMs),
|
|
10
|
+
stopMusic: () => audio.stopMusic(),
|
|
11
|
+
duck: (factor) => audio.duckMusic(factor),
|
|
12
|
+
unduck: () => audio.unduckMusic(),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -1,31 +1,96 @@
|
|
|
1
|
+
import type { Container } from 'pixi.js';
|
|
1
2
|
import type { SlotSpinResultBase } from '@energy8platform/platform-core/slot-result';
|
|
2
3
|
|
|
3
|
-
/** Everything a scene needs to render one
|
|
4
|
+
/** Everything a scene needs to render one segment. The host builds it per segment. */
|
|
4
5
|
export interface RenderContext {
|
|
5
|
-
/** Bet for this round (major units). Stable for the whole round
|
|
6
|
+
/** Bet for this round (major units). Stable for the whole round. */
|
|
6
7
|
bet: number;
|
|
7
|
-
/** Trigger action in the game's own vocabulary (
|
|
8
|
-
* 'buy_bonus' | … Stable for the whole round. */
|
|
8
|
+
/** Trigger action in the game's own vocabulary ('spin' | 'ante' | 'buy_bonus' | …). */
|
|
9
9
|
action: string;
|
|
10
|
-
/** Stake bet-mode of the round (
|
|
11
|
-
* Canonical per-round identifier of WHICH bonus/feature this is. Stable for the whole round. */
|
|
10
|
+
/** Stake bet-mode of the round ('BASE' | 'ANTE' | 'BONUS' | …). */
|
|
12
11
|
mode: string;
|
|
13
|
-
/** Currency-aware money formatter.
|
|
12
|
+
/** Currency-aware money formatter. */
|
|
14
13
|
formatAmount(value: number): string;
|
|
15
|
-
/** LIVE turbo level (0 = off, 1..3 = escalating speed)
|
|
16
|
-
* the moment of access (getter) so a mid-round toggle is reflected. */
|
|
14
|
+
/** LIVE turbo level (0 = off, 1..3 = escalating speed). Read at access. */
|
|
17
15
|
readonly turbo: number;
|
|
16
|
+
/** Aborted when the player skips this segment (double-tap). The scene's async pacing can race or
|
|
17
|
+
* cancel on it; on abort the scene must collapse to the segment's final visual state. */
|
|
18
|
+
signal: AbortSignal;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
21
|
+
/** Playback-only audio handle. Volume/mute are shell settings → host, never the scene. */
|
|
22
|
+
export interface SceneAudio {
|
|
23
|
+
play(alias: string, opts?: { volume?: number; loop?: boolean; speed?: number }): void;
|
|
24
|
+
playMusic(alias: string, fadeMs?: number): void;
|
|
25
|
+
stopMusic(): void;
|
|
26
|
+
duck(factor: number): void;
|
|
27
|
+
unduck(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OverlayShowOptions {
|
|
31
|
+
/** Draw the overlay content into `container` (sized to the canvas). */
|
|
32
|
+
build(container: Container, size: { width: number; height: number }): void;
|
|
33
|
+
/** Auto-close after N ms (combine with closeOn — whichever fires first). */
|
|
34
|
+
autoCloseMs?: number;
|
|
35
|
+
/** Dismiss on a single tap. Default 'tap'. Set false to require an explicit close(). */
|
|
36
|
+
closeOn?: 'tap' | false;
|
|
37
|
+
/** Optional host-drawn backdrop alpha (0..1). Default: none (game draws its own). */
|
|
38
|
+
dim?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Single host-owned layer above scene + shell. Eats pointer events so shell controls are
|
|
42
|
+
* unreachable while open. */
|
|
43
|
+
export interface SceneOverlay {
|
|
44
|
+
/** Resolves when the overlay closes. Rejects if one is already open. */
|
|
45
|
+
show(opts: OverlayShowOptions): Promise<void>;
|
|
46
|
+
close(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SceneShell {
|
|
50
|
+
/** Live insets (px). `bottom` = the shell bar height; read inside onResize. */
|
|
51
|
+
readonly safeArea: { top: number; right: number; bottom: number; left: number };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AutoplaySceneState {
|
|
55
|
+
running: boolean;
|
|
56
|
+
remaining: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Stable capabilities injected once via onCreate. */
|
|
60
|
+
export interface SceneApi {
|
|
61
|
+
audio: SceneAudio;
|
|
62
|
+
overlay: SceneOverlay;
|
|
63
|
+
shell: SceneShell;
|
|
64
|
+
formatAmount(value: number): string;
|
|
65
|
+
readonly bet: number;
|
|
66
|
+
readonly mode: string;
|
|
67
|
+
readonly turbo: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The contract a slot scene implements. The HOST owns the play→present→ack→drain loop and the
|
|
71
|
+
* shell; the scene only renders + reacts. The core spin-lifecycle hooks are REQUIRED (implement
|
|
72
|
+
* them — empty bodies are fine where a game has nothing to do); the incidental reactions below
|
|
73
|
+
* stay optional. */
|
|
22
74
|
export interface SlotSceneController<T extends SlotSpinResultBase = SlotSpinResultBase> {
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
|
|
75
|
+
/** Injected ONCE before the first round — capabilities, subscriptions, one-time setup. */
|
|
76
|
+
onCreate(api: SceneApi): void;
|
|
77
|
+
/** Fires once per round when the player presses spin (before the network result). */
|
|
78
|
+
onSpinStart(): void;
|
|
79
|
+
/** Render ONE segment (a spin or one free spin). Await your own pacing. */
|
|
80
|
+
onSpin(result: T, ctx: RenderContext): Promise<void>;
|
|
81
|
+
/** Fires when ctx.mode changes between segments (entering a non-BASE mode/bonus). */
|
|
82
|
+
onEnterMode(result: T, ctx: RenderContext): Promise<void>;
|
|
83
|
+
/** Fires when leaving a mode (back toward BASE). */
|
|
84
|
+
onExitMode(result: T, ctx: RenderContext): Promise<void>;
|
|
85
|
+
/** Fires once per round after the full drain (controls unlocked). */
|
|
86
|
+
onSpinEnd(result: T, ctx: RenderContext): void;
|
|
87
|
+
/** Shell events (may fire while idle) — optional. */
|
|
88
|
+
onBetChanged?(bet: number): void;
|
|
89
|
+
onTurboChanged?(level: number): void;
|
|
90
|
+
onAutoplayChanged?(state: AutoplaySceneState): void;
|
|
91
|
+
/** Double-tap skip during an active onSpin (gated by the skipGesture setting). */
|
|
92
|
+
onSkip?(): void;
|
|
93
|
+
/** Tab focus lost / regained. */
|
|
94
|
+
onPause?(): void;
|
|
95
|
+
onResume?(): void;
|
|
31
96
|
}
|
package/src/host/shellConfig.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// packages/game-engine/src/host/shellConfig.ts
|
|
2
|
+
// `socialize` is a runtime helper that pixi-shell does NOT re-export (its index only re-exports
|
|
3
|
+
// types), so it stays sourced from platform-core/shell; the shapes are structurally identical.
|
|
2
4
|
import { socialize } from '@energy8platform/platform-core/shell';
|
|
3
5
|
import type {
|
|
4
|
-
|
|
6
|
+
PixiShellConfig, ShellMode, CurrencyConfig, GameInfoContent, GameInfoSection, PaytableRow,
|
|
5
7
|
BonusOption, ShellFeatures, GameMode,
|
|
6
|
-
} from '@energy8platform/
|
|
8
|
+
} from '@energy8platform/pixi-shell';
|
|
7
9
|
import type { GameModel } from '@energy8platform/platform-core/game-spec';
|
|
8
10
|
import type { WinTier } from '../slot';
|
|
9
11
|
|
|
10
12
|
export interface SlotShellOptions {
|
|
11
|
-
mount?: HTMLElement;
|
|
12
13
|
/** Override the derived currency (normally taken from initData). */
|
|
13
14
|
currency?: CurrencyConfig;
|
|
14
15
|
/** Author-supplied info sections, MERGED over the host-derived set by section identity (an author
|
|
@@ -319,8 +320,13 @@ function socializeBonusOptions(options: BonusOption[], isSocial: boolean): Bonus
|
|
|
319
320
|
return options.map((o) => ({ ...o, title: socialize(o.title), description: socialize(o.description) }));
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
/** Pure: assemble
|
|
323
|
-
|
|
323
|
+
/** Pure: assemble the shell config (sans mount target) from the model + runtime context
|
|
324
|
+
* (currency/balance/language/mode). The host adds `app` at the call site. */
|
|
325
|
+
export function buildShellConfig(
|
|
326
|
+
opts: SlotShellOptions,
|
|
327
|
+
model: GameModel,
|
|
328
|
+
runtime: ShellRuntime,
|
|
329
|
+
): Omit<PixiShellConfig, 'app' | 'parent'> {
|
|
324
330
|
// Prefer the currency-specific ladder from /wallet/authenticate; fall back to the spec (dev/devBridge).
|
|
325
331
|
const betLevels = runtime.betLevels?.length ? runtime.betLevels : model.spec.betLevels;
|
|
326
332
|
// Stake requires the default to come from authenticate on every entry; spec default is the dev fallback.
|
|
@@ -363,7 +369,6 @@ export function buildShellConfig(opts: SlotShellOptions, model: GameModel, runti
|
|
|
363
369
|
} as ShellFeatures;
|
|
364
370
|
applyJurisdiction(features, runtime.jurisdiction);
|
|
365
371
|
return {
|
|
366
|
-
mount: opts.mount ?? (typeof document !== 'undefined' ? document.body : (undefined as never)),
|
|
367
372
|
language: runtime.language ?? 'en',
|
|
368
373
|
isSocial,
|
|
369
374
|
currency,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface SkipDeps {
|
|
2
|
+
/** The skipGesture setting is on. */
|
|
3
|
+
enabled(): boolean;
|
|
4
|
+
/** An onSpin is currently presenting (skippable window). */
|
|
5
|
+
active(): boolean;
|
|
6
|
+
onSkip(): void;
|
|
7
|
+
/** Max ms between the two taps. Default 300. */
|
|
8
|
+
thresholdMs?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Pure double-tap recognizer. The host feeds it pointer `tap(now)` (e.g. performance.now()) and
|
|
12
|
+
* supplies the enabled/active gates + the onSkip effect. */
|
|
13
|
+
export function createDoubleTapSkip(deps: SkipDeps): { tap(now: number): void; destroy(): void } {
|
|
14
|
+
const threshold = deps.thresholdMs ?? 300;
|
|
15
|
+
let last = -Infinity;
|
|
16
|
+
return {
|
|
17
|
+
tap(now: number): void {
|
|
18
|
+
const isDouble = now - last <= threshold;
|
|
19
|
+
last = isDouble ? -Infinity : now; // consume the pair so a 3rd tap starts fresh
|
|
20
|
+
if (isDouble && deps.enabled() && deps.active()) deps.onSkip();
|
|
21
|
+
},
|
|
22
|
+
destroy(): void { last = -Infinity; },
|
|
23
|
+
};
|
|
24
|
+
}
|
package/src/host/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { ApplicationOptions } from 'pixi.js';
|
|
3
3
|
import type { GameModel } from '@energy8platform/platform-core/game-spec';
|
|
4
4
|
import type { AssetManifest, LoadingScreenConfig } from '@energy8platform/platform-core';
|
|
5
|
-
import type {
|
|
5
|
+
import type { PixiGameShell } from '@energy8platform/pixi-shell';
|
|
6
6
|
import type { AudioConfig, ScaleMode, Orientation, SceneConstructor } from '../types';
|
|
7
7
|
import type { BookAdapter, AdapterModule, StakeBridge } from '@energy8platform/stake-bridge';
|
|
8
8
|
import type { GameApplication } from '../core';
|
|
@@ -61,11 +61,14 @@ export interface CreateSlotGameOptions<T extends SlotSpinResultBase = SlotSpinRe
|
|
|
61
61
|
dev?: boolean;
|
|
62
62
|
stake?: StakeIntegration;
|
|
63
63
|
shell?: SlotShellOptions;
|
|
64
|
+
/** Double-tap on the play area to skip the current spin animation. Default `true`. Set `false`
|
|
65
|
+
* to disable the gesture (e.g. games where a tap means something else). */
|
|
66
|
+
skipGesture?: boolean;
|
|
64
67
|
onFatalError?: (message: string) => void;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export interface SlotGameHandle {
|
|
68
71
|
game: GameApplication;
|
|
69
72
|
stakeBridge: StakeBridge | null;
|
|
70
|
-
shell:
|
|
73
|
+
shell: PixiGameShell | null;
|
|
71
74
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Application } from 'pixi.js';
|
|
1
|
+
import type { Application, Container } from 'pixi.js';
|
|
2
2
|
import { EventEmitter } from '../core/EventEmitter';
|
|
3
3
|
import { ScaleMode, Orientation } from '../types';
|
|
4
4
|
|
|
@@ -45,6 +45,7 @@ export class ViewportManager extends EventEmitter<ViewportEvents> {
|
|
|
45
45
|
private _app: Application;
|
|
46
46
|
private _container: HTMLElement;
|
|
47
47
|
private _config: ViewportConfig;
|
|
48
|
+
private _target: Container;
|
|
48
49
|
private _resizeObserver: ResizeObserver | null = null;
|
|
49
50
|
private _currentOrientation: Orientation = Orientation.LANDSCAPE;
|
|
50
51
|
private _currentWidth = 0;
|
|
@@ -53,11 +54,20 @@ export class ViewportManager extends EventEmitter<ViewportEvents> {
|
|
|
53
54
|
private _destroyed = false;
|
|
54
55
|
private _resizeTimeout: number | null = null;
|
|
55
56
|
|
|
56
|
-
constructor(
|
|
57
|
+
constructor(
|
|
58
|
+
app: Application,
|
|
59
|
+
container: HTMLElement,
|
|
60
|
+
config: ViewportConfig,
|
|
61
|
+
target?: Container,
|
|
62
|
+
) {
|
|
57
63
|
super();
|
|
58
64
|
this._app = app;
|
|
59
65
|
this._container = container;
|
|
60
66
|
this._config = config;
|
|
67
|
+
// The container this manager scales/offsets. Defaults to app.stage for backward
|
|
68
|
+
// compatibility; the engine passes a dedicated scaled world root so app.stage stays
|
|
69
|
+
// identity (screen space) for unscaled UI layers.
|
|
70
|
+
this._target = target ?? app.stage;
|
|
61
71
|
|
|
62
72
|
this.setupObserver();
|
|
63
73
|
}
|
|
@@ -164,18 +174,18 @@ export class ViewportManager extends EventEmitter<ViewportEvents> {
|
|
|
164
174
|
? Math.min(containerWidth / designWidth, containerHeight / designHeight)
|
|
165
175
|
: scale;
|
|
166
176
|
|
|
167
|
-
this.
|
|
177
|
+
this._target.scale.set(stageScale);
|
|
168
178
|
|
|
169
179
|
// Center the stage for FIT mode
|
|
170
180
|
if (scaleMode === ScaleMode.FIT) {
|
|
171
|
-
this.
|
|
172
|
-
this.
|
|
181
|
+
this._target.x = Math.round((containerWidth - designWidth * stageScale) / 2);
|
|
182
|
+
this._target.y = Math.round((containerHeight - designHeight * stageScale) / 2);
|
|
173
183
|
} else if (scaleMode === ScaleMode.FILL) {
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
184
|
+
this._target.x = Math.round((containerWidth - gameWidth * stageScale) / 2);
|
|
185
|
+
this._target.y = Math.round((containerHeight - gameHeight * stageScale) / 2);
|
|
176
186
|
} else {
|
|
177
|
-
this.
|
|
178
|
-
this.
|
|
187
|
+
this._target.x = 0;
|
|
188
|
+
this._target.y = 0;
|
|
179
189
|
}
|
|
180
190
|
|
|
181
191
|
this._currentWidth = gameWidth;
|