powergrid-viewer 2.0.5 → 2.0.7
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 +112 -2
- package/src/components/boards/ResourceBoxes.vue +63 -1
- package/src/components/boards/Resources.vue +80 -16
- package/src/components/pieces/Coal.vue +1 -1
- package/src/components/pieces/Garbage.vue +1 -1
- package/src/components/pieces/Oil.vue +1 -1
- package/src/components/pieces/Piece.vue +14 -0
- package/src/components/pieces/Uranium.vue +1 -1
- package/src/types/ui-data.ts +5 -0
- package/src/util/turn-buffer.ts +66 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "powergrid-viewer",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/boardgamers/powergrid.git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"vue": "^2.6.11",
|
|
19
19
|
"vue-class-component": "^7.2.3",
|
|
20
20
|
"vue-property-decorator": "^8.4.2",
|
|
21
|
-
"powergrid-engine": "2.0.
|
|
21
|
+
"powergrid-engine": "2.0.4"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/assert": "^1.4.7",
|
package/src/components/Game.vue
CHANGED
|
@@ -156,7 +156,9 @@
|
|
|
156
156
|
:buyableResources="buyableResources()"
|
|
157
157
|
:resourceResupply="getResourceResupply()"
|
|
158
158
|
:resourceResupplyNorth="getResourceResupplyNorth()"
|
|
159
|
+
:bufferedBuys="bufferedBuys"
|
|
159
160
|
@buyResource="buyResource($event)"
|
|
161
|
+
@unbuyResource="unbuyResource($event)"
|
|
160
162
|
/>
|
|
161
163
|
<Resources
|
|
162
164
|
v-else
|
|
@@ -174,7 +176,9 @@
|
|
|
174
176
|
:coalStorage="G.coalStorage"
|
|
175
177
|
:resourceResupply="getResourceResupply()"
|
|
176
178
|
:resourceResupplyNorth="getResourceResupplyNorth()"
|
|
179
|
+
:bufferedBuys="bufferedBuys"
|
|
177
180
|
@buyResource="buyResource($event)"
|
|
181
|
+
@unbuyResource="unbuyResource($event)"
|
|
178
182
|
/>
|
|
179
183
|
</g>
|
|
180
184
|
|
|
@@ -721,7 +725,13 @@ import { Vue, Component, Prop, Watch, Provide, ProvideReactive, Ref } from 'vue-
|
|
|
721
725
|
import { MoveName, ended, playersSortedByScore, reconstructState } from 'powergrid-engine';
|
|
722
726
|
import type { GameState, LogItem, Move, Player } from 'powergrid-engine';
|
|
723
727
|
import { EventEmitter } from 'events';
|
|
724
|
-
import {
|
|
728
|
+
import {
|
|
729
|
+
bufferedBuyCounts,
|
|
730
|
+
lastBuyIndex,
|
|
731
|
+
matchesTurnBuffer,
|
|
732
|
+
rebaseTurnBuffer,
|
|
733
|
+
replayTurnBuffer as replayBuffer,
|
|
734
|
+
} from '../util/turn-buffer';
|
|
725
735
|
import { UIData, Preferences } from '../types/ui-data';
|
|
726
736
|
import { Card, House, Coal, Oil, Garbage, Uranium } from './pieces';
|
|
727
737
|
import {
|
|
@@ -1261,6 +1271,47 @@ export default class Game extends Vue {
|
|
|
1261
1271
|
this.sendMove({ name: MoveName.BuyResource, data });
|
|
1262
1272
|
}
|
|
1263
1273
|
|
|
1274
|
+
/**
|
|
1275
|
+
* Take back a resource bought earlier in this same turn (#127): a market's empty
|
|
1276
|
+
* spaces are clickable while the buffer still holds a purchase from that source.
|
|
1277
|
+
*
|
|
1278
|
+
* This is not an engine undo — there is no Undo move any more (2.0.0). A tentative
|
|
1279
|
+
* turn lives only in this buffer, so a purchase is taken back by dropping it and
|
|
1280
|
+
* replaying the rest, exactly as `undo()` drops the last move. Removing a buy only
|
|
1281
|
+
* ever frees money and plant capacity, so the remaining buffer always replays.
|
|
1282
|
+
*/
|
|
1283
|
+
unbuyResource(payload: { resource: ResourceType, side?: 'north' | 'south', fromStorage?: boolean }) {
|
|
1284
|
+
if (this.paused || !this.committedState) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const index = lastBuyIndex(this.turnMoves, payload);
|
|
1289
|
+
|
|
1290
|
+
if (index < 0) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
this.turnMoves.splice(index, 1);
|
|
1295
|
+
|
|
1296
|
+
if (this.turnMoves.length > 0) {
|
|
1297
|
+
const preview = this.replayTurnBuffer();
|
|
1298
|
+
this.emitter.emit('move', [...this.turnMoves]);
|
|
1299
|
+
this.replaceState(preview, false);
|
|
1300
|
+
} else {
|
|
1301
|
+
// Empty buffer: nothing to send — nothing was ever persisted for this turn,
|
|
1302
|
+
// so the last committed state IS the turn start.
|
|
1303
|
+
this.replaceState(this.committedState, false);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Cubes the turn buffer would give back, per source — how many of each market's
|
|
1309
|
+
* empty spaces are clickable.
|
|
1310
|
+
*/
|
|
1311
|
+
get bufferedBuys(): Record<string, number> {
|
|
1312
|
+
return bufferedBuyCounts(this.turnMoves);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1264
1315
|
bid(bid: number) {
|
|
1265
1316
|
this.sendMove({ name: MoveName.Bid, data: bid });
|
|
1266
1317
|
}
|
|
@@ -1973,6 +2024,10 @@ export default class Game extends Vue {
|
|
|
1973
2024
|
* the choice would disappear along with the layout.
|
|
1974
2025
|
*/
|
|
1975
2026
|
portraitViewport = false;
|
|
2027
|
+
|
|
2028
|
+
/** Viewport the board was last laid out against, as `WxH` — see onViewportResize. */
|
|
2029
|
+
private lastViewport = '';
|
|
2030
|
+
private viewportObserver: ResizeObserver | null = null;
|
|
1976
2031
|
/** Height of the stacked canvas, in scene units. */
|
|
1977
2032
|
stackHeight = STACK_WIDTH;
|
|
1978
2033
|
/** Wrapper transform per slot; empty means "render as authored". */
|
|
@@ -2011,6 +2066,16 @@ export default class Game extends Vue {
|
|
|
2011
2066
|
return window.innerWidth < 900 && window.innerHeight > window.innerWidth * 1.1;
|
|
2012
2067
|
}
|
|
2013
2068
|
|
|
2069
|
+
/**
|
|
2070
|
+
* The platform mounts the viewer in an iframe that is `display: none` until the
|
|
2071
|
+
* game state arrives, so the first measurement here is 0x0 — which is not a
|
|
2072
|
+
* landscape phone, it is nothing at all. Answering then would settle the layout
|
|
2073
|
+
* against a viewport that does not exist yet.
|
|
2074
|
+
*/
|
|
2075
|
+
private viewportIsMeasurable(): boolean {
|
|
2076
|
+
return window.innerWidth > 0 && window.innerHeight > 0;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2014
2079
|
private shouldStack(): boolean {
|
|
2015
2080
|
return this.isPortraitViewport() && this.preferences.stackOnPortrait !== false;
|
|
2016
2081
|
}
|
|
@@ -2024,6 +2089,11 @@ export default class Game extends Vue {
|
|
|
2024
2089
|
}
|
|
2025
2090
|
|
|
2026
2091
|
relayout() {
|
|
2092
|
+
if (!this.viewportIsMeasurable()) {
|
|
2093
|
+
// Still hidden. Whoever reveals us triggers the observer below.
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2027
2097
|
this.portraitViewport = this.isPortraitViewport();
|
|
2028
2098
|
|
|
2029
2099
|
if (!this.shouldStack()) {
|
|
@@ -2138,17 +2208,57 @@ export default class Game extends Vue {
|
|
|
2138
2208
|
this.$nextTick(() => this.relayout());
|
|
2139
2209
|
}
|
|
2140
2210
|
|
|
2141
|
-
|
|
2211
|
+
/**
|
|
2212
|
+
* Re-lay out only when the viewport itself changed size. The observer below also
|
|
2213
|
+
* fires for our OWN output — laying out the board changes the document's box — and
|
|
2214
|
+
* without this that would be a loop.
|
|
2215
|
+
*/
|
|
2216
|
+
private onViewportResize = () => {
|
|
2217
|
+
const size = `${window.innerWidth}x${window.innerHeight}`;
|
|
2218
|
+
|
|
2219
|
+
if (size === this.lastViewport) {
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
this.lastViewport = size;
|
|
2224
|
+
this.scheduleRelayout();
|
|
2225
|
+
};
|
|
2142
2226
|
|
|
2143
2227
|
mounted() {
|
|
2144
2228
|
window.addEventListener('resize', this.onViewportResize);
|
|
2145
2229
|
window.addEventListener('orientationchange', this.onViewportResize);
|
|
2230
|
+
window.addEventListener('load', this.onViewportResize);
|
|
2231
|
+
|
|
2232
|
+
if (window.visualViewport) {
|
|
2233
|
+
window.visualViewport.addEventListener('resize', this.onViewportResize);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// The one signal that does not depend on the browser dispatching an event:
|
|
2237
|
+
// being revealed changes the document's own box. Safari does not reliably fire
|
|
2238
|
+
// `resize` inside an iframe going from `display: none` to visible, and the
|
|
2239
|
+
// board then stays in the landscape layout with no toggle to escape it — the
|
|
2240
|
+
// measurement was taken at 0x0 and nothing ever asked again.
|
|
2241
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
2242
|
+
this.viewportObserver = new ResizeObserver(this.onViewportResize);
|
|
2243
|
+
this.viewportObserver.observe(document.documentElement);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2146
2246
|
this.scheduleRelayout();
|
|
2147
2247
|
}
|
|
2148
2248
|
|
|
2149
2249
|
beforeDestroy() {
|
|
2150
2250
|
window.removeEventListener('resize', this.onViewportResize);
|
|
2151
2251
|
window.removeEventListener('orientationchange', this.onViewportResize);
|
|
2252
|
+
window.removeEventListener('load', this.onViewportResize);
|
|
2253
|
+
|
|
2254
|
+
if (window.visualViewport) {
|
|
2255
|
+
window.visualViewport.removeEventListener('resize', this.onViewportResize);
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
if (this.viewportObserver) {
|
|
2259
|
+
this.viewportObserver.disconnect();
|
|
2260
|
+
this.viewportObserver = null;
|
|
2261
|
+
}
|
|
2152
2262
|
}
|
|
2153
2263
|
|
|
2154
2264
|
// Boards grow as players buy plants, so the row heights are re-derived on
|
|
@@ -116,6 +116,44 @@
|
|
|
116
116
|
step {{ step }} sells to ${{ maxPrice }}
|
|
117
117
|
</text>
|
|
118
118
|
|
|
119
|
+
<!-- A purchase made this turn can be handed back (#127). The box
|
|
120
|
+
market has no empty spaces to click — the printed track's
|
|
121
|
+
ghosts are exactly what it replaces — so the take-back lives
|
|
122
|
+
on a chip, and stops the click reaching the buy target under
|
|
123
|
+
it. Sits in the free band above the cubes, and appears whether
|
|
124
|
+
or not the row is still buyable: being unable to afford the
|
|
125
|
+
next cube is precisely when you want the last one back. -->
|
|
126
|
+
<g v-if="canUnbuy(row)" class="canClick" @click.stop="$emit('unbuyResource', row.move)">
|
|
127
|
+
<rect
|
|
128
|
+
:x="chipBox(row).hx"
|
|
129
|
+
:y="chipBox(row).hy"
|
|
130
|
+
:width="chipBox(row).hw"
|
|
131
|
+
:height="chipBox(row).hh"
|
|
132
|
+
fill="transparent"
|
|
133
|
+
pointer-events="all"
|
|
134
|
+
/>
|
|
135
|
+
<rect
|
|
136
|
+
:x="chipBox(row).x"
|
|
137
|
+
:y="chipBox(row).y"
|
|
138
|
+
:width="chipBox(row).w"
|
|
139
|
+
:height="chipBox(row).h"
|
|
140
|
+
rx="10"
|
|
141
|
+
fill="#f0e6c8"
|
|
142
|
+
stroke="#6b5312"
|
|
143
|
+
stroke-width="2"
|
|
144
|
+
/>
|
|
145
|
+
<text
|
|
146
|
+
:x="chipBox(row).x + chipBox(row).w / 2"
|
|
147
|
+
:y="chipBox(row).y + chipBox(row).h / 2 + 8"
|
|
148
|
+
text-anchor="middle"
|
|
149
|
+
font-weight="700"
|
|
150
|
+
fill="#3a2c08"
|
|
151
|
+
:style="`font-size: ${chipBox(row).fontSize}px`"
|
|
152
|
+
>
|
|
153
|
+
− take back
|
|
154
|
+
</text>
|
|
155
|
+
</g>
|
|
156
|
+
|
|
119
157
|
<title>{{ row.title }}</title>
|
|
120
158
|
</g>
|
|
121
159
|
</g>
|
|
@@ -126,7 +164,8 @@
|
|
|
126
164
|
<script lang="ts">
|
|
127
165
|
import { GameState } from 'powergrid-engine';
|
|
128
166
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
129
|
-
import { BuyMove, ResourceBlock, resourceBlocks } from '../../util/resource-rows';
|
|
167
|
+
import { BuyMove, ResourceBlock, ResourceRow, resourceBlocks } from '../../util/resource-rows';
|
|
168
|
+
import { buySourceKey } from '../../util/turn-buffer';
|
|
130
169
|
|
|
131
170
|
const BOX_W = 700;
|
|
132
171
|
const BOX_H = 104;
|
|
@@ -167,6 +206,8 @@ export default class ResourceBoxes extends Vue {
|
|
|
167
206
|
@Prop() isUsaRecharged?: boolean;
|
|
168
207
|
/** The seat looking at the board — only needed for Central Europe's Wien discount. */
|
|
169
208
|
@Prop() player?: number;
|
|
209
|
+
/** Purchases still sitting in this turn's buffer, per source (#127). */
|
|
210
|
+
@Prop() bufferedBuys?: Record<string, number>;
|
|
170
211
|
|
|
171
212
|
BOX_W = BOX_W;
|
|
172
213
|
BOX_H = BOX_H;
|
|
@@ -174,6 +215,27 @@ export default class ResourceBoxes extends Vue {
|
|
|
174
215
|
HEAD_H = HEAD_H;
|
|
175
216
|
CUBE_BAND_CENTRE = CUBE_BAND_CENTRE;
|
|
176
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Where a row's take-back button sits: in the gap between the price block and the
|
|
220
|
+
* cubes, and big enough for a thumb (~28 css px tall on a phone).
|
|
221
|
+
*
|
|
222
|
+
* The two pool rows carry a long label right across that gap — but they are
|
|
223
|
+
* flat-priced, so nothing sits below them, and the button drops under the label at
|
|
224
|
+
* the same size rather than shrinking.
|
|
225
|
+
*/
|
|
226
|
+
chipBox(row: ResourceRow) {
|
|
227
|
+
const labelFillsTheTop = row.label.length > 8;
|
|
228
|
+
|
|
229
|
+
return labelFillsTheTop
|
|
230
|
+
? { x: 130, y: 36, w: 140, h: 52, hx: 120, hy: 32, hw: 164, hh: 60, fontSize: 22 }
|
|
231
|
+
: { x: 130, y: 6, w: 140, h: 52, hx: 120, hy: 2, hw: 164, hh: 60, fontSize: 22 };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Does this turn's buffer hold a purchase from this row's source? */
|
|
235
|
+
canUnbuy(row: ResourceRow): boolean {
|
|
236
|
+
return !!this.bufferedBuys && !!this.bufferedBuys[buySourceKey(row.move)];
|
|
237
|
+
}
|
|
238
|
+
|
|
177
239
|
/** Centre the cube cluster in that band, however many cubes there are. */
|
|
178
240
|
cubeX(count: number, n: number): number {
|
|
179
241
|
return CUBE_BAND_CENTRE - ((count - 1) * CUBE_PITCH) / 2 + (n - 1) * CUBE_PITCH;
|
|
@@ -75,10 +75,13 @@
|
|
|
75
75
|
:key="coal.id"
|
|
76
76
|
:pieceId="coal.id"
|
|
77
77
|
:targetState="{ x: coal.x, y: coal.y }"
|
|
78
|
-
:canClick="
|
|
78
|
+
:canClick="
|
|
79
|
+
coal.transparent ? canUnbuy('coal', coal) : canBuyResource('coal', coal.side, coal.fromStorage)
|
|
80
|
+
"
|
|
79
81
|
:transparent="coal.transparent"
|
|
82
|
+
:restorable="canUnbuy('coal', coal)"
|
|
80
83
|
:scale="0.08"
|
|
81
|
-
@click="
|
|
84
|
+
@click="clickResource('coal', coal)"
|
|
82
85
|
/>
|
|
83
86
|
</template>
|
|
84
87
|
<template v-for="oil in oilsNorth">
|
|
@@ -86,9 +89,10 @@
|
|
|
86
89
|
:key="oil.id"
|
|
87
90
|
:pieceId="oil.id"
|
|
88
91
|
:targetState="{ x: oil.x, y: oil.y }"
|
|
89
|
-
:canClick="
|
|
92
|
+
:canClick="oil.transparent ? canUnbuy('oil', oil) : canBuyResource('oil', oil.side)"
|
|
90
93
|
:transparent="oil.transparent"
|
|
91
|
-
|
|
94
|
+
:restorable="canUnbuy('oil', oil)"
|
|
95
|
+
@click="clickResource('oil', oil)"
|
|
92
96
|
/>
|
|
93
97
|
</template>
|
|
94
98
|
<template v-for="garbage in garbagesNorth">
|
|
@@ -96,9 +100,12 @@
|
|
|
96
100
|
:key="garbage.id"
|
|
97
101
|
:pieceId="garbage.id"
|
|
98
102
|
:targetState="{ x: garbage.x, y: garbage.y }"
|
|
99
|
-
:canClick="
|
|
103
|
+
:canClick="
|
|
104
|
+
garbage.transparent ? canUnbuy('garbage', garbage) : canBuyResource('garbage', garbage.side)
|
|
105
|
+
"
|
|
100
106
|
:transparent="garbage.transparent"
|
|
101
|
-
|
|
107
|
+
:restorable="canUnbuy('garbage', garbage)"
|
|
108
|
+
@click="clickResource('garbage', garbage)"
|
|
102
109
|
/>
|
|
103
110
|
</template>
|
|
104
111
|
<text x="20" y="35" font-weight="700" fill="black" style="font-size: 22px">South Market</text>
|
|
@@ -326,10 +333,13 @@
|
|
|
326
333
|
:key="coal.id"
|
|
327
334
|
:pieceId="coal.id"
|
|
328
335
|
:targetState="{ x: coal.x, y: coal.y }"
|
|
329
|
-
:canClick="
|
|
336
|
+
:canClick="
|
|
337
|
+
coal.transparent ? canUnbuy('coal', coal) : canBuyResource('coal', coal.side, coal.fromStorage)
|
|
338
|
+
"
|
|
330
339
|
:transparent="coal.transparent"
|
|
340
|
+
:restorable="canUnbuy('coal', coal)"
|
|
331
341
|
:scale="isIndiaResourceMarket ? 0.06 : 0.08"
|
|
332
|
-
@click="
|
|
342
|
+
@click="clickResource('coal', coal)"
|
|
333
343
|
/>
|
|
334
344
|
</template>
|
|
335
345
|
|
|
@@ -338,9 +348,10 @@
|
|
|
338
348
|
:key="oil.id"
|
|
339
349
|
:pieceId="oil.id"
|
|
340
350
|
:targetState="{ x: oil.x, y: oil.y }"
|
|
341
|
-
:canClick="
|
|
351
|
+
:canClick="oil.transparent ? canUnbuy('oil', oil) : canBuyResource('oil', oil.side)"
|
|
342
352
|
:transparent="oil.transparent"
|
|
343
|
-
|
|
353
|
+
:restorable="canUnbuy('oil', oil)"
|
|
354
|
+
@click="clickResource('oil', oil)"
|
|
344
355
|
/>
|
|
345
356
|
</template>
|
|
346
357
|
|
|
@@ -349,10 +360,11 @@
|
|
|
349
360
|
:key="garbage.id"
|
|
350
361
|
:pieceId="garbage.id"
|
|
351
362
|
:targetState="{ x: garbage.x, y: garbage.y }"
|
|
352
|
-
:canClick="
|
|
363
|
+
:canClick="garbage.transparent ? canUnbuy('garbage', garbage) : canBuyResource('garbage', garbage.side)"
|
|
353
364
|
:transparent="garbage.transparent"
|
|
365
|
+
:restorable="canUnbuy('garbage', garbage)"
|
|
354
366
|
:scale="isIndiaResourceMarket ? 0.8 : 1"
|
|
355
|
-
@click="
|
|
367
|
+
@click="clickResource('garbage', garbage)"
|
|
356
368
|
/>
|
|
357
369
|
</template>
|
|
358
370
|
|
|
@@ -361,9 +373,10 @@
|
|
|
361
373
|
:key="uranium.id"
|
|
362
374
|
:pieceId="uranium.id"
|
|
363
375
|
:targetState="{ x: uranium.x, y: uranium.y }"
|
|
364
|
-
:canClick="
|
|
376
|
+
:canClick="uranium.transparent ? canUnbuy('uranium', uranium) : canBuyResource('uranium', uranium.side)"
|
|
365
377
|
:transparent="uranium.transparent"
|
|
366
|
-
|
|
378
|
+
:restorable="canUnbuy('uranium', uranium)"
|
|
379
|
+
@click="clickResource('uranium', uranium)"
|
|
367
380
|
/>
|
|
368
381
|
</template>
|
|
369
382
|
|
|
@@ -426,6 +439,7 @@ import { Vue, Component, Prop, Inject } from 'vue-property-decorator';
|
|
|
426
439
|
import { Coal, Garbage, Oil, Uranium } from '../pieces';
|
|
427
440
|
import { Piece, Preferences } from '../../types/ui-data';
|
|
428
441
|
import { range } from 'lodash';
|
|
442
|
+
import { buySourceKey } from '../../util/turn-buffer';
|
|
429
443
|
|
|
430
444
|
@Component({
|
|
431
445
|
components: {
|
|
@@ -443,6 +457,9 @@ export default class Resources extends Vue {
|
|
|
443
457
|
// South Africa: number of coal cubes in the storage pool below the market.
|
|
444
458
|
// Undefined on all other maps (panel is hidden).
|
|
445
459
|
@Prop() coalStorage?: number;
|
|
460
|
+
// Purchases still sitting in this turn's buffer, per source (#127). Each one can be
|
|
461
|
+
// taken back by clicking the empty space it left behind.
|
|
462
|
+
@Prop() bufferedBuys?: Record<string, number>;
|
|
446
463
|
|
|
447
464
|
@Inject() preferences!: Preferences;
|
|
448
465
|
|
|
@@ -452,6 +469,7 @@ export default class Resources extends Vue {
|
|
|
452
469
|
uraniums: Piece[] = [];
|
|
453
470
|
|
|
454
471
|
isKorea: boolean = false;
|
|
472
|
+
coalComesFromSupply: boolean = false;
|
|
455
473
|
isNinePriceMarket: boolean = false;
|
|
456
474
|
// Australia: coal/oil/garbage-only market that can reach $10 after the Step 3
|
|
457
475
|
// CO2 tax. co2TaxActive closes the $1/$2 columns once that shift has happened.
|
|
@@ -501,6 +519,7 @@ export default class Resources extends Vue {
|
|
|
501
519
|
x,
|
|
502
520
|
y,
|
|
503
521
|
transparent: i < (prices.length - market),
|
|
522
|
+
ghostRank: (prices.length - market) - i,
|
|
504
523
|
side: options.side,
|
|
505
524
|
});
|
|
506
525
|
}
|
|
@@ -511,6 +530,10 @@ export default class Resources extends Vue {
|
|
|
511
530
|
createPieces(gameState: GameState) {
|
|
512
531
|
if (!gameState) return;
|
|
513
532
|
|
|
533
|
+
// USA recharged: once the printed track is bare, coal is bought from the $8
|
|
534
|
+
// supply pile, and a cube taken back returns THERE — so the empty track spaces
|
|
535
|
+
// must not offer to give it back (#127).
|
|
536
|
+
this.coalComesFromSupply = !!this.isUsaRecharged && gameState.coalMarket === 0 && gameState.coalSupply > 0;
|
|
514
537
|
this.isAustraliaMarket = gameState.map?.name === 'Australia';
|
|
515
538
|
this.co2TaxActive = this.isAustraliaMarket && gameState.step >= 3;
|
|
516
539
|
this.isBremenMarket = gameState.map?.name === 'Bremen';
|
|
@@ -551,6 +574,7 @@ export default class Resources extends Vue {
|
|
|
551
574
|
id: `uranium_${i}`,
|
|
552
575
|
x, y,
|
|
553
576
|
transparent: i < (uPrices.length - gameState.uraniumMarket),
|
|
577
|
+
ghostRank: (uPrices.length - gameState.uraniumMarket) - i,
|
|
554
578
|
side: 'south',
|
|
555
579
|
});
|
|
556
580
|
});
|
|
@@ -665,7 +689,8 @@ export default class Resources extends Vue {
|
|
|
665
689
|
id: 'coal_' + i,
|
|
666
690
|
x: 672 - 17 * index - 17 * Math.floor(index / 4),
|
|
667
691
|
y: 48,
|
|
668
|
-
transparent: i >= gameState!.coalMarket
|
|
692
|
+
transparent: i >= gameState!.coalMarket,
|
|
693
|
+
ghostRank: i - (gameState!.coalMarket) + 1,
|
|
669
694
|
});
|
|
670
695
|
});
|
|
671
696
|
} else {
|
|
@@ -677,6 +702,7 @@ export default class Resources extends Vue {
|
|
|
677
702
|
x: 668 - 23.5 * i - 14.5 * Math.floor(i / 3),
|
|
678
703
|
y: 48,
|
|
679
704
|
transparent: i >= gameState!.coalMarket,
|
|
705
|
+
ghostRank: i - (gameState!.coalMarket) + 1,
|
|
680
706
|
});
|
|
681
707
|
});
|
|
682
708
|
}
|
|
@@ -720,6 +746,7 @@ export default class Resources extends Vue {
|
|
|
720
746
|
x: 651 - 16 * adjustedIndex - 37 * Math.floor(adjustedIndex / 3),
|
|
721
747
|
y: 70,
|
|
722
748
|
transparent: i >= availableRegularOil,
|
|
749
|
+
ghostRank: i - (availableRegularOil) + 1,
|
|
723
750
|
});
|
|
724
751
|
});
|
|
725
752
|
} else {
|
|
@@ -731,6 +758,7 @@ export default class Resources extends Vue {
|
|
|
731
758
|
x: 651 - 16 * i - 37 * Math.floor(i / 3),
|
|
732
759
|
y: 70,
|
|
733
760
|
transparent: i >= gameState!.oilMarket,
|
|
761
|
+
ghostRank: i - (gameState!.oilMarket) + 1,
|
|
734
762
|
});
|
|
735
763
|
});
|
|
736
764
|
}
|
|
@@ -746,6 +774,7 @@ export default class Resources extends Vue {
|
|
|
746
774
|
x: 672 - 17 * index - 17 * Math.floor(index / 4),
|
|
747
775
|
y: 94,
|
|
748
776
|
transparent: i >= gameState!.garbageMarket,
|
|
777
|
+
ghostRank: i - (gameState!.garbageMarket) + 1,
|
|
749
778
|
});
|
|
750
779
|
});
|
|
751
780
|
} else {
|
|
@@ -757,6 +786,7 @@ export default class Resources extends Vue {
|
|
|
757
786
|
x: 668 - 23.5 * i - 14.5 * Math.floor(i / 3),
|
|
758
787
|
y: 94,
|
|
759
788
|
transparent: i >= gameState!.garbageMarket,
|
|
789
|
+
ghostRank: i - (gameState!.garbageMarket) + 1,
|
|
760
790
|
});
|
|
761
791
|
});
|
|
762
792
|
}
|
|
@@ -770,7 +800,8 @@ export default class Resources extends Vue {
|
|
|
770
800
|
id: 'uranium_' + i,
|
|
771
801
|
x: 670 - 85 * i,
|
|
772
802
|
y: 72,
|
|
773
|
-
transparent: i >= gameState!.uraniumMarket
|
|
803
|
+
transparent: i >= gameState!.uraniumMarket,
|
|
804
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
774
805
|
});
|
|
775
806
|
});
|
|
776
807
|
} else {
|
|
@@ -783,6 +814,7 @@ export default class Resources extends Vue {
|
|
|
783
814
|
x: 750,
|
|
784
815
|
y: 91,
|
|
785
816
|
transparent: i >= gameState!.uraniumMarket,
|
|
817
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
786
818
|
});
|
|
787
819
|
} else if (i == 1) {
|
|
788
820
|
this.uraniums.push({
|
|
@@ -790,6 +822,7 @@ export default class Resources extends Vue {
|
|
|
790
822
|
x: 710,
|
|
791
823
|
y: 91,
|
|
792
824
|
transparent: i >= gameState!.uraniumMarket,
|
|
825
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
793
826
|
});
|
|
794
827
|
} else if (i == 2) {
|
|
795
828
|
this.uraniums.push({
|
|
@@ -797,6 +830,7 @@ export default class Resources extends Vue {
|
|
|
797
830
|
x: 750,
|
|
798
831
|
y: 52,
|
|
799
832
|
transparent: i >= gameState!.uraniumMarket,
|
|
833
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
800
834
|
});
|
|
801
835
|
} else if (i == 3) {
|
|
802
836
|
this.uraniums.push({
|
|
@@ -804,6 +838,7 @@ export default class Resources extends Vue {
|
|
|
804
838
|
x: 710,
|
|
805
839
|
y: 52,
|
|
806
840
|
transparent: i >= gameState!.uraniumMarket,
|
|
841
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
807
842
|
});
|
|
808
843
|
} else {
|
|
809
844
|
this.uraniums.push({
|
|
@@ -811,6 +846,7 @@ export default class Resources extends Vue {
|
|
|
811
846
|
x: 1010 - 85 * i,
|
|
812
847
|
y: 72,
|
|
813
848
|
transparent: i >= gameState!.uraniumMarket,
|
|
849
|
+
ghostRank: i - (gameState!.uraniumMarket) + 1,
|
|
814
850
|
});
|
|
815
851
|
}
|
|
816
852
|
});
|
|
@@ -835,5 +871,33 @@ export default class Resources extends Vue {
|
|
|
835
871
|
buyResource(resource: string, side?: 'north' | 'south', fromStorage?: boolean) {
|
|
836
872
|
this.$emit('buyResource', { resource, side, fromStorage });
|
|
837
873
|
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Can this empty space give a cube back? Buying empties a track from its cheap end,
|
|
877
|
+
* so the `n` spaces nearest the fill line hold the `n` purchases this turn's buffer
|
|
878
|
+
* still has from that source — `ghostRank` counts out from that line.
|
|
879
|
+
*/
|
|
880
|
+
canUnbuy(resource: string, piece: Piece) {
|
|
881
|
+
if (!piece.transparent || !piece.ghostRank || !this.bufferedBuys) {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (this.coalComesFromSupply && resource === 'coal' && !piece.fromStorage) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const buffered = this.bufferedBuys[buySourceKey({ resource, side: piece.side, fromStorage: piece.fromStorage })];
|
|
890
|
+
|
|
891
|
+
return !!buffered && piece.ghostRank <= buffered;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** A cube buys; the empty space it left takes the purchase back. */
|
|
895
|
+
clickResource(resource: string, piece: Piece) {
|
|
896
|
+
if (piece.transparent) {
|
|
897
|
+
this.$emit('unbuyResource', { resource, side: piece.side, fromStorage: piece.fromStorage });
|
|
898
|
+
} else {
|
|
899
|
+
this.buyResource(resource, piece.side, piece.fromStorage);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
838
902
|
}
|
|
839
903
|
</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
:id="elId"
|
|
4
4
|
:class="[{ canClick: canClick }]"
|
|
5
5
|
:transform="`translate(${currentX}, ${currentY}) scale(${scale})`"
|
|
6
|
-
:opacity="
|
|
6
|
+
:opacity="pieceOpacity"
|
|
7
7
|
@click="canClick && $emit('click')"
|
|
8
8
|
>
|
|
9
9
|
<path d="M2,5 A9,5,0 0,1 18,5 L18,15 A9,5 0 0,1 2,15Z" fill="none" stroke="black" stroke-miterlimit="0" />
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
:id="elId"
|
|
4
4
|
:class="[{ canClick: canClick }]"
|
|
5
5
|
:transform="`translate(${currentX}, ${currentY}) scale(${scale})`"
|
|
6
|
-
:opacity="
|
|
6
|
+
:opacity="pieceOpacity"
|
|
7
7
|
@click="canClick && $emit('click')"
|
|
8
8
|
>
|
|
9
9
|
<path d="M6,4 A6,4,0 0,1 14,4 L14,16 A6,4 0 0,1 6,16Z" fill="none" stroke="black" />
|
|
@@ -27,6 +27,12 @@ export default class Piece extends Vue {
|
|
|
27
27
|
@Prop()
|
|
28
28
|
transparent?: boolean;
|
|
29
29
|
|
|
30
|
+
// An empty market space holding a purchase that can still be taken back (#127).
|
|
31
|
+
// Drawn brighter than a plain empty space so the click target is visible — there
|
|
32
|
+
// is no hover cursor to discover it with on a phone.
|
|
33
|
+
@Prop()
|
|
34
|
+
restorable?: boolean;
|
|
35
|
+
|
|
30
36
|
@Inject()
|
|
31
37
|
readonly communicator!: EventEmitter;
|
|
32
38
|
|
|
@@ -43,6 +49,14 @@ export default class Piece extends Vue {
|
|
|
43
49
|
|
|
44
50
|
moving = false;
|
|
45
51
|
|
|
52
|
+
get pieceOpacity(): number {
|
|
53
|
+
if (!this.transparent) {
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return this.restorable ? 0.6 : 0.3;
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
startTransitioning() {
|
|
47
61
|
if (this.transitioning) {
|
|
48
62
|
return;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
:id="elId"
|
|
4
4
|
:class="[{ canClick: canClick }]"
|
|
5
5
|
:transform="`translate(${currentX}, ${currentY})`"
|
|
6
|
-
:opacity="
|
|
6
|
+
:opacity="pieceOpacity"
|
|
7
7
|
@click="canClick && $emit('click')"
|
|
8
8
|
>
|
|
9
9
|
<path d="M3,3 l4,-2 l6,0 l4,2 l0,11 l-4,2 l-6,0 l-4,-2Z" fill="#F00" stroke="black" />
|
package/src/types/ui-data.ts
CHANGED
|
@@ -30,6 +30,11 @@ export interface Piece {
|
|
|
30
30
|
// South Africa: this coal cube sits in the storage pool below the market.
|
|
31
31
|
// Clicking emits buyResource with fromStorage:true (flat $8 buy).
|
|
32
32
|
fromStorage?: boolean;
|
|
33
|
+
// Empty spaces only: distance from the market's fill line, 1 for the space the
|
|
34
|
+
// next cube would come back to. A purchase made this turn can be taken back by
|
|
35
|
+
// clicking the ghost that holds it (#127), so the first `n` ranks are clickable
|
|
36
|
+
// when the turn buffer holds `n` buys from that source.
|
|
37
|
+
ghostRank?: number;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export enum PieceType {
|