powergrid-viewer 1.9.4 → 1.9.6

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "powergrid-viewer",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
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": "1.13.3"
21
+ "powergrid-engine": "1.13.5"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/assert": "^1.4.7",
@@ -59,6 +59,40 @@
59
59
  @build="build($event)"
60
60
  />
61
61
 
62
+ <!-- Japan: Free Jump indicator -->
63
+ <g v-if="G.map.name === 'Japan'">
64
+ <text x="330" y="93" font-size="20" font-weight="bold" fill="black">Free Jump:</text>
65
+ <template v-for="(fjPlayer, i) in G.players">
66
+ <g
67
+ :key="'fj_' + i"
68
+ :transform="`translate(${480 + i * 30}, 80) scale(0.045)`"
69
+ :opacity="playerHasUsedFreeJump(i) ? 0.2 : 1"
70
+ >
71
+ <path
72
+ d="M187.698 263.636V456.017L3 341.204V169.522L80.8579 108.141L187.698 263.636Z"
73
+ :fill="playerColors[i]"
74
+ stroke="#010101"
75
+ stroke-width="12"
76
+ stroke-miterlimit="10"
77
+ />
78
+ <path
79
+ d="M395.724 136.361V300.164L187.698 456.017V263.636L395.724 136.361Z"
80
+ :fill="playerColors[i]"
81
+ stroke="#010101"
82
+ stroke-width="12"
83
+ stroke-miterlimit="10"
84
+ />
85
+ <path
86
+ d="M395.724 136.361L187.698 263.636L80.8579 108.141L304.771 4L395.724 136.361Z"
87
+ :fill="playerColors[i]"
88
+ stroke="#010101"
89
+ stroke-width="12"
90
+ stroke-miterlimit="10"
91
+ />
92
+ </g>
93
+ </template>
94
+ </g>
95
+
62
96
  <Resources
63
97
  ref="resources"
64
98
  :transform="`translate(${G.map.supplyPosition[0]}, ${G.map.supplyPosition[1]})`"
@@ -168,6 +202,45 @@
168
202
  </div>
169
203
  </div>
170
204
 
205
+ <div v-if="G && freeJumpCity" :class="['modal', { visible: freeJumpVisible }]">
206
+ <div class="modal-content">
207
+ <span
208
+ class="close"
209
+ @click="
210
+ freeJumpVisible = false;
211
+ freeJumpCity = null;
212
+ "
213
+ >&times;</span
214
+ >
215
+ <div class="modal-title">Free Jump — {{ freeJumpCity.name }}</div>
216
+ <div class="confirm-message" v-if="freeJumpNormalPrice !== null">
217
+ Use your <b>Free Jump</b> to build here for ${{ freeJumpSlotPrice }} (slot cost only), or pay the
218
+ full connection price of ${{ freeJumpNormalPrice }} and save the jump for later.
219
+ </div>
220
+ <div class="confirm-message" v-else>
221
+ Use your <b>Free Jump</b> to build here for ${{ freeJumpSlotPrice }} (slot cost only)? This city is
222
+ not reachable from your network otherwise.
223
+ </div>
224
+ <div class="confirm-buttons">
225
+ <button class="confirm-button" @click="confirmFreeJump(true)">
226
+ Use Free Jump (${{ freeJumpSlotPrice }})
227
+ </button>
228
+ <button v-if="freeJumpNormalPrice !== null" class="confirm-button" @click="confirmFreeJump(false)">
229
+ Pay Full Price (${{ freeJumpNormalPrice }})
230
+ </button>
231
+ <button
232
+ class="confirm-button"
233
+ @click="
234
+ freeJumpVisible = false;
235
+ freeJumpCity = null;
236
+ "
237
+ >
238
+ Cancel
239
+ </button>
240
+ </div>
241
+ </div>
242
+ </div>
243
+
171
244
  <div v-if="G && discardedPowerPlant" :class="['modal', { visible: discardVisible }]">
172
245
  <div class="modal-content">
173
246
  <div class="modal-title">Discard Resources</div>
@@ -385,6 +458,7 @@ import Resources from './boards/Resources.vue';
385
458
  import { LogMove } from 'powergrid-engine/src/log';
386
459
  import { Phase, PowerPlant, PowerPlantType, ResourceType } from 'powergrid-engine/src/gamestate';
387
460
  import { City } from 'powergrid-engine/src/maps';
461
+ import { countNetworks } from 'powergrid-engine/src/available-moves';
388
462
 
389
463
  @Component({
390
464
  created(this: Game) {
@@ -488,6 +562,11 @@ export default class Game extends Vue {
488
562
  discardVisible: boolean = false;
489
563
  resourcesToDiscard: { name: string, max: number, value: string }[] = [];
490
564
 
565
+ freeJumpCity: City | null = null;
566
+ freeJumpNormalPrice: number | null = null;
567
+ freeJumpSlotPrice: number = 0;
568
+ freeJumpVisible: boolean = false;
569
+
491
570
  disablePass: boolean = false;
492
571
 
493
572
  @Ref() powerPlantMarket!: PowerPlantMarket;
@@ -651,8 +730,41 @@ export default class Game extends Vue {
651
730
  build(city: City) {
652
731
  const currentPlayer = this.G!.players[this.player!];
653
732
  const availableMoves = currentPlayer.availableMoves!;
654
- const move = availableMoves[MoveName.Build]!.find((c) => c.name == city.name)!;
655
- this.sendMove({ name: MoveName.Build, data: { name: city.name, price: move.price } });
733
+ const buildMoves = availableMoves[MoveName.Build]!;
734
+ const freeJumpMove = buildMoves.find((c) => c.name === city.name && c.freeJump);
735
+ const normalMove = buildMoves.find((c) => c.name === city.name && !c.freeJump);
736
+
737
+ if (freeJumpMove && normalMove) {
738
+ // Player can choose: use the free jump or pay full price
739
+ this.freeJumpCity = city;
740
+ this.freeJumpSlotPrice = freeJumpMove.price;
741
+ this.freeJumpNormalPrice = normalMove.price;
742
+ this.freeJumpVisible = true;
743
+ } else if (freeJumpMove && !normalMove) {
744
+ // Only reachable via free jump — confirm before using it
745
+ this.freeJumpCity = city;
746
+ this.freeJumpSlotPrice = freeJumpMove.price;
747
+ this.freeJumpNormalPrice = null;
748
+ this.freeJumpVisible = true;
749
+ } else {
750
+ // Normal build, no free jump involved
751
+ this.sendMove({ name: MoveName.Build, data: { name: city.name, price: normalMove!.price } });
752
+ }
753
+ }
754
+
755
+ confirmFreeJump(useJump: boolean) {
756
+ const city = this.freeJumpCity!;
757
+ const currentPlayer = this.G!.players[this.player!];
758
+ const buildMoves = currentPlayer.availableMoves![MoveName.Build]!;
759
+ this.freeJumpVisible = false;
760
+ this.freeJumpCity = null;
761
+ if (useJump) {
762
+ const freeJumpMove = buildMoves.find((c) => c.name === city.name && c.freeJump)!;
763
+ this.sendMove({ name: MoveName.Build, data: { name: city.name, price: freeJumpMove.price, freeJump: true } });
764
+ } else {
765
+ const normalMove = buildMoves.find((c) => c.name === city.name && !c.freeJump)!;
766
+ this.sendMove({ name: MoveName.Build, data: { name: city.name, price: normalMove.price } });
767
+ }
656
768
  }
657
769
 
658
770
  confirmDiscard() {
@@ -924,6 +1036,19 @@ export default class Game extends Vue {
924
1036
  return availableMoves[MoveName.Build] && availableMoves[MoveName.Build]!.map((c) => c.name) || [];
925
1037
  }
926
1038
 
1039
+ playerHasUsedFreeJump(playerIndex: number): boolean {
1040
+ const player = this.G?.players[playerIndex];
1041
+ if (!player) return false;
1042
+ // Primary: trust the engine flag (set for all new games).
1043
+ if (player.usedFreeJump) return true;
1044
+ // Fallback: detect from network topology for game states where
1045
+ // usedFreeJump wasn't tracked (games started before that field existed).
1046
+ if (player.cities.length >= 2) {
1047
+ return countNetworks(this.G!.map.connections, player.cities.map(c => c.name)) >= 2;
1048
+ }
1049
+ return false;
1050
+ }
1051
+
927
1052
  canBuild(city: City) {
928
1053
  if (!this.canMove()) return false;
929
1054
 
@@ -152,6 +152,23 @@
152
152
  stroke-linecap="round"
153
153
  />
154
154
  </template>
155
+ <!-- [10,10,20] city: "10" label at bottom-left (two 10-Elektro slots) -->
156
+ <text
157
+ v-if="
158
+ city.slotCosts &&
159
+ city.slotCosts.length === 3 &&
160
+ city.slotCosts[0] === 10 &&
161
+ city.slotCosts[1] === 10
162
+ "
163
+ :key="city.name + '_10label'"
164
+ :x="city.x - 20"
165
+ :y="city.y + 19"
166
+ font-size="17"
167
+ font-weight="bold"
168
+ fill="black"
169
+ text-anchor="middle"
170
+ >10</text
171
+ >
155
172
  <!-- [15,20] city: X at top-middle -->
156
173
  <template v-if="city.slotCosts && city.slotCosts.length === 2 && city.slotCosts[0] === 15">
157
174
  <line