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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "powergrid-viewer",
3
- "version": "2.0.14",
3
+ "version": "2.1.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/boardgamers/powergrid.git",
@@ -3,14 +3,14 @@
3
3
  <div class="statusBar">
4
4
  {{ getStatusMessage() }}
5
5
  </div>
6
- <audio id="piece-drop" preload="none">
7
- <source src="../audio/piece-drop.mp3" type="audio/mpeg" />
8
- </audio>
9
- <audio id="notification" preload="none">
10
- <source src="../audio/notification.mp3" type="audio/mpeg" />
11
- <source src="../audio/notification.ogg" type="audio/ogg" />
12
- </audio>
13
- <svg v-if="G" id="scene" :class="{ stacked: stacked }" :viewBox="sceneViewBox" style="width: 100%">
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">&times;</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, max: number, value: 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 (this.G.phase == Phase.Bureaucracy && player.powerPlantsNotUsed.length > 0
1161
- && Object.keys(player.availableMoves).includes('UsePowerPlant')
1162
- && !this.canPowerAllCitiesWithUsedPlants(player)) {
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 = 'Are you sure you want to skip buying resources without enough to power all your plants?';
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 = 'Are you sure you want to pass? You didn\'t use any power plant!';
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, side?: 'north' | 'south', fromStorage?: boolean }) {
1297
- const data: { resource: ResourceType, side?: 'north' | 'south', fromStorage?: boolean } = { resource: payload.resource };
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, side?: 'north' | 'south', fromStorage?: boolean }) {
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({ name: MoveName.Build, data: { name: city.name, price: freeJumpMove.price, freeJump: true } });
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 = currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 > 0 ? Math.max(0, currentPlayer.oilLeft - currentPlayer.oilCapacity) : 0;
1410
- return currentPlayer.coalCapacity + currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 + parseInt(this.resourcesToDiscard[0].value) < currentPlayer.coalLeft + hybridCapacityUsed;
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 = currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 > 0 ? Math.max(0, currentPlayer.coalLeft - currentPlayer.coalCapacity) : 0;
1414
- return currentPlayer.oilCapacity + currentPlayer.hybridCapacity - this.discardedPowerPlant!.cost * 2 + parseInt(this.resourcesToDiscard[0].value) < currentPlayer.oilLeft + hybridCapacityUsed;
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 currentPlayer.garbageCapacity - this.discardedPowerPlant!.cost * 2 - currentPlayer.garbageLeft + parseInt(this.resourcesToDiscard[0].value) < 0;
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 currentPlayer.uraniumCapacity - this.discardedPowerPlant!.cost * 2 - currentPlayer.uraniumLeft + parseInt(this.resourcesToDiscard[0].value) < 0;
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 (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length + currentPlayer.powerPlants.filter(pp => pp.type == PowerPlantType.Hybrid).length == 1) {
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 (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length + currentPlayer.powerPlants.filter(pp => pp.type == PowerPlantType.Hybrid).length == 1) {
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 (currentPlayer.powerPlants.filter(pp => pp.type == powerPlant.type).length + currentPlayer.powerPlants.filter(pp => pp.type == PowerPlantType.Coal).length + currentPlayer.powerPlants.filter(pp => pp.type == PowerPlantType.Oil).length == 1) {
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 = [{ name: 'Coal', value: '0', max: currentPlayer.coalLeft }, { name: 'Oil', value: '0', max: currentPlayer.oilLeft }];
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((x) => x != powerPlant.number);
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, side?: 'north' | 'south', fromStorage?: boolean }[] {
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 (coalUsed > player.coalLeft ||
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 'It\'s your turn to bid!';
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 'It\'s your turn!';
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 ? ((p.totalSpentCities + p.totalSpentConnections) / p.cities.length).toFixed(1) : '-',
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
- `translate(${round(x - bb.x * scale)}, ${round(top - bb.y * scale)}) scale(${round(scale, 4)})`;
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: 100%;
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: fixed;
2514
+ position: sticky;
2515
+ top: 0;
2516
+ z-index: 10;
2517
+ flex-shrink: 0;
2458
2518
  }
2459
2519
 
2460
2520
  #scene {
2461
- max-height: calc(100% - 40px);
2462
- flex-grow: 1;
2463
- margin: 40px auto auto auto;
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: 40px;
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>