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.
@@ -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
- <text x="10" y="14" font-weight="600" fill="black">Power Plant Deck:</text>
4
- <template v-if="cardsLeft > 0">
5
- <rect
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
@@ -21,6 +21,7 @@ function launch(selector: string) {
21
21
  adjustPlayerOrder: false,
22
22
  undoWholeTurn: true,
23
23
  fitToScreen: true,
24
+ stackOnPortrait: true,
24
25
  },
25
26
  avatars: [],
26
27
  };
@@ -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
- let gameState = setup(6, { map: 'Bremen', variant: 'recharged', showMoney: true, randomizeMap: false }, '0');
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
@@ -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 {