powergrid-viewer 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/powergrid-viewer.css +1 -1
- package/dist/powergrid-viewer.umd.min.js +4 -4
- package/package.json +2 -2
- package/src/components/Game.vue +616 -142
- package/src/components/boards/Map.vue +90 -0
- package/src/components/boards/PowerPlantMarket.vue +3 -28
- package/src/components/buttons/LayoutButton.vue +27 -0
- package/src/components/buttons/index.js +2 -1
- package/src/launch.ts +1 -0
- package/src/self-contained.ts +15 -1
- package/src/types/ui-data.ts +3 -0
package/src/components/Game.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :class="['game', { fitToScreen: preferences.fitToScreen }]">
|
|
2
|
+
<div :class="['game', { fitToScreen: preferences.fitToScreen && !stacked, stacked: stacked }]">
|
|
3
3
|
<div class="statusBar">
|
|
4
4
|
{{ getStatusMessage() }}
|
|
5
5
|
</div>
|
|
@@ -10,62 +10,106 @@
|
|
|
10
10
|
<source src="../audio/notification.mp3" type="audio/mpeg" />
|
|
11
11
|
<source src="../audio/notification.ogg" type="audio/ogg" />
|
|
12
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
|
-
>
|
|
13
|
+
<svg v-if="G" id="scene" :class="{ stacked: stacked }" :viewBox="sceneViewBox" style="width: 100%">
|
|
19
14
|
<rect width="100%" height="100%" x="0" y="0" fill="yellowgreen" />
|
|
20
15
|
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
16
|
+
<g ref="slotPlayerOrder" :transform="slotT('playerOrder')">
|
|
17
|
+
<PlayerOrder
|
|
18
|
+
ref="playerOrder"
|
|
19
|
+
:transform="`translate(${G.map.playerOrderPosition[0]}, ${G.map.playerOrderPosition[1]})`"
|
|
20
|
+
:playerColors="playerColors"
|
|
21
|
+
/>
|
|
22
|
+
</g>
|
|
23
|
+
|
|
24
|
+
<g ref="slotCityCount" :transform="slotT('cityCount')">
|
|
25
|
+
<CityCount
|
|
26
|
+
ref="cityCount"
|
|
27
|
+
:transform="`translate(${G.map.cityCountPosition[0]}, ${G.map.cityCountPosition[1]})`"
|
|
28
|
+
:playerColors="playerColors"
|
|
29
|
+
:citiesToEndGame="G.citiesToEndGame"
|
|
30
|
+
:citiesToStep2="G.map.name === 'Manhattan' ? undefined : G.citiesToStep2"
|
|
31
|
+
/>
|
|
32
|
+
</g>
|
|
33
|
+
|
|
34
|
+
<!-- The power-plant draw pile. Authored at the market's own origin so
|
|
35
|
+
the landscape board is unchanged, but kept as a separate group so
|
|
36
|
+
the portrait layout can move it off the market's row. -->
|
|
37
|
+
<g
|
|
38
|
+
ref="slotPowerPlantDeck"
|
|
39
|
+
:transform="
|
|
40
|
+
slotT('powerPlantDeck') ||
|
|
41
|
+
`translate(${G.map.powerPlantMarketPosition[0]}, ${G.map.powerPlantMarketPosition[1]})`
|
|
42
|
+
"
|
|
43
|
+
>
|
|
44
|
+
<text x="10" y="14" font-weight="600" fill="black">Power Plant Deck:</text>
|
|
45
|
+
<template v-if="G.cardsLeft > 0">
|
|
46
|
+
<rect
|
|
47
|
+
v-for="index in G.cardsLeft"
|
|
48
|
+
:key="'deckcard' + index"
|
|
49
|
+
:x="35 + index / 5"
|
|
50
|
+
:y="38 - index / 10"
|
|
51
|
+
width="60"
|
|
52
|
+
height="40"
|
|
53
|
+
fill="gray"
|
|
54
|
+
stroke="black"
|
|
55
|
+
stroke-width="2"
|
|
56
|
+
rx="4"
|
|
57
|
+
/>
|
|
58
|
+
<rect
|
|
59
|
+
:x="35 + G.cardsLeft / 5"
|
|
60
|
+
:y="38 - G.cardsLeft / 10"
|
|
61
|
+
width="60"
|
|
62
|
+
height="40"
|
|
63
|
+
:fill="G.nextCardWeak ? 'gray' : 'lightgray'"
|
|
64
|
+
stroke="black"
|
|
65
|
+
stroke-width="2"
|
|
66
|
+
rx="4"
|
|
67
|
+
>
|
|
68
|
+
<title>
|
|
69
|
+
{{ G.cardsLeft }} cards left{{ G.nextCardWeak ? ', next is an initial plant' : '' }}
|
|
70
|
+
</title>
|
|
71
|
+
</rect>
|
|
72
|
+
</template>
|
|
73
|
+
</g>
|
|
74
|
+
|
|
75
|
+
<g ref="slotPowerPlantMarket" :transform="slotT('powerPlantMarket')">
|
|
76
|
+
<PowerPlantMarket
|
|
77
|
+
ref="powerPlantMarket"
|
|
78
|
+
:transform="`translate(${G.map.powerPlantMarketPosition[0]}, ${G.map.powerPlantMarketPosition[1]})`"
|
|
79
|
+
:canBid="canBid()"
|
|
80
|
+
:canChoose="canChoose()"
|
|
81
|
+
:chooseablePowerPlants="getChooseablePowerPlants()"
|
|
82
|
+
:cardsLeft="G.cardsLeft"
|
|
83
|
+
:minBid="G.currentBid + 1 || G.minimunBid"
|
|
84
|
+
:maxBid="G.players[player] ? G.players[player].money : 0"
|
|
85
|
+
:nextCardWeak="G.nextCardWeak"
|
|
86
|
+
:plantDiscountActive="G.plantDiscountActive"
|
|
87
|
+
@choosePowerPlant="choosePowerPlant($event)"
|
|
88
|
+
@bid="bid($event)"
|
|
89
|
+
/>
|
|
90
|
+
</g>
|
|
91
|
+
|
|
92
|
+
<g ref="slotMap" :transform="slotT('map')">
|
|
93
|
+
<Map
|
|
94
|
+
ref="map"
|
|
95
|
+
:transform="mapTransform"
|
|
96
|
+
:playerColors="playerColors"
|
|
97
|
+
:cities="G.map.cities"
|
|
98
|
+
:connections="G.map.connections"
|
|
99
|
+
:polygons="G.map.polygons"
|
|
100
|
+
:buildableCities="getBuildableCities()"
|
|
101
|
+
:blockedCities="G.blockedCities"
|
|
102
|
+
:pickableRegions="getPickableRegions()"
|
|
103
|
+
:noUraniumRegions="G.map.noUraniumRegions"
|
|
104
|
+
:devBackdrop="G.map.devBackdrop"
|
|
105
|
+
:mapRotation="G.map.mapRotation || 0"
|
|
106
|
+
@build="build($event)"
|
|
107
|
+
@pickRegion="pickRegion($event)"
|
|
108
|
+
/>
|
|
109
|
+
</g>
|
|
66
110
|
|
|
67
111
|
<!-- Japan: Free Jump indicator -->
|
|
68
|
-
<g v-if="G.map.name === 'Japan'">
|
|
112
|
+
<g v-if="G.map.name === 'Japan'" ref="slotFreeJump" :transform="slotT('freeJump')">
|
|
69
113
|
<text x="330" y="93" font-size="20" font-weight="bold" fill="black">Free Jump:</text>
|
|
70
114
|
<template v-for="(fjPlayer, i) in G.players">
|
|
71
115
|
<g
|
|
@@ -98,27 +142,35 @@
|
|
|
98
142
|
</template>
|
|
99
143
|
</g>
|
|
100
144
|
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
145
|
+
<g ref="slotResources" :transform="slotT('resources')">
|
|
146
|
+
<Resources
|
|
147
|
+
ref="resources"
|
|
148
|
+
:transform="`translate(${G.map.supplyPosition[0]}, ${G.map.supplyPosition[1]})`"
|
|
149
|
+
:isUsaRecharged="G.options.variant == 'recharged' && G.map.name == 'USA'"
|
|
150
|
+
:isMiddleEast="G.map.name == 'Middle East'"
|
|
151
|
+
:isIndiaResourceMarket="G.map.name == 'India' && G.coalPrices && G.garbagePrices && G.uraniumPrices"
|
|
152
|
+
:availableSurplusOil="
|
|
153
|
+
G.map.name == 'Middle East'
|
|
154
|
+
? Math.max(G.oilMarket - G.oilPrices.filter((p) => p > 1).length, 0)
|
|
155
|
+
: 0
|
|
156
|
+
"
|
|
157
|
+
:buyableResources="buyableResources()"
|
|
158
|
+
:coalStorage="G.coalStorage"
|
|
159
|
+
:resourceResupply="getResourceResupply()"
|
|
160
|
+
:resourceResupplyNorth="getResourceResupplyNorth()"
|
|
161
|
+
@buyResource="buyResource($event)"
|
|
162
|
+
/>
|
|
163
|
+
</g>
|
|
116
164
|
|
|
117
165
|
<!-- Australia: uranium-mine selling table. Six price rows $7 (top) →
|
|
118
166
|
$2 (bottom), two token slots each. Sellers place one token per mine
|
|
119
167
|
on the highest empty slot; the resource refill removes from the
|
|
120
168
|
cheap (bottom) end. Lives in the clear upper-left margin. -->
|
|
121
|
-
<g
|
|
169
|
+
<g
|
|
170
|
+
v-if="G.map.name === 'Australia' && G.uraniumMineMarket"
|
|
171
|
+
ref="slotUraniumMines"
|
|
172
|
+
:transform="slotT('uraniumMines') || 'translate(15, 70)'"
|
|
173
|
+
>
|
|
122
174
|
<rect x="0" y="0" width="104" height="430" rx="6" fill="#8aa84a" stroke="#4d6322" stroke-width="3" />
|
|
123
175
|
<text x="52" y="22" text-anchor="middle" font-weight="700" fill="black" style="font-size: 15px">
|
|
124
176
|
Uranium
|
|
@@ -176,7 +228,12 @@
|
|
|
176
228
|
</text>
|
|
177
229
|
</g>
|
|
178
230
|
|
|
179
|
-
<g
|
|
231
|
+
<g
|
|
232
|
+
ref="slotRoundInfo"
|
|
233
|
+
:transform="
|
|
234
|
+
slotT('roundInfo') || `translate(${G.map.roundInfoPosition[0]}, ${G.map.roundInfoPosition[1]})`
|
|
235
|
+
"
|
|
236
|
+
>
|
|
180
237
|
<template v-if="gameEnded(G)">
|
|
181
238
|
<Button
|
|
182
239
|
:transform="`translate(20, 50)`"
|
|
@@ -204,7 +261,10 @@
|
|
|
204
261
|
</template>
|
|
205
262
|
</g>
|
|
206
263
|
|
|
207
|
-
<g
|
|
264
|
+
<g
|
|
265
|
+
ref="slotButtons"
|
|
266
|
+
:transform="slotT('buttons') || `translate(${G.map.buttonsPosition[0]}, ${G.map.buttonsPosition[1]})`"
|
|
267
|
+
>
|
|
208
268
|
<PassButton
|
|
209
269
|
transform="translate(15, 15)"
|
|
210
270
|
:enabled="canPass()"
|
|
@@ -219,33 +279,46 @@
|
|
|
219
279
|
@click="undo()"
|
|
220
280
|
/>
|
|
221
281
|
<LogButton transform="translate(15, 97)" @click="showLog()" />
|
|
222
|
-
<SoundButton transform="
|
|
223
|
-
<HelpButton transform="
|
|
224
|
-
<RulesButton transform="
|
|
282
|
+
<SoundButton :transform="iconButton(0)" :isOn="preferences.sound" @click="toggleSound()" />
|
|
283
|
+
<HelpButton :transform="iconButton(1)" :isOn="!preferences.disableHelp" @click="toggleHelp()" />
|
|
284
|
+
<RulesButton :transform="iconButton(2)" @click="rulesVisible = true" />
|
|
285
|
+
<!-- Only offered where it means something. Shown whenever the
|
|
286
|
+
viewport is portrait — not only while stacking is active — so
|
|
287
|
+
it can undo its own effect. Sits under Rules rather than under
|
|
288
|
+
Log: the left column's next slot overlaps the turn-order table
|
|
289
|
+
on the authored board. -->
|
|
290
|
+
<LayoutButton
|
|
291
|
+
v-if="portraitViewport"
|
|
292
|
+
:transform="iconButton(3)"
|
|
293
|
+
:isOn="stacked"
|
|
294
|
+
@click="toggleStackLayout()"
|
|
295
|
+
/>
|
|
225
296
|
</g>
|
|
226
297
|
|
|
227
|
-
<
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
G.map.playerBoardsPosition[
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
298
|
+
<g ref="slotPlayerBoards" :transform="slotT('playerBoards')">
|
|
299
|
+
<template v-for="(playerIndex, i) in adjustedPlayerOrder">
|
|
300
|
+
<PlayerBoard
|
|
301
|
+
:key="'B' + playerIndex"
|
|
302
|
+
:transform="`translate(${G.map.playerBoardsPosition[0]}, ${
|
|
303
|
+
G.map.playerBoardsPosition[1] + 110 * i
|
|
304
|
+
})`"
|
|
305
|
+
:player="G.players[playerIndex]"
|
|
306
|
+
:color="playerColors[playerIndex]"
|
|
307
|
+
:avatar="avatars[playerIndex]"
|
|
308
|
+
:owner="playerIndex"
|
|
309
|
+
:isCurrentPlayer="isCurrentPlayer(playerIndex)"
|
|
310
|
+
:ended="gameEnded(G)"
|
|
311
|
+
:isPlayer="player == playerIndex"
|
|
312
|
+
:ranking="sortedPlayers.findIndex((x) => x.id == G.players[playerIndex].id) + 1"
|
|
313
|
+
:showMoney="player == playerIndex || gameEnded(G) || G.options.showMoney"
|
|
314
|
+
:showBid="!G.options.fastBid"
|
|
315
|
+
:phase="G.phase"
|
|
316
|
+
:isAustralia="G.map.name === 'Australia'"
|
|
317
|
+
@powerPlantClick="powerPlantClick($event)"
|
|
318
|
+
@discardResource="discardResource($event)"
|
|
319
|
+
/>
|
|
320
|
+
</template>
|
|
321
|
+
</g>
|
|
249
322
|
</svg>
|
|
250
323
|
|
|
251
324
|
<div v-if="G" :class="['modal', { visible: logVisible }]">
|
|
@@ -371,22 +444,24 @@
|
|
|
371
444
|
<div class="modal-content">
|
|
372
445
|
<span class="close" @click="endScoreVisible = false">×</span>
|
|
373
446
|
<div class="modal-title">Final Score</div>
|
|
374
|
-
<
|
|
375
|
-
<
|
|
376
|
-
<
|
|
377
|
-
|
|
378
|
-
<
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
447
|
+
<div class="table-scroll">
|
|
448
|
+
<table class="final-score-table">
|
|
449
|
+
<tr>
|
|
450
|
+
<th><div>Player</div></th>
|
|
451
|
+
<th v-for="player in sortedPlayers" :key="'FS' + player.id">
|
|
452
|
+
<div :style="'background-color: ' + playerColors[player.id]">{{ player.name }}</div>
|
|
453
|
+
</th>
|
|
454
|
+
</tr>
|
|
455
|
+
<tr v-for="(cat, i) in ['Cities Powered', 'Money', 'Total Cities']" :key="'FC_' + cat">
|
|
456
|
+
<td>{{ cat }}</td>
|
|
457
|
+
<td v-for="player in sortedPlayers" :key="'FS' + player.id + i">
|
|
458
|
+
<div>
|
|
459
|
+
{{ i == 0 ? player.citiesPowered : i == 1 ? player.money : player.cities.length }}
|
|
460
|
+
</div>
|
|
461
|
+
</td>
|
|
462
|
+
</tr>
|
|
463
|
+
</table>
|
|
464
|
+
</div>
|
|
390
465
|
</div>
|
|
391
466
|
</div>
|
|
392
467
|
|
|
@@ -394,20 +469,22 @@
|
|
|
394
469
|
<div class="modal-content">
|
|
395
470
|
<span class="close" @click="spendingVisible = false">×</span>
|
|
396
471
|
<div class="modal-title">Spending</div>
|
|
397
|
-
<
|
|
398
|
-
<
|
|
399
|
-
<
|
|
400
|
-
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
<
|
|
406
|
-
|
|
407
|
-
<
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
472
|
+
<div class="table-scroll">
|
|
473
|
+
<table class="spending-table">
|
|
474
|
+
<tr>
|
|
475
|
+
<th><div>Player</div></th>
|
|
476
|
+
<th v-for="player in sortedPlayers" :key="'FS' + player.id">
|
|
477
|
+
<div :style="'background-color: ' + playerColors[player.id]">{{ player.name }}</div>
|
|
478
|
+
</th>
|
|
479
|
+
</tr>
|
|
480
|
+
<tr v-for="row in spendingRows" :key="'FC_' + row.label">
|
|
481
|
+
<td>{{ row.label }}</td>
|
|
482
|
+
<td v-for="player in sortedPlayers" :key="'FS' + player.id + row.label">
|
|
483
|
+
<div>{{ row.value(player) }}</div>
|
|
484
|
+
</td>
|
|
485
|
+
</tr>
|
|
486
|
+
</table>
|
|
487
|
+
</div>
|
|
411
488
|
</div>
|
|
412
489
|
</div>
|
|
413
490
|
|
|
@@ -520,26 +597,39 @@
|
|
|
520
597
|
<br />
|
|
521
598
|
<div>
|
|
522
599
|
<strong>Map Specific Rules:</strong><br />
|
|
523
|
-
<span
|
|
600
|
+
<span class="map-specific-rules">{{ G.map.mapSpecificRules }}</span>
|
|
524
601
|
</div>
|
|
525
602
|
</template>
|
|
526
603
|
<br />
|
|
604
|
+
<div>
|
|
605
|
+
<strong>Deck build:</strong><br />
|
|
606
|
+
<span class="map-specific-rules">{{ standardDeckBuild }}</span>
|
|
607
|
+
<template v-if="mapDeckBuild">
|
|
608
|
+
<br />
|
|
609
|
+
<span class="map-specific-rules"
|
|
610
|
+
><strong>On {{ G.map.name }}:</strong> {{ mapDeckBuild }}</span
|
|
611
|
+
>
|
|
612
|
+
</template>
|
|
613
|
+
</div>
|
|
614
|
+
<br />
|
|
527
615
|
<div>
|
|
528
616
|
<strong>Payment Table</strong>
|
|
529
|
-
<
|
|
530
|
-
<
|
|
531
|
-
<
|
|
532
|
-
|
|
533
|
-
<
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
<
|
|
538
|
-
|
|
539
|
-
<
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
617
|
+
<div class="table-scroll">
|
|
618
|
+
<table class="payment-table">
|
|
619
|
+
<tr>
|
|
620
|
+
<td><strong>Cities</strong></td>
|
|
621
|
+
<template v-for="index in G.citiesToEndGame">
|
|
622
|
+
<td :key="'cities' + index">{{ index - 1 }}</td>
|
|
623
|
+
</template>
|
|
624
|
+
</tr>
|
|
625
|
+
<tr>
|
|
626
|
+
<td><strong>Payment</strong></td>
|
|
627
|
+
<template v-for="index in G.citiesToEndGame">
|
|
628
|
+
<td :key="'payment' + index">${{ G.paymentTable[index - 1] }}</td>
|
|
629
|
+
</template>
|
|
630
|
+
</tr>
|
|
631
|
+
</table>
|
|
632
|
+
</div>
|
|
543
633
|
</div>
|
|
544
634
|
</div>
|
|
545
635
|
</div>
|
|
@@ -554,7 +644,16 @@ import { EventEmitter } from 'events';
|
|
|
554
644
|
import { matchesTurnBuffer, rebaseTurnBuffer, replayTurnBuffer as replayBuffer } from '../util/turn-buffer';
|
|
555
645
|
import { UIData, Preferences } from '../types/ui-data';
|
|
556
646
|
import { Card, House, Coal, Oil, Garbage, Uranium } from './pieces';
|
|
557
|
-
import {
|
|
647
|
+
import {
|
|
648
|
+
Button,
|
|
649
|
+
PassButton,
|
|
650
|
+
UndoButton,
|
|
651
|
+
LogButton,
|
|
652
|
+
SoundButton,
|
|
653
|
+
HelpButton,
|
|
654
|
+
RulesButton,
|
|
655
|
+
LayoutButton,
|
|
656
|
+
} from './buttons';
|
|
558
657
|
import PlayerBoard from './PlayerBoard.vue';
|
|
559
658
|
import Calculator from './Calculator.vue';
|
|
560
659
|
import PowerPlantMarket from './boards/PowerPlantMarket.vue';
|
|
@@ -567,6 +666,84 @@ import { Phase, playerTimeUsed, PowerPlant, PowerPlantType, ResourceType } from
|
|
|
567
666
|
import { City } from 'powergrid-engine/src/maps';
|
|
568
667
|
import { formatDuration } from '../util/time';
|
|
569
668
|
|
|
669
|
+
// Portrait layout: the rows the scene is broken into, top to bottom. Slots on
|
|
670
|
+
// the same row sit side by side and share one scale. Names map to the `slotX`
|
|
671
|
+
// refs on the wrapper groups in the template; a slot whose group is absent for
|
|
672
|
+
// this map (Australia's mine market, Japan's free jump) is simply skipped.
|
|
673
|
+
const STACK_ROWS: string[][] = [
|
|
674
|
+
['cityCount', 'playerOrder'],
|
|
675
|
+
['map'],
|
|
676
|
+
// Everything that is chrome or reference-only shares ONE row, so the three
|
|
677
|
+
// markets below can have a row of their own to grow into. John's call at the
|
|
678
|
+
// phone, 2026-08-19: the market is what you read and tap during an auction;
|
|
679
|
+
// the draw pile and the round readout are things you glance at.
|
|
680
|
+
['buttons', 'roundInfo', 'powerPlantDeck'],
|
|
681
|
+
['powerPlantMarket'],
|
|
682
|
+
['resources'],
|
|
683
|
+
['uraniumMines'],
|
|
684
|
+
['freeJump'],
|
|
685
|
+
['playerBoards'],
|
|
686
|
+
];
|
|
687
|
+
/** Width of the stacked canvas in scene units — kept at the usual scene width
|
|
688
|
+
* so the numbers stay recognisable next to the per-map coordinates. */
|
|
689
|
+
const STACK_WIDTH = 1465;
|
|
690
|
+
const STACK_PAD = 16;
|
|
691
|
+
const STACK_GAP = 28;
|
|
692
|
+
/** A small strip magnified without limit looks broken rather than legible. */
|
|
693
|
+
const STACK_MAX_SCALE = 4;
|
|
694
|
+
/**
|
|
695
|
+
* No single row may fill more than this share of the screen, so the row below it
|
|
696
|
+
* always peeks and the page reads as scrollable. Without it a tall, narrow board
|
|
697
|
+
* (Manhattan takes 92% of an iPhone XR) looks like the whole page.
|
|
698
|
+
*/
|
|
699
|
+
const STACK_MAX_ROW_SCREENS = 0.72;
|
|
700
|
+
/**
|
|
701
|
+
* Per-slot ceilings, for groups that share a row with something that deserves
|
|
702
|
+
* the space more. The round/step/phase readout is text and only needs to be
|
|
703
|
+
* readable; the buttons next to it are tap targets and want every pixel.
|
|
704
|
+
*/
|
|
705
|
+
const STACK_SLOT_MAX_SCALE: Record<string, number> = {
|
|
706
|
+
roundInfo: 1.9,
|
|
707
|
+
playerOrder: 2.5,
|
|
708
|
+
};
|
|
709
|
+
/**
|
|
710
|
+
* Share of the canvas width a slot may occupy. The market is held short of the
|
|
711
|
+
* full width on purpose: stretched edge to edge, "Actual Market" sits under the
|
|
712
|
+
* far left of the screen and the bid OK button under the far right — the two
|
|
713
|
+
* spots a thumb reaches worst while holding the phone.
|
|
714
|
+
*
|
|
715
|
+
* This has to be a WIDTH budget rather than a scale ceiling, because the market's
|
|
716
|
+
* own width varies: Benelux authors `actualMarketWidth: 495`, and it shrinks again
|
|
717
|
+
* in Step 3 once the future market is gone. A fixed scale that centred Bremen left
|
|
718
|
+
* Benelux at 97% of the canvas, i.e. still touching both edges.
|
|
719
|
+
*/
|
|
720
|
+
const STACK_SLOT_MAX_WIDTH: Record<string, number> = {
|
|
721
|
+
powerPlantMarket: 0.88,
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* How the deck and opening market are built when a map says nothing special —
|
|
726
|
+
* i.e. the rule the per-map `deckBuild` notes are differences FROM. Wording is
|
|
727
|
+
* the legend row of the deck-build grid Mike reviewed on 2026-08-13.
|
|
728
|
+
*/
|
|
729
|
+
const STANDARD_DECK_BUILD: Record<string, string> = {
|
|
730
|
+
recharged:
|
|
731
|
+
'Shuffle the 13 low power plants — the "plug plants" (3-15), named for the plug on the back of ' +
|
|
732
|
+
'the printed cards. Draw 8 and sort them ascending: the 4 cheapest form the ' +
|
|
733
|
+
'current market, the next 4 the future market. One of the remaining low plants goes face down on ' +
|
|
734
|
+
'top of the draw deck. Player-count removal: 2 players remove 1 low + 5 higher plants; 3 players ' +
|
|
735
|
+
'remove 2 low + 6 higher; 4 players remove 1 low + 3 higher; 5-6 players remove none. The leftover ' +
|
|
736
|
+
'low plants are shuffled into the deck, with the Step 3 card on the bottom.',
|
|
737
|
+
original:
|
|
738
|
+
'The market is fixed: power plants 3, 4, 5, 6 are the current market and 7, 8, 9, 10 the future ' +
|
|
739
|
+
'market. Power plant 13 and the Step 3 card are set aside and the rest of the deck is shuffled, ' +
|
|
740
|
+
'then 8 random plants are removed for 2-3 players, 4 for 4 players, and none for 5-6 players. ' +
|
|
741
|
+
'Power plant 13 goes on top of the deck and the Step 3 card on the bottom.',
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const slotRef = (name: string) => `slot${name[0].toUpperCase()}${name.slice(1)}`;
|
|
745
|
+
const round = (n: number, digits = 2) => Number(n.toFixed(digits));
|
|
746
|
+
|
|
570
747
|
@Component({
|
|
571
748
|
created(this: Game) {
|
|
572
749
|
this.emitter.on('replayStart', () => {
|
|
@@ -610,6 +787,7 @@ import { formatDuration } from '../util/time';
|
|
|
610
787
|
SoundButton,
|
|
611
788
|
HelpButton,
|
|
612
789
|
RulesButton,
|
|
790
|
+
LayoutButton,
|
|
613
791
|
Button,
|
|
614
792
|
Calculator,
|
|
615
793
|
PowerPlantMarket,
|
|
@@ -797,6 +975,10 @@ export default class Game extends Vue {
|
|
|
797
975
|
this.cityCount.createPieces(this.G!);
|
|
798
976
|
this.map.createPieces(this.G!);
|
|
799
977
|
this.resources.createPieces(this.G!);
|
|
978
|
+
// Pieces are added imperatively, so the groups only reach their
|
|
979
|
+
// final size here — the portrait layout must measure after this,
|
|
980
|
+
// not on the state change that triggered it.
|
|
981
|
+
this.scheduleRelayout();
|
|
800
982
|
});
|
|
801
983
|
}
|
|
802
984
|
|
|
@@ -825,6 +1007,9 @@ export default class Game extends Vue {
|
|
|
825
1007
|
setTimeout(() => this.updateUI());
|
|
826
1008
|
return;
|
|
827
1009
|
}
|
|
1010
|
+
|
|
1011
|
+
// Pieces have finished moving, so the groups are at their settled size.
|
|
1012
|
+
this.scheduleRelayout();
|
|
828
1013
|
}
|
|
829
1014
|
|
|
830
1015
|
checkPass() {
|
|
@@ -1584,6 +1769,21 @@ export default class Game extends Vue {
|
|
|
1584
1769
|
];
|
|
1585
1770
|
}
|
|
1586
1771
|
|
|
1772
|
+
// The standard deck build for the variant in play. Shown on EVERY map: the
|
|
1773
|
+
// per-map notes are written as differences from it, so without it a line like
|
|
1774
|
+
// "18, 22 and 27 are set aside" tells a new player nothing about the rest.
|
|
1775
|
+
get standardDeckBuild(): string {
|
|
1776
|
+
return STANDARD_DECK_BUILD[this.G?.options.variant ?? 'recharged'] ?? STANDARD_DECK_BUILD.recharged;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/** This map's departure from the standard build, or '' when it follows it. */
|
|
1780
|
+
get mapDeckBuild(): string {
|
|
1781
|
+
const deckBuild = this.G?.map.deckBuild;
|
|
1782
|
+
if (!deckBuild) return '';
|
|
1783
|
+
if (typeof deckBuild === 'string') return deckBuild;
|
|
1784
|
+
return deckBuild[(this.G!.options.variant ?? 'recharged') as 'recharged' | 'original'] ?? '';
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1587
1787
|
get mapTransform() {
|
|
1588
1788
|
if (!this.G?.map) return '';
|
|
1589
1789
|
const [mx, my] = this.G.map.mapPosition!;
|
|
@@ -1597,6 +1797,201 @@ export default class Game extends Vue {
|
|
|
1597
1797
|
return `translate(${mx}, ${my}) rotate(${rotation}, ${cx}, ${cy})`;
|
|
1598
1798
|
}
|
|
1599
1799
|
|
|
1800
|
+
// ── Portrait ("stacked") layout ─────────────────────────────────────────
|
|
1801
|
+
// The whole game is ONE svg laid out in per-map absolute coordinates: board
|
|
1802
|
+
// on the left, market and player boards in a right-hand column, resource
|
|
1803
|
+
// supply along the bottom. That composition assumes a landscape viewport.
|
|
1804
|
+
// On a portrait phone it letterboxes: measured at 390x844 the board drew at
|
|
1805
|
+
// 390x229 and left 73% of the screen empty, with 7x10px city slots and
|
|
1806
|
+
// 21x7px buttons.
|
|
1807
|
+
//
|
|
1808
|
+
// Rather than author a second set of coordinates for 24 maps, we re-use the
|
|
1809
|
+
// ones we already have: each group is measured with getBBox() and mapped
|
|
1810
|
+
// onto a full-width row by a single transform on its wrapper <g>. Nothing
|
|
1811
|
+
// inside the components changes, and on a landscape/desktop viewport no
|
|
1812
|
+
// transform is emitted at all, so those layouts render exactly as before.
|
|
1813
|
+
|
|
1814
|
+
/** True while the portrait row layout is in effect. */
|
|
1815
|
+
stacked = false;
|
|
1816
|
+
/**
|
|
1817
|
+
* True when the viewport is one the row layout applies to, whether or not the
|
|
1818
|
+
* player has it switched on. Kept separate from `stacked` so the toggle stays
|
|
1819
|
+
* visible after someone turns stacking off — otherwise the button that undoes
|
|
1820
|
+
* the choice would disappear along with the layout.
|
|
1821
|
+
*/
|
|
1822
|
+
portraitViewport = false;
|
|
1823
|
+
/** Height of the stacked canvas, in scene units. */
|
|
1824
|
+
stackHeight = STACK_WIDTH;
|
|
1825
|
+
/** Wrapper transform per slot; empty means "render as authored". */
|
|
1826
|
+
slotTransforms: Record<string, string> = {};
|
|
1827
|
+
|
|
1828
|
+
get sceneViewBox() {
|
|
1829
|
+
if (this.stacked) {
|
|
1830
|
+
return `0 0 ${STACK_WIDTH} ${this.stackHeight}`;
|
|
1831
|
+
}
|
|
1832
|
+
return this.G?.map?.viewBox ? `0 0 ${this.G.map.viewBox[0]} ${this.G.map.viewBox[1]}` : '0 0 1500 800';
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
slotT(name: string): string | undefined {
|
|
1836
|
+
return this.slotTransforms[name];
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* Placement of the icon column beside Pass / Undo / Log. On a portrait viewport
|
|
1841
|
+
* that column gains a fourth button — the layout toggle — in a space authored for
|
|
1842
|
+
* three, and at the authored pitch the fourth one hung 41 units below everything
|
|
1843
|
+
* else and crowded the market row beneath it. On portrait the icons shrink to the
|
|
1844
|
+
* 26-unit height of the text buttons next to them and re-pitch so all four end
|
|
1845
|
+
* level with Log. Landscape and desktop keep the authored positions exactly.
|
|
1846
|
+
*/
|
|
1847
|
+
iconButton(index: number): string {
|
|
1848
|
+
if (!this.portraitViewport) return `translate(110, ${13 + 41 * index})`;
|
|
1849
|
+
return `translate(110, ${13 + 30 * index}) scale(${round(26 / 30, 4)})`;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* Only portrait viewports are re-laid out. A landscape phone already reads
|
|
1854
|
+
* well (the scene's own aspect ratio is close to the screen's), so leaving
|
|
1855
|
+
* it alone keeps the change surface small.
|
|
1856
|
+
*/
|
|
1857
|
+
private isPortraitViewport(): boolean {
|
|
1858
|
+
return window.innerWidth < 900 && window.innerHeight > window.innerWidth * 1.1;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
private shouldStack(): boolean {
|
|
1862
|
+
return this.isPortraitViewport() && this.preferences.stackOnPortrait !== false;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
toggleStackLayout() {
|
|
1866
|
+
const newVal = !this.stacked;
|
|
1867
|
+
|
|
1868
|
+
this.emitter.emit('update:preference', { name: 'stackOnPortrait', value: newVal });
|
|
1869
|
+
this.preferences.stackOnPortrait = newVal;
|
|
1870
|
+
this.relayout();
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
relayout() {
|
|
1874
|
+
this.portraitViewport = this.isPortraitViewport();
|
|
1875
|
+
|
|
1876
|
+
if (!this.shouldStack()) {
|
|
1877
|
+
if (this.stacked) {
|
|
1878
|
+
this.stacked = false;
|
|
1879
|
+
this.slotTransforms = {};
|
|
1880
|
+
}
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// getBBox() reports a group's box in its OWN user space, i.e. before its
|
|
1885
|
+
// own transform is applied — so measuring is safe even while a previous
|
|
1886
|
+
// stacked transform is in place, and a re-layout never feeds on its own
|
|
1887
|
+
// output.
|
|
1888
|
+
const boxes: Record<string, { x: number; y: number; width: number; height: number }> = {};
|
|
1889
|
+
for (const name of STACK_ROWS.flat()) {
|
|
1890
|
+
const el = this.$refs[slotRef(name)] as SVGGraphicsElement | undefined;
|
|
1891
|
+
if (!el || typeof el.getBBox !== 'function') continue;
|
|
1892
|
+
try {
|
|
1893
|
+
const bb = el.getBBox();
|
|
1894
|
+
if (bb.width > 0 && bb.height > 0) {
|
|
1895
|
+
boxes[name] = { x: bb.x, y: bb.y, width: bb.width, height: bb.height };
|
|
1896
|
+
}
|
|
1897
|
+
} catch {
|
|
1898
|
+
// Not rendered yet (or detached) — skip; the next relayout catches it.
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// A row of `h` scene units renders at h * (innerWidth / STACK_WIDTH) css px,
|
|
1903
|
+
// because the canvas width always maps to the viewport width.
|
|
1904
|
+
const unitsPerScreen = (STACK_MAX_ROW_SCREENS * window.innerHeight * STACK_WIDTH) / window.innerWidth;
|
|
1905
|
+
const heightCapFor = (height: number) => unitsPerScreen / height;
|
|
1906
|
+
|
|
1907
|
+
const transforms: Record<string, string> = {};
|
|
1908
|
+
let y = STACK_PAD;
|
|
1909
|
+
for (const row of STACK_ROWS) {
|
|
1910
|
+
const present = row.filter((name) => boxes[name]);
|
|
1911
|
+
if (!present.length) continue;
|
|
1912
|
+
|
|
1913
|
+
const gaps = STACK_GAP * (present.length - 1);
|
|
1914
|
+
const usable = STACK_WIDTH - 2 * STACK_PAD - gaps;
|
|
1915
|
+
const combined = present.reduce((sum, name) => sum + boxes[name].width, 0);
|
|
1916
|
+
// A slot may decline part of the row's scale so a neighbour keeps the
|
|
1917
|
+
// space, and no slot may grow past the height budget for one row.
|
|
1918
|
+
const ceilingOf = (name: string) =>
|
|
1919
|
+
Math.min(
|
|
1920
|
+
STACK_SLOT_MAX_SCALE[name] ?? Infinity,
|
|
1921
|
+
((STACK_SLOT_MAX_WIDTH[name] ?? Infinity) * usable) / boxes[name].width,
|
|
1922
|
+
heightCapFor(boxes[name].height),
|
|
1923
|
+
STACK_MAX_SCALE
|
|
1924
|
+
);
|
|
1925
|
+
// What a capped slot declines is width left on the table: without this it
|
|
1926
|
+
// became margin either side of the row. Hand it to the slots that can
|
|
1927
|
+
// still grow — the round readout gives up ~130 units beside the buttons,
|
|
1928
|
+
// and the buttons are the part a thumb needs. Repeat until it settles:
|
|
1929
|
+
// growing the free slots can push one of them into its own ceiling, and
|
|
1930
|
+
// then there is more to re-split. rowScale only ever rises here, and a
|
|
1931
|
+
// slot that caps mid-loop leaves the row shorter than solved for, so the
|
|
1932
|
+
// row can never end up wider than the space it was given.
|
|
1933
|
+
// One scale per row keeps neighbours visually consistent. The cap stops a
|
|
1934
|
+
// small strip (the turn-order token row) from being blown up absurdly.
|
|
1935
|
+
let rowScale = Math.min(usable / combined, STACK_MAX_SCALE);
|
|
1936
|
+
for (let pass = 0; pass < present.length; pass++) {
|
|
1937
|
+
let cappedWidth = 0;
|
|
1938
|
+
let freeWidth = 0;
|
|
1939
|
+
for (const name of present) {
|
|
1940
|
+
const ceiling = ceilingOf(name);
|
|
1941
|
+
if (ceiling < rowScale) cappedWidth += boxes[name].width * ceiling;
|
|
1942
|
+
else freeWidth += boxes[name].width;
|
|
1943
|
+
}
|
|
1944
|
+
if (!freeWidth) break;
|
|
1945
|
+
const grown = Math.min((usable - cappedWidth) / freeWidth, STACK_MAX_SCALE);
|
|
1946
|
+
if (grown <= rowScale + 0.0001) break;
|
|
1947
|
+
rowScale = grown;
|
|
1948
|
+
}
|
|
1949
|
+
const scaleOf = (name: string) => Math.min(rowScale, ceilingOf(name));
|
|
1950
|
+
const rowWidth = present.reduce((sum, name) => sum + boxes[name].width * scaleOf(name), 0);
|
|
1951
|
+
const rowHeight = Math.max(...present.map((name) => boxes[name].height * scaleOf(name)));
|
|
1952
|
+
|
|
1953
|
+
let x = (STACK_WIDTH - (rowWidth + gaps)) / 2;
|
|
1954
|
+
for (const name of present) {
|
|
1955
|
+
const bb = boxes[name];
|
|
1956
|
+
const scale = scaleOf(name);
|
|
1957
|
+
// Centre a shorter slot against the tallest one in its row.
|
|
1958
|
+
const top = y + (rowHeight - bb.height * scale) / 2;
|
|
1959
|
+
transforms[name] =
|
|
1960
|
+
`translate(${round(x - bb.x * scale)}, ${round(top - bb.y * scale)}) scale(${round(scale, 4)})`;
|
|
1961
|
+
x += bb.width * scale + STACK_GAP;
|
|
1962
|
+
}
|
|
1963
|
+
y += rowHeight + STACK_GAP;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
this.stackHeight = Math.round(y + STACK_PAD - STACK_GAP);
|
|
1967
|
+
this.slotTransforms = transforms;
|
|
1968
|
+
this.stacked = true;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
private scheduleRelayout() {
|
|
1972
|
+
this.$nextTick(() => this.relayout());
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
private onViewportResize = () => this.scheduleRelayout();
|
|
1976
|
+
|
|
1977
|
+
mounted() {
|
|
1978
|
+
window.addEventListener('resize', this.onViewportResize);
|
|
1979
|
+
window.addEventListener('orientationchange', this.onViewportResize);
|
|
1980
|
+
this.scheduleRelayout();
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
beforeDestroy() {
|
|
1984
|
+
window.removeEventListener('resize', this.onViewportResize);
|
|
1985
|
+
window.removeEventListener('orientationchange', this.onViewportResize);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Boards grow as players buy plants, so the row heights are re-derived on
|
|
1989
|
+
// every state change rather than measured once at mount.
|
|
1990
|
+
@Watch('G')
|
|
1991
|
+
onSceneContentChanged() {
|
|
1992
|
+
this.scheduleRelayout();
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1600
1995
|
get adjustedPlayerOrder() {
|
|
1601
1996
|
if (!this.G) {
|
|
1602
1997
|
return [];
|
|
@@ -1665,6 +2060,20 @@ ul {
|
|
|
1665
2060
|
margin: 40px auto auto auto;
|
|
1666
2061
|
}
|
|
1667
2062
|
|
|
2063
|
+
// Portrait: the scene is taller than the viewport by design (each row is scaled
|
|
2064
|
+
// to the full width), so it must be allowed to overflow and scroll instead of
|
|
2065
|
+
// being squeezed back into one screen.
|
|
2066
|
+
.game.stacked {
|
|
2067
|
+
height: auto;
|
|
2068
|
+
align-items: stretch;
|
|
2069
|
+
|
|
2070
|
+
#scene {
|
|
2071
|
+
max-height: none;
|
|
2072
|
+
flex-grow: 0;
|
|
2073
|
+
margin-top: 40px;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1668
2077
|
body,
|
|
1669
2078
|
html {
|
|
1670
2079
|
height: 100%;
|
|
@@ -1766,6 +2175,8 @@ text {
|
|
|
1766
2175
|
margin: auto;
|
|
1767
2176
|
padding: 10px 20px 20px 20px;
|
|
1768
2177
|
border: 1px solid #888;
|
|
2178
|
+
box-sizing: border-box;
|
|
2179
|
+
max-width: 100%;
|
|
1769
2180
|
}
|
|
1770
2181
|
|
|
1771
2182
|
@media only screen and (min-width: 1240px) {
|
|
@@ -1820,6 +2231,25 @@ text {
|
|
|
1820
2231
|
cursor: pointer;
|
|
1821
2232
|
}
|
|
1822
2233
|
|
|
2234
|
+
// Printed rules text keeps its authored line breaks, but a long unbroken run
|
|
2235
|
+
// (a city list, a URL) must still fold rather than widen the dialog.
|
|
2236
|
+
.map-specific-rules {
|
|
2237
|
+
display: block;
|
|
2238
|
+
white-space: pre-wrap;
|
|
2239
|
+
overflow-wrap: anywhere;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// Tables are naturally wider than a phone (one column per player, plus a wide
|
|
2243
|
+
// label column). Without a scroll container they simply overflowed the modal and
|
|
2244
|
+
// the far columns were unreachable — on a 6-player game only the last player was
|
|
2245
|
+
// visible. The container scrolls; the label column stays pinned so a value never
|
|
2246
|
+
// loses its row.
|
|
2247
|
+
.table-scroll {
|
|
2248
|
+
max-width: 100%;
|
|
2249
|
+
overflow-x: auto;
|
|
2250
|
+
-webkit-overflow-scrolling: touch;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
1823
2253
|
.payment-table {
|
|
1824
2254
|
border: 1px solid black;
|
|
1825
2255
|
margin: 5px auto;
|
|
@@ -1854,6 +2284,11 @@ text {
|
|
|
1854
2284
|
|
|
1855
2285
|
td:first-child,
|
|
1856
2286
|
th:first-child {
|
|
2287
|
+
position: sticky;
|
|
2288
|
+
left: 0;
|
|
2289
|
+
background-color: #fefefe;
|
|
2290
|
+
box-shadow: 2px 0 3px -1px rgba(0, 0, 0, 0.3);
|
|
2291
|
+
|
|
1857
2292
|
div {
|
|
1858
2293
|
width: 250px;
|
|
1859
2294
|
}
|
|
@@ -1861,6 +2296,45 @@ text {
|
|
|
1861
2296
|
}
|
|
1862
2297
|
}
|
|
1863
2298
|
|
|
2299
|
+
// Phone-sized screens: shrink the columns so more than one player fits on
|
|
2300
|
+
// screen before any scrolling is needed.
|
|
2301
|
+
@media only screen and (max-width: 700px) {
|
|
2302
|
+
.modal-content {
|
|
2303
|
+
padding: 10px;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
.final-score-table,
|
|
2307
|
+
.spending-table {
|
|
2308
|
+
tr {
|
|
2309
|
+
td,
|
|
2310
|
+
th {
|
|
2311
|
+
div {
|
|
2312
|
+
width: auto;
|
|
2313
|
+
min-width: 60px;
|
|
2314
|
+
padding: 0 6px;
|
|
2315
|
+
line-height: 32px;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
td:first-child,
|
|
2320
|
+
th:first-child {
|
|
2321
|
+
div {
|
|
2322
|
+
width: auto;
|
|
2323
|
+
min-width: 0;
|
|
2324
|
+
max-width: 38vw;
|
|
2325
|
+
text-align: left;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
.payment-table {
|
|
2332
|
+
tr td {
|
|
2333
|
+
padding: 0 6px;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
1864
2338
|
.confirm-message {
|
|
1865
2339
|
font-size: 18px;
|
|
1866
2340
|
padding: 10px;
|