@vpmedia/phaser 1.126.0 → 1.127.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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -4
- package/dist/index.js.map +1 -1
- package/dist/phaser/core/page_lifecycle.d.ts +49 -0
- package/dist/phaser/core/page_lifecycle.d.ts.map +1 -0
- package/dist/phaser/core/sound_manager.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/index.ts +13 -0
- package/src/phaser/core/page_lifecycle.test.ts +178 -0
- package/src/phaser/core/page_lifecycle.ts +142 -0
- package/src/phaser/core/sound_manager.ts +3 -8
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal page lifecycle observer.
|
|
3
|
+
* @see https://developer.chrome.com/docs/web-platform/page-lifecycle-api
|
|
4
|
+
*/
|
|
5
|
+
/** The page is visible and has focus. */
|
|
6
|
+
export declare const PAGE_STATE_ACTIVE = "active";
|
|
7
|
+
/** The page is visible but does not have focus. */
|
|
8
|
+
export declare const PAGE_STATE_PASSIVE = "passive";
|
|
9
|
+
/** The page is not visible. */
|
|
10
|
+
export declare const PAGE_STATE_HIDDEN = "hidden";
|
|
11
|
+
/** The browser has frozen the page to reclaim its resources. */
|
|
12
|
+
export declare const PAGE_STATE_FROZEN = "frozen";
|
|
13
|
+
/** The page is being unloaded. */
|
|
14
|
+
export declare const PAGE_STATE_TERMINATED = "terminated";
|
|
15
|
+
/** One of the states a page can be in. */
|
|
16
|
+
export type PageState = typeof PAGE_STATE_ACTIVE | typeof PAGE_STATE_PASSIVE | typeof PAGE_STATE_HIDDEN | typeof PAGE_STATE_FROZEN | typeof PAGE_STATE_TERMINATED;
|
|
17
|
+
/** Called with the state the page has just entered and the one it left. */
|
|
18
|
+
export type PageStateListener = (state: PageState, previous: PageState) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Reads the state the page is in right now.
|
|
21
|
+
* @returns {PageState} The current page state.
|
|
22
|
+
*/
|
|
23
|
+
export declare const readPageState: () => PageState;
|
|
24
|
+
/**
|
|
25
|
+
* Starts observing the page, if it is not observed already.
|
|
26
|
+
*/
|
|
27
|
+
export declare const startPageLifecycle: () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the state the page is in, starting the observer on first use.
|
|
30
|
+
* @returns {PageState} The current page state.
|
|
31
|
+
*/
|
|
32
|
+
export declare const getPageState: () => PageState;
|
|
33
|
+
/**
|
|
34
|
+
* Registers a listener for every state change.
|
|
35
|
+
* @param {PageStateListener} listener - The listener to call on each change.
|
|
36
|
+
* @returns {() => void} A function that removes the listener again.
|
|
37
|
+
*/
|
|
38
|
+
export declare const onPageStateChange: (listener: PageStateListener) => (() => void);
|
|
39
|
+
/**
|
|
40
|
+
* Runs a callback the next time the page becomes active, once.
|
|
41
|
+
* @param {() => void} callback - The callback to run.
|
|
42
|
+
* @returns {() => void} A function that cancels the pending callback.
|
|
43
|
+
*/
|
|
44
|
+
export declare const oncePageActive: (callback: () => void) => (() => void);
|
|
45
|
+
/**
|
|
46
|
+
* Drops every listener and stops tracking. Only the tests need this.
|
|
47
|
+
*/
|
|
48
|
+
export declare const resetPageLifecycle: () => void;
|
|
49
|
+
//# sourceMappingURL=page_lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page_lifecycle.d.ts","sourceRoot":"","sources":["../../../src/phaser/core/page_lifecycle.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,yCAAyC;AACzC,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAC1C,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAC5C,+BAA+B;AAC/B,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAC1C,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAC1C,kCAAkC;AAClC,eAAO,MAAM,qBAAqB,eAAe,CAAC;AAElD,0CAA0C;AAC1C,MAAM,MAAM,SAAS,GACjB,OAAO,iBAAiB,GACxB,OAAO,kBAAkB,GACzB,OAAO,iBAAiB,GACxB,OAAO,iBAAiB,GACxB,OAAO,qBAAqB,CAAC;AAEjC,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAC;AAMhF;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAO,SAKhC,CAAC;AAkBF;;GAEG;AACH,eAAO,MAAM,kBAAkB,QAAO,IAkCrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,QAAO,SAG/B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,aAAc,iBAAiB,KAAG,CAAC,MAAM,IAAI,CAM1E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,aAAc,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAQhE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,QAAO,IAIrC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sound_manager.d.ts","sourceRoot":"","sources":["../../../src/phaser/core/sound_manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"sound_manager.d.ts","sourceRoot":"","sources":["../../../src/phaser/core/sound_manager.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vpmedia/phaser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.127.0",
|
|
4
4
|
"description": "@vpmedia/phaser is the modern ECMAScript port of the popular Phaser game engine v2.6.2",
|
|
5
5
|
"author": "Andras Csizmadia <andras@vpmedia.hu> (www.vpmedia.hu)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@logtape/logtape": "^2.3.3",
|
|
38
|
-
"@vpmedia/simplify": "^1.79.0",
|
|
39
38
|
"uuid": "^14.0.2"
|
|
40
39
|
},
|
|
41
40
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,19 @@ export * as Const from './phaser/core/const.js';
|
|
|
3
3
|
export { GameObjectFactory } from './phaser/core/factory.js';
|
|
4
4
|
export { Game } from './phaser/core/game.js';
|
|
5
5
|
export type { GameConfig } from './phaser/core/game.js';
|
|
6
|
+
export {
|
|
7
|
+
getPageState,
|
|
8
|
+
onPageStateChange,
|
|
9
|
+
oncePageActive,
|
|
10
|
+
PAGE_STATE_ACTIVE,
|
|
11
|
+
PAGE_STATE_FROZEN,
|
|
12
|
+
PAGE_STATE_HIDDEN,
|
|
13
|
+
PAGE_STATE_PASSIVE,
|
|
14
|
+
PAGE_STATE_TERMINATED,
|
|
15
|
+
readPageState,
|
|
16
|
+
startPageLifecycle,
|
|
17
|
+
} from './phaser/core/page_lifecycle.js';
|
|
18
|
+
export type { PageState, PageStateListener } from './phaser/core/page_lifecycle.js';
|
|
6
19
|
export type {
|
|
7
20
|
AppliedCallback,
|
|
8
21
|
Callback,
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
getPageState,
|
|
4
|
+
onPageStateChange,
|
|
5
|
+
oncePageActive,
|
|
6
|
+
PAGE_STATE_ACTIVE,
|
|
7
|
+
PAGE_STATE_FROZEN,
|
|
8
|
+
PAGE_STATE_HIDDEN,
|
|
9
|
+
PAGE_STATE_PASSIVE,
|
|
10
|
+
PAGE_STATE_TERMINATED,
|
|
11
|
+
readPageState,
|
|
12
|
+
resetPageLifecycle,
|
|
13
|
+
startPageLifecycle,
|
|
14
|
+
} from './page_lifecycle.js';
|
|
15
|
+
|
|
16
|
+
const setVisibility = (value: DocumentVisibilityState): void => {
|
|
17
|
+
Object.defineProperty(document, 'visibilityState', { value, configurable: true });
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const setFocus = (value: boolean): void => {
|
|
21
|
+
document.hasFocus = (): boolean => value;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const dispatch = (type: string, init: EventInit = {}): void => {
|
|
25
|
+
globalThis.dispatchEvent(new Event(type, init));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('page lifecycle', () => {
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
resetPageLifecycle();
|
|
31
|
+
setVisibility('visible');
|
|
32
|
+
setFocus(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('readPageState', () => {
|
|
36
|
+
it('reads a visible, focused page as active', () => {
|
|
37
|
+
setVisibility('visible');
|
|
38
|
+
setFocus(true);
|
|
39
|
+
expect(readPageState()).toBe(PAGE_STATE_ACTIVE);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reads a visible, unfocused page as passive', () => {
|
|
43
|
+
setVisibility('visible');
|
|
44
|
+
setFocus(false);
|
|
45
|
+
expect(readPageState()).toBe(PAGE_STATE_PASSIVE);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('reads a hidden page as hidden', () => {
|
|
49
|
+
setVisibility('hidden');
|
|
50
|
+
setFocus(false);
|
|
51
|
+
expect(readPageState()).toBe(PAGE_STATE_HIDDEN);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('getPageState', () => {
|
|
56
|
+
it('starts the observer and reports the current state', () => {
|
|
57
|
+
setVisibility('visible');
|
|
58
|
+
setFocus(true);
|
|
59
|
+
expect(getPageState()).toBe(PAGE_STATE_ACTIVE);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('onPageStateChange', () => {
|
|
64
|
+
it('reports the state the page moved to and the one it left', () => {
|
|
65
|
+
setFocus(true);
|
|
66
|
+
startPageLifecycle();
|
|
67
|
+
const seen: string[][] = [];
|
|
68
|
+
onPageStateChange((state, previous) => {
|
|
69
|
+
seen.push([state, previous]);
|
|
70
|
+
});
|
|
71
|
+
setVisibility('hidden');
|
|
72
|
+
dispatch('visibilitychange');
|
|
73
|
+
expect(seen).toStrictEqual([[PAGE_STATE_HIDDEN, PAGE_STATE_ACTIVE]]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('says nothing when the state has not moved', () => {
|
|
77
|
+
setFocus(true);
|
|
78
|
+
startPageLifecycle();
|
|
79
|
+
const listener = vi.fn<(state: string, previous: string) => void>();
|
|
80
|
+
onPageStateChange(listener);
|
|
81
|
+
dispatch('visibilitychange');
|
|
82
|
+
expect(listener).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('stops reporting once the listener is removed', () => {
|
|
86
|
+
setFocus(true);
|
|
87
|
+
startPageLifecycle();
|
|
88
|
+
const listener = vi.fn<(state: string, previous: string) => void>();
|
|
89
|
+
onPageStateChange(listener)();
|
|
90
|
+
setVisibility('hidden');
|
|
91
|
+
dispatch('visibilitychange');
|
|
92
|
+
expect(listener).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('reports a freeze', () => {
|
|
96
|
+
startPageLifecycle();
|
|
97
|
+
const listener = vi.fn<(state: string, previous: string) => void>();
|
|
98
|
+
onPageStateChange(listener);
|
|
99
|
+
dispatch('freeze');
|
|
100
|
+
expect(listener).toHaveBeenCalledWith(PAGE_STATE_FROZEN, PAGE_STATE_ACTIVE);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('reports an unloading page as terminated', () => {
|
|
104
|
+
startPageLifecycle();
|
|
105
|
+
const listener = vi.fn<(state: string, previous: string) => void>();
|
|
106
|
+
onPageStateChange(listener);
|
|
107
|
+
globalThis.dispatchEvent(new Event('pagehide'));
|
|
108
|
+
expect(listener).toHaveBeenCalledWith(PAGE_STATE_TERMINATED, PAGE_STATE_ACTIVE);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('ignores focus events that came from an element rather than the page', () => {
|
|
112
|
+
setFocus(true);
|
|
113
|
+
startPageLifecycle();
|
|
114
|
+
const listener = vi.fn<(state: string, previous: string) => void>();
|
|
115
|
+
onPageStateChange(listener);
|
|
116
|
+
const input = document.createElement('input');
|
|
117
|
+
document.body.append(input);
|
|
118
|
+
setFocus(false);
|
|
119
|
+
input.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
120
|
+
expect(listener).not.toHaveBeenCalled();
|
|
121
|
+
input.remove();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('oncePageActive', () => {
|
|
126
|
+
it('runs the callback the next time the page becomes active', () => {
|
|
127
|
+
setFocus(false);
|
|
128
|
+
setVisibility('hidden');
|
|
129
|
+
startPageLifecycle();
|
|
130
|
+
const callback = vi.fn<() => void>();
|
|
131
|
+
oncePageActive(callback);
|
|
132
|
+
setVisibility('visible');
|
|
133
|
+
setFocus(true);
|
|
134
|
+
dispatch('visibilitychange');
|
|
135
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('runs the callback only once', () => {
|
|
139
|
+
setFocus(false);
|
|
140
|
+
setVisibility('hidden');
|
|
141
|
+
startPageLifecycle();
|
|
142
|
+
const callback = vi.fn<() => void>();
|
|
143
|
+
oncePageActive(callback);
|
|
144
|
+
setVisibility('visible');
|
|
145
|
+
setFocus(true);
|
|
146
|
+
dispatch('visibilitychange');
|
|
147
|
+
setVisibility('hidden');
|
|
148
|
+
setFocus(false);
|
|
149
|
+
dispatch('visibilitychange');
|
|
150
|
+
setVisibility('visible');
|
|
151
|
+
setFocus(true);
|
|
152
|
+
dispatch('visibilitychange');
|
|
153
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('does not run the callback while the page stays hidden', () => {
|
|
157
|
+
setFocus(false);
|
|
158
|
+
setVisibility('hidden');
|
|
159
|
+
startPageLifecycle();
|
|
160
|
+
const callback = vi.fn<() => void>();
|
|
161
|
+
oncePageActive(callback);
|
|
162
|
+
dispatch('freeze');
|
|
163
|
+
expect(callback).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('cancels a pending callback', () => {
|
|
167
|
+
setFocus(false);
|
|
168
|
+
setVisibility('hidden');
|
|
169
|
+
startPageLifecycle();
|
|
170
|
+
const callback = vi.fn<() => void>();
|
|
171
|
+
oncePageActive(callback)();
|
|
172
|
+
setVisibility('visible');
|
|
173
|
+
setFocus(true);
|
|
174
|
+
dispatch('visibilitychange');
|
|
175
|
+
expect(callback).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal page lifecycle observer.
|
|
3
|
+
* @see https://developer.chrome.com/docs/web-platform/page-lifecycle-api
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The page is visible and has focus. */
|
|
7
|
+
export const PAGE_STATE_ACTIVE = 'active';
|
|
8
|
+
/** The page is visible but does not have focus. */
|
|
9
|
+
export const PAGE_STATE_PASSIVE = 'passive';
|
|
10
|
+
/** The page is not visible. */
|
|
11
|
+
export const PAGE_STATE_HIDDEN = 'hidden';
|
|
12
|
+
/** The browser has frozen the page to reclaim its resources. */
|
|
13
|
+
export const PAGE_STATE_FROZEN = 'frozen';
|
|
14
|
+
/** The page is being unloaded. */
|
|
15
|
+
export const PAGE_STATE_TERMINATED = 'terminated';
|
|
16
|
+
|
|
17
|
+
/** One of the states a page can be in. */
|
|
18
|
+
export type PageState =
|
|
19
|
+
| typeof PAGE_STATE_ACTIVE
|
|
20
|
+
| typeof PAGE_STATE_PASSIVE
|
|
21
|
+
| typeof PAGE_STATE_HIDDEN
|
|
22
|
+
| typeof PAGE_STATE_FROZEN
|
|
23
|
+
| typeof PAGE_STATE_TERMINATED;
|
|
24
|
+
|
|
25
|
+
/** Called with the state the page has just entered and the one it left. */
|
|
26
|
+
export type PageStateListener = (state: PageState, previous: PageState) => void;
|
|
27
|
+
|
|
28
|
+
const listeners = new Set<PageStateListener>();
|
|
29
|
+
let currentState: PageState | null = null;
|
|
30
|
+
let started = false;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reads the state the page is in right now.
|
|
34
|
+
* @returns {PageState} The current page state.
|
|
35
|
+
*/
|
|
36
|
+
export const readPageState = (): PageState => {
|
|
37
|
+
if (document.visibilityState === 'hidden') {
|
|
38
|
+
return PAGE_STATE_HIDDEN;
|
|
39
|
+
}
|
|
40
|
+
return document.hasFocus() ? PAGE_STATE_ACTIVE : PAGE_STATE_PASSIVE;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Records a new state and tells the listeners, ignoring transitions to the state already held.
|
|
45
|
+
* @param {PageState} next - The state the page has entered.
|
|
46
|
+
*/
|
|
47
|
+
const enterState = (next: PageState): void => {
|
|
48
|
+
const previous = currentState ?? readPageState();
|
|
49
|
+
if (next === previous) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
currentState = next;
|
|
53
|
+
const current = [...listeners];
|
|
54
|
+
for (const listener of current) {
|
|
55
|
+
listener(next, previous);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Starts observing the page, if it is not observed already.
|
|
61
|
+
*/
|
|
62
|
+
export const startPageLifecycle = (): void => {
|
|
63
|
+
if (started) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
started = true;
|
|
67
|
+
currentState = readPageState();
|
|
68
|
+
for (const type of ['pageshow', 'focus', 'blur', 'visibilitychange', 'resume']) {
|
|
69
|
+
globalThis.addEventListener(
|
|
70
|
+
type,
|
|
71
|
+
(event: Event): void => {
|
|
72
|
+
// blur and focus also fire for elements inside the page, which say nothing about the page
|
|
73
|
+
const fromElement = event.target !== globalThis && event.target !== document;
|
|
74
|
+
if ((event.type === 'blur' || event.type === 'focus') && fromElement) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
enterState(readPageState());
|
|
78
|
+
},
|
|
79
|
+
true
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
globalThis.addEventListener(
|
|
83
|
+
'freeze',
|
|
84
|
+
(): void => {
|
|
85
|
+
enterState(PAGE_STATE_FROZEN);
|
|
86
|
+
},
|
|
87
|
+
true
|
|
88
|
+
);
|
|
89
|
+
globalThis.addEventListener(
|
|
90
|
+
'pagehide',
|
|
91
|
+
(event: Event): void => {
|
|
92
|
+
enterState((event as PageTransitionEvent).persisted ? PAGE_STATE_FROZEN : PAGE_STATE_TERMINATED);
|
|
93
|
+
},
|
|
94
|
+
true
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns the state the page is in, starting the observer on first use.
|
|
100
|
+
* @returns {PageState} The current page state.
|
|
101
|
+
*/
|
|
102
|
+
export const getPageState = (): PageState => {
|
|
103
|
+
startPageLifecycle();
|
|
104
|
+
return currentState ?? readPageState();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Registers a listener for every state change.
|
|
109
|
+
* @param {PageStateListener} listener - The listener to call on each change.
|
|
110
|
+
* @returns {() => void} A function that removes the listener again.
|
|
111
|
+
*/
|
|
112
|
+
export const onPageStateChange = (listener: PageStateListener): (() => void) => {
|
|
113
|
+
startPageLifecycle();
|
|
114
|
+
listeners.add(listener);
|
|
115
|
+
return (): void => {
|
|
116
|
+
listeners.delete(listener);
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Runs a callback the next time the page becomes active, once.
|
|
122
|
+
* @param {() => void} callback - The callback to run.
|
|
123
|
+
* @returns {() => void} A function that cancels the pending callback.
|
|
124
|
+
*/
|
|
125
|
+
export const oncePageActive = (callback: () => void): (() => void) => {
|
|
126
|
+
const stop = onPageStateChange((state: PageState): void => {
|
|
127
|
+
if (state === PAGE_STATE_ACTIVE) {
|
|
128
|
+
stop();
|
|
129
|
+
callback();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return stop;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Drops every listener and stops tracking. Only the tests need this.
|
|
137
|
+
*/
|
|
138
|
+
export const resetPageLifecycle = (): void => {
|
|
139
|
+
listeners.clear();
|
|
140
|
+
currentState = null;
|
|
141
|
+
started = false;
|
|
142
|
+
};
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { Game } from './game.js';
|
|
2
|
-
import {
|
|
3
|
-
addPageLifecycleCallback,
|
|
4
|
-
getPageLifecycleEventEmitter,
|
|
5
|
-
PAGE_LIFECYCLE_STATE_ACTIVE,
|
|
6
|
-
PAGE_LIFECYCLE_STATE_CHANGE_EVENT,
|
|
7
|
-
} from '@vpmedia/simplify';
|
|
8
2
|
import { ArraySet } from './array_set.js';
|
|
9
3
|
import { AUDIO_DISABLED, AUDIO_STANDARD, AUDIO_WEBKIT } from './const.js';
|
|
10
4
|
import { ENGINE_ERROR_CREATING_AUDIO_CONTEXT } from './error_code.js';
|
|
@@ -12,6 +6,7 @@ import { Signal } from './signal.js';
|
|
|
12
6
|
import { Sound } from './sound.js';
|
|
13
7
|
import { SoundSprite } from './sound_sprite.js';
|
|
14
8
|
import type { Callback } from './callback.js';
|
|
9
|
+
import { onPageStateChange, oncePageActive } from './page_lifecycle.js';
|
|
15
10
|
|
|
16
11
|
export class SoundManager {
|
|
17
12
|
public game!: Game;
|
|
@@ -115,7 +110,7 @@ export class SoundManager {
|
|
|
115
110
|
this.addUnlockHandlers();
|
|
116
111
|
}
|
|
117
112
|
this.context.addEventListener('statechange', this.onContextStateChange, false);
|
|
118
|
-
|
|
113
|
+
onPageStateChange(this.onPageLifecycleChange);
|
|
119
114
|
}
|
|
120
115
|
|
|
121
116
|
/**
|
|
@@ -293,7 +288,7 @@ export class SoundManager {
|
|
|
293
288
|
const typedError = error instanceof Error ? error : new Error(String(error));
|
|
294
289
|
this.game.logger.fatal('SoundManager', { error: typedError, tags: { 'asset.key': key } });
|
|
295
290
|
if (typedError.name === 'InvalidStateError') {
|
|
296
|
-
|
|
291
|
+
oncePageActive((): void => {
|
|
297
292
|
void this.decode(key);
|
|
298
293
|
});
|
|
299
294
|
} else if (typedError.name === 'EncodingError') {
|