powergrid-viewer 2.0.14 → 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 +1 -1
- package/src/components/Game.vue +153 -69
- package/src/components/InlineLog.vue +108 -0
- 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
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')">
|
|
@@ -414,6 +414,11 @@
|
|
|
414
414
|
</g>
|
|
415
415
|
</svg>
|
|
416
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
|
+
|
|
417
422
|
<div v-if="G" :class="['modal', { visible: logVisible }]">
|
|
418
423
|
<div class="modal-content">
|
|
419
424
|
<span class="close" @click="logVisible = false">×</span>
|
|
@@ -735,6 +740,7 @@
|
|
|
735
740
|
</div>
|
|
736
741
|
</template>
|
|
737
742
|
<script lang="ts">
|
|
743
|
+
import InlineLog from './InlineLog.vue';
|
|
738
744
|
import { Vue, Component, Prop, Watch, Provide, ProvideReactive, Ref } from 'vue-property-decorator';
|
|
739
745
|
import { MoveName, ended, playersSortedByScore, reconstructState } from 'powergrid-engine';
|
|
740
746
|
import type { GameState, LogItem, Move, Player } from 'powergrid-engine';
|
|
@@ -867,21 +873,21 @@ const round = (n: number, digits = 2) => Number(n.toFixed(digits));
|
|
|
867
873
|
this.paused = true;
|
|
868
874
|
this.emitter.emit('replay:info', {
|
|
869
875
|
start: 1,
|
|
870
|
-
current: this.G!.log.filter(l => l.type == 'move').length,
|
|
871
|
-
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,
|
|
872
878
|
});
|
|
873
879
|
});
|
|
874
880
|
|
|
875
881
|
this.emitter.on('replayTo', (to: number) => {
|
|
876
|
-
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');
|
|
877
883
|
to = log[to - 1].index;
|
|
878
884
|
|
|
879
885
|
this.replaceState(reconstructState(this._futureState!, to + 1), false);
|
|
880
886
|
|
|
881
887
|
this.emitter.emit('replay:info', {
|
|
882
888
|
start: 1,
|
|
883
|
-
current: this.G!.log.filter(l => l.type == 'move').length + this.G!.hiddenLog.length,
|
|
884
|
-
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,
|
|
885
891
|
});
|
|
886
892
|
});
|
|
887
893
|
|
|
@@ -891,6 +897,7 @@ const round = (n: number, digits = 2) => Number(n.toFixed(digits));
|
|
|
891
897
|
});
|
|
892
898
|
},
|
|
893
899
|
components: {
|
|
900
|
+
InlineLog,
|
|
894
901
|
PlayerBoard,
|
|
895
902
|
Card,
|
|
896
903
|
House,
|
|
@@ -913,7 +920,7 @@ const round = (n: number, digits = 2) => Number(n.toFixed(digits));
|
|
|
913
920
|
CityCount,
|
|
914
921
|
Map,
|
|
915
922
|
Resources,
|
|
916
|
-
ResourceBoxes
|
|
923
|
+
ResourceBoxes,
|
|
917
924
|
},
|
|
918
925
|
})
|
|
919
926
|
export default class Game extends Vue {
|
|
@@ -981,7 +988,7 @@ export default class Game extends Vue {
|
|
|
981
988
|
|
|
982
989
|
discardedPowerPlant: PowerPlant | null = null;
|
|
983
990
|
discardVisible: boolean = false;
|
|
984
|
-
resourcesToDiscard: { name: string
|
|
991
|
+
resourcesToDiscard: { name: string; max: number; value: string }[] = [];
|
|
985
992
|
|
|
986
993
|
freeJumpCity: City | null = null;
|
|
987
994
|
freeJumpNormalPrice: number | null = null;
|
|
@@ -1096,7 +1103,7 @@ export default class Game extends Vue {
|
|
|
1096
1103
|
player = {
|
|
1097
1104
|
coalLeft: this.G.players[this.player].coalLeft,
|
|
1098
1105
|
oilLeft: this.G.players[this.player].oilLeft,
|
|
1099
|
-
resourcesUsed: this.G.players[this.player].resourcesUsed
|
|
1106
|
+
resourcesUsed: this.G.players[this.player].resourcesUsed,
|
|
1100
1107
|
};
|
|
1101
1108
|
}
|
|
1102
1109
|
}
|
|
@@ -1122,19 +1129,6 @@ export default class Game extends Vue {
|
|
|
1122
1129
|
this.scheduleRelayout();
|
|
1123
1130
|
});
|
|
1124
1131
|
}
|
|
1125
|
-
|
|
1126
|
-
if (playSound && this.preferences.sound && this.G?.log[this.G?.log.length - 1].type == 'move') {
|
|
1127
|
-
const move = (this.G?.log[this.G?.log.length - 1] as LogMove).move;
|
|
1128
|
-
if (move.name == MoveName.Pass && this.G.currentPlayers.includes(this.player!)) {
|
|
1129
|
-
(document.getElementById('notification')!.cloneNode(true) as HTMLAudioElement).play();
|
|
1130
|
-
} else {
|
|
1131
|
-
if (move.name == MoveName.Build) {
|
|
1132
|
-
setTimeout(() => {
|
|
1133
|
-
(document.getElementById('piece-drop')!.cloneNode(true) as HTMLAudioElement).play();
|
|
1134
|
-
}, 800);
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
1132
|
}
|
|
1139
1133
|
|
|
1140
1134
|
@Watch('ui.waitingAnimations')
|
|
@@ -1157,9 +1151,12 @@ export default class Game extends Vue {
|
|
|
1157
1151
|
if (this.G && this.player != null && !this.G.chosenPowerPlant) {
|
|
1158
1152
|
const player = this.G.players[this.player];
|
|
1159
1153
|
if (player && player.availableMoves && Object.keys(player.availableMoves).length > 1) {
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
+
) {
|
|
1163
1160
|
this.confirmMessage = 'Are you sure you want to pass? You have unused power plants!';
|
|
1164
1161
|
this.confirmVisible = true;
|
|
1165
1162
|
return;
|
|
@@ -1171,7 +1168,8 @@ export default class Game extends Vue {
|
|
|
1171
1168
|
// nothing this turn — buying some but not enough is a deliberate
|
|
1172
1169
|
// choice, not a slip.
|
|
1173
1170
|
if (this.G.map.name !== 'India' || !this.playerBoughtResourceThisTurn()) {
|
|
1174
|
-
this.confirmMessage =
|
|
1171
|
+
this.confirmMessage =
|
|
1172
|
+
'Are you sure you want to skip buying resources without enough to power all your plants?';
|
|
1175
1173
|
this.confirmVisible = true;
|
|
1176
1174
|
return;
|
|
1177
1175
|
}
|
|
@@ -1194,7 +1192,7 @@ export default class Game extends Vue {
|
|
|
1194
1192
|
this.confirmMessage = 'Are you sure you want to skip building?';
|
|
1195
1193
|
break;
|
|
1196
1194
|
case Phase.Bureaucracy:
|
|
1197
|
-
this.confirmMessage =
|
|
1195
|
+
this.confirmMessage = "Are you sure you want to pass? You didn't use any power plant!";
|
|
1198
1196
|
break;
|
|
1199
1197
|
default:
|
|
1200
1198
|
this.confirmMessage = 'Are you sure you want to pass?';
|
|
@@ -1293,8 +1291,10 @@ export default class Game extends Vue {
|
|
|
1293
1291
|
this.soleBuyerPlant = null;
|
|
1294
1292
|
}
|
|
1295
1293
|
|
|
1296
|
-
buyResource(payload: { resource: ResourceType
|
|
1297
|
-
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
|
+
};
|
|
1298
1298
|
if (payload.side) {
|
|
1299
1299
|
data.side = payload.side;
|
|
1300
1300
|
}
|
|
@@ -1313,7 +1313,7 @@ export default class Game extends Vue {
|
|
|
1313
1313
|
* replaying the rest, exactly as `undo()` drops the last move. Removing a buy only
|
|
1314
1314
|
* ever frees money and plant capacity, so the remaining buffer always replays.
|
|
1315
1315
|
*/
|
|
1316
|
-
unbuyResource(payload: { resource: ResourceType
|
|
1316
|
+
unbuyResource(payload: { resource: ResourceType; side?: 'north' | 'south'; fromStorage?: boolean }) {
|
|
1317
1317
|
if (this.paused || !this.committedState) {
|
|
1318
1318
|
return;
|
|
1319
1319
|
}
|
|
@@ -1382,7 +1382,10 @@ export default class Game extends Vue {
|
|
|
1382
1382
|
this.freeJumpCity = null;
|
|
1383
1383
|
if (useJump) {
|
|
1384
1384
|
const freeJumpMove = buildMoves.find((c) => c.name === city.name && c.freeJump)!;
|
|
1385
|
-
this.sendMove({
|
|
1385
|
+
this.sendMove({
|
|
1386
|
+
name: MoveName.Build,
|
|
1387
|
+
data: { name: city.name, price: freeJumpMove.price, freeJump: true },
|
|
1388
|
+
});
|
|
1386
1389
|
} else {
|
|
1387
1390
|
const normalMove = buildMoves.find((c) => c.name === city.name && !c.freeJump)!;
|
|
1388
1391
|
this.sendMove({ name: MoveName.Build, data: { name: city.name, price: normalMove.price } });
|
|
@@ -1390,7 +1393,7 @@ export default class Game extends Vue {
|
|
|
1390
1393
|
}
|
|
1391
1394
|
|
|
1392
1395
|
confirmDiscard() {
|
|
1393
|
-
const values = this.resourcesToDiscard.map(r => parseInt(r.value));
|
|
1396
|
+
const values = this.resourcesToDiscard.map((r) => parseInt(r.value));
|
|
1394
1397
|
if (values.reduce((acc, cur) => acc + cur, 0) > 0) {
|
|
1395
1398
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: this.discardedPowerPlant!.number, extra: values });
|
|
1396
1399
|
} else {
|
|
@@ -1406,18 +1409,48 @@ export default class Game extends Vue {
|
|
|
1406
1409
|
let hybridCapacityUsed;
|
|
1407
1410
|
switch (this.discardedPowerPlant!.type) {
|
|
1408
1411
|
case PowerPlantType.Coal:
|
|
1409
|
-
hybridCapacityUsed =
|
|
1410
|
-
|
|
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
|
+
);
|
|
1411
1423
|
|
|
1412
1424
|
case PowerPlantType.Oil:
|
|
1413
|
-
hybridCapacityUsed =
|
|
1414
|
-
|
|
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
|
+
);
|
|
1415
1436
|
|
|
1416
1437
|
case PowerPlantType.Garbage:
|
|
1417
|
-
return
|
|
1438
|
+
return (
|
|
1439
|
+
currentPlayer.garbageCapacity -
|
|
1440
|
+
this.discardedPowerPlant!.cost * 2 -
|
|
1441
|
+
currentPlayer.garbageLeft +
|
|
1442
|
+
parseInt(this.resourcesToDiscard[0].value) <
|
|
1443
|
+
0
|
|
1444
|
+
);
|
|
1418
1445
|
|
|
1419
1446
|
case PowerPlantType.Uranium:
|
|
1420
|
-
return
|
|
1447
|
+
return (
|
|
1448
|
+
currentPlayer.uraniumCapacity -
|
|
1449
|
+
this.discardedPowerPlant!.cost * 2 -
|
|
1450
|
+
currentPlayer.uraniumLeft +
|
|
1451
|
+
parseInt(this.resourcesToDiscard[0].value) <
|
|
1452
|
+
0
|
|
1453
|
+
);
|
|
1421
1454
|
|
|
1422
1455
|
case PowerPlantType.Hybrid:
|
|
1423
1456
|
const coalDiscarded = parseInt(this.resourcesToDiscard[0].value);
|
|
@@ -1441,7 +1474,11 @@ export default class Game extends Vue {
|
|
|
1441
1474
|
|
|
1442
1475
|
switch (powerPlant.type) {
|
|
1443
1476
|
case PowerPlantType.Coal:
|
|
1444
|
-
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
|
+
) {
|
|
1445
1482
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1446
1483
|
return;
|
|
1447
1484
|
}
|
|
@@ -1455,7 +1492,11 @@ export default class Game extends Vue {
|
|
|
1455
1492
|
break;
|
|
1456
1493
|
|
|
1457
1494
|
case PowerPlantType.Oil:
|
|
1458
|
-
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
|
+
) {
|
|
1459
1500
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1460
1501
|
return;
|
|
1461
1502
|
}
|
|
@@ -1469,7 +1510,7 @@ export default class Game extends Vue {
|
|
|
1469
1510
|
break;
|
|
1470
1511
|
|
|
1471
1512
|
case PowerPlantType.Garbage:
|
|
1472
|
-
if (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length == 1) {
|
|
1513
|
+
if (currentPlayer.powerPlants.filter((pp) => pp.type == powerPlant.type).length == 1) {
|
|
1473
1514
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1474
1515
|
return;
|
|
1475
1516
|
}
|
|
@@ -1484,7 +1525,7 @@ export default class Game extends Vue {
|
|
|
1484
1525
|
break;
|
|
1485
1526
|
|
|
1486
1527
|
case PowerPlantType.Uranium:
|
|
1487
|
-
if (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length == 1) {
|
|
1528
|
+
if (currentPlayer.powerPlants.filter((pp) => pp.type == powerPlant.type).length == 1) {
|
|
1488
1529
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1489
1530
|
return;
|
|
1490
1531
|
}
|
|
@@ -1498,7 +1539,12 @@ export default class Game extends Vue {
|
|
|
1498
1539
|
break;
|
|
1499
1540
|
|
|
1500
1541
|
case PowerPlantType.Hybrid:
|
|
1501
|
-
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
|
+
) {
|
|
1502
1548
|
this.sendMove({ name: MoveName.DiscardPowerPlant, data: powerPlant.number });
|
|
1503
1549
|
return;
|
|
1504
1550
|
}
|
|
@@ -1508,7 +1554,10 @@ export default class Game extends Vue {
|
|
|
1508
1554
|
return;
|
|
1509
1555
|
}
|
|
1510
1556
|
|
|
1511
|
-
this.resourcesToDiscard = [
|
|
1557
|
+
this.resourcesToDiscard = [
|
|
1558
|
+
{ name: 'Coal', value: '0', max: currentPlayer.coalLeft },
|
|
1559
|
+
{ name: 'Oil', value: '0', max: currentPlayer.oilLeft },
|
|
1560
|
+
];
|
|
1512
1561
|
break;
|
|
1513
1562
|
}
|
|
1514
1563
|
|
|
@@ -1535,7 +1584,9 @@ export default class Game extends Vue {
|
|
|
1535
1584
|
resourcesSpent = currentPlayer.resourcesUsed;
|
|
1536
1585
|
resourcesSpent.sort();
|
|
1537
1586
|
currentPlayer.resourcesUsed = [];
|
|
1538
|
-
currentPlayer.powerPlantsNotUsed = currentPlayer.powerPlantsNotUsed.filter(
|
|
1587
|
+
currentPlayer.powerPlantsNotUsed = currentPlayer.powerPlantsNotUsed.filter(
|
|
1588
|
+
(x) => x != powerPlant.number
|
|
1589
|
+
);
|
|
1539
1590
|
|
|
1540
1591
|
break;
|
|
1541
1592
|
}
|
|
@@ -1546,7 +1597,7 @@ export default class Game extends Vue {
|
|
|
1546
1597
|
});
|
|
1547
1598
|
|
|
1548
1599
|
this.disablePass = true;
|
|
1549
|
-
setTimeout(() => this.disablePass = false, 1000);
|
|
1600
|
+
setTimeout(() => (this.disablePass = false), 1000);
|
|
1550
1601
|
}
|
|
1551
1602
|
}
|
|
1552
1603
|
|
|
@@ -1705,7 +1756,7 @@ export default class Game extends Vue {
|
|
|
1705
1756
|
return !!availableMoves[MoveName.ChoosePowerPlant];
|
|
1706
1757
|
}
|
|
1707
1758
|
|
|
1708
|
-
buyableResources(): { resource: ResourceType
|
|
1759
|
+
buyableResources(): { resource: ResourceType; side?: 'north' | 'south'; fromStorage?: boolean }[] {
|
|
1709
1760
|
if (!this.canMove()) return [];
|
|
1710
1761
|
|
|
1711
1762
|
const currentPlayer = this.G!.players[this.player!];
|
|
@@ -1745,7 +1796,7 @@ export default class Game extends Vue {
|
|
|
1745
1796
|
const currentPlayer = this.G!.players[this.player!];
|
|
1746
1797
|
const availableMoves = currentPlayer.availableMoves!;
|
|
1747
1798
|
|
|
1748
|
-
return availableMoves[MoveName.Build] && availableMoves[MoveName.Build]!.map((c) => c.name) || [];
|
|
1799
|
+
return (availableMoves[MoveName.Build] && availableMoves[MoveName.Build]!.map((c) => c.name)) || [];
|
|
1749
1800
|
}
|
|
1750
1801
|
|
|
1751
1802
|
/**
|
|
@@ -1895,10 +1946,12 @@ export default class Game extends Vue {
|
|
|
1895
1946
|
}
|
|
1896
1947
|
|
|
1897
1948
|
// Check if player has enough resources, accounting for hybrid plants which can use either coal or oil
|
|
1898
|
-
if (
|
|
1949
|
+
if (
|
|
1950
|
+
coalUsed > player.coalLeft ||
|
|
1899
1951
|
oilUsed > player.oilLeft ||
|
|
1900
1952
|
garbageUsed > player.garbageLeft ||
|
|
1901
|
-
uraniumUsed > player.uraniumLeft
|
|
1953
|
+
uraniumUsed > player.uraniumLeft
|
|
1954
|
+
) {
|
|
1902
1955
|
return false;
|
|
1903
1956
|
}
|
|
1904
1957
|
const remainingCoal = player.coalLeft - coalUsed;
|
|
@@ -1980,7 +2033,7 @@ export default class Game extends Vue {
|
|
|
1980
2033
|
|
|
1981
2034
|
return 'Choose a Power Plant to start an auction.';
|
|
1982
2035
|
} else if (currentPlayer.availableMoves![MoveName.Bid]) {
|
|
1983
|
-
return
|
|
2036
|
+
return "It's your turn to bid!";
|
|
1984
2037
|
} else if (currentPlayer.availableMoves![MoveName.BuyResource]) {
|
|
1985
2038
|
return 'Buy resources on the market, or pass.';
|
|
1986
2039
|
} else if (currentPlayer.availableMoves![MoveName.Build]) {
|
|
@@ -1997,7 +2050,7 @@ export default class Game extends Vue {
|
|
|
1997
2050
|
return 'Choose which resources to discard.';
|
|
1998
2051
|
}
|
|
1999
2052
|
|
|
2000
|
-
return
|
|
2053
|
+
return "It's your turn!";
|
|
2001
2054
|
} else {
|
|
2002
2055
|
let log = this.G.log[this.G.log.length - 1];
|
|
2003
2056
|
if (log.type == 'move') {
|
|
@@ -2066,7 +2119,9 @@ export default class Game extends Vue {
|
|
|
2066
2119
|
// the connection cost paid to reach it, averaged over cities built.
|
|
2067
2120
|
label: 'Cost per City',
|
|
2068
2121
|
value: (p) =>
|
|
2069
|
-
p.cities.length
|
|
2122
|
+
p.cities.length
|
|
2123
|
+
? ((p.totalSpentCities + p.totalSpentConnections) / p.cities.length).toFixed(1)
|
|
2124
|
+
: '-',
|
|
2070
2125
|
},
|
|
2071
2126
|
{ label: 'Spending: Plants', value: (p) => p.totalSpentPlants },
|
|
2072
2127
|
{ label: 'Spending: Resources', value: (p) => p.totalSpentResources },
|
|
@@ -2318,8 +2373,10 @@ export default class Game extends Vue {
|
|
|
2318
2373
|
const scale = scaleOf(name);
|
|
2319
2374
|
// Centre a shorter slot against the tallest one in its row.
|
|
2320
2375
|
const top = y + (rowHeight - bb.height * scale) / 2;
|
|
2321
|
-
transforms[name] =
|
|
2322
|
-
|
|
2376
|
+
transforms[name] = `translate(${round(x - bb.x * scale)}, ${round(top - bb.y * scale)}) scale(${round(
|
|
2377
|
+
scale,
|
|
2378
|
+
4
|
|
2379
|
+
)})`;
|
|
2323
2380
|
x += bb.width * scale + STACK_GAP;
|
|
2324
2381
|
}
|
|
2325
2382
|
y += rowHeight + STACK_GAP;
|
|
@@ -2442,8 +2499,8 @@ ul {
|
|
|
2442
2499
|
flex-direction: column;
|
|
2443
2500
|
}
|
|
2444
2501
|
|
|
2445
|
-
.fitToScreen {
|
|
2446
|
-
height:
|
|
2502
|
+
.fitToScreen #scene {
|
|
2503
|
+
max-height: calc(100vh - 40px);
|
|
2447
2504
|
}
|
|
2448
2505
|
|
|
2449
2506
|
.statusBar {
|
|
@@ -2454,13 +2511,17 @@ ul {
|
|
|
2454
2511
|
text-align: center;
|
|
2455
2512
|
line-height: 40px;
|
|
2456
2513
|
font-size: 20px;
|
|
2457
|
-
position:
|
|
2514
|
+
position: sticky;
|
|
2515
|
+
top: 0;
|
|
2516
|
+
z-index: 10;
|
|
2517
|
+
flex-shrink: 0;
|
|
2458
2518
|
}
|
|
2459
2519
|
|
|
2460
2520
|
#scene {
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2521
|
+
display: block;
|
|
2522
|
+
height: auto;
|
|
2523
|
+
flex: none;
|
|
2524
|
+
margin: 0 auto;
|
|
2464
2525
|
}
|
|
2465
2526
|
|
|
2466
2527
|
// Portrait: the scene is taller than the viewport by design (each row is scaled
|
|
@@ -2473,12 +2534,14 @@ ul {
|
|
|
2473
2534
|
#scene {
|
|
2474
2535
|
max-height: none;
|
|
2475
2536
|
flex-grow: 0;
|
|
2476
|
-
margin-top:
|
|
2537
|
+
margin-top: 0;
|
|
2477
2538
|
}
|
|
2478
2539
|
}
|
|
2479
2540
|
|
|
2480
2541
|
body,
|
|
2481
2542
|
html {
|
|
2543
|
+
background: #e7ebda;
|
|
2544
|
+
color: #263521;
|
|
2482
2545
|
height: 100%;
|
|
2483
2546
|
width: 100%;
|
|
2484
2547
|
margin: 0;
|
|
@@ -2751,3 +2814,24 @@ text {
|
|
|
2751
2814
|
margin: 15px 0 0 15px;
|
|
2752
2815
|
}
|
|
2753
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>
|