powergrid-viewer 1.5.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.
Files changed (57) hide show
  1. package/README.md +35 -0
  2. package/dist/img/help.ba7779cc.svg +1 -0
  3. package/dist/img/log.04ef6981.svg +1 -0
  4. package/dist/img/pass.da9065dc.svg +3 -0
  5. package/dist/img/rules.64f9aae5.svg +28 -0
  6. package/dist/img/sound-off.72ada995.svg +3 -0
  7. package/dist/img/sound-on.c55edd90.svg +3 -0
  8. package/dist/img/undo.208666d2.svg +3 -0
  9. package/dist/media/notification.55fa47dd.ogg +0 -0
  10. package/dist/media/notification.ac905963.mp3 +0 -0
  11. package/dist/media/piece-drop.eef5f607.mp3 +0 -0
  12. package/dist/powergrid-viewer.css +1 -0
  13. package/dist/powergrid-viewer.umd.min.js +35 -0
  14. package/package.json +49 -0
  15. package/src/audio/notification.mp3 +0 -0
  16. package/src/audio/notification.ogg +0 -0
  17. package/src/audio/piece-drop.mp3 +0 -0
  18. package/src/components/Calculator.vue +62 -0
  19. package/src/components/Game.vue +1354 -0
  20. package/src/components/PlayerBoard.vue +230 -0
  21. package/src/components/boards/CityCount.vue +82 -0
  22. package/src/components/boards/Map.vue +196 -0
  23. package/src/components/boards/PlayerOrder.vue +68 -0
  24. package/src/components/boards/PowerPlantMarket.vue +184 -0
  25. package/src/components/boards/Resources.vue +446 -0
  26. package/src/components/buttons/Button.vue +26 -0
  27. package/src/components/buttons/HelpButton.vue +18 -0
  28. package/src/components/buttons/LogButton.vue +15 -0
  29. package/src/components/buttons/PassButton.vue +18 -0
  30. package/src/components/buttons/RulesButton.vue +14 -0
  31. package/src/components/buttons/SoundButton.vue +18 -0
  32. package/src/components/buttons/UndoButton.vue +17 -0
  33. package/src/components/buttons/index.js +9 -0
  34. package/src/components/pieces/Card.vue +131 -0
  35. package/src/components/pieces/Coal.vue +40 -0
  36. package/src/components/pieces/Garbage.vue +40 -0
  37. package/src/components/pieces/House.vue +51 -0
  38. package/src/components/pieces/Hybrid.vue +37 -0
  39. package/src/components/pieces/Oil.vue +40 -0
  40. package/src/components/pieces/Piece.vue +104 -0
  41. package/src/components/pieces/Uranium.vue +32 -0
  42. package/src/components/pieces/index.js +10 -0
  43. package/src/icons/help.svg +1 -0
  44. package/src/icons/log.svg +1 -0
  45. package/src/icons/pass.svg +3 -0
  46. package/src/icons/rules.svg +28 -0
  47. package/src/icons/sound-off.svg +3 -0
  48. package/src/icons/sound-on.svg +3 -0
  49. package/src/icons/undo.svg +3 -0
  50. package/src/launch.ts +87 -0
  51. package/src/main.ts +3 -0
  52. package/src/self-contained.ts +97 -0
  53. package/src/shims-tsx.d.ts +13 -0
  54. package/src/shims-vue.d.ts +4 -0
  55. package/src/types/ui-data.ts +34 -0
  56. package/src/wrapper.ts +8 -0
  57. package/tsconfig.json +23 -0
@@ -0,0 +1,1354 @@
1
+ <template>
2
+ <div :class="['game', { fitToScreen: preferences.fitToScreen }]">
3
+ <div class="statusBar">
4
+ {{ getStatusMessage() }}
5
+ </div>
6
+ <audio id="piece-drop" preload="none">
7
+ <source src="../audio/piece-drop.mp3" type="audio/mpeg" />
8
+ </audio>
9
+ <audio id="notification" preload="none">
10
+ <source src="../audio/notification.mp3" type="audio/mpeg" />
11
+ <source src="../audio/notification.ogg" type="audio/ogg" />
12
+ </audio>
13
+ <svg
14
+ v-if="G"
15
+ id="scene"
16
+ :viewBox="G.map.viewBox ? `0 0 ${G.map.viewBox[0]} ${G.map.viewBox[1]}` : '0 0 1500 800'"
17
+ style="width: 100%"
18
+ >
19
+ <rect width="100%" height="100%" x="0" y="0" fill="yellowgreen" />
20
+
21
+ <PlayerOrder
22
+ ref="playerOrder"
23
+ :transform="`translate(${G.map.playerOrderPosition[0]}, ${G.map.playerOrderPosition[1]})`"
24
+ :playerColors="playerColors"
25
+ />
26
+
27
+ <CityCount
28
+ ref="cityCount"
29
+ :transform="`translate(${G.map.cityCountPosition[0]}, ${G.map.cityCountPosition[1]})`"
30
+ :playerColors="playerColors"
31
+ :citiesToEndGame="G.citiesToEndGame"
32
+ :citiesToStep2="G.citiesToStep2"
33
+ />
34
+
35
+ <PowerPlantMarket
36
+ ref="powerPlantMarket"
37
+ :transform="`translate(${G.map.powerPlantMarketPosition[0]}, ${G.map.powerPlantMarketPosition[1]})`"
38
+ :canBid="canBid()"
39
+ :canChoose="canChoose()"
40
+ :chooseablePowerPlants="getChooseablePowerPlants()"
41
+ :cardsLeft="G.cardsLeft"
42
+ :minBid="G.currentBid + 1 || G.minimunBid"
43
+ :maxBid="G.players[player] ? G.players[player].money : 0"
44
+ :nextCardWeak="G.nextCardWeak"
45
+ :plantDiscountActive="G.plantDiscountActive"
46
+ @choosePowerPlant="choosePowerPlant($event)"
47
+ @bid="bid($event)"
48
+ />
49
+
50
+ <Map
51
+ ref="map"
52
+ :transform="`translate(${G.map.mapPosition[0]}, ${G.map.mapPosition[1]})`"
53
+ :playerColors="playerColors"
54
+ :cities="G.map.cities"
55
+ :connections="G.map.connections"
56
+ :polygons="G.map.polygons"
57
+ :buildableCities="getBuildableCities()"
58
+ @build="build($event)"
59
+ />
60
+
61
+ <Resources
62
+ ref="resources"
63
+ :transform="`translate(${G.map.supplyPosition[0]}, ${G.map.supplyPosition[1]})`"
64
+ :isUsaRecharged="G.options.variant == 'recharged' && G.map.name == 'USA'"
65
+ :isMiddleEast="G.map.name == 'Middle East'"
66
+ :isIndiaResourceMarket="G.map.name == 'India' && G.coalPrices && G.garbagePrices && G.uraniumPrices"
67
+ :availableSurplusOil="
68
+ G.map.name == 'Middle East' ? Math.max(G.oilMarket - G.oilPrices.filter((p) => p > 1).length, 0) : 0
69
+ "
70
+ :buyableResources="buyableResources()"
71
+ :resourceResupply="getResourceResupply()"
72
+ @buyResource="buyResource($event)"
73
+ />
74
+
75
+ <g :transform="`translate(${G.map.roundInfoPosition[0]}, ${G.map.roundInfoPosition[1]})`">
76
+ <template v-if="gameEnded(G)">
77
+ <Button
78
+ :transform="`translate(20, 50)`"
79
+ :width="130"
80
+ :text="'Final Score'"
81
+ @click="endScoreVisible = true"
82
+ />
83
+ <template v-if="G.options.trackTotalSpent">
84
+ <Button
85
+ :transform="`translate(180, 50)`"
86
+ :width="120"
87
+ :text="'Game Stats'"
88
+ @click="spendingVisible = true"
89
+ />
90
+ </template>
91
+ </template>
92
+ <template v-else>
93
+ <text x="10" y="20" font-weight="600" fill="black" style="font-size: 32px">
94
+ Round: {{ G.round }}
95
+ </text>
96
+ <text x="10" y="60" font-weight="600" fill="black" style="font-size: 32px">Step: {{ G.step }}</text>
97
+ <text x="10" y="100" font-weight="600" fill="black" style="font-size: 32px">
98
+ Phase: {{ G.phase }}
99
+ </text>
100
+ </template>
101
+ </g>
102
+
103
+ <g :transform="`translate(${G.map.buttonsPosition[0]}, ${G.map.buttonsPosition[1]})`">
104
+ <PassButton
105
+ transform="translate(15, 15)"
106
+ :enabled="canPass()"
107
+ :highlightButton="canPass() && !preferences.disableHelp"
108
+ :text="canUndo() ? 'Done' : 'Pass'"
109
+ @click="checkPass()"
110
+ />
111
+ <UndoButton
112
+ transform="translate(15, 56)"
113
+ :enabled="canUndo()"
114
+ :highlightButton="canUndo() && !preferences.disableHelp"
115
+ @click="undo()"
116
+ />
117
+ <LogButton transform="translate(15, 97)" @click="showLog()" />
118
+ <SoundButton transform="translate(110, 13)" :isOn="preferences.sound" @click="toggleSound()" />
119
+ <HelpButton transform="translate(110, 54)" :isOn="!preferences.disableHelp" @click="toggleHelp()" />
120
+ <RulesButton transform="translate(110, 95)" @click="rulesVisible = true" />
121
+ </g>
122
+
123
+ <template v-for="(playerIndex, i) in adjustedPlayerOrder">
124
+ <PlayerBoard
125
+ :key="'B' + playerIndex"
126
+ :transform="`translate(${G.map.playerBoardsPosition[0]}, ${
127
+ G.map.playerBoardsPosition[1] + 110 * i
128
+ })`"
129
+ :player="G.players[playerIndex]"
130
+ :color="playerColors[playerIndex]"
131
+ :avatar="avatars[playerIndex]"
132
+ :owner="playerIndex"
133
+ :isCurrentPlayer="isCurrentPlayer(playerIndex)"
134
+ :ended="gameEnded(G)"
135
+ :isPlayer="player == playerIndex"
136
+ :ranking="sortedPlayers.findIndex((x) => x.id == G.players[playerIndex].id) + 1"
137
+ :showMoney="player == playerIndex || gameEnded(G) || G.options.showMoney"
138
+ :showBid="!G.options.fastBid"
139
+ :phase="G.phase"
140
+ @powerPlantClick="powerPlantClick($event)"
141
+ @discardResource="discardResource($event)"
142
+ />
143
+ </template>
144
+ </svg>
145
+
146
+ <div v-if="G" :class="['modal', { visible: logVisible }]">
147
+ <div class="modal-content">
148
+ <span class="close" @click="logVisible = false">&times;</span>
149
+ <div class="modal-title">Log</div>
150
+ <div class="modal-log">
151
+ <div v-for="(log, i) in logReversed" :key="'L' + i" class="log-line" v-html="log" />
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <div v-if="G" :class="['modal', { visible: confirmVisible }]">
157
+ <div class="modal-content">
158
+ <span class="close" @click="confirmVisible = false">&times;</span>
159
+ <div class="modal-title">Confirm</div>
160
+ <div class="confirm-message">{{ confirmMessage }}</div>
161
+ <div class="confirm-buttons">
162
+ <button class="confirm-button" @click="confirmPass()">OK</button>
163
+ <button class="confirm-button" @click="confirmVisible = false">Cancel</button>
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <div v-if="G && discardedPowerPlant" :class="['modal', { visible: discardVisible }]">
169
+ <div class="modal-content">
170
+ <div class="modal-title">Discard Resources</div>
171
+ <div class="confirm-message">Choose which resources to discard:</div>
172
+ <div
173
+ v-for="(r, i) in resourcesToDiscard"
174
+ :key="'R' + i"
175
+ class="confirm-message"
176
+ style="text-align: center"
177
+ >
178
+ {{ r.name }}: <input v-model="r.value" type="number" min="0" :max="r.max" style="width: 3em" />
179
+ </div>
180
+ <div class="confirm-buttons">
181
+ <button class="confirm-button" :disabled="discardInvalid()" @click="confirmDiscard()">OK</button>
182
+ </div>
183
+ </div>
184
+ </div>
185
+
186
+ <div v-if="G && gameEnded(G)" :class="['modal', { visible: endScoreVisible }]">
187
+ <div class="modal-content">
188
+ <span class="close" @click="endScoreVisible = false">&times;</span>
189
+ <div class="modal-title">Final Score</div>
190
+ <table class="final-score-table">
191
+ <tr>
192
+ <th><div>Player</div></th>
193
+ <th v-for="player in sortedPlayers" :key="'FS' + player.id">
194
+ <div :style="'background-color: ' + playerColors[player.id]">{{ player.name }}</div>
195
+ </th>
196
+ </tr>
197
+ <tr v-for="(cat, i) in ['Cities Powered', 'Money', 'Total Cities']" :key="'FC_' + cat">
198
+ <td>{{ cat }}</td>
199
+ <td v-for="player in sortedPlayers" :key="'FS' + player.id + i">
200
+ <div>
201
+ {{ i == 0 ? player.citiesPowered : i == 1 ? player.money : player.cities.length }}
202
+ </div>
203
+ </td>
204
+ </tr>
205
+ </table>
206
+ </div>
207
+ </div>
208
+
209
+ <div v-if="G && gameEnded(G)" :class="['modal', { visible: spendingVisible }]">
210
+ <div class="modal-content">
211
+ <span class="close" @click="spendingVisible = false">&times;</span>
212
+ <div class="modal-title">Spending</div>
213
+ <table class="spending-table">
214
+ <tr>
215
+ <th><div>Player</div></th>
216
+ <th v-for="player in sortedPlayers" :key="'FS' + player.id">
217
+ <div :style="'background-color: ' + playerColors[player.id]">{{ player.name }}</div>
218
+ </th>
219
+ </tr>
220
+ <tr
221
+ v-for="(cat, i) in [
222
+ 'Income',
223
+ 'Spending: Cities',
224
+ 'Spending: Connections',
225
+ 'Spending: Plants',
226
+ 'Spending: Resources',
227
+ ]"
228
+ :key="'FC_' + cat"
229
+ >
230
+ <td>{{ cat }}</td>
231
+ <td v-for="player in sortedPlayers" :key="'FS' + player.id + i">
232
+ <div>
233
+ {{
234
+ i == 0
235
+ ? player.totalIncome
236
+ : i == 1
237
+ ? player.totalSpentCities
238
+ : i == 2
239
+ ? player.totalSpentConnections
240
+ : i == 3
241
+ ? player.totalSpentPlants
242
+ : player.totalSpentResources
243
+ }}
244
+ </div>
245
+ </td>
246
+ </tr>
247
+ </table>
248
+ </div>
249
+ </div>
250
+
251
+ <div v-if="G" :class="['modal', { visible: rulesVisible }]">
252
+ <div class="modal-content">
253
+ <span class="close" @click="rulesVisible = false">&times;</span>
254
+ <div class="modal-title">Rules Summary: {{ G.map.name }}</div>
255
+ <div class="modal-body">
256
+ <div>
257
+ <strong>Phases:</strong>
258
+ <ul>
259
+ <li>
260
+ <strong>Determine Turn Order</strong> by number of cities built and highest power plant
261
+ owned
262
+ </li>
263
+ <li>
264
+ <strong>Buy Power Plants</strong> from the actual market (minimun bid is power plant
265
+ number)
266
+ </li>
267
+ <li>
268
+ <strong>Buy Resources</strong> in <strong>reverse</strong> turn order from the resource
269
+ market
270
+ </li>
271
+ <li>
272
+ <strong>Build Cities</strong> in <strong>reverse</strong> turn order paying
273
+ <strong>10/15/20</strong> plus connection cost
274
+ </li>
275
+ <li>
276
+ <strong>Bureaucracy:</strong> spend resources to use power plants, collect money
277
+ according to cities supplied, resupply resource market
278
+ </li>
279
+ </ul>
280
+ </div>
281
+ <div>
282
+ <strong>Steps:</strong>
283
+ <ul>
284
+ <li>
285
+ <strong>Step 1:</strong>
286
+ <ul>
287
+ <li><strong>One</strong> player per city</li>
288
+ <li>
289
+ Resource Resupply: <strong>{{ G.resourceResupply[0] }}</strong>
290
+ </li>
291
+ <li>Bureaucracy: remove <strong>highest</strong> power plant from market</li>
292
+ </ul>
293
+ </li>
294
+ <li>
295
+ <strong>Step 2:</strong>
296
+ <ul>
297
+ <li>
298
+ Starts after building phase where a player has
299
+ <strong>{{ G.citiesToStep2 }}</strong> or more cities
300
+ </li>
301
+ <li><strong>Two</strong> players per city</li>
302
+ <li>
303
+ Resource Resupply: <strong>{{ G.resourceResupply[1] }}</strong>
304
+ </li>
305
+ <li>Bureaucracy: remove <strong>highest</strong> power plant from market</li>
306
+ </ul>
307
+ </li>
308
+ <li>
309
+ <strong>Step 3:</strong>
310
+ <ul>
311
+ <li>Starts after the "Step 3" card is drawn from the deck</li>
312
+ <li><strong>Three</strong> players per city</li>
313
+ <li>
314
+ Resource Resupply: <strong>{{ G.resourceResupply[2] }}</strong>
315
+ </li>
316
+ <li>Bureaucracy: remove <strong>lowest</strong> power plant from market</li>
317
+ <li>All power plants available for auction</li>
318
+ </ul>
319
+ </li>
320
+ </ul>
321
+ </div>
322
+ <div>
323
+ Game ends after building phase where a player has <strong>{{ G.citiesToEndGame }}</strong> or
324
+ more cities.<br />
325
+ The winner is the player that can power the most cities. Money and number of cities built are
326
+ tiebreakers.
327
+ </div>
328
+ <template v-if="G.map.mapSpecificRules">
329
+ <br />
330
+ <div>
331
+ <strong>Map Specific Rules:</strong><br />
332
+ <span style="white-space: pre">{{ G.map.mapSpecificRules }}</span>
333
+ </div>
334
+ </template>
335
+ <br />
336
+ <div>
337
+ <strong>Payment Table</strong>
338
+ <table class="payment-table">
339
+ <tr>
340
+ <td><strong>Cities</strong></td>
341
+ <template v-for="index in G.citiesToEndGame">
342
+ <td :key="'cities' + index">{{ index - 1 }}</td>
343
+ </template>
344
+ </tr>
345
+ <tr>
346
+ <td><strong>Payment</strong></td>
347
+ <template v-for="index in G.citiesToEndGame">
348
+ <td :key="'payment' + index">${{ G.paymentTable[index - 1] }}</td>
349
+ </template>
350
+ </tr>
351
+ </table>
352
+ </div>
353
+ </div>
354
+ </div>
355
+ </div>
356
+ </div>
357
+ </template>
358
+ <script lang="ts">
359
+ import { Vue, Component, Prop, Watch, Provide, ProvideReactive, Ref } from 'vue-property-decorator';
360
+ import { MoveName, ended, playersSortedByScore, reconstructState } from 'powergrid-engine';
361
+ import type { GameState, Player } from 'powergrid-engine';
362
+ import { EventEmitter } from 'events';
363
+ import { UIData, Preferences } from '../types/ui-data';
364
+ import { Card, House, Coal, Oil, Garbage, Uranium } from './pieces';
365
+ import { Button, PassButton, UndoButton, LogButton, SoundButton, HelpButton, RulesButton } from './buttons';
366
+ import PlayerBoard from './PlayerBoard.vue';
367
+ import Calculator from './Calculator.vue';
368
+ import PowerPlantMarket from './boards/PowerPlantMarket.vue';
369
+ import PlayerOrder from './boards/PlayerOrder.vue';
370
+ import CityCount from './boards/CityCount.vue';
371
+ import Map from './boards/Map.vue';
372
+ import Resources from './boards/Resources.vue';
373
+ import { LogMove } from 'powergrid-engine/src/log';
374
+ import { Phase, PowerPlant, PowerPlantType, ResourceType } from 'powergrid-engine/src/gamestate';
375
+ import { City } from 'powergrid-engine/src/maps';
376
+
377
+ @Component({
378
+ created(this: Game) {
379
+ this.emitter.on('replayStart', () => {
380
+ this.paused = true;
381
+ this.emitter.emit('replay:info', {
382
+ start: 1,
383
+ current: this.G!.log.filter(l => l.type == 'move').length,
384
+ end: this._futureState!.log.filter(l => l.type == 'move').length,
385
+ });
386
+ });
387
+
388
+ this.emitter.on('replayTo', (to: number) => {
389
+ const log = this._futureState!.log.map((l, i) => ({ index: i, ...l })).filter(l => l.type == 'move');
390
+ to = log[to - 1].index;
391
+
392
+ this.replaceState(reconstructState(this._futureState!, to + 1), false);
393
+
394
+ this.emitter.emit('replay:info', {
395
+ start: 1,
396
+ current: this.G!.log.filter(l => l.type == 'move').length + this.G!.hiddenLog.length,
397
+ end: this._futureState!.log.filter(l => l.type == 'move').length,
398
+ });
399
+ });
400
+
401
+ this.emitter.on('replayEnd', () => {
402
+ this.paused = false;
403
+ this.emitter.emit('fetchState');
404
+ });
405
+ },
406
+ components: {
407
+ PlayerBoard,
408
+ Card,
409
+ House,
410
+ Coal,
411
+ Oil,
412
+ Garbage,
413
+ Uranium,
414
+ PassButton,
415
+ UndoButton,
416
+ LogButton,
417
+ SoundButton,
418
+ HelpButton,
419
+ RulesButton,
420
+ Button,
421
+ Calculator,
422
+ PowerPlantMarket,
423
+ PlayerOrder,
424
+ CityCount,
425
+ Map,
426
+ Resources
427
+ },
428
+ })
429
+ export default class Game extends Vue {
430
+ @Prop()
431
+ private state?: GameState;
432
+
433
+ @Prop()
434
+ @ProvideReactive()
435
+ player?: number;
436
+
437
+ @Prop()
438
+ emitter!: EventEmitter;
439
+
440
+ @Prop()
441
+ avatars!: string[];
442
+
443
+ @Prop()
444
+ @ProvideReactive()
445
+ preferences!: Preferences;
446
+
447
+ @Provide()
448
+ ui: UIData = {
449
+ waitingAnimations: 0,
450
+ };
451
+
452
+ paused = false;
453
+
454
+ @Provide()
455
+ communicator: EventEmitter = new EventEmitter();
456
+
457
+ @ProvideReactive()
458
+ G?: GameState | null = null;
459
+ _futureState?: GameState;
460
+
461
+ playerColors = ['limegreen', 'mediumorchid', 'red', 'dodgerblue', 'yellow', 'brown'];
462
+
463
+ animationQueue: Array<Function> = [];
464
+
465
+ logVisible = false;
466
+ endScoreVisible = false;
467
+ spendingVisible = false;
468
+ rulesVisible = false;
469
+
470
+ totalBid: number = 0;
471
+
472
+ confirmMessage = '';
473
+ confirmVisible = false;
474
+
475
+ discardedPowerPlant: PowerPlant | null = null;
476
+ discardVisible: boolean = false;
477
+ resourcesToDiscard: { name: string, max: number, value: string }[] = [];
478
+
479
+ disablePass: boolean = false;
480
+
481
+ @Ref() powerPlantMarket!: PowerPlantMarket;
482
+ @Ref() playerOrder!: PlayerOrder;
483
+ @Ref() cityCount!: CityCount;
484
+ @Ref() map!: Map;
485
+ @Ref() resources!: Resources;
486
+
487
+ @Watch('state', { immediate: true })
488
+ onStateChanged(state: GameState) {
489
+ this.replaceState(state);
490
+ }
491
+
492
+ replaceState(state: GameState, replaceState = true) {
493
+ if (replaceState) {
494
+ this._futureState = state;
495
+ }
496
+
497
+ // if player is selecting resources, keep the state
498
+ let player: any;
499
+ if (this.player != null) {
500
+ if (this.G && this.G.players && this.G.players[this.player].resourcesUsed.some((r) => r == null)) {
501
+ player = {
502
+ coalLeft: this.G.players[this.player].coalLeft,
503
+ oilLeft: this.G.players[this.player].oilLeft,
504
+ resourcesUsed: this.G.players[this.player].resourcesUsed
505
+ };
506
+ }
507
+ }
508
+
509
+ this.G = JSON.parse(JSON.stringify(state));
510
+
511
+ if (player) {
512
+ Object.assign(this.G!.players![this.player!], player);
513
+ }
514
+
515
+ if (this.G) {
516
+ // workaround: refs are not set the first time
517
+ this.$nextTick(() => {
518
+ this.powerPlantMarket.createPieces(this.G!);
519
+ this.playerOrder.createPieces(this.G!);
520
+ this.cityCount.createPieces(this.G!);
521
+ this.map.createPieces(this.G!);
522
+ this.resources.createPieces(this.G!);
523
+ });
524
+ }
525
+
526
+ if (this.preferences.sound && this.G?.log[this.G?.log.length - 1].type == 'move') {
527
+ const move = (this.G?.log[this.G?.log.length - 1] as LogMove).move;
528
+ if (move.name == MoveName.Pass && this.G.currentPlayers.includes(this.player!)) {
529
+ (document.getElementById('notification')!.cloneNode(true) as HTMLAudioElement).play();
530
+ } else {
531
+ if (move.name == MoveName.Build) {
532
+ setTimeout(() => {
533
+ (document.getElementById('piece-drop')!.cloneNode(true) as HTMLAudioElement).play();
534
+ }, 800);
535
+ }
536
+ }
537
+ }
538
+ }
539
+
540
+ @Watch('ui.waitingAnimations')
541
+ updateUI() {
542
+ if (this.ui.waitingAnimations > 0) {
543
+ return;
544
+ }
545
+
546
+ if (this.animationQueue.length > 0) {
547
+ this.animationQueue.shift()!();
548
+ setTimeout(() => this.updateUI());
549
+ return;
550
+ }
551
+ }
552
+
553
+ checkPass() {
554
+ if (this.G && this.player != null && !this.G.chosenPowerPlant) {
555
+ const player = this.G.players[this.player];
556
+ if (player && player.availableMoves && Object.keys(player.availableMoves).length > 1) {
557
+ if (this.G.phase == Phase.Bureaucracy && player.powerPlantsNotUsed.length > 0
558
+ && Object.keys(player.availableMoves).includes('UsePowerPlant')) {
559
+ this.confirmMessage = 'Are you sure you want to pass? You have unused power plants!';
560
+ this.confirmVisible = true;
561
+ return;
562
+ }
563
+ if (this.G.phase == Phase.Resources && !this.canPowerAllPlants(player)) {
564
+ this.confirmMessage = 'Are you sure you want to skip buying resources without enough to power all your plants?';
565
+ this.confirmVisible = true;
566
+ return;
567
+ }
568
+
569
+ if (
570
+ this.G.phase != Phase.Bureaucracy ||
571
+ player.powerPlantsNotUsed.length == player.powerPlants.length
572
+ ) {
573
+ const lastMove = this.G.log[this.G.log.length - 1] as LogMove;
574
+ if (lastMove.player != this.player || lastMove.move.name == MoveName.Pass) {
575
+ switch (this.G.phase) {
576
+ case Phase.Auction:
577
+ this.confirmMessage = 'Are you sure you want to skip auctions?';
578
+ break;
579
+ case Phase.Resources:
580
+ this.confirmMessage = 'Are you sure you want to skip buying resources?';
581
+ break;
582
+ case Phase.Building:
583
+ this.confirmMessage = 'Are you sure you want to skip building?';
584
+ break;
585
+ case Phase.Bureaucracy:
586
+ this.confirmMessage = 'Are you sure you want to pass? You didn\'t use any power plant!';
587
+ break;
588
+ default:
589
+ this.confirmMessage = 'Are you sure you want to pass?';
590
+ }
591
+
592
+ this.confirmVisible = true;
593
+ return;
594
+ }
595
+ }
596
+ }
597
+ }
598
+
599
+ this.pass();
600
+ }
601
+
602
+ confirmPass() {
603
+ this.confirmVisible = false;
604
+ this.pass();
605
+ }
606
+
607
+ pass() {
608
+ this.sendMove({ name: MoveName.Pass, data: true });
609
+ }
610
+
611
+ undo() {
612
+ this.sendMove({ name: MoveName.Undo, data: this.preferences.undoWholeTurn });
613
+ }
614
+
615
+ choosePowerPlant(powerPlant: PowerPlant) {
616
+ const currentPlayer = this.G!.players[this.player!];
617
+ const availableMoves = currentPlayer.availableMoves!;
618
+
619
+ if (availableMoves.ChoosePowerPlant && availableMoves.ChoosePowerPlant.includes(powerPlant.number)) {
620
+ this.sendMove({ name: MoveName.ChoosePowerPlant, data: powerPlant.number });
621
+ }
622
+ }
623
+
624
+ buyResource(resource: ResourceType) {
625
+ this.sendMove({ name: MoveName.BuyResource, data: { resource } });
626
+ }
627
+
628
+ bid(bid: number) {
629
+ this.sendMove({ name: MoveName.Bid, data: bid });
630
+ }
631
+
632
+ build(city: City) {
633
+ const currentPlayer = this.G!.players[this.player!];
634
+ const availableMoves = currentPlayer.availableMoves!;
635
+ const move = availableMoves[MoveName.Build]!.find((c) => c.name == city.name)!;
636
+ this.sendMove({ name: MoveName.Build, data: { name: city.name, price: move.price } });
637
+ }
638
+
639
+ confirmDiscard() {
640
+ const values = this.resourcesToDiscard.map(r => parseInt(r.value));
641
+ if (values.reduce((acc, cur) => acc + cur, 0) > 0) {
642
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: this.discardedPowerPlant!.number, extra: values });
643
+ } else {
644
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: this.discardedPowerPlant!.number });
645
+ }
646
+
647
+ this.discardedPowerPlant = null;
648
+ this.discardVisible = false;
649
+ }
650
+
651
+ discardInvalid() {
652
+ const currentPlayer = this.G!.players[this.player!];
653
+ let hybridCapacityUsed;
654
+ switch (this.discardedPowerPlant!.type) {
655
+ case PowerPlantType.Coal:
656
+ hybridCapacityUsed = currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 > 0 ? Math.max(0, currentPlayer.oilLeft - currentPlayer.oilCapacity) : 0;
657
+ return currentPlayer.coalCapacity + currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 + parseInt(this.resourcesToDiscard[0].value) < currentPlayer.coalLeft + hybridCapacityUsed;
658
+
659
+ case PowerPlantType.Oil:
660
+ hybridCapacityUsed = currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 > 0 ? Math.max(0, currentPlayer.coalLeft - currentPlayer.coalCapacity) : 0;
661
+ return currentPlayer.oilCapacity + currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 + parseInt(this.resourcesToDiscard[0].value) < currentPlayer.oilLeft + hybridCapacityUsed;
662
+
663
+ case PowerPlantType.Garbage:
664
+ return currentPlayer.garbageCapacity - this.discardedPowerPlant!.cost * 2 - currentPlayer.garbageLeft + parseInt(this.resourcesToDiscard[0].value) < 0;
665
+
666
+ case PowerPlantType.Uranium:
667
+ return currentPlayer.uraniumCapacity - this.discardedPowerPlant!.cost * 2 - currentPlayer.uraniumLeft + parseInt(this.resourcesToDiscard[0].value) < 0;
668
+
669
+ case PowerPlantType.Hybrid:
670
+ const coalDiscarded = parseInt(this.resourcesToDiscard[0].value);
671
+ const oilDiscarded = parseInt(this.resourcesToDiscard[1].value);
672
+ const newHybridCapacity = currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2;
673
+ const coalInHybrid = Math.max(0, currentPlayer.coalLeft - currentPlayer.coalCapacity - coalDiscarded);
674
+ const oilInHybrid = Math.max(0, currentPlayer.oilLeft - currentPlayer.oilCapacity - oilDiscarded);
675
+
676
+ return newHybridCapacity < coalInHybrid + oilInHybrid;
677
+ }
678
+
679
+ return true;
680
+ }
681
+
682
+ powerPlantClick(powerPlant: PowerPlant) {
683
+ if (this.G?.phase == Phase.Auction) {
684
+ if (powerPlant.type == PowerPlantType.Wind || powerPlant.type == PowerPlantType.Nuclear) {
685
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
686
+ } else {
687
+ const currentPlayer = this.G!.players[this.player!];
688
+
689
+ switch (powerPlant.type) {
690
+ case PowerPlantType.Coal:
691
+ if (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length + currentPlayer.powerPlants.filter(pp => pp.type == PowerPlantType.Hybrid).length == 1) {
692
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
693
+ return;
694
+ }
695
+
696
+ if (currentPlayer.coalLeft == 0) {
697
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
698
+ return;
699
+ }
700
+
701
+ this.resourcesToDiscard = [{ name: 'Coal', value: '0', max: currentPlayer.coalLeft }];
702
+ break;
703
+
704
+ case PowerPlantType.Oil:
705
+ if (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length + currentPlayer.powerPlants.filter(pp => pp.type == PowerPlantType.Hybrid).length == 1) {
706
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
707
+ return;
708
+ }
709
+
710
+ if (currentPlayer.oilLeft == 0) {
711
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
712
+ return;
713
+ }
714
+
715
+ this.resourcesToDiscard = [{ name: 'Oil', value: '0', max: currentPlayer.oilLeft }];
716
+ break;
717
+
718
+ case PowerPlantType.Garbage:
719
+ if (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length == 1) {
720
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
721
+ return;
722
+ }
723
+
724
+ if (currentPlayer.garbageLeft == 0) {
725
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
726
+ return;
727
+ }
728
+
729
+ this.resourcesToDiscard = [{ name: 'Garbage', value: '0', max: currentPlayer.garbageLeft }];
730
+
731
+ break;
732
+
733
+ case PowerPlantType.Uranium:
734
+ if (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length == 1) {
735
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
736
+ return;
737
+ }
738
+
739
+ if (currentPlayer.uraniumLeft == 0) {
740
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
741
+ return;
742
+ }
743
+
744
+ this.resourcesToDiscard = [{ name: 'Uranium', value: '0', max: currentPlayer.uraniumLeft }];
745
+ break;
746
+
747
+ case PowerPlantType.Hybrid:
748
+ if (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length + currentPlayer.powerPlants.filter(pp => pp.type == PowerPlantType.Coal).length + currentPlayer.powerPlants.filter(pp => pp.type == PowerPlantType.Oil).length == 1) {
749
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
750
+ return;
751
+ }
752
+
753
+ if (currentPlayer.coalLeft + currentPlayer.oilLeft == 0) {
754
+ this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
755
+ return;
756
+ }
757
+
758
+ this.resourcesToDiscard = [{ name: 'Coal', value: '0', max: currentPlayer.coalLeft }, { name: 'Oil', value: '0', max: currentPlayer.oilLeft }];
759
+ break;
760
+ }
761
+
762
+ this.discardedPowerPlant = powerPlant;
763
+ this.discardVisible = true;
764
+ }
765
+ } else if (this.G?.phase == Phase.Bureaucracy) {
766
+ let resourcesSpent: ResourceType[] = [];
767
+ switch (powerPlant.type) {
768
+ case PowerPlantType.Coal:
769
+ resourcesSpent = Array(powerPlant.cost).fill(ResourceType.Coal);
770
+ break;
771
+ case PowerPlantType.Oil:
772
+ resourcesSpent = Array(powerPlant.cost).fill(ResourceType.Oil);
773
+ break;
774
+ case PowerPlantType.Garbage:
775
+ resourcesSpent = Array(powerPlant.cost).fill(ResourceType.Garbage);
776
+ break;
777
+ case PowerPlantType.Uranium:
778
+ resourcesSpent = Array(powerPlant.cost).fill(ResourceType.Uranium);
779
+ break;
780
+ case PowerPlantType.Hybrid:
781
+ const currentPlayer = this.G!.players[this.player!];
782
+ resourcesSpent = currentPlayer.resourcesUsed;
783
+ resourcesSpent.sort();
784
+ currentPlayer.resourcesUsed = [];
785
+ currentPlayer.powerPlantsNotUsed = currentPlayer.powerPlantsNotUsed.filter((x) => x != powerPlant.number);
786
+
787
+ break;
788
+ }
789
+
790
+ this.sendMove({
791
+ name: MoveName.UsePowerPlant,
792
+ data: { powerPlant: powerPlant.number, resourcesSpent, citiesPowered: powerPlant.citiesPowered },
793
+ });
794
+
795
+ this.disablePass = true;
796
+ setTimeout(() => this.disablePass = false, 1000);
797
+ }
798
+ }
799
+
800
+ discardResource(resource) {
801
+ this.sendMove({
802
+ name: MoveName.DiscardResources,
803
+ data: resource,
804
+ });
805
+ }
806
+
807
+ sendMove(move) {
808
+ if (!this.paused) {
809
+ this.emitter.emit('move', move);
810
+ }
811
+ }
812
+
813
+ gameEnded(G: GameState) {
814
+ return ended(G);
815
+ }
816
+
817
+ canMove() {
818
+ return (
819
+ this.player != undefined &&
820
+ this.G &&
821
+ this.G.currentPlayers.includes(this.player!) &&
822
+ this.G.players[this.player!] &&
823
+ this.G.players[this.player!].availableMoves
824
+ );
825
+ }
826
+
827
+ canPass() {
828
+ if (!this.canMove()) return false;
829
+
830
+ if (this.disablePass) return false;
831
+
832
+ const currentPlayer = this.G!.players[this.player!];
833
+ const availableMoves = currentPlayer.availableMoves!;
834
+
835
+ return !!availableMoves[MoveName.Pass];
836
+ }
837
+
838
+ canUndo() {
839
+ if (!this.canMove()) return false;
840
+
841
+ const currentPlayer = this.G!.players[this.player!];
842
+ const availableMoves = currentPlayer.availableMoves!;
843
+
844
+ return !!availableMoves[MoveName.Undo];
845
+ }
846
+
847
+ canBid() {
848
+ if (!this.canMove()) return false;
849
+
850
+ const currentPlayer = this.G!.players[this.player!];
851
+ const availableMoves = currentPlayer.availableMoves!;
852
+
853
+ return !!availableMoves[MoveName.Bid];
854
+ }
855
+
856
+ canChoose() {
857
+ if (!this.canMove()) return false;
858
+
859
+ const currentPlayer = this.G!.players[this.player!];
860
+ const availableMoves = currentPlayer.availableMoves!;
861
+
862
+ return !!availableMoves[MoveName.ChoosePowerPlant];
863
+ }
864
+
865
+ buyableResources() {
866
+ if (!this.canMove()) return [];
867
+
868
+ const currentPlayer = this.G!.players[this.player!];
869
+ const availableMoves = currentPlayer.availableMoves!;
870
+
871
+ return !!availableMoves[MoveName.BuyResource] && availableMoves[MoveName.BuyResource]!.map((m) => m.resource) || [];
872
+ }
873
+
874
+ canBuyResource(resource?: ResourceType) {
875
+ if (!this.canMove()) return false;
876
+
877
+ const currentPlayer = this.G!.players[this.player!];
878
+ const availableMoves = currentPlayer.availableMoves!;
879
+
880
+ if (!resource) {
881
+ return !!availableMoves[MoveName.BuyResource];
882
+ } else {
883
+ return (
884
+ !!availableMoves[MoveName.BuyResource] &&
885
+ availableMoves[MoveName.BuyResource]!.find((m) => m.resource == resource)
886
+ );
887
+ }
888
+ }
889
+
890
+ getChooseablePowerPlants() {
891
+ if (!this.canMove()) return [];
892
+
893
+ const currentPlayer = this.G!.players[this.player!];
894
+ const availableMoves = currentPlayer.availableMoves!;
895
+
896
+ return availableMoves[MoveName.ChoosePowerPlant];
897
+ }
898
+
899
+ getBuildableCities() {
900
+ if (!this.canMove()) return [];
901
+
902
+ const currentPlayer = this.G!.players[this.player!];
903
+ const availableMoves = currentPlayer.availableMoves!;
904
+
905
+ return availableMoves[MoveName.Build] && availableMoves[MoveName.Build]!.map((c) => c.name) || [];
906
+ }
907
+
908
+ canBuild(city: City) {
909
+ if (!this.canMove()) return false;
910
+
911
+ const currentPlayer = this.G!.players[this.player!];
912
+ const availableMoves = currentPlayer.availableMoves!;
913
+
914
+ return !!availableMoves[MoveName.Build] && availableMoves[MoveName.Build]!.find((c) => c.name == city.name);
915
+ }
916
+
917
+ canUsePowerPlant(powerPlant: PowerPlant) {
918
+ if (!this.canMove()) return false;
919
+
920
+ const currentPlayer = this.G!.players[this.player!];
921
+ const availableMoves = currentPlayer.availableMoves!;
922
+
923
+ if (currentPlayer.resourcesUsed.length > 0) {
924
+ return false;
925
+ } else if (this.G?.phase == Phase.Bureaucracy) {
926
+ return (
927
+ !!availableMoves[MoveName.UsePowerPlant] &&
928
+ availableMoves[MoveName.UsePowerPlant]!.find((p) => p.powerPlant == powerPlant.number)
929
+ );
930
+ } else if (this.G?.phase == Phase.Auction) {
931
+ return (
932
+ !!availableMoves[MoveName.DiscardPowerPlant] &&
933
+ availableMoves[MoveName.DiscardPowerPlant]!.find((p) => p == powerPlant.number)
934
+ );
935
+ }
936
+ }
937
+
938
+ canPowerAllPlants(player: Player): boolean {
939
+ // Calculate total resource requirements for all power plants
940
+ let coalUsed = 0;
941
+ let oilUsed = 0;
942
+ let garbageUsed = 0;
943
+ let uraniumUsed = 0;
944
+ let hybridUsed = 0;
945
+ for (const powerPlant of player.powerPlants) {
946
+ switch (powerPlant.type) {
947
+ case PowerPlantType.Coal:
948
+ coalUsed += powerPlant.cost;
949
+ break;
950
+ case PowerPlantType.Oil:
951
+ oilUsed += powerPlant.cost;
952
+ break;
953
+ case PowerPlantType.Garbage:
954
+ garbageUsed += powerPlant.cost;
955
+ break;
956
+ case PowerPlantType.Uranium:
957
+ uraniumUsed += powerPlant.cost;
958
+ break;
959
+ case PowerPlantType.Hybrid:
960
+ hybridUsed += powerPlant.cost;
961
+ break;
962
+ }
963
+ }
964
+
965
+ // Check if player has enough resources, accounting for hybrid plants which can use either coal or oil
966
+ if (coalUsed > player.coalLeft ||
967
+ oilUsed > player.oilLeft ||
968
+ garbageUsed > player.garbageLeft ||
969
+ uraniumUsed > player.uraniumLeft) {
970
+ return false;
971
+ }
972
+ const remainingCoal = player.coalLeft - coalUsed;
973
+ const remainingOil = player.oilLeft - oilUsed;
974
+ if (hybridUsed > remainingCoal + remainingOil) {
975
+ return false;
976
+ }
977
+ return true;
978
+ }
979
+
980
+ toggleSound() {
981
+ const newSound = !this.preferences.sound;
982
+
983
+ this.emitter.emit('update:preference', { name: 'sound', value: newSound });
984
+ this.preferences.sound = newSound;
985
+ }
986
+
987
+ toggleHelp() {
988
+ const newVal = !this.preferences.disableHelp;
989
+
990
+ this.emitter.emit('update:preference', { name: 'disableHelp', value: newVal });
991
+ this.preferences.disableHelp = newVal;
992
+ }
993
+
994
+ showLog() {
995
+ this.logVisible = true;
996
+ }
997
+
998
+ getStatusMessage() {
999
+ if (!this.G || this.G.log.length == 1) {
1000
+ return 'Game Start!';
1001
+ } else if (this.G.currentPlayers == []) {
1002
+ return 'Game ended!';
1003
+ } else if (this.player !== undefined && this.G?.currentPlayers.includes(this.player)) {
1004
+ const currentPlayer = this.G.players[this.player];
1005
+ if (currentPlayer.availableMoves![MoveName.ChoosePowerPlant]) {
1006
+ if (currentPlayer.availableMoves![MoveName.Pass]) {
1007
+ return 'Choose a Power Plant to start an auction, or pass.';
1008
+ }
1009
+
1010
+ return 'Choose a Power Plant to start an auction.';
1011
+ } else if (currentPlayer.availableMoves![MoveName.Bid]) {
1012
+ return 'It\'s your turn to bid!';
1013
+ } else if (currentPlayer.availableMoves![MoveName.BuyResource]) {
1014
+ return 'Buy resources on the market, or pass.';
1015
+ } else if (currentPlayer.availableMoves![MoveName.Build]) {
1016
+ return 'Build a new city, or pass.';
1017
+ } else if (currentPlayer.availableMoves![MoveName.UsePowerPlant]) {
1018
+ if (currentPlayer.resourcesUsed.length != 0) {
1019
+ return 'Choose which resources to spend.';
1020
+ }
1021
+
1022
+ return 'Choose which Power Plant to use.';
1023
+ } else if (currentPlayer.availableMoves![MoveName.DiscardPowerPlant]) {
1024
+ return 'Choose which Power Plant to discard.';
1025
+ } else if (currentPlayer.availableMoves![MoveName.DiscardResources]) {
1026
+ return 'Choose which resources to discard.';
1027
+ }
1028
+
1029
+ return 'It\'s your turn!';
1030
+ } else {
1031
+ let log = this.G.log[this.G.log.length - 1];
1032
+ if (log.type == 'move') {
1033
+ return log.simple;
1034
+ } else if (log.type == 'event') {
1035
+ return log.event;
1036
+ }
1037
+ }
1038
+ }
1039
+
1040
+ isCurrentPlayer(player) {
1041
+ return this.G && this.G.currentPlayers.includes(player);
1042
+ }
1043
+
1044
+ get logReversed() {
1045
+ let logReversed: string[] = [];
1046
+ if (this.G && this.G.log) {
1047
+ this.G.log.forEach((log) => {
1048
+ if (log.type == 'event') {
1049
+ logReversed.push(log.pretty || log.event);
1050
+ } else if (log.type == 'move') {
1051
+ logReversed.push(log.pretty);
1052
+ }
1053
+ });
1054
+
1055
+ logReversed.reverse();
1056
+ }
1057
+
1058
+ return logReversed;
1059
+ }
1060
+
1061
+ get logReversedSimple() {
1062
+ let logReversed: string[] = [];
1063
+ if (this.G && this.G.log) {
1064
+ this.G.log.forEach((log) => {
1065
+ if (log.type == 'event') {
1066
+ logReversed.push(log.event);
1067
+ } else if (log.type == 'move') {
1068
+ logReversed.push(log.simple);
1069
+ }
1070
+ });
1071
+
1072
+ logReversed.reverse();
1073
+ }
1074
+
1075
+ return logReversed;
1076
+ }
1077
+
1078
+ @Watch('G.log')
1079
+ onLogChanged() {
1080
+ this.emitter.emit('replaceLog', [...this.logReversedSimple].reverse());
1081
+ }
1082
+
1083
+ get sortedPlayers() {
1084
+ return playersSortedByScore(this.G!);
1085
+ }
1086
+
1087
+ get adjustedPlayerOrder() {
1088
+ if (!this.G) {
1089
+ return [];
1090
+ }
1091
+
1092
+ if (!this.preferences.adjustPlayerOrder) {
1093
+ return this.G.players.map((_p, i) => i); // 0 1 2 3 ...
1094
+ }
1095
+
1096
+ if (this.G.phase == Phase.Auction) {
1097
+ return this.G.playerOrder;
1098
+ }
1099
+
1100
+ return this.G.playerOrder.reverse();
1101
+ }
1102
+
1103
+ getResourceResupply() {
1104
+ if (this.G) {
1105
+ let str = this.G.resourceResupply[this.G.step - 1];
1106
+ str = str.substr(1, str.length - 2);
1107
+ return str.split(',');
1108
+ }
1109
+
1110
+ return [0, 0, 0, 0];
1111
+ }
1112
+ }
1113
+ </script>
1114
+ <style lang="scss">
1115
+ ul {
1116
+ margin-block-start: 0;
1117
+ }
1118
+
1119
+ .game {
1120
+ display: flex;
1121
+ align-items: center;
1122
+ flex-direction: column;
1123
+ }
1124
+
1125
+ .fitToScreen {
1126
+ height: 100%;
1127
+ }
1128
+
1129
+ .statusBar {
1130
+ height: 40px;
1131
+ width: 100%;
1132
+ background-color: black;
1133
+ color: #fff;
1134
+ text-align: center;
1135
+ line-height: 40px;
1136
+ font-size: 20px;
1137
+ position: fixed;
1138
+ }
1139
+
1140
+ #scene {
1141
+ max-height: calc(100% - 40px);
1142
+ flex-grow: 1;
1143
+ margin: 40px auto auto auto;
1144
+ }
1145
+
1146
+ body,
1147
+ html {
1148
+ height: 100%;
1149
+ width: 100%;
1150
+ margin: 0;
1151
+ padding: 0;
1152
+ }
1153
+
1154
+ text {
1155
+ font-family: 'Arial';
1156
+ pointer-events: none;
1157
+ dominant-baseline: central;
1158
+
1159
+ -webkit-user-select: none;
1160
+ -moz-user-select: none;
1161
+ -ms-user-select: none;
1162
+ user-select: none;
1163
+ }
1164
+
1165
+ .button {
1166
+ &.highlightButton {
1167
+ rect {
1168
+ stroke: blue;
1169
+ stroke-width: 4px;
1170
+ }
1171
+ }
1172
+
1173
+ &.enabled {
1174
+ cursor: pointer;
1175
+
1176
+ &:hover {
1177
+ rect {
1178
+ fill: silver;
1179
+ stroke: white;
1180
+ }
1181
+
1182
+ circle {
1183
+ fill: silver;
1184
+ stroke: white;
1185
+ }
1186
+
1187
+ line {
1188
+ stroke: white;
1189
+ }
1190
+
1191
+ image {
1192
+ filter: invert(1);
1193
+ }
1194
+
1195
+ text {
1196
+ fill: white;
1197
+ }
1198
+ }
1199
+ }
1200
+
1201
+ &:not(.enabled) {
1202
+ rect {
1203
+ stroke: silver;
1204
+ }
1205
+
1206
+ circle {
1207
+ stroke: silver;
1208
+ }
1209
+
1210
+ line {
1211
+ stroke: silver;
1212
+ }
1213
+
1214
+ image {
1215
+ filter: invert(0.75);
1216
+ }
1217
+
1218
+ text {
1219
+ fill: silver;
1220
+ }
1221
+ }
1222
+ }
1223
+
1224
+ .modal {
1225
+ display: none; /* Hidden by default */
1226
+ position: fixed; /* Stay in place */
1227
+ z-index: 1; /* Sit on top */
1228
+ padding-top: 10vh; /* Location of the box */
1229
+ left: 0;
1230
+ top: 0;
1231
+ width: 100%; /* Full width */
1232
+ height: 100%; /* Full height */
1233
+ overflow: auto; /* Enable scroll if needed */
1234
+ background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
1235
+
1236
+ &.visible {
1237
+ display: block;
1238
+ }
1239
+ }
1240
+
1241
+ .modal-content {
1242
+ border-radius: 5px;
1243
+ background-color: #fefefe;
1244
+ margin: auto;
1245
+ padding: 10px 20px 20px 20px;
1246
+ border: 1px solid #888;
1247
+ }
1248
+
1249
+ @media only screen and (min-width: 1240px) {
1250
+ .modal-content {
1251
+ position: absolute;
1252
+ left: 50%;
1253
+ transform: translate(-50%);
1254
+ }
1255
+ }
1256
+
1257
+ .modal-body {
1258
+ max-height: calc(80vh - 64px);
1259
+ overflow: auto;
1260
+ }
1261
+
1262
+ .modal-log {
1263
+ max-height: calc(80vh - 75px);
1264
+ overflow: auto;
1265
+ border: 1px solid black;
1266
+ }
1267
+
1268
+ .log-line {
1269
+ padding: 5px;
1270
+
1271
+ &:nth-last-child(even) {
1272
+ background: #ccc;
1273
+ }
1274
+
1275
+ &:nth-last-child(odd) {
1276
+ background: #fff;
1277
+ }
1278
+ }
1279
+
1280
+ .modal-title {
1281
+ font-size: 28px;
1282
+ font-weight: bold;
1283
+ text-align: center;
1284
+ margin-bottom: 10px;
1285
+ }
1286
+
1287
+ .close {
1288
+ color: #aaaaaa;
1289
+ float: right;
1290
+ font-size: 28px;
1291
+ font-weight: bold;
1292
+ }
1293
+
1294
+ .close:hover,
1295
+ .close:focus {
1296
+ color: #000;
1297
+ text-decoration: none;
1298
+ cursor: pointer;
1299
+ }
1300
+
1301
+ .payment-table {
1302
+ border: 1px solid black;
1303
+ margin: 5px auto;
1304
+
1305
+ tr {
1306
+ td {
1307
+ border: 1px solid black;
1308
+ text-align: center;
1309
+ padding: 0 10px 0 10px;
1310
+ }
1311
+ }
1312
+ }
1313
+
1314
+ .final-score-table,
1315
+ .spending-table {
1316
+ margin: auto;
1317
+ border: 1px solid black;
1318
+
1319
+ tr {
1320
+ td,
1321
+ th {
1322
+ border: 1px solid black;
1323
+ text-align: center;
1324
+ overflow: hidden;
1325
+ text-overflow: ellipsis;
1326
+
1327
+ div {
1328
+ width: 120px;
1329
+ line-height: 38px;
1330
+ }
1331
+ }
1332
+
1333
+ td:first-child,
1334
+ th:first-child {
1335
+ div {
1336
+ width: 250px;
1337
+ }
1338
+ }
1339
+ }
1340
+ }
1341
+
1342
+ .confirm-message {
1343
+ font-size: 18px;
1344
+ padding: 10px;
1345
+ }
1346
+
1347
+ .confirm-buttons {
1348
+ text-align: center;
1349
+ }
1350
+
1351
+ .confirm-button {
1352
+ margin: 15px 0 0 15px;
1353
+ }
1354
+ </style>