powergrid-viewer 2.1.2 → 2.1.4
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/powergrid-viewer.umd.min.js +11 -11
- package/package.json +7 -4
- package/src/components/Game.vue +2 -1
- package/src/game-chat.ts +119 -267
- package/src/launch.ts +68 -64
- package/src/sounds.ts +6 -4
- package/tsconfig.json +4 -2
package/src/launch.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createViewer } from '@boardgamers/protocol/viewer';
|
|
1
2
|
import { EventEmitter } from 'events';
|
|
2
3
|
import type { GameState, Move } from 'powergrid-engine';
|
|
3
4
|
import Vue from 'vue';
|
|
@@ -7,7 +8,14 @@ import { installActionSounds } from './sounds';
|
|
|
7
8
|
import type { Preferences } from './types/ui-data';
|
|
8
9
|
import { shouldAdoptLogState } from './util/turn-buffer';
|
|
9
10
|
|
|
11
|
+
let dispose: (() => void) | undefined;
|
|
12
|
+
|
|
10
13
|
function launch(selector: string) {
|
|
14
|
+
const target = document.querySelector(selector);
|
|
15
|
+
if (!target) throw new Error(`Viewer mount point not found: ${selector}`);
|
|
16
|
+
dispose?.();
|
|
17
|
+
const mountPoint = document.createElement('div');
|
|
18
|
+
target.append(mountPoint);
|
|
11
19
|
let params: {
|
|
12
20
|
state: null | GameState;
|
|
13
21
|
player?: number;
|
|
@@ -35,75 +43,71 @@ function launch(selector: string) {
|
|
|
35
43
|
|
|
36
44
|
const app = new Vue({
|
|
37
45
|
render: (h) => h(Game, { props: params }, []),
|
|
38
|
-
}).$mount(
|
|
46
|
+
}).$mount(mountPoint);
|
|
39
47
|
|
|
40
|
-
const item: EventEmitter = new EventEmitter();
|
|
41
48
|
let replaying = false;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
params.emitter.on('move', (moves: Move[]) => item.emit('move', moves));
|
|
46
|
-
params.emitter.on('fetchState', () => item.emit('fetchState'));
|
|
47
|
-
params.emitter.on('addLog', (data: string[]) => item.emit('addLog', data));
|
|
48
|
-
params.emitter.on('replaceLog', (data: string[]) => item.emit('replaceLog', data));
|
|
49
|
-
params.emitter.on('replay:info', (info: { start: number; current: number; end: number }) =>
|
|
50
|
-
item.emit('replay:info', info)
|
|
51
|
-
);
|
|
52
|
-
params.emitter.on('update:preference', (data: { name: string; value: any }) =>
|
|
53
|
-
item.emit('update:preference', data)
|
|
54
|
-
);
|
|
55
|
-
item.addListener('avatars', (data) => {
|
|
56
|
-
params.avatars = data;
|
|
57
|
-
app.$forceUpdate();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
item.addListener('state', (data) => {
|
|
61
|
-
params.state = data;
|
|
62
|
-
app.$forceUpdate();
|
|
63
|
-
app.$nextTick().then(() => item.emit('ready'));
|
|
64
|
-
});
|
|
65
|
-
item.addListener('state:updated', () => item.emit('fetchState'));
|
|
66
|
-
item.addListener('player', (data) => {
|
|
67
|
-
params.player = data.index;
|
|
68
|
-
app.$forceUpdate();
|
|
69
|
-
});
|
|
70
|
-
item.addListener('preferences', (data) => {
|
|
71
|
-
// Mutate (don't replace) the observable object so the update stays reactive
|
|
72
|
-
Object.assign(params.preferences, data);
|
|
73
|
-
app.$forceUpdate();
|
|
74
|
-
});
|
|
75
|
-
item.addListener('gamelog', (logData) => {
|
|
76
|
-
if (replaying) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (shouldAdoptLogState(logData?.data?.state)) {
|
|
81
|
-
// Move responses carry the (possibly tentative) resulting state. Tentative
|
|
82
|
-
// states are never persisted or broadcast by the platform — this is the
|
|
83
|
-
// only way they reach the acting player's viewer. Committed states are
|
|
84
|
-
// refetched; see shouldAdoptLogState for why adopting them is unsafe.
|
|
85
|
-
params.state = logData.data.state;
|
|
49
|
+
const viewer = createViewer<GameState, Move[]>({
|
|
50
|
+
async onState(data) {
|
|
51
|
+
params.state = data;
|
|
86
52
|
app.$forceUpdate();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
53
|
+
await app.$nextTick();
|
|
54
|
+
},
|
|
55
|
+
onPlayer(data) {
|
|
56
|
+
params.player = data.index;
|
|
57
|
+
app.$forceUpdate();
|
|
58
|
+
},
|
|
59
|
+
onPreferences(data) {
|
|
60
|
+
Object.assign(params.preferences, data);
|
|
61
|
+
app.$forceUpdate();
|
|
62
|
+
},
|
|
63
|
+
onAvatars(data) {
|
|
64
|
+
params.avatars = data;
|
|
65
|
+
app.$forceUpdate();
|
|
66
|
+
},
|
|
67
|
+
onUpdate() {
|
|
68
|
+
if (!replaying) viewer.fetchState();
|
|
69
|
+
},
|
|
70
|
+
async onLog(logData) {
|
|
71
|
+
if (replaying) return;
|
|
72
|
+
const data = logData.data as { state?: GameState } | undefined;
|
|
73
|
+
// Only tentative move responses may replace the local turn buffer.
|
|
74
|
+
if (shouldAdoptLogState(data?.state)) {
|
|
75
|
+
params.state = data!.state!;
|
|
76
|
+
app.$forceUpdate();
|
|
77
|
+
await app.$nextTick();
|
|
78
|
+
} else viewer.fetchState();
|
|
79
|
+
},
|
|
80
|
+
onReplayStart() {
|
|
81
|
+
replaying = true;
|
|
82
|
+
params.emitter.emit('replayStart');
|
|
83
|
+
},
|
|
84
|
+
onReplayTo(index) {
|
|
85
|
+
params.emitter.emit('replayTo', index);
|
|
86
|
+
},
|
|
87
|
+
onReplayEnd() {
|
|
88
|
+
params.emitter.emit('replayEnd');
|
|
89
|
+
replaying = false;
|
|
90
|
+
viewer.fetchState();
|
|
91
|
+
},
|
|
103
92
|
});
|
|
104
|
-
|
|
93
|
+
const item = viewer.emitter;
|
|
94
|
+
params.emitter.on('move', (moves: Move[]) => viewer.move(moves));
|
|
95
|
+
params.emitter.on('fetchState', () => viewer.fetchState());
|
|
96
|
+
params.emitter.on('addLog', (data: string[]) => viewer.addLog(data));
|
|
97
|
+
params.emitter.on('replaceLog', (data: string[]) => viewer.replaceLog(data));
|
|
98
|
+
params.emitter.on('replay:info', (info) => viewer.setReplayInfo(info));
|
|
99
|
+
params.emitter.on('update:preference', ({ name, value }) => viewer.updatePreference(name, value));
|
|
105
100
|
installActionSounds(item);
|
|
106
|
-
mountGameChat(item, app.$el);
|
|
101
|
+
const removeChat = mountGameChat(item, app.$el);
|
|
102
|
+
app.$once('hook:beforeDestroy', () => {
|
|
103
|
+
removeChat();
|
|
104
|
+
viewer.destroy();
|
|
105
|
+
params.emitter.removeAllListeners();
|
|
106
|
+
});
|
|
107
|
+
dispose = () => {
|
|
108
|
+
app.$destroy();
|
|
109
|
+
target.replaceChildren();
|
|
110
|
+
};
|
|
107
111
|
return item;
|
|
108
112
|
}
|
|
109
113
|
|
package/src/sounds.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ViewerEmitter } from '@boardgamers/protocol/viewer';
|
|
1
2
|
type Note = [number, number, number, number, number?];
|
|
2
3
|
type Cue = { label: string; notes: Note[] };
|
|
3
4
|
export const soundCues: Record<string, Cue> = {
|
|
@@ -97,7 +98,7 @@ export function playSound(name: string): void {
|
|
|
97
98
|
.catch(() => undefined);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
export function installActionSounds(emitter:
|
|
101
|
+
export function installActionSounds(emitter: Pick<ViewerEmitter<any, any>, 'on'>): void {
|
|
101
102
|
let previous: string[] | undefined;
|
|
102
103
|
let replaying = false;
|
|
103
104
|
emitter.on('update:preference', (pref) => {
|
|
@@ -139,13 +140,14 @@ export function installActionSounds(emitter: { on: (event: string, fn: (value: a
|
|
|
139
140
|
};
|
|
140
141
|
emitter.on('state', receiveState);
|
|
141
142
|
emitter.on('gamelog', (event) => {
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
const data = event?.data as { state?: any } | undefined;
|
|
144
|
+
if (data?.state) {
|
|
145
|
+
receiveState(data.state);
|
|
144
146
|
}
|
|
145
147
|
});
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
export function mountSoundTests(emitter:
|
|
150
|
+
export function mountSoundTests(emitter: Pick<ViewerEmitter<any, any>, 'emit'>): void {
|
|
149
151
|
const panel = document.createElement('details');
|
|
150
152
|
panel.style.cssText =
|
|
151
153
|
'position:relative;z-index:5;padding:10px 16px;margin:8px;background:#172638;color:#f0f4f8;border:1px solid #56718a;border-radius:8px;font:14px system-ui';
|
package/tsconfig.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"strict": true,
|
|
6
6
|
"jsx": "preserve",
|
|
7
7
|
"importHelpers": true,
|
|
8
|
-
"moduleResolution": "
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
9
|
"experimentalDecorators": true,
|
|
10
10
|
"esModuleInterop": true,
|
|
11
11
|
"allowSyntheticDefaultImports": true,
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"paths": {
|
|
17
17
|
"@/*": ["src/*"]
|
|
18
18
|
},
|
|
19
|
-
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
|
19
|
+
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"useDefineForClassFields": false
|
|
20
22
|
},
|
|
21
23
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
|
|
22
24
|
"exclude": ["node_modules"]
|