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.
- package/README.md +35 -0
- package/dist/img/help.ba7779cc.svg +1 -0
- package/dist/img/log.04ef6981.svg +1 -0
- package/dist/img/pass.da9065dc.svg +3 -0
- package/dist/img/rules.64f9aae5.svg +28 -0
- package/dist/img/sound-off.72ada995.svg +3 -0
- package/dist/img/sound-on.c55edd90.svg +3 -0
- package/dist/img/undo.208666d2.svg +3 -0
- package/dist/media/notification.55fa47dd.ogg +0 -0
- package/dist/media/notification.ac905963.mp3 +0 -0
- package/dist/media/piece-drop.eef5f607.mp3 +0 -0
- package/dist/powergrid-viewer.css +1 -0
- package/dist/powergrid-viewer.umd.min.js +35 -0
- package/package.json +49 -0
- package/src/audio/notification.mp3 +0 -0
- package/src/audio/notification.ogg +0 -0
- package/src/audio/piece-drop.mp3 +0 -0
- package/src/components/Calculator.vue +62 -0
- package/src/components/Game.vue +1354 -0
- package/src/components/PlayerBoard.vue +230 -0
- package/src/components/boards/CityCount.vue +82 -0
- package/src/components/boards/Map.vue +196 -0
- package/src/components/boards/PlayerOrder.vue +68 -0
- package/src/components/boards/PowerPlantMarket.vue +184 -0
- package/src/components/boards/Resources.vue +446 -0
- package/src/components/buttons/Button.vue +26 -0
- package/src/components/buttons/HelpButton.vue +18 -0
- package/src/components/buttons/LogButton.vue +15 -0
- package/src/components/buttons/PassButton.vue +18 -0
- package/src/components/buttons/RulesButton.vue +14 -0
- package/src/components/buttons/SoundButton.vue +18 -0
- package/src/components/buttons/UndoButton.vue +17 -0
- package/src/components/buttons/index.js +9 -0
- package/src/components/pieces/Card.vue +131 -0
- package/src/components/pieces/Coal.vue +40 -0
- package/src/components/pieces/Garbage.vue +40 -0
- package/src/components/pieces/House.vue +51 -0
- package/src/components/pieces/Hybrid.vue +37 -0
- package/src/components/pieces/Oil.vue +40 -0
- package/src/components/pieces/Piece.vue +104 -0
- package/src/components/pieces/Uranium.vue +32 -0
- package/src/components/pieces/index.js +10 -0
- package/src/icons/help.svg +1 -0
- package/src/icons/log.svg +1 -0
- package/src/icons/pass.svg +3 -0
- package/src/icons/rules.svg +28 -0
- package/src/icons/sound-off.svg +3 -0
- package/src/icons/sound-on.svg +3 -0
- package/src/icons/undo.svg +3 -0
- package/src/launch.ts +87 -0
- package/src/main.ts +3 -0
- package/src/self-contained.ts +97 -0
- package/src/shims-tsx.d.ts +13 -0
- package/src/shims-vue.d.ts +4 -0
- package/src/types/ui-data.ts +34 -0
- package/src/wrapper.ts +8 -0
- 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">×</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">×</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">×</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">×</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">×</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>
|