powergrid-viewer 2.0.3 → 2.0.5
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 +1 -1
- package/src/components/Game.vue +220 -19
- package/src/components/boards/Map.vue +90 -0
- package/src/components/boards/ResourceBoxes.vue +212 -0
- package/src/launch.ts +8 -3
- package/src/util/resource-rows.ts +246 -0
- package/dist/img/help.ba7779cc.svg +0 -1
- package/dist/img/log.04ef6981.svg +0 -1
- package/dist/img/pass.da9065dc.svg +0 -3
- package/dist/img/rules.64f9aae5.svg +0 -28
- package/dist/img/sound-off.72ada995.svg +0 -3
- package/dist/img/sound-on.c55edd90.svg +0 -3
- package/dist/img/undo.208666d2.svg +0 -3
- 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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<g>
|
|
3
|
+
<template v-for="(block, b) in blocks">
|
|
4
|
+
<g :key="'block' + b" :transform="`translate(0, ${block.y})`">
|
|
5
|
+
<text v-if="block.title" x="4" y="16" font-weight="700" fill="black" style="font-size: 26px">
|
|
6
|
+
{{ block.title }}
|
|
7
|
+
</text>
|
|
8
|
+
|
|
9
|
+
<!-- Restock, as the printed refill card reads it: one number per row,
|
|
10
|
+
in the same order the rows are drawn. -->
|
|
11
|
+
<text
|
|
12
|
+
v-if="block.restock"
|
|
13
|
+
:x="BOX_W - 4"
|
|
14
|
+
y="16"
|
|
15
|
+
text-anchor="end"
|
|
16
|
+
font-weight="600"
|
|
17
|
+
fill="#3a2c08"
|
|
18
|
+
style="font-size: 24px"
|
|
19
|
+
>
|
|
20
|
+
Restock {{ block.restock }}
|
|
21
|
+
</text>
|
|
22
|
+
|
|
23
|
+
<text x="4" y="42" fill="#3a2c08" style="font-size: 24px; letter-spacing: 1px">CURRENT PRICE</text>
|
|
24
|
+
<text
|
|
25
|
+
:x="CUBE_BAND_CENTRE"
|
|
26
|
+
y="42"
|
|
27
|
+
text-anchor="middle"
|
|
28
|
+
fill="#3a2c08"
|
|
29
|
+
style="font-size: 24px; letter-spacing: 1px"
|
|
30
|
+
>
|
|
31
|
+
AT THIS PRICE
|
|
32
|
+
</text>
|
|
33
|
+
<text
|
|
34
|
+
:x="BOX_W - 4"
|
|
35
|
+
y="42"
|
|
36
|
+
text-anchor="end"
|
|
37
|
+
fill="#3a2c08"
|
|
38
|
+
style="font-size: 24px; letter-spacing: 1px"
|
|
39
|
+
>
|
|
40
|
+
CUBES LEFT
|
|
41
|
+
</text>
|
|
42
|
+
|
|
43
|
+
<g
|
|
44
|
+
v-for="(row, r) in block.rows"
|
|
45
|
+
:key="'row' + r"
|
|
46
|
+
:transform="`translate(0, ${HEAD_H + r * (BOX_H + BOX_GAP)})`"
|
|
47
|
+
:class="[{ canClick: row.buyable }]"
|
|
48
|
+
@click="row.buyable && $emit('buyResource', row.move)"
|
|
49
|
+
>
|
|
50
|
+
<rect
|
|
51
|
+
:width="BOX_W"
|
|
52
|
+
:height="BOX_H"
|
|
53
|
+
rx="6"
|
|
54
|
+
:fill="row.cubes > 0 ? 'darkgoldenrod' : '#9c8236'"
|
|
55
|
+
:stroke="row.buyable ? '#1e63d0' : '#6b5312'"
|
|
56
|
+
:stroke-width="row.buyable ? 5 : 2"
|
|
57
|
+
:opacity="row.cubes > 0 ? 1 : 0.55"
|
|
58
|
+
/>
|
|
59
|
+
<!-- Whole box is the tap target, cubes and all. -->
|
|
60
|
+
<rect
|
|
61
|
+
v-if="row.buyable"
|
|
62
|
+
:width="BOX_W"
|
|
63
|
+
:height="BOX_H"
|
|
64
|
+
rx="6"
|
|
65
|
+
fill="transparent"
|
|
66
|
+
pointer-events="all"
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
<text x="18" y="26" font-weight="700" fill="black" style="font-size: 22px; letter-spacing: 1px">
|
|
70
|
+
{{ row.label }}
|
|
71
|
+
</text>
|
|
72
|
+
|
|
73
|
+
<template v-if="row.price != null">
|
|
74
|
+
<text x="18" y="80" font-weight="700" fill="black" style="font-size: 26px">$</text>
|
|
75
|
+
<text x="40" y="84" font-weight="700" fill="black" style="font-size: 46px">
|
|
76
|
+
{{ row.price }}
|
|
77
|
+
</text>
|
|
78
|
+
<text v-if="row.next != null" x="132" y="80" fill="#3a2c08" style="font-size: 22px">
|
|
79
|
+
then ${{ row.next }}
|
|
80
|
+
</text>
|
|
81
|
+
</template>
|
|
82
|
+
<text v-else x="18" y="78" font-weight="700" fill="black" style="font-size: 28px">out</text>
|
|
83
|
+
|
|
84
|
+
<!-- One dot per cube still buyable at the current price. A pool
|
|
85
|
+
with no price track behind it has no such number: every cube
|
|
86
|
+
in it costs the same, so say that instead of drawing dots. -->
|
|
87
|
+
<circle
|
|
88
|
+
v-for="n in row.atPrice"
|
|
89
|
+
:key="'cube' + n"
|
|
90
|
+
:cx="cubeX(row.atPrice, n)"
|
|
91
|
+
:cy="58"
|
|
92
|
+
r="17"
|
|
93
|
+
:fill="row.color"
|
|
94
|
+
stroke="black"
|
|
95
|
+
stroke-width="2"
|
|
96
|
+
/>
|
|
97
|
+
<text v-if="row.flat && row.cubes > 0" x="300" y="66" fill="#3a2c08" style="font-size: 22px">
|
|
98
|
+
every cube at this price
|
|
99
|
+
</text>
|
|
100
|
+
|
|
101
|
+
<text
|
|
102
|
+
:x="BOX_W - 18"
|
|
103
|
+
y="68"
|
|
104
|
+
text-anchor="end"
|
|
105
|
+
font-weight="700"
|
|
106
|
+
fill="black"
|
|
107
|
+
style="font-size: 40px"
|
|
108
|
+
>
|
|
109
|
+
{{ row.cubes }}
|
|
110
|
+
</text>
|
|
111
|
+
<text :x="BOX_W - 18" y="90" text-anchor="end" fill="#3a2c08" style="font-size: 20px">LEFT</text>
|
|
112
|
+
|
|
113
|
+
<!-- India caps which prices are on sale per step; a row above the
|
|
114
|
+
cap is on the board but cannot be bought by anyone yet. -->
|
|
115
|
+
<text v-if="row.capped" x="130" y="96" fill="#7a1d12" font-weight="600" style="font-size: 20px">
|
|
116
|
+
step {{ step }} sells to ${{ maxPrice }}
|
|
117
|
+
</text>
|
|
118
|
+
|
|
119
|
+
<title>{{ row.title }}</title>
|
|
120
|
+
</g>
|
|
121
|
+
</g>
|
|
122
|
+
</template>
|
|
123
|
+
</g>
|
|
124
|
+
</template>
|
|
125
|
+
|
|
126
|
+
<script lang="ts">
|
|
127
|
+
import { GameState } from 'powergrid-engine';
|
|
128
|
+
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
129
|
+
import { BuyMove, ResourceBlock, resourceBlocks } from '../../util/resource-rows';
|
|
130
|
+
|
|
131
|
+
const BOX_W = 700;
|
|
132
|
+
const BOX_H = 104;
|
|
133
|
+
const BOX_GAP = 8;
|
|
134
|
+
/** Block title + column captions above the first box. */
|
|
135
|
+
const HEAD_H = 62;
|
|
136
|
+
const BLOCK_GAP = 30;
|
|
137
|
+
const CUBE_PITCH = 44;
|
|
138
|
+
/** Middle of the band between the price block and the cubes-left count. */
|
|
139
|
+
const CUBE_BAND_CENTRE = 420;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The portrait resource market.
|
|
143
|
+
*
|
|
144
|
+
* The printed price track is a beautiful object and an unreadable one on a phone:
|
|
145
|
+
* eight to ten columns of three-cube slots, drawn at whatever width the map author
|
|
146
|
+
* chose and then squeezed into a strip. This draws the same market as one box per
|
|
147
|
+
* buyable source, carrying only what a purchase actually needs — what the next cube
|
|
148
|
+
* costs, how many are left at that price, how many are left at all.
|
|
149
|
+
*
|
|
150
|
+
* Every number comes out of the engine state rather than the printed geometry, so the
|
|
151
|
+
* map-specific markets come along for free: Europe's nine price columns, Korea's two
|
|
152
|
+
* sides, Australia's Step-3 CO2 shift (the engine rewrites the price array and the box
|
|
153
|
+
* is only ever a view of it), India's per-step price cap, South Africa's $8 storage
|
|
154
|
+
* pool, USA recharged's $8-from-supply once the track is bare.
|
|
155
|
+
*
|
|
156
|
+
* Which rows a player may actually tap is NOT decided here: `buyableResources` is the
|
|
157
|
+
* engine's own availableMoves, so money, storage capacity, the hybrid tank and Korea's
|
|
158
|
+
* side lock are all accounted for already. The row model itself lives in
|
|
159
|
+
* `util/resource-rows`, where it can be tested against the engine's own arithmetic.
|
|
160
|
+
*/
|
|
161
|
+
@Component
|
|
162
|
+
export default class ResourceBoxes extends Vue {
|
|
163
|
+
@Prop() gameState!: GameState;
|
|
164
|
+
@Prop() buyableResources?: BuyMove[];
|
|
165
|
+
@Prop() resourceResupply?: (number | string)[];
|
|
166
|
+
@Prop() resourceResupplyNorth?: (number | string)[];
|
|
167
|
+
@Prop() isUsaRecharged?: boolean;
|
|
168
|
+
/** The seat looking at the board — only needed for Central Europe's Wien discount. */
|
|
169
|
+
@Prop() player?: number;
|
|
170
|
+
|
|
171
|
+
BOX_W = BOX_W;
|
|
172
|
+
BOX_H = BOX_H;
|
|
173
|
+
BOX_GAP = BOX_GAP;
|
|
174
|
+
HEAD_H = HEAD_H;
|
|
175
|
+
CUBE_BAND_CENTRE = CUBE_BAND_CENTRE;
|
|
176
|
+
|
|
177
|
+
/** Centre the cube cluster in that band, however many cubes there are. */
|
|
178
|
+
cubeX(count: number, n: number): number {
|
|
179
|
+
return CUBE_BAND_CENTRE - ((count - 1) * CUBE_PITCH) / 2 + (n - 1) * CUBE_PITCH;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
get step(): number {
|
|
183
|
+
return this.gameState.step;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
get maxPrice(): number {
|
|
187
|
+
const table = (this.gameState.map as { maxPriceAvailable?: number[] }).maxPriceAvailable;
|
|
188
|
+
return table ? table[this.gameState.step - 1] : 16;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get blocks(): (ResourceBlock & { y: number })[] {
|
|
192
|
+
let y = 0;
|
|
193
|
+
return resourceBlocks(this.gameState, {
|
|
194
|
+
player: this.player,
|
|
195
|
+
buyable: this.buyableResources,
|
|
196
|
+
resupply: this.resourceResupply,
|
|
197
|
+
resupplyNorth: this.resourceResupplyNorth,
|
|
198
|
+
isUsaRecharged: this.isUsaRecharged,
|
|
199
|
+
}).map((block) => {
|
|
200
|
+
const placed = { ...block, y };
|
|
201
|
+
y += HEAD_H + block.rows.length * (BOX_H + BOX_GAP) - BOX_GAP + BLOCK_GAP;
|
|
202
|
+
return placed;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
</script>
|
|
207
|
+
|
|
208
|
+
<style lang="scss" scoped>
|
|
209
|
+
.canClick {
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
}
|
|
212
|
+
</style>
|
package/src/launch.ts
CHANGED
|
@@ -15,14 +15,18 @@ function launch(selector: string) {
|
|
|
15
15
|
} = {
|
|
16
16
|
state: null,
|
|
17
17
|
emitter: new EventEmitter(),
|
|
18
|
-
|
|
18
|
+
// Observable so preference changes update the UI immediately: Game receives
|
|
19
|
+
// this object as a prop, and Vue 2 does not deep-observe prop values coming
|
|
20
|
+
// from a non-reactive parent — plain-object mutations (the in-game sound/help
|
|
21
|
+
// toggles, platform preference pushes) would only paint on the next re-render.
|
|
22
|
+
preferences: Vue.observable({
|
|
19
23
|
sound: true,
|
|
20
24
|
disableHelp: false,
|
|
21
25
|
adjustPlayerOrder: false,
|
|
22
26
|
undoWholeTurn: true,
|
|
23
27
|
fitToScreen: true,
|
|
24
28
|
stackOnPortrait: true,
|
|
25
|
-
},
|
|
29
|
+
}),
|
|
26
30
|
avatars: [],
|
|
27
31
|
};
|
|
28
32
|
|
|
@@ -61,7 +65,8 @@ function launch(selector: string) {
|
|
|
61
65
|
app.$forceUpdate();
|
|
62
66
|
});
|
|
63
67
|
item.addListener('preferences', (data) => {
|
|
64
|
-
|
|
68
|
+
// Mutate (don't replace) the observable object so the update stays reactive
|
|
69
|
+
Object.assign(params.preferences, data);
|
|
65
70
|
app.$forceUpdate();
|
|
66
71
|
});
|
|
67
72
|
item.addListener('gamelog', (logData) => {
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { GameState } from 'powergrid-engine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The row model behind the portrait resource market (`boards/ResourceBoxes.vue`).
|
|
5
|
+
*
|
|
6
|
+
* Kept out of the component so it can be tested against the engine directly: every
|
|
7
|
+
* row's `price` is the money the engine will actually take for the next cube bought
|
|
8
|
+
* from that source, and `resource-rows.spec.ts` asserts exactly that by buying one.
|
|
9
|
+
*
|
|
10
|
+
* Written without optional chaining: webpack 4 parses the unit-test bundle and
|
|
11
|
+
* rejects `?.` (same note as `util/turn-buffer.ts`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type BuyMove = { resource: string; side?: 'north' | 'south'; fromStorage?: boolean };
|
|
15
|
+
|
|
16
|
+
export interface ResourceRow {
|
|
17
|
+
label: string;
|
|
18
|
+
color: string;
|
|
19
|
+
/** What a tap sends. Identical in shape to the engine's BuyResource payload. */
|
|
20
|
+
move: BuyMove;
|
|
21
|
+
/** What the engine charges for the next cube here, or null when it is empty. */
|
|
22
|
+
price: number | null;
|
|
23
|
+
/** The price once the cubes at the current price run out. */
|
|
24
|
+
next: number | null;
|
|
25
|
+
/** How many cubes are still on sale at `price`. Zero for a flat pool. */
|
|
26
|
+
atPrice: number;
|
|
27
|
+
/** Cubes left in this source altogether. */
|
|
28
|
+
cubes: number;
|
|
29
|
+
buyable: boolean;
|
|
30
|
+
/** On the board but above the per-step price cap, so nobody may buy it yet. */
|
|
31
|
+
capped: boolean;
|
|
32
|
+
/** A flat-price pool with no track behind it: every cube costs the same. */
|
|
33
|
+
flat: boolean;
|
|
34
|
+
title: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ResourceBlock {
|
|
38
|
+
title?: string;
|
|
39
|
+
restock?: string;
|
|
40
|
+
rows: ResourceRow[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const RESOURCE_COLORS: Record<string, string> = {
|
|
44
|
+
coal: '#6b2028',
|
|
45
|
+
oil: '#15130f',
|
|
46
|
+
garbage: '#f2d024',
|
|
47
|
+
uranium: '#dc2626',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The engine's own defaults, for maps that do not author their own arrays. */
|
|
51
|
+
const DEFAULT_PRICES: Record<string, number[]> = {
|
|
52
|
+
coal: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8],
|
|
53
|
+
oil: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8],
|
|
54
|
+
garbage: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8],
|
|
55
|
+
uranium: [1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const FLAT_POOL_PRICE = 8;
|
|
59
|
+
|
|
60
|
+
export interface RowOptions {
|
|
61
|
+
/** The seat reading the board — only needed for Central Europe's Wien discount. */
|
|
62
|
+
player?: number;
|
|
63
|
+
buyable?: BuyMove[];
|
|
64
|
+
/** Resupply for this step, indexed coal / oil / garbage / uranium. */
|
|
65
|
+
resupply?: (number | string)[];
|
|
66
|
+
resupplyNorth?: (number | string)[];
|
|
67
|
+
isUsaRecharged?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function capitalize(s: string): string {
|
|
71
|
+
return s[0].toUpperCase() + s.slice(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isKorea(G: GameState): boolean {
|
|
75
|
+
return G.coalPricesNorth !== undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** India and friends only sell up to a per-step price. 16 = no cap in practice. */
|
|
79
|
+
function maxPrice(G: GameState): number {
|
|
80
|
+
const table = (G.map as { maxPriceAvailable?: number[] }).maxPriceAvailable;
|
|
81
|
+
return table ? table[G.step - 1] : 16;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function pricesFor(G: GameState, resource: string, north: boolean): number[] {
|
|
85
|
+
const bag = G as unknown as Record<string, number[] | undefined>;
|
|
86
|
+
const authored = bag[resource + 'Prices' + (north ? 'North' : '')];
|
|
87
|
+
return authored !== undefined ? authored : DEFAULT_PRICES[resource];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cubesIn(G: GameState, resource: string, north: boolean): number {
|
|
91
|
+
const bag = G as unknown as Record<string, number | undefined>;
|
|
92
|
+
const count = bag[resource + 'Market' + (north ? 'North' : '')];
|
|
93
|
+
return count === undefined ? 0 : count;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sameMove(a: BuyMove, b: BuyMove): boolean {
|
|
97
|
+
return a.resource === b.resource && a.side === b.side && !!a.fromStorage === !!b.fromStorage;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Central Europe sells garbage a dollar cheaper to whoever holds Wien — and the
|
|
102
|
+
* engine really does charge the lower price, not just offer the move. The box shows
|
|
103
|
+
* the price THIS player would pay, because that is the only number a tap can act on.
|
|
104
|
+
*/
|
|
105
|
+
function wienDiscount(G: GameState, resource: string, player: number | undefined): number {
|
|
106
|
+
if (resource !== 'garbage' || G.map.name !== 'Central Europe' || player === undefined) return 0;
|
|
107
|
+
const me = G.players[player];
|
|
108
|
+
if (!me) return 0;
|
|
109
|
+
return me.cities.some((c) => c.name === 'Wien') ? 1 : 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function marketRow(G: GameState, resource: string, north: boolean, opts: RowOptions): ResourceRow {
|
|
113
|
+
const prices = pricesFor(G, resource, north);
|
|
114
|
+
const cubes = cubesIn(G, resource, north);
|
|
115
|
+
// Cubes fill from the dear end, so the next one sold is at `length - cubes`.
|
|
116
|
+
// Clamped only so a market holding more cubes than its track has slots reads as
|
|
117
|
+
// the cheapest price rather than as a blank — the engine indexes the same array
|
|
118
|
+
// the same way, so it could not price such a state either. Middle East's "surplus
|
|
119
|
+
// oil" is not that case: its track is the full 24 and the surplus IS the $1 end.
|
|
120
|
+
const idx = Math.max(0, prices.length - cubes);
|
|
121
|
+
|
|
122
|
+
let price: number | null = null;
|
|
123
|
+
let atPrice = 0;
|
|
124
|
+
let next: number | null = null;
|
|
125
|
+
|
|
126
|
+
if (cubes > 0) {
|
|
127
|
+
price = prices[idx] - (north ? 0 : wienDiscount(G, resource, opts.player));
|
|
128
|
+
for (let i = idx; i < prices.length && prices[i] === prices[idx]; i++) atPrice++;
|
|
129
|
+
for (let i = idx; i < prices.length; i++) {
|
|
130
|
+
if (prices[i] !== prices[idx]) {
|
|
131
|
+
next = prices[i] - (north ? 0 : wienDiscount(G, resource, opts.player));
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const move: BuyMove = isKorea(G) ? { resource, side: north ? 'north' : 'south' } : { resource };
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
label: capitalize(resource),
|
|
141
|
+
color: RESOURCE_COLORS[resource],
|
|
142
|
+
move,
|
|
143
|
+
price,
|
|
144
|
+
next,
|
|
145
|
+
atPrice,
|
|
146
|
+
cubes,
|
|
147
|
+
buyable: !!opts.buyable && opts.buyable.some((b) => sameMove(b, move)),
|
|
148
|
+
capped: price !== null && price > maxPrice(G),
|
|
149
|
+
flat: false,
|
|
150
|
+
title:
|
|
151
|
+
cubes > 0
|
|
152
|
+
? `${capitalize(resource)} — $${price} each, ${atPrice} at that price, ${cubes} left`
|
|
153
|
+
: `${capitalize(resource)} — market empty`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** South Africa's storage pool and USA recharged's supply: a flat $8, no track. */
|
|
158
|
+
function flatRow(G: GameState, cubes: number, fromStorage: boolean, label: string, opts: RowOptions): ResourceRow {
|
|
159
|
+
const move: BuyMove = fromStorage ? { resource: 'coal', fromStorage: true } : { resource: 'coal' };
|
|
160
|
+
return {
|
|
161
|
+
label,
|
|
162
|
+
color: RESOURCE_COLORS.coal,
|
|
163
|
+
move,
|
|
164
|
+
price: cubes > 0 ? FLAT_POOL_PRICE : null,
|
|
165
|
+
next: null,
|
|
166
|
+
atPrice: 0,
|
|
167
|
+
cubes,
|
|
168
|
+
buyable: !!opts.buyable && opts.buyable.some((b) => sameMove(b, move)),
|
|
169
|
+
capped: cubes > 0 && FLAT_POOL_PRICE > maxPrice(G),
|
|
170
|
+
flat: true,
|
|
171
|
+
title: `${label} — $${FLAT_POOL_PRICE} each, ${cubes} left`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Does this map sell uranium on the main track at all? Australia moves it to a
|
|
177
|
+
* separate mine market and Bremen has no nuclear, so on both the row would be a
|
|
178
|
+
* permanently empty box.
|
|
179
|
+
*/
|
|
180
|
+
function uraniumOnTrack(G: GameState): boolean {
|
|
181
|
+
return G.map.name !== 'Australia' && G.map.name !== 'Bremen';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function rowsFor(G: GameState, north: boolean, opts: RowOptions): ResourceRow[] {
|
|
185
|
+
const rows: ResourceRow[] = [];
|
|
186
|
+
|
|
187
|
+
// USA recharged keeps selling coal at a flat $8 out of the supply once the printed
|
|
188
|
+
// track is bare, so the row would otherwise read "out" while the move is on offer.
|
|
189
|
+
if (!north && opts.isUsaRecharged && G.coalMarket === 0 && G.coalSupply > 0) {
|
|
190
|
+
rows.push(flatRow(G, G.coalSupply, false, 'Coal (from supply)', opts));
|
|
191
|
+
} else {
|
|
192
|
+
rows.push(marketRow(G, 'coal', north, opts));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// South Africa: used coal returns to a storage pool below the market and can
|
|
196
|
+
// always be bought back at $8, alongside whatever the track is charging. It sits
|
|
197
|
+
// directly under the market coal rather than at the bottom of the list, so the two
|
|
198
|
+
// coal prices are read together — the track is usually the cheaper of the two and
|
|
199
|
+
// that should be obvious without hunting.
|
|
200
|
+
if (!north && G.coalStorage !== undefined) {
|
|
201
|
+
rows.push(flatRow(G, G.coalStorage, true, 'Coal (from storage)', opts));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
rows.push(marketRow(G, 'oil', north, opts));
|
|
205
|
+
rows.push(marketRow(G, 'garbage', north, opts));
|
|
206
|
+
|
|
207
|
+
// Korea's north side has no uranium row.
|
|
208
|
+
if (!north && uraniumOnTrack(G)) {
|
|
209
|
+
rows.push(marketRow(G, 'uranium', north, opts));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return rows;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The resupply table is indexed coal / oil / garbage / uranium, and the track rows
|
|
217
|
+
* are drawn in that order — so the numbers line up with the boxes below them. Rows
|
|
218
|
+
* that are not part of the printed track (the flat pools) get no number.
|
|
219
|
+
*/
|
|
220
|
+
function restockText(table: (number | string)[] | undefined, rows: ResourceRow[]): string | undefined {
|
|
221
|
+
if (!table) return undefined;
|
|
222
|
+
const numbers = rows.filter((r) => !r.flat).map((r, i) => table[i]);
|
|
223
|
+
return numbers.every((n) => n === undefined) ? undefined : numbers.filter((n) => n !== undefined).join(' / ');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function resourceBlocks(G: GameState, opts: RowOptions = {}): ResourceBlock[] {
|
|
227
|
+
const blocks: ResourceBlock[] = [];
|
|
228
|
+
|
|
229
|
+
if (isKorea(G)) {
|
|
230
|
+
const northRows = rowsFor(G, true, opts);
|
|
231
|
+
blocks.push({
|
|
232
|
+
title: 'North Market',
|
|
233
|
+
restock: restockText(opts.resupplyNorth, northRows),
|
|
234
|
+
rows: northRows,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const southRows = rowsFor(G, false, opts);
|
|
239
|
+
blocks.push({
|
|
240
|
+
title: isKorea(G) ? 'South Market' : undefined,
|
|
241
|
+
restock: restockText(opts.resupply, southRows),
|
|
242
|
+
rows: southRows,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return blocks;
|
|
246
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M14.601 21.5c0 1.38-1.116 2.5-2.499 2.5-1.378 0-2.499-1.12-2.499-2.5s1.121-2.5 2.499-2.5c1.383 0 2.499 1.119 2.499 2.5zm-2.42-21.5c-4.029 0-7.06 2.693-7.06 8h3.955c0-2.304.906-4.189 3.024-4.189 1.247 0 2.57.828 2.684 2.411.123 1.666-.767 2.511-1.892 3.582-2.924 2.78-2.816 4.049-2.816 7.196h3.943c0-1.452-.157-2.508 1.838-4.659 1.331-1.436 2.986-3.222 3.021-5.943.047-3.963-2.751-6.398-6.697-6.398z"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 12c0 6.627-5.373 12-12 12s-12-5.373-12-12h2c0 5.514 4.486 10 10 10s10-4.486 10-10-4.486-10-10-10c-2.777 0-5.287 1.141-7.099 2.977l2.061 2.061-6.962 1.354 1.305-7.013 2.179 2.18c2.172-2.196 5.182-3.559 8.516-3.559 6.627 0 12 5.373 12 12zm-13-6v8h7v-2h-5v-6h-2z"/></svg>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="475.452px" height="475.451px" viewBox="0 0 475.452 475.451">
|
|
2
|
-
<g>
|
|
3
|
-
<path d="M468.083,118.385c-3.99-5.33-9.61-9.419-16.854-12.275c0.387,6.665-0.086,12.09-1.42,16.281l-85.65,281.789
|
|
4
|
-
c-1.526,4.948-4.859,8.897-9.992,11.848c-5.141,2.953-10.469,4.428-15.989,4.428H74.66c-22.84,0-36.542-6.652-41.112-19.985
|
|
5
|
-
c-1.903-5.14-1.807-9.229,0.288-12.275c2.092-2.857,5.708-4.288,10.85-4.288h248.102c17.702,0,29.93-3.285,36.688-9.852
|
|
6
|
-
c6.763-6.567,13.565-21.177,20.413-43.824l78.228-258.669c4.186-14.084,2.474-26.457-5.141-37.113s-18.462-15.987-32.548-15.987
|
|
7
|
-
H173.163c-2.474,0-7.329,0.854-14.562,2.568l0.284-0.859c-5.33-1.14-9.851-1.662-13.562-1.571
|
|
8
|
-
c-3.71,0.099-7.137,1.192-10.277,3.289c-3.14,2.094-5.664,4.328-7.566,6.706c-1.903,2.38-3.761,5.426-5.568,9.136
|
|
9
|
-
c-1.805,3.715-3.33,7.142-4.567,10.282c-1.237,3.14-2.666,6.473-4.281,9.998c-1.62,3.521-3.186,6.423-4.71,8.706
|
|
10
|
-
c-1.143,1.523-2.758,3.521-4.854,5.996c-2.091,2.474-3.805,4.664-5.137,6.567c-1.331,1.903-2.19,3.616-2.568,5.14
|
|
11
|
-
c-0.378,1.711-0.19,4.233,0.571,7.566c0.76,3.328,1.047,5.753,0.854,7.277c-0.76,7.232-3.378,16.414-7.849,27.552
|
|
12
|
-
c-4.471,11.136-8.52,19.18-12.135,24.126c-0.761,0.95-2.853,3.092-6.28,6.424c-3.427,3.33-5.52,6.23-6.279,8.704
|
|
13
|
-
c-0.762,0.951-0.81,3.617-0.144,7.994c0.666,4.38,0.907,7.423,0.715,9.136c-0.765,6.473-3.14,15.037-7.139,25.697
|
|
14
|
-
c-3.999,10.657-7.994,19.414-11.993,26.265c-0.569,1.141-2.185,3.328-4.853,6.567c-2.662,3.237-4.283,5.902-4.853,7.99
|
|
15
|
-
c-0.38,1.523-0.33,4.188,0.144,7.994c0.473,3.806,0.426,6.66-0.144,8.562c-1.521,7.228-4.377,15.94-8.565,26.125
|
|
16
|
-
c-4.187,10.178-8.47,18.896-12.851,26.121c-1.138,1.906-2.712,4.145-4.708,6.711c-1.999,2.566-3.568,4.805-4.711,6.707
|
|
17
|
-
c-1.141,1.903-1.903,3.901-2.284,5.996c-0.19,1.143,0.098,2.998,0.859,5.571c0.76,2.566,1.047,4.612,0.854,6.14
|
|
18
|
-
c-0.192,2.662-0.57,6.187-1.141,10.567c-0.572,4.373-0.859,6.939-0.859,7.699c-4.187,11.424-3.999,23.511,0.572,36.269
|
|
19
|
-
c5.33,14.838,14.797,27.36,28.406,37.541c13.61,10.185,27.74,15.27,42.398,15.27h263.521c12.367,0,24.026-4.141,34.971-12.416
|
|
20
|
-
c10.944-8.281,18.227-18.507,21.837-30.696l78.511-258.662C477.412,141.51,475.701,129.234,468.083,118.385z M164.31,118.956
|
|
21
|
-
l5.997-18.274c0.76-2.474,2.329-4.615,4.709-6.423c2.38-1.805,4.808-2.712,7.282-2.712h173.589c2.663,0,4.565,0.903,5.708,2.712
|
|
22
|
-
c1.14,1.809,1.335,3.949,0.575,6.423l-6.002,18.274c-0.764,2.475-2.327,4.611-4.713,6.424c-2.382,1.805-4.805,2.708-7.278,2.708
|
|
23
|
-
H170.593c-2.666,0-4.568-0.9-5.711-2.708C163.74,123.567,163.55,121.431,164.31,118.956z M140.615,192.045l5.996-18.271
|
|
24
|
-
c0.76-2.474,2.331-4.615,4.709-6.423c2.38-1.809,4.805-2.712,7.282-2.712h173.583c2.666,0,4.572,0.9,5.712,2.712
|
|
25
|
-
c1.14,1.809,1.331,3.949,0.568,6.423l-5.996,18.271c-0.759,2.474-2.33,4.617-4.708,6.423c-2.383,1.809-4.805,2.712-7.283,2.712
|
|
26
|
-
H146.895c-2.664,0-4.567-0.9-5.708-2.712C140.043,196.662,139.854,194.519,140.615,192.045z"/>
|
|
27
|
-
</g>
|
|
28
|
-
</svg>
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
-
<path d="M5 17h-5v-10h5v10zm2-10v10l9 5v-20l-9 5zm15.324 4.993l1.646-1.659-1.324-1.324-1.651 1.67-1.665-1.648-1.316 1.318 1.67 1.657-1.65 1.669 1.318 1.317 1.658-1.672 1.666 1.653 1.324-1.325-1.676-1.656z"/>
|
|
3
|
-
</svg>
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
<svg width="497.25px" height="497.25px" viewBox="0 0 497.25 497.25" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<path fill="black" d="M248.625,89.25 V0 l-127.5,127.5 l127.5,127.5 V140.25 c84.15,0,153,68.85,153,153 c0,84.15-68.85,153-153,153 c-84.15,0-153-68.85-153-153 h-51 c0,112.2,91.8,204,204,204 s204-91.8,204-204 S360.825,89.25,248.625,89.25z"/>
|
|
3
|
-
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|