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/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(selector);
46
+ }).$mount(mountPoint);
39
47
 
40
- const item: EventEmitter = new EventEmitter();
41
48
  let replaying = false;
42
-
43
- // The move payload is the whole current turn so far (an array of atomic moves),
44
- // replayed by the engine wrapper from the last committed state.
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
- } else {
88
- item.emit('fetchState');
89
- }
90
- });
91
-
92
- item.addListener('replay:start', () => {
93
- params.emitter.emit('replayStart');
94
- replaying = true;
95
- });
96
- item.addListener('replay:to', (info) => {
97
- params.emitter.emit('replayTo', info);
98
- });
99
- item.addListener('replay:end', () => {
100
- params.emitter.emit('replayEnd');
101
- replaying = false;
102
- item.emit('fetchState');
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: { on: (event: string, fn: (value: any) => void) => unknown }): void {
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
- if (event?.data?.state) {
143
- receiveState(event.data.state);
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: { emit: (event: string, value: any) => unknown }): void {
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": "node",
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"]