powergrid-viewer 2.0.3 → 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/package.json
CHANGED
package/src/components/Game.vue
CHANGED
|
@@ -279,9 +279,9 @@
|
|
|
279
279
|
@click="undo()"
|
|
280
280
|
/>
|
|
281
281
|
<LogButton transform="translate(15, 97)" @click="showLog()" />
|
|
282
|
-
<SoundButton transform="
|
|
283
|
-
<HelpButton transform="
|
|
284
|
-
<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
285
|
<!-- Only offered where it means something. Shown whenever the
|
|
286
286
|
viewport is portrait — not only while stacking is active — so
|
|
287
287
|
it can undo its own effect. Sits under Rules rather than under
|
|
@@ -289,7 +289,7 @@
|
|
|
289
289
|
on the authored board. -->
|
|
290
290
|
<LayoutButton
|
|
291
291
|
v-if="portraitViewport"
|
|
292
|
-
transform="
|
|
292
|
+
:transform="iconButton(3)"
|
|
293
293
|
:isOn="stacked"
|
|
294
294
|
@click="toggleStackLayout()"
|
|
295
295
|
/>
|
|
@@ -703,7 +703,7 @@ const STACK_MAX_ROW_SCREENS = 0.72;
|
|
|
703
703
|
* readable; the buttons next to it are tap targets and want every pixel.
|
|
704
704
|
*/
|
|
705
705
|
const STACK_SLOT_MAX_SCALE: Record<string, number> = {
|
|
706
|
-
roundInfo:
|
|
706
|
+
roundInfo: 1.9,
|
|
707
707
|
playerOrder: 2.5,
|
|
708
708
|
};
|
|
709
709
|
/**
|
|
@@ -1836,6 +1836,19 @@ export default class Game extends Vue {
|
|
|
1836
1836
|
return this.slotTransforms[name];
|
|
1837
1837
|
}
|
|
1838
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
|
+
|
|
1839
1852
|
/**
|
|
1840
1853
|
* Only portrait viewports are re-laid out. A landscape phone already reads
|
|
1841
1854
|
* well (the scene's own aspect ratio is close to the screen's), so leaving
|
|
@@ -1900,18 +1913,40 @@ export default class Game extends Vue {
|
|
|
1900
1913
|
const gaps = STACK_GAP * (present.length - 1);
|
|
1901
1914
|
const usable = STACK_WIDTH - 2 * STACK_PAD - gaps;
|
|
1902
1915
|
const combined = present.reduce((sum, name) => sum + boxes[name].width, 0);
|
|
1903
|
-
//
|
|
1904
|
-
//
|
|
1905
|
-
const
|
|
1906
|
-
// A slot may decline part of that scale so a neighbour keeps the space,
|
|
1907
|
-
// and no slot may grow past the height budget for one row.
|
|
1908
|
-
const scaleOf = (name: string) =>
|
|
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) =>
|
|
1909
1919
|
Math.min(
|
|
1910
|
-
rowScale,
|
|
1911
1920
|
STACK_SLOT_MAX_SCALE[name] ?? Infinity,
|
|
1912
1921
|
((STACK_SLOT_MAX_WIDTH[name] ?? Infinity) * usable) / boxes[name].width,
|
|
1913
|
-
heightCapFor(boxes[name].height)
|
|
1922
|
+
heightCapFor(boxes[name].height),
|
|
1923
|
+
STACK_MAX_SCALE
|
|
1914
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));
|
|
1915
1950
|
const rowWidth = present.reduce((sum, name) => sum + boxes[name].width * scaleOf(name), 0);
|
|
1916
1951
|
const rowHeight = Math.max(...present.map((name) => boxes[name].height * scaleOf(name)));
|
|
1917
1952
|
|
|
@@ -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
|
}
|