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
|
@@ -386,6 +386,55 @@
|
|
|
386
386
|
/>
|
|
387
387
|
</template>
|
|
388
388
|
|
|
389
|
+
<!-- City tap targets, drawn LAST so they sit above the houses already built
|
|
390
|
+
there. Houses are painted after the city's own circle, and SVG hit-testing
|
|
391
|
+
takes the topmost element — so once a city held houses the active player
|
|
392
|
+
could only pick at the gray slivers between them (eric-hu, #125).
|
|
393
|
+
|
|
394
|
+
The radius reaches out to the printed region ring, which is what the eye
|
|
395
|
+
reads as the edge of the city, but never past half the distance to the
|
|
396
|
+
nearest city: the maps disagree enormously about spacing (Bremen's closest
|
|
397
|
+
pair is 90.8 apart, South Africa's Johannesburg N/S is 21), so the limit
|
|
398
|
+
has to be measured rather than picked.
|
|
399
|
+
|
|
400
|
+
Only rendered while the city can actually be acted on, so houses keep
|
|
401
|
+
their own tooltips the rest of the time. -->
|
|
402
|
+
<template v-for="city in cities">
|
|
403
|
+
<circle
|
|
404
|
+
v-if="city.connectionCost == null && (canBuild(city) || canPickRegion(city))"
|
|
405
|
+
:key="city.name + '_tap'"
|
|
406
|
+
class="canClick"
|
|
407
|
+
:r="tapRadius(city)"
|
|
408
|
+
:cx="city.x"
|
|
409
|
+
:cy="city.y"
|
|
410
|
+
fill="none"
|
|
411
|
+
pointer-events="all"
|
|
412
|
+
@click="onCityClick(city)"
|
|
413
|
+
>
|
|
414
|
+
<title>{{ city.name }}</title>
|
|
415
|
+
</circle>
|
|
416
|
+
<!-- Node-weighted tiles (Bremen) get the same treatment at the tile's own
|
|
417
|
+
size — their houses sit in slots inside the diamond. Manhattan's spaces
|
|
418
|
+
hold one house each, so a built space is never buildable again and needs
|
|
419
|
+
no overlay. -->
|
|
420
|
+
<rect
|
|
421
|
+
v-if="isNodeTile(city) && (canBuild(city) || canPickRegion(city))"
|
|
422
|
+
:key="city.name + '_tapTile'"
|
|
423
|
+
class="canClick"
|
|
424
|
+
:x="city.x - 23"
|
|
425
|
+
:y="city.y - 23"
|
|
426
|
+
width="46"
|
|
427
|
+
height="46"
|
|
428
|
+
rx="7"
|
|
429
|
+
fill="none"
|
|
430
|
+
pointer-events="all"
|
|
431
|
+
:transform="`rotate(45, ${city.x}, ${city.y})`"
|
|
432
|
+
@click="onCityClick(city)"
|
|
433
|
+
>
|
|
434
|
+
<title>{{ city.name }}</title>
|
|
435
|
+
</rect>
|
|
436
|
+
</template>
|
|
437
|
+
|
|
389
438
|
<!-- Regions where nuclear plants are banned: a nuclear-plant tile under a red
|
|
390
439
|
prohibition sign (ring + diagonal bar), matching the marker printed on
|
|
391
440
|
the physical board. -->
|
|
@@ -421,6 +470,11 @@ import { House } from '../pieces';
|
|
|
421
470
|
import { Piece, Preferences } from '../../types/ui-data';
|
|
422
471
|
import { City, Connection, Polygon } from 'powergrid-engine/src/maps';
|
|
423
472
|
|
|
473
|
+
/** The gray disc a city is drawn as, and the tap target it has always had. */
|
|
474
|
+
const CITY_RADIUS = 20;
|
|
475
|
+
/** The coloured region ring drawn around that disc — the city's visible edge. */
|
|
476
|
+
const CITY_RING_RADIUS = 25;
|
|
477
|
+
|
|
424
478
|
@Component({
|
|
425
479
|
components: {
|
|
426
480
|
House,
|
|
@@ -605,6 +659,42 @@ export default class Map extends Vue {
|
|
|
605
659
|
return markers;
|
|
606
660
|
}
|
|
607
661
|
|
|
662
|
+
/**
|
|
663
|
+
* Distance from each city to its nearest neighbour. Cached as a computed so the
|
|
664
|
+
* O(n^2) sweep runs once per board rather than once per tap target; n is at most
|
|
665
|
+
* a few dozen cities.
|
|
666
|
+
*/
|
|
667
|
+
get nearestCityDistance(): Record<string, number> {
|
|
668
|
+
const out: Record<string, number> = {};
|
|
669
|
+
const cities = this.cities ?? [];
|
|
670
|
+
for (const a of cities) {
|
|
671
|
+
let min = Infinity;
|
|
672
|
+
for (const b of cities) {
|
|
673
|
+
if (b === a) continue;
|
|
674
|
+
const d = Math.hypot(b.x - a.x, b.y - a.y);
|
|
675
|
+
if (d < min) min = d;
|
|
676
|
+
}
|
|
677
|
+
out[a.name] = min;
|
|
678
|
+
}
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* How far a city's tap target reaches. Out to the region ring where there is room
|
|
684
|
+
* for it, but never more than halfway to the next city — on the tight maps two
|
|
685
|
+
* targets would otherwise overlap and whichever is drawn later would swallow both
|
|
686
|
+
* cities' taps. Never smaller than the disc it has always been.
|
|
687
|
+
*/
|
|
688
|
+
tapRadius(city: City): number {
|
|
689
|
+
const halfWayToNeighbour = (this.nearestCityDistance[city.name] ?? Infinity) / 2;
|
|
690
|
+
return Math.max(CITY_RADIUS, Math.min(CITY_RING_RADIUS, halfWayToNeighbour));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** A Bremen-style node-weighted district: a diamond tile with several house slots. */
|
|
694
|
+
isNodeTile(city: City) {
|
|
695
|
+
return city.connectionCost != null && !!city.slotCosts && city.slotCosts.length >= 2;
|
|
696
|
+
}
|
|
697
|
+
|
|
608
698
|
canBuild(city: City) {
|
|
609
699
|
return !!this.buildableCities!.find((cityName) => cityName == city.name);
|
|
610
700
|
}
|
|
@@ -1,33 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<g>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
v-for="index in cardsLeft"
|
|
7
|
-
:key="'card' + index"
|
|
8
|
-
:x="35 + index / 5"
|
|
9
|
-
:y="38 - index / 10"
|
|
10
|
-
width="60"
|
|
11
|
-
height="40"
|
|
12
|
-
fill="gray"
|
|
13
|
-
stroke="black"
|
|
14
|
-
stroke-width="2"
|
|
15
|
-
rx="4"
|
|
16
|
-
/>
|
|
17
|
-
<rect
|
|
18
|
-
:x="35 + cardsLeft / 5"
|
|
19
|
-
:y="38 - cardsLeft / 10"
|
|
20
|
-
width="60"
|
|
21
|
-
height="40"
|
|
22
|
-
:fill="nextCardWeak ? 'gray' : 'lightgray'"
|
|
23
|
-
stroke="black"
|
|
24
|
-
stroke-width="2"
|
|
25
|
-
rx="4"
|
|
26
|
-
>
|
|
27
|
-
<title>{{ cardsLeft }} cards left{{ nextCardWeak ? ', next is an initial plant' : '' }}</title>
|
|
28
|
-
</rect>
|
|
29
|
-
</template>
|
|
30
|
-
|
|
3
|
+
<!-- The draw pile is rendered by Game.vue as its own group, not here: it is
|
|
4
|
+
static reference art next to three interactive markets, so the portrait
|
|
5
|
+
layout needs to be able to place it on a different row. -->
|
|
31
6
|
<text x="165" y="14" font-weight="600" fill="black">Actual Market:</text>
|
|
32
7
|
<template v-for="(card, i) in actualMarketCards">
|
|
33
8
|
<Card
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<g class="button enabled" @click="$emit('click')">
|
|
3
|
+
<circle cx="15" cy="15" r="15" fill="gainsboro" stroke="black" stroke-width="2px" />
|
|
4
|
+
<!-- Stacked: three full-width rows. Wide: the authored side-by-side board. -->
|
|
5
|
+
<template v-if="isOn">
|
|
6
|
+
<rect x="6" y="7" width="18" height="4" rx="1" fill="black" />
|
|
7
|
+
<rect x="6" y="13" width="18" height="4" rx="1" fill="black" />
|
|
8
|
+
<rect x="6" y="19" width="18" height="4" rx="1" fill="black" />
|
|
9
|
+
</template>
|
|
10
|
+
<template v-else>
|
|
11
|
+
<rect x="6" y="7" width="10" height="16" rx="1" fill="black" />
|
|
12
|
+
<rect x="18" y="7" width="6" height="7" rx="1" fill="black" />
|
|
13
|
+
<rect x="18" y="16" width="6" height="7" rx="1" fill="black" />
|
|
14
|
+
</template>
|
|
15
|
+
<title v-if="isOn">Switch to the full board layout</title>
|
|
16
|
+
<title v-else>Stack the board into rows</title>
|
|
17
|
+
</g>
|
|
18
|
+
</template>
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
import { Vue, Component, Prop } from 'vue-property-decorator';
|
|
21
|
+
|
|
22
|
+
@Component
|
|
23
|
+
export default class LayoutButton extends Vue {
|
|
24
|
+
@Prop()
|
|
25
|
+
isOn?: boolean;
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import Button from './Button.vue';
|
|
2
2
|
import HelpButton from './HelpButton.vue';
|
|
3
|
+
import LayoutButton from './LayoutButton.vue';
|
|
3
4
|
import LogButton from './LogButton.vue';
|
|
4
5
|
import PassButton from './PassButton.vue';
|
|
5
6
|
import SoundButton from './SoundButton.vue';
|
|
6
7
|
import RulesButton from './RulesButton.vue';
|
|
7
8
|
import UndoButton from './UndoButton.vue';
|
|
8
9
|
|
|
9
|
-
export { Button, HelpButton, LogButton, PassButton, SoundButton, RulesButton, UndoButton };
|
|
10
|
+
export { Button, HelpButton, LayoutButton, LogButton, PassButton, SoundButton, RulesButton, UndoButton };
|
package/src/launch.ts
CHANGED
package/src/self-contained.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cloneDeep } from 'lodash';
|
|
2
2
|
import { move as execMove, Move, Phase, setup, stripSecret } from 'powergrid-engine';
|
|
3
3
|
import { moveAI } from 'powergrid-engine/src/engine';
|
|
4
|
+
import type { MapName, Variant } from 'powergrid-engine/src/gamestate';
|
|
4
5
|
import launch from './launch';
|
|
5
6
|
|
|
6
7
|
const delayBase = 0;
|
|
@@ -10,7 +11,20 @@ function launchSelfContained(selector = '#app') {
|
|
|
10
11
|
|
|
11
12
|
const emitter = launch(selector);
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
// The sandbox game can be steered from the URL so a layout or rules change can
|
|
15
|
+
// be checked against several maps without editing this file each time, e.g.
|
|
16
|
+
// ?map=Australia&players=3&variant=original&seed=7
|
|
17
|
+
const params = new URLSearchParams(window.location.search);
|
|
18
|
+
// Note: randomizeMap short-circuits region selection in setup(), so ticking
|
|
19
|
+
// both gives a randomized board with no draft. They are alternatives.
|
|
20
|
+
const gameOptions = {
|
|
21
|
+
map: (params.get('map') || 'Bremen') as MapName,
|
|
22
|
+
variant: (params.get('variant') || 'recharged') as Variant,
|
|
23
|
+
showMoney: true,
|
|
24
|
+
randomizeMap: params.get('randomize') === '1',
|
|
25
|
+
chooseRegions: params.get('regions') === '1',
|
|
26
|
+
};
|
|
27
|
+
let gameState = setup(Number(params.get('players')) || 6, gameOptions, params.get('seed') || '0');
|
|
14
28
|
|
|
15
29
|
// Dev coord-picker example (commented; uncomment + drop a board photo into
|
|
16
30
|
// viewer/public/ to author city coordinates for a new map). See the picker
|
package/src/types/ui-data.ts
CHANGED
|
@@ -10,6 +10,9 @@ export type Preferences = {
|
|
|
10
10
|
adjustPlayerOrder: boolean;
|
|
11
11
|
undoWholeTurn: boolean;
|
|
12
12
|
fitToScreen: boolean;
|
|
13
|
+
// Portrait phones stack the board into full-width rows. Per player, not per
|
|
14
|
+
// game: it describes the screen you are holding, not the rules being played.
|
|
15
|
+
stackOnPortrait: boolean;
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
export interface Piece {
|