powergrid-viewer 2.0.13 → 2.1.0
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 +187 -69
- package/src/components/InlineLog.vue +108 -0
- package/src/components/boards/Map.vue +34 -4
- package/src/game-chat.ts +309 -0
- package/src/launch.ts +4 -0
- package/src/self-contained.ts +4 -0
- package/src/sounds.ts +187 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "powergrid-viewer",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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": "2.0.
|
|
21
|
+
"powergrid-engine": "2.0.6"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/assert": "^1.4.7",
|
package/src/components/Game.vue
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
<div class="statusBar">
|
|
4
4
|
{{ getStatusMessage() }}
|
|
5
5
|
</div>
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
<svg
|
|
7
|
+
v-if="G"
|
|
8
|
+
id="scene"
|
|
9
|
+
:class="{ stacked: stacked }"
|
|
10
|
+
:viewBox="sceneViewBox"
|
|
11
|
+
:style="{ width: '100%', aspectRatio: sceneViewBox.split(' ').slice(2).join(' / ') }"
|
|
12
|
+
preserveAspectRatio="xMidYMin meet"
|
|
13
|
+
>
|
|
14
14
|
<rect width="100%" height="100%" x="0" y="0" fill="yellowgreen" />
|
|
15
15
|
|
|
16
16
|
<g ref="slotPlayerOrder" :transform="slotT('playerOrder')">
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
:connections="G.map.connections"
|
|
99
99
|
:polygons="G.map.polygons"
|
|
100
100
|
:buildableCities="getBuildableCities()"
|
|
101
|
+
:buildPrices="getBuildableCityPrices()"
|
|
101
102
|
:blockedCities="G.blockedCities"
|
|
102
103
|
:pickableRegions="getPickableRegions()"
|
|
103
104
|
:noUraniumRegions="G.map.noUraniumRegions"
|
|
@@ -413,6 +414,11 @@
|
|
|
413
414
|
</g>
|
|
414
415
|
</svg>
|
|
415
416
|
|
|
417
|
+
<div class="journal-and-chat">
|
|
418
|
+
<InlineLog v-if="G" :entries="logReversed.slice().reverse()" :mapName="G.options.map" />
|
|
419
|
+
<div class="chat-host"></div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
416
422
|
<div v-if="G" :class="['modal', { visible: logVisible }]">
|
|
417
423
|
<div class="modal-content">
|
|
418
424
|
<span class="close" @click="logVisible = false">×</span>
|
|
@@ -734,6 +740,7 @@
|
|
|
734
740
|
</div>
|
|
735
741
|
</template>
|
|
736
742
|
<script lang="ts">
|
|
743
|
+
import InlineLog from './InlineLog.vue';
|
|
737
744
|
import { Vue, Component, Prop, Watch, Provide, ProvideReactive, Ref } from 'vue-property-decorator';
|
|
738
745
|
import { MoveName, ended, playersSortedByScore, reconstructState } from 'powergrid-engine';
|
|
739
746
|
import type { GameState, LogItem, Move, Player } from 'powergrid-engine';
|
|
@@ -866,21 +873,21 @@ const round = (n: number, digits = 2) => Number(n.toFixed(digits));
|
|
|
866
873
|
this.paused = true;
|
|
867
874
|
this.emitter.emit('replay:info', {
|
|
868
875
|
start: 1,
|
|
869
|
-
current: this.G!.log.filter(l => l.type == 'move').length,
|
|
870
|
-
end: this._futureState!.log.filter(l => l.type == 'move').length,
|
|
876
|
+
current: this.G!.log.filter((l) => l.type == 'move').length,
|
|
877
|
+
end: this._futureState!.log.filter((l) => l.type == 'move').length,
|
|
871
878
|
});
|
|
872
879
|
});
|
|
873
880
|
|
|
874
881
|
this.emitter.on('replayTo', (to: number) => {
|
|
875
|
-
const log = this._futureState!.log.map((l, i) => ({ index: i, ...l })).filter(l => l.type == 'move');
|
|
882
|
+
const log = this._futureState!.log.map((l, i) => ({ index: i, ...l })).filter((l) => l.type == 'move');
|
|
876
883
|
to = log[to - 1].index;
|
|
877
884
|
|
|
878
885
|
this.replaceState(reconstructState(this._futureState!, to + 1), false);
|
|
879
886
|
|
|
880
887
|
this.emitter.emit('replay:info', {
|
|
881
888
|
start: 1,
|
|
882
|
-
current: this.G!.log.filter(l => l.type == 'move').length + this.G!.hiddenLog.length,
|
|
883
|
-
end: this._futureState!.log.filter(l => l.type == 'move').length,
|
|
889
|
+
current: this.G!.log.filter((l) => l.type == 'move').length + this.G!.hiddenLog.length,
|
|
890
|
+
end: this._futureState!.log.filter((l) => l.type == 'move').length,
|
|
884
891
|
});
|
|
885
892
|
});
|
|
886
893
|
|
|
@@ -890,6 +897,7 @@ const round = (n: number, digits = 2) => Number(n.toFixed(digits));
|
|
|
890
897
|
});
|
|
891
898
|
},
|
|
892
899
|
components: {
|
|
900
|
+
InlineLog,
|
|
893
901
|
PlayerBoard,
|
|
894
902
|
Card,
|
|
895
903
|
House,
|
|
@@ -912,7 +920,7 @@ const round = (n: number, digits = 2) => Number(n.toFixed(digits));
|
|
|
912
920
|
CityCount,
|
|
913
921
|
Map,
|
|
914
922
|
Resources,
|
|
915
|
-
ResourceBoxes
|
|
923
|
+
ResourceBoxes,
|
|
916
924
|
},
|
|
917
925
|
})
|
|
918
926
|
export default class Game extends Vue {
|
|
@@ -980,7 +988,7 @@ export default class Game extends Vue {
|
|
|
980
988
|
|
|
981
989
|
discardedPowerPlant: PowerPlant | null = null;
|
|
982
990
|
discardVisible: boolean = false;
|
|
983
|
-
resourcesToDiscard: { name: string
|
|
991
|
+
resourcesToDiscard: { name: string; max: number; value: string }[] = [];
|
|
984
992
|
|
|
985
993
|
freeJumpCity: City | null = null;
|
|
986
994
|
freeJumpNormalPrice: number | null = null;
|
|
@@ -1095,7 +1103,7 @@ export default class Game extends Vue {
|
|
|
1095
1103
|
player = {
|
|
1096
1104
|
coalLeft: this.G.players[this.player].coalLeft,
|
|
1097
1105
|
oilLeft: this.G.players[this.player].oilLeft,
|
|
1098
|
-
resourcesUsed: this.G.players[this.player].resourcesUsed
|
|
1106
|
+
resourcesUsed: this.G.players[this.player].resourcesUsed,
|
|
1099
1107
|
};
|
|
1100
1108
|
}
|
|
1101
1109
|
}
|
|
@@ -1121,19 +1129,6 @@ export default class Game extends Vue {
|
|
|
1121
1129
|
this.scheduleRelayout();
|
|
1122
1130
|
});
|
|
1123
1131
|
}
|
|
1124
|
-
|
|
1125
|
-
if (playSound && this.preferences.sound && this.G?.log[this.G?.log.length - 1].type == 'move') {
|
|
1126
|
-
const move = (this.G?.log[this.G?.log.length - 1] as LogMove).move;
|
|
1127
|
-
if (move.name == MoveName.Pass && this.G.currentPlayers.includes(this.player!)) {
|
|
1128
|
-
(document.getElementById('notification')!.cloneNode(true) as HTMLAudioElement).play();
|
|
1129
|
-
} else {
|
|
1130
|
-
if (move.name == MoveName.Build) {
|
|
1131
|
-
setTimeout(() => {
|
|
1132
|
-
(document.getElementById('piece-drop')!.cloneNode(true) as HTMLAudioElement).play();
|
|
1133
|
-
}, 800);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
1132
|
}
|
|
1138
1133
|
|
|
1139
1134
|
@Watch('ui.waitingAnimations')
|
|
@@ -1156,9 +1151,12 @@ export default class Game extends Vue {
|
|
|
1156
1151
|
if (this.G && this.player != null && !this.G.chosenPowerPlant) {
|
|
1157
1152
|
const player = this.G.players[this.player];
|
|
1158
1153
|
if (player && player.availableMoves && Object.keys(player.availableMoves).length > 1) {
|
|
1159
|
-
if (
|
|
1160
|
-
|
|
1161
|
-
|
|
1154
|
+
if (
|
|
1155
|
+
this.G.phase == Phase.Bureaucracy &&
|
|
1156
|
+
player.powerPlantsNotUsed.length > 0 &&
|
|
1157
|
+
Object.keys(player.availableMoves).includes('UsePowerPlant') &&
|
|
1158
|
+
!this.canPowerAllCitiesWithUsedPlants(player)
|
|
1159
|
+
) {
|
|
1162
1160
|
this.confirmMessage = 'Are you sure you want to pass? You have unused power plants!';
|
|
1163
1161
|
this.confirmVisible = true;
|
|
1164
1162
|
return;
|
|
@@ -1170,7 +1168,8 @@ export default class Game extends Vue {
|
|
|
1170
1168
|
// nothing this turn — buying some but not enough is a deliberate
|
|
1171
1169
|
// choice, not a slip.
|
|
1172
1170
|
if (this.G.map.name !== 'India' || !this.playerBoughtResourceThisTurn()) {
|
|
1173
|
-
this.confirmMessage =
|
|
1171
|
+
this.confirmMessage =
|
|
1172
|
+
'Are you sure you want to skip buying resources without enough to power all your plants?';
|
|
1174
1173
|
this.confirmVisible = true;
|
|
1175
1174
|
return;
|
|
1176
1175
|
}
|
|
@@ -1193,7 +1192,7 @@ export default class Game extends Vue {
|
|
|
1193
1192
|
this.confirmMessage = 'Are you sure you want to skip building?';
|
|
1194
1193
|
break;
|
|
1195
1194
|
case Phase.Bureaucracy:
|
|
1196
|
-
this.confirmMessage =
|
|
1195
|
+
this.confirmMessage = "Are you sure you want to pass? You didn't use any power plant!";
|
|
1197
1196
|
break;
|
|
1198
1197
|
default:
|
|
1199
1198
|
this.confirmMessage = 'Are you sure you want to pass?';
|
|
@@ -1292,8 +1291,10 @@ export default class Game extends Vue {
|
|
|
1292
1291
|
this.soleBuyerPlant = null;
|
|
1293
1292
|
}
|
|
1294
1293
|
|
|
1295
|
-
buyResource(payload: { resource: ResourceType
|
|
1296
|
-
const data: { resource: ResourceType
|
|
1294
|
+
buyResource(payload: { resource: ResourceType; side?: 'north' | 'south'; fromStorage?: boolean }) {
|
|
1295
|
+
const data: { resource: ResourceType; side?: 'north' | 'south'; fromStorage?: boolean } = {
|
|
1296
|
+
resource: payload.resource,
|
|
1297
|
+
};
|
|
1297
1298
|
if (payload.side) {
|
|
1298
1299
|
data.side = payload.side;
|
|
1299
1300
|
}
|
|
@@ -1312,7 +1313,7 @@ export default class Game extends Vue {
|
|
|
1312
1313
|
* replaying the rest, exactly as `undo()` drops the last move. Removing a buy only
|
|
1313
1314
|
* ever frees money and plant capacity, so the remaining buffer always replays.
|
|
1314
1315
|
*/
|
|
1315
|
-
unbuyResource(payload: { resource: ResourceType
|
|
1316
|
+
unbuyResource(payload: { resource: ResourceType; side?: 'north' | 'south'; fromStorage?: boolean }) {
|
|
1316
1317
|
if (this.paused || !this.committedState) {
|
|
1317
1318
|
return;
|
|
1318
1319
|
}
|
|
@@ -1381,7 +1382,10 @@ export default class Game extends Vue {
|
|
|
1381
1382
|
this.freeJumpCity = null;
|
|
1382
1383
|
if (useJump) {
|
|
1383
1384
|
const freeJumpMove = buildMoves.find((c) => c.name === city.name && c.freeJump)!;
|
|
1384
|
-
this.sendMove({
|
|
1385
|
+
this.sendMove({
|
|
1386
|
+
name: MoveName.Build,
|
|
1387
|
+
data: { name: city.name, price: freeJumpMove.price, freeJump: true },
|
|
1388
|
+
});
|
|
1385
1389
|
} else {
|
|
1386
1390
|
const normalMove = buildMoves.find((c) => c.name === city.name && !c.freeJump)!;
|
|
1387
1391
|
this.sendMove({ name: MoveName.Build, data: { name: city.name, price: normalMove.price } });
|
|
@@ -1389,7 +1393,7 @@ export default class Game extends Vue {
|
|
|
1389
1393
|
}
|
|
1390
1394
|
|
|
1391
1395
|
confirmDiscard() {
|
|
1392
|
-
const values = this.resourcesToDiscard.map(r => parseInt(r.value));
|
|
1396
|
+
const values = this.resourcesToDiscard.map((r) => parseInt(r.value));
|
|
1393
1397
|
if (values.reduce((acc, cur) => acc + cur, 0) > 0) {
|
|
1394
1398
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: this.discardedPowerPlant!.number, extra: values });
|
|
1395
1399
|
} else {
|
|
@@ -1405,18 +1409,48 @@ export default class Game extends Vue {
|
|
|
1405
1409
|
let hybridCapacityUsed;
|
|
1406
1410
|
switch (this.discardedPowerPlant!.type) {
|
|
1407
1411
|
case PowerPlantType.Coal:
|
|
1408
|
-
hybridCapacityUsed =
|
|
1409
|
-
|
|
1412
|
+
hybridCapacityUsed =
|
|
1413
|
+
currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 > 0
|
|
1414
|
+
? Math.max(0, currentPlayer.oilLeft - currentPlayer.oilCapacity)
|
|
1415
|
+
: 0;
|
|
1416
|
+
return (
|
|
1417
|
+
currentPlayer.coalCapacity +
|
|
1418
|
+
currentPlayer.hybridCapacity -
|
|
1419
|
+
this.discardedPowerPlant!.cost * 2 +
|
|
1420
|
+
parseInt(this.resourcesToDiscard[0].value) <
|
|
1421
|
+
currentPlayer.coalLeft + hybridCapacityUsed
|
|
1422
|
+
);
|
|
1410
1423
|
|
|
1411
1424
|
case PowerPlantType.Oil:
|
|
1412
|
-
hybridCapacityUsed =
|
|
1413
|
-
|
|
1425
|
+
hybridCapacityUsed =
|
|
1426
|
+
currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 > 0
|
|
1427
|
+
? Math.max(0, currentPlayer.coalLeft - currentPlayer.coalCapacity)
|
|
1428
|
+
: 0;
|
|
1429
|
+
return (
|
|
1430
|
+
currentPlayer.oilCapacity +
|
|
1431
|
+
currentPlayer.hybridCapacity -
|
|
1432
|
+
this.discardedPowerPlant!.cost * 2 +
|
|
1433
|
+
parseInt(this.resourcesToDiscard[0].value) <
|
|
1434
|
+
currentPlayer.oilLeft + hybridCapacityUsed
|
|
1435
|
+
);
|
|
1414
1436
|
|
|
1415
1437
|
case PowerPlantType.Garbage:
|
|
1416
|
-
return
|
|
1438
|
+
return (
|
|
1439
|
+
currentPlayer.garbageCapacity -
|
|
1440
|
+
this.discardedPowerPlant!.cost * 2 -
|
|
1441
|
+
currentPlayer.garbageLeft +
|
|
1442
|
+
parseInt(this.resourcesToDiscard[0].value) <
|
|
1443
|
+
0
|
|
1444
|
+
);
|
|
1417
1445
|
|
|
1418
1446
|
case PowerPlantType.Uranium:
|
|
1419
|
-
return
|
|
1447
|
+
return (
|
|
1448
|
+
currentPlayer.uraniumCapacity -
|
|
1449
|
+
this.discardedPowerPlant!.cost * 2 -
|
|
1450
|
+
currentPlayer.uraniumLeft +
|
|
1451
|
+
parseInt(this.resourcesToDiscard[0].value) <
|
|
1452
|
+
0
|
|
1453
|
+
);
|
|
1420
1454
|
|
|
1421
1455
|
case PowerPlantType.Hybrid:
|
|
1422
1456
|
const coalDiscarded = parseInt(this.resourcesToDiscard[0].value);
|
|
@@ -1440,7 +1474,11 @@ export default class Game extends Vue {
|
|
|
1440
1474
|
|
|
1441
1475
|
switch (powerPlant.type) {
|
|
1442
1476
|
case PowerPlantType.Coal:
|
|
1443
|
-
if (
|
|
1477
|
+
if (
|
|
1478
|
+
currentPlayer.powerPlants.filter((pp) => pp.type == powerPlant.type).length +
|
|
1479
|
+
currentPlayer.powerPlants.filter((pp) => pp.type == PowerPlantType.Hybrid).length ==
|
|
1480
|
+
1
|
|
1481
|
+
) {
|
|
1444
1482
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1445
1483
|
return;
|
|
1446
1484
|
}
|
|
@@ -1454,7 +1492,11 @@ export default class Game extends Vue {
|
|
|
1454
1492
|
break;
|
|
1455
1493
|
|
|
1456
1494
|
case PowerPlantType.Oil:
|
|
1457
|
-
if (
|
|
1495
|
+
if (
|
|
1496
|
+
currentPlayer.powerPlants.filter((pp) => pp.type == powerPlant.type).length +
|
|
1497
|
+
currentPlayer.powerPlants.filter((pp) => pp.type == PowerPlantType.Hybrid).length ==
|
|
1498
|
+
1
|
|
1499
|
+
) {
|
|
1458
1500
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1459
1501
|
return;
|
|
1460
1502
|
}
|
|
@@ -1468,7 +1510,7 @@ export default class Game extends Vue {
|
|
|
1468
1510
|
break;
|
|
1469
1511
|
|
|
1470
1512
|
case PowerPlantType.Garbage:
|
|
1471
|
-
if (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length == 1) {
|
|
1513
|
+
if (currentPlayer.powerPlants.filter((pp) => pp.type == powerPlant.type).length == 1) {
|
|
1472
1514
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1473
1515
|
return;
|
|
1474
1516
|
}
|
|
@@ -1483,7 +1525,7 @@ export default class Game extends Vue {
|
|
|
1483
1525
|
break;
|
|
1484
1526
|
|
|
1485
1527
|
case PowerPlantType.Uranium:
|
|
1486
|
-
if (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length == 1) {
|
|
1528
|
+
if (currentPlayer.powerPlants.filter((pp) => pp.type == powerPlant.type).length == 1) {
|
|
1487
1529
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1488
1530
|
return;
|
|
1489
1531
|
}
|
|
@@ -1497,7 +1539,12 @@ export default class Game extends Vue {
|
|
|
1497
1539
|
break;
|
|
1498
1540
|
|
|
1499
1541
|
case PowerPlantType.Hybrid:
|
|
1500
|
-
if (
|
|
1542
|
+
if (
|
|
1543
|
+
currentPlayer.powerPlants.filter((pp) => pp.type == powerPlant.type).length +
|
|
1544
|
+
currentPlayer.powerPlants.filter((pp) => pp.type == PowerPlantType.Coal).length +
|
|
1545
|
+
currentPlayer.powerPlants.filter((pp) => pp.type == PowerPlantType.Oil).length ==
|
|
1546
|
+
1
|
|
1547
|
+
) {
|
|
1501
1548
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1502
1549
|
return;
|
|
1503
1550
|
}
|
|
@@ -1507,7 +1554,10 @@ export default class Game extends Vue {
|
|
|
1507
1554
|
return;
|
|
1508
1555
|
}
|
|
1509
1556
|
|
|
1510
|
-
this.resourcesToDiscard = [
|
|
1557
|
+
this.resourcesToDiscard = [
|
|
1558
|
+
{ name: 'Coal', value: '0', max: currentPlayer.coalLeft },
|
|
1559
|
+
{ name: 'Oil', value: '0', max: currentPlayer.oilLeft },
|
|
1560
|
+
];
|
|
1511
1561
|
break;
|
|
1512
1562
|
}
|
|
1513
1563
|
|
|
@@ -1534,7 +1584,9 @@ export default class Game extends Vue {
|
|
|
1534
1584
|
resourcesSpent = currentPlayer.resourcesUsed;
|
|
1535
1585
|
resourcesSpent.sort();
|
|
1536
1586
|
currentPlayer.resourcesUsed = [];
|
|
1537
|
-
currentPlayer.powerPlantsNotUsed = currentPlayer.powerPlantsNotUsed.filter(
|
|
1587
|
+
currentPlayer.powerPlantsNotUsed = currentPlayer.powerPlantsNotUsed.filter(
|
|
1588
|
+
(x) => x != powerPlant.number
|
|
1589
|
+
);
|
|
1538
1590
|
|
|
1539
1591
|
break;
|
|
1540
1592
|
}
|
|
@@ -1545,7 +1597,7 @@ export default class Game extends Vue {
|
|
|
1545
1597
|
});
|
|
1546
1598
|
|
|
1547
1599
|
this.disablePass = true;
|
|
1548
|
-
setTimeout(() => this.disablePass = false, 1000);
|
|
1600
|
+
setTimeout(() => (this.disablePass = false), 1000);
|
|
1549
1601
|
}
|
|
1550
1602
|
}
|
|
1551
1603
|
|
|
@@ -1704,7 +1756,7 @@ export default class Game extends Vue {
|
|
|
1704
1756
|
return !!availableMoves[MoveName.ChoosePowerPlant];
|
|
1705
1757
|
}
|
|
1706
1758
|
|
|
1707
|
-
buyableResources(): { resource: ResourceType
|
|
1759
|
+
buyableResources(): { resource: ResourceType; side?: 'north' | 'south'; fromStorage?: boolean }[] {
|
|
1708
1760
|
if (!this.canMove()) return [];
|
|
1709
1761
|
|
|
1710
1762
|
const currentPlayer = this.G!.players[this.player!];
|
|
@@ -1744,7 +1796,40 @@ export default class Game extends Vue {
|
|
|
1744
1796
|
const currentPlayer = this.G!.players[this.player!];
|
|
1745
1797
|
const availableMoves = currentPlayer.availableMoves!;
|
|
1746
1798
|
|
|
1747
|
-
return availableMoves[MoveName.Build] && availableMoves[MoveName.Build]!.map((c) => c.name) || [];
|
|
1799
|
+
return (availableMoves[MoveName.Build] && availableMoves[MoveName.Build]!.map((c) => c.name)) || [];
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* What each buildable city would cost this player, keyed by city name (#148).
|
|
1804
|
+
*
|
|
1805
|
+
* The total is not computed here: `available-moves` already sums the shortest-path
|
|
1806
|
+
* connection cost and the slot cost for the number of players ALREADY in the city
|
|
1807
|
+
* (10/15/20 in the standard build, per-map `slotCosts` otherwise), then drops every
|
|
1808
|
+
* city the player cannot afford. That per-city occupancy is exactly what #148 asks
|
|
1809
|
+
* about in Step 3, so the number only needed surfacing — the viewer was discarding
|
|
1810
|
+
* it and keeping the names.
|
|
1811
|
+
*
|
|
1812
|
+
* A map with a free jump (Japan) offers the same city twice at two prices, so both
|
|
1813
|
+
* are kept; a city reachable ONLY by jump has no `price`.
|
|
1814
|
+
*/
|
|
1815
|
+
getBuildableCityPrices(): Record<string, { price?: number; jumpPrice?: number }> {
|
|
1816
|
+
if (!this.canMove()) return {};
|
|
1817
|
+
|
|
1818
|
+
const moves = this.G!.players[this.player!].availableMoves![MoveName.Build];
|
|
1819
|
+
if (!moves) return {};
|
|
1820
|
+
|
|
1821
|
+
const prices: Record<string, { price?: number; jumpPrice?: number }> = {};
|
|
1822
|
+
for (const move of moves) {
|
|
1823
|
+
if (!prices[move.name]) {
|
|
1824
|
+
prices[move.name] = {};
|
|
1825
|
+
}
|
|
1826
|
+
if (move.freeJump) {
|
|
1827
|
+
prices[move.name].jumpPrice = move.price;
|
|
1828
|
+
} else {
|
|
1829
|
+
prices[move.name].price = move.price;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
return prices;
|
|
1748
1833
|
}
|
|
1749
1834
|
|
|
1750
1835
|
getPickableRegions(): string[] {
|
|
@@ -1861,10 +1946,12 @@ export default class Game extends Vue {
|
|
|
1861
1946
|
}
|
|
1862
1947
|
|
|
1863
1948
|
// Check if player has enough resources, accounting for hybrid plants which can use either coal or oil
|
|
1864
|
-
if (
|
|
1949
|
+
if (
|
|
1950
|
+
coalUsed > player.coalLeft ||
|
|
1865
1951
|
oilUsed > player.oilLeft ||
|
|
1866
1952
|
garbageUsed > player.garbageLeft ||
|
|
1867
|
-
uraniumUsed > player.uraniumLeft
|
|
1953
|
+
uraniumUsed > player.uraniumLeft
|
|
1954
|
+
) {
|
|
1868
1955
|
return false;
|
|
1869
1956
|
}
|
|
1870
1957
|
const remainingCoal = player.coalLeft - coalUsed;
|
|
@@ -1946,7 +2033,7 @@ export default class Game extends Vue {
|
|
|
1946
2033
|
|
|
1947
2034
|
return 'Choose a Power Plant to start an auction.';
|
|
1948
2035
|
} else if (currentPlayer.availableMoves![MoveName.Bid]) {
|
|
1949
|
-
return
|
|
2036
|
+
return "It's your turn to bid!";
|
|
1950
2037
|
} else if (currentPlayer.availableMoves![MoveName.BuyResource]) {
|
|
1951
2038
|
return 'Buy resources on the market, or pass.';
|
|
1952
2039
|
} else if (currentPlayer.availableMoves![MoveName.Build]) {
|
|
@@ -1963,7 +2050,7 @@ export default class Game extends Vue {
|
|
|
1963
2050
|
return 'Choose which resources to discard.';
|
|
1964
2051
|
}
|
|
1965
2052
|
|
|
1966
|
-
return
|
|
2053
|
+
return "It's your turn!";
|
|
1967
2054
|
} else {
|
|
1968
2055
|
let log = this.G.log[this.G.log.length - 1];
|
|
1969
2056
|
if (log.type == 'move') {
|
|
@@ -2032,7 +2119,9 @@ export default class Game extends Vue {
|
|
|
2032
2119
|
// the connection cost paid to reach it, averaged over cities built.
|
|
2033
2120
|
label: 'Cost per City',
|
|
2034
2121
|
value: (p) =>
|
|
2035
|
-
p.cities.length
|
|
2122
|
+
p.cities.length
|
|
2123
|
+
? ((p.totalSpentCities + p.totalSpentConnections) / p.cities.length).toFixed(1)
|
|
2124
|
+
: '-',
|
|
2036
2125
|
},
|
|
2037
2126
|
{ label: 'Spending: Plants', value: (p) => p.totalSpentPlants },
|
|
2038
2127
|
{ label: 'Spending: Resources', value: (p) => p.totalSpentResources },
|
|
@@ -2284,8 +2373,10 @@ export default class Game extends Vue {
|
|
|
2284
2373
|
const scale = scaleOf(name);
|
|
2285
2374
|
// Centre a shorter slot against the tallest one in its row.
|
|
2286
2375
|
const top = y + (rowHeight - bb.height * scale) / 2;
|
|
2287
|
-
transforms[name] =
|
|
2288
|
-
|
|
2376
|
+
transforms[name] = `translate(${round(x - bb.x * scale)}, ${round(top - bb.y * scale)}) scale(${round(
|
|
2377
|
+
scale,
|
|
2378
|
+
4
|
|
2379
|
+
)})`;
|
|
2289
2380
|
x += bb.width * scale + STACK_GAP;
|
|
2290
2381
|
}
|
|
2291
2382
|
y += rowHeight + STACK_GAP;
|
|
@@ -2408,8 +2499,8 @@ ul {
|
|
|
2408
2499
|
flex-direction: column;
|
|
2409
2500
|
}
|
|
2410
2501
|
|
|
2411
|
-
.fitToScreen {
|
|
2412
|
-
height:
|
|
2502
|
+
.fitToScreen #scene {
|
|
2503
|
+
max-height: calc(100vh - 40px);
|
|
2413
2504
|
}
|
|
2414
2505
|
|
|
2415
2506
|
.statusBar {
|
|
@@ -2420,13 +2511,17 @@ ul {
|
|
|
2420
2511
|
text-align: center;
|
|
2421
2512
|
line-height: 40px;
|
|
2422
2513
|
font-size: 20px;
|
|
2423
|
-
position:
|
|
2514
|
+
position: sticky;
|
|
2515
|
+
top: 0;
|
|
2516
|
+
z-index: 10;
|
|
2517
|
+
flex-shrink: 0;
|
|
2424
2518
|
}
|
|
2425
2519
|
|
|
2426
2520
|
#scene {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2521
|
+
display: block;
|
|
2522
|
+
height: auto;
|
|
2523
|
+
flex: none;
|
|
2524
|
+
margin: 0 auto;
|
|
2430
2525
|
}
|
|
2431
2526
|
|
|
2432
2527
|
// Portrait: the scene is taller than the viewport by design (each row is scaled
|
|
@@ -2439,12 +2534,14 @@ ul {
|
|
|
2439
2534
|
#scene {
|
|
2440
2535
|
max-height: none;
|
|
2441
2536
|
flex-grow: 0;
|
|
2442
|
-
margin-top:
|
|
2537
|
+
margin-top: 0;
|
|
2443
2538
|
}
|
|
2444
2539
|
}
|
|
2445
2540
|
|
|
2446
2541
|
body,
|
|
2447
2542
|
html {
|
|
2543
|
+
background: #e7ebda;
|
|
2544
|
+
color: #263521;
|
|
2448
2545
|
height: 100%;
|
|
2449
2546
|
width: 100%;
|
|
2450
2547
|
margin: 0;
|
|
@@ -2717,3 +2814,24 @@ text {
|
|
|
2717
2814
|
margin: 15px 0 0 15px;
|
|
2718
2815
|
}
|
|
2719
2816
|
</style>
|
|
2817
|
+
|
|
2818
|
+
<style>
|
|
2819
|
+
.journal-and-chat {
|
|
2820
|
+
width: 100%;
|
|
2821
|
+
display: grid;
|
|
2822
|
+
gap: 8px;
|
|
2823
|
+
margin-top: 8px;
|
|
2824
|
+
align-items: start;
|
|
2825
|
+
}
|
|
2826
|
+
.journal-and-chat > * {
|
|
2827
|
+
min-width: 0;
|
|
2828
|
+
}
|
|
2829
|
+
.journal-and-chat .bgs-game-chat {
|
|
2830
|
+
margin: 0;
|
|
2831
|
+
}
|
|
2832
|
+
@media (min-width: 1000px) {
|
|
2833
|
+
.journal-and-chat {
|
|
2834
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
</style>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<details class="inline-game-log" open>
|
|
3
|
+
<summary>Journal</summary>
|
|
4
|
+
<div ref="feed" class="journal-feed" @scroll="onScroll">
|
|
5
|
+
<div v-for="(entry, index) in illustratedEntries" :key="index" class="journal-entry">
|
|
6
|
+
<template v-for="(part, i) in entry">
|
|
7
|
+
<svg
|
|
8
|
+
v-if="part.plant"
|
|
9
|
+
:key="i"
|
|
10
|
+
class="journal-plant"
|
|
11
|
+
viewBox="-2 -2 64 44"
|
|
12
|
+
role="img"
|
|
13
|
+
:aria-label="plantLabel(part.plant)"
|
|
14
|
+
>
|
|
15
|
+
<title>{{ plantLabel(part.plant) }}</title>
|
|
16
|
+
<Card :powerPlant="part.plant" />
|
|
17
|
+
</svg>
|
|
18
|
+
<span v-else :key="i" v-html="part.html" />
|
|
19
|
+
</template>
|
|
20
|
+
</div>
|
|
21
|
+
<div v-if="!entries.length">No actions yet.</div>
|
|
22
|
+
</div>
|
|
23
|
+
</details>
|
|
24
|
+
</template>
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
|
27
|
+
import Card from './pieces/Card.vue';
|
|
28
|
+
import { getPowerPlant } from 'powergrid-engine/src/engine';
|
|
29
|
+
import type { PowerPlant } from 'powergrid-engine/src/gamestate';
|
|
30
|
+
@Component({ components: { Card } })
|
|
31
|
+
export default class InlineLog extends Vue {
|
|
32
|
+
@Prop({ default: () => [] }) entries!: string[];
|
|
33
|
+
@Prop({ default: '' }) mapName!: string;
|
|
34
|
+
follow = true;
|
|
35
|
+
get illustratedEntries() {
|
|
36
|
+
return this.entries.map((entry) => {
|
|
37
|
+
const parts: { html?: string; plant?: PowerPlant }[] = [];
|
|
38
|
+
const pattern = /Power Plant\s+(?:<b>)?(\d+)(?:<\/b>)?/gi;
|
|
39
|
+
let offset = 0;
|
|
40
|
+
let match: RegExpExecArray | null;
|
|
41
|
+
while ((match = pattern.exec(entry))) {
|
|
42
|
+
const plant = getPowerPlant(Number(match[1]), this.mapName);
|
|
43
|
+
if (!plant) continue;
|
|
44
|
+
parts.push({ html: entry.slice(offset, match.index) }, { plant });
|
|
45
|
+
offset = match.index + match[0].length;
|
|
46
|
+
}
|
|
47
|
+
parts.push({ html: entry.slice(offset) });
|
|
48
|
+
return parts;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
plantLabel(plant: PowerPlant) {
|
|
52
|
+
return `Power plant ${plant.number}: ${plant.cost ? `${plant.cost} ${plant.type}` : 'no fuel'} → ${
|
|
53
|
+
plant.citiesPowered
|
|
54
|
+
} cities`;
|
|
55
|
+
}
|
|
56
|
+
mounted() {
|
|
57
|
+
this.scrollToLatest();
|
|
58
|
+
}
|
|
59
|
+
@Watch('entries') changed() {
|
|
60
|
+
if (this.follow) {
|
|
61
|
+
this.$nextTick(this.scrollToLatest);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
scrollToLatest() {
|
|
65
|
+
const feed = this.$refs.feed as HTMLElement;
|
|
66
|
+
if (feed) {
|
|
67
|
+
feed.scrollTop = feed.scrollHeight;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
onScroll() {
|
|
71
|
+
const feed = this.$refs.feed as HTMLElement;
|
|
72
|
+
this.follow = feed.scrollHeight - feed.scrollTop - feed.clientHeight < 32;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
<style scoped>
|
|
77
|
+
.inline-game-log {
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
width: 100%;
|
|
80
|
+
padding: 10px 14px;
|
|
81
|
+
border: 1px solid #a2aa82;
|
|
82
|
+
border-radius: 3px;
|
|
83
|
+
background: #f3f0dc;
|
|
84
|
+
color: #263521;
|
|
85
|
+
font: 14px Arial, sans-serif;
|
|
86
|
+
}
|
|
87
|
+
summary {
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
font-weight: 600;
|
|
90
|
+
}
|
|
91
|
+
.journal-feed {
|
|
92
|
+
max-height: 220px;
|
|
93
|
+
overflow: auto;
|
|
94
|
+
overscroll-behavior: auto;
|
|
95
|
+
margin-top: 8px;
|
|
96
|
+
}
|
|
97
|
+
.journal-plant {
|
|
98
|
+
width: 76px;
|
|
99
|
+
height: 52px;
|
|
100
|
+
vertical-align: middle;
|
|
101
|
+
margin: 1px 4px;
|
|
102
|
+
overflow: visible;
|
|
103
|
+
}
|
|
104
|
+
.journal-entry {
|
|
105
|
+
padding: 5px 0;
|
|
106
|
+
border-bottom: 1px solid #75818d33;
|
|
107
|
+
}
|
|
108
|
+
</style>
|