BGA Pythia - 7 Wonders game helper

Visual aid that extends BGA game interface with useful information

  1. // ==UserScript==
  2. // @name BGA Pythia - 7 Wonders game helper
  3. // @description Visual aid that extends BGA game interface with useful information
  4. // @namespace https://github.com/dpavliuchkov/bga-pythia
  5. // @author https://github.com/dpavliuchkov
  6. // @version 1.3.3
  7. // @license MIT
  8. // @include *boardgamearena.com/*
  9. // @grant none
  10. // ==/UserScript==
  11. //
  12. // On boardgamearena.com, you can play an exciting board game of 7 wonders.
  13. // However, it is hard to remember which cards each player has. Pythia has
  14. // godlike powers and will share this information with you. It will also
  15. // display total player's score based on the current shields situation.
  16. // And it will mark leader and runner up players and their boards. And
  17. // Pythia will calculate how much points and coins some cards are worth to you.
  18. // Works with Tampermonkey only.
  19. // ==/UserScript==
  20.  
  21. // System variables - don't edit
  22. const Enable_Logging = false;
  23. const Is_Inside_Game = /\?table=[0-9]*/.test(window.location.href);
  24. const Cards_Image = "https://x.boardgamearena.net/data/themereleases/current/games/sevenwonders/200914-1526/img/cards.jpg";
  25. const Cards_Image_V2 = "https://x.boardgamearena.net/data/themereleases/current/games/sevenwonders/200914-1526/img/cards_v2.jpg";
  26. const BGA_Player_Board_Id_Prefix = "player_board_wrap_";
  27. const BGA_Player_Score_Id_Prefix = "player_score_";
  28. const Card_Worth_Id_Prefix = "pythia_card_worth_container_";
  29. const Discard_Card_Worth_Id_Prefix = "pythia_discard_card_worth_container_";
  30. const Card_Worth_Class = "pythia_card_worth";
  31. const Card_Worth_Coins_Class = "pythia_card_coins_worth";
  32. const Player_Cards_Id_Prefix = "pythia_cards_wrap_";
  33. const Player_Hand_Card_Id_Prefix = "pythia_hand_card_";
  34. const Player_Score_Id_Prefix = "pythia_score_";
  35. const Player_Military_Power_Id_Prefix = "pythia_military_power_";
  36. const Player_War_Score_Id_Prefix = "pythia_player_war_score_";
  37. const Player_Cards_Div_Class = "pythia_cards_container";
  38. const Player_Score_Span_Class = "pythia_score";
  39. const Player_Leader_Class = "pythia_leader";
  40. const Player_Runnerup_Class = "pythia_runnerup";
  41. const War_Points_Per_Age = {
  42. "1": 1,
  43. "2": 3,
  44. "3": 5
  45. };
  46. const Victory_Points_Image = {
  47. "-1": "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/minus%201%20point.png?raw=true",
  48. "-2": "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/minus%202%20points.png?raw=true",
  49. 0: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/0%20points.png?raw=true",
  50. 1: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/1%20point.png?raw=true",
  51. 2: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/2%20points.png?raw=true",
  52. 3: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/3%20points.png?raw=true",
  53. 4: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/4%20points.png?raw=true",
  54. 5: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/5%20points.png?raw=true",
  55. 6: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/6%20points.png?raw=true",
  56. 7: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/7%20points.png?raw=true",
  57. 8: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/8%20points.png?raw=true",
  58. 9: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/9%20points.png?raw=true",
  59. 10: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/10%20points.png?raw=true",
  60. 11: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/11%20points.png?raw=true",
  61. 12: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/12%20points.png?raw=true",
  62. 13: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/13%20points.png?raw=true",
  63. 14: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/14%20points.png?raw=true",
  64. };
  65. const Coins_Image = {
  66. 0: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/0%20coins.png?raw=true",
  67. 1: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/1%20coin.png?raw=true",
  68. 2: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/2%20coins.png?raw=true",
  69. 3: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/3%20coins.png?raw=true",
  70. 4: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/4%20coins.png?raw=true",
  71. 5: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/5%20coins.png?raw=true",
  72. 6: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/6%20coins.png?raw=true",
  73. 7: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/7%20coins.png?raw=true",
  74. 8: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/8%20coins.png?raw=true",
  75. 9: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/9%20coins.png?raw=true",
  76. 10: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/10%20coins.png?raw=true",
  77. 11: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/11%20coins.png?raw=true",
  78. 12: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/12%20coins.png?raw=true",
  79. 13: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/13%20coins.png?raw=true",
  80. 14: "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/14%20coins.png?raw=true",
  81. };
  82. const Military_Power_Icon = "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/military-power-icon.png?raw=true";
  83. const HD_Boards = "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/boards_hd.jpg?raw=true";
  84. const HD_Cards = "https://github.com/dpavliuchkov/bga-pythia/blob/master/images/cards_hd.jpg?raw=true&version=1";
  85.  
  86. // Main Pythia object
  87. var pythia = {
  88. isStarted: false,
  89. isFinished: false,
  90. dojo: null,
  91. game: null,
  92. edition: null,
  93. isNewEdition: null,
  94. mainPlayer: null,
  95. currentAge: 1,
  96. playersCount: 0,
  97. players: [],
  98.  
  99. // Init Pythia
  100. init: function() {
  101. this.isStarted = true;
  102. // Check if the site was loaded correctly
  103. if (!window.parent || !window.parent.dojo || !window.parent.gameui.gamedatas ||
  104. !window.parent.gameui.gamedatas.playerorder || !window.parent.gameui.gamedatas.playerorder[0] ||
  105. !window.parent.gameui.gamedatas.card_types || !window.parent.gameui.gamedatas.wonders) {
  106. return;
  107. }
  108. this.dojo = window.parent.dojo;
  109. this.game = window.parent.gameui.gamedatas;
  110. this.edition = parseInt(this.game.game_edition);
  111. this.isNewEdition = this.edition == 1;
  112.  
  113. var playerOrder = this.game.playerorder;
  114. this.playersCount = playerOrder.length;
  115. this.mainPlayer = playerOrder[0];
  116. // local storage stores value as strings, so we need to parse "false" and "true" to get boolean
  117. this.settings = {
  118. "enableWarScores": localStorage.getItem("pythia-seetings-warscores") === null ?
  119. true : String(localStorage.getItem("pythia-seetings-warscores")) == "true",
  120. "enableRichBoards": localStorage.getItem("pythia-seetings-richboards") === null ?
  121. true : String(localStorage.getItem("pythia-seetings-richboards")) == "true",
  122. "enableCardPoints": localStorage.getItem("pythia-seetings-cardpoints") === null ?
  123. true : String(localStorage.getItem("pythia-seetings-cardpoints")) == "true",
  124. "enableHD": localStorage.getItem("pythia-seetings-hd") === null ?
  125. true : String(localStorage.getItem("pythia-seetings-hd")) == "true",
  126. "enableFullwidth": localStorage.getItem("pythia-seetings-fullwidth") === null ?
  127. false : String(localStorage.getItem("pythia-seetings-fullwidth")) == "true",
  128. "showMenu": localStorage.getItem("pythia-seetings-showmenu") === null ?
  129. true : String(localStorage.getItem("pythia-seetings-showmenu")) == "true",
  130. };
  131.  
  132. for (var i = 0; i < this.playersCount; i++) {
  133. var playerId = playerOrder[i];
  134. this.players[playerId] = {
  135. hand: {},
  136. coins: 3,
  137. shields: 0,
  138. defeats: 0,
  139. bgaScore: 1,
  140. warScore: 0,
  141. wonder: 0,
  142. wonderStages: 0,
  143. maxWonderStages: 0,
  144. playedTypes: {
  145. "raw": 0,
  146. "man": 0,
  147. "com": 0,
  148. "mil": 0,
  149. "civ": 0,
  150. "sci": 0,
  151. "gui": 0,
  152. },
  153. playedCards: {
  154. "lighthouse": false,
  155. "haven": false,
  156. "ludus": false,
  157. "chamberOfCommerce": false,
  158. "shipownersGuild": false,
  159. },
  160. science: {
  161. "gears": 0, // 1 in BGA
  162. "tablets": 0, // 2 in BGA
  163. "compasses": 0, // 3 in BGA
  164. "jokers": 0, // ? in BGA
  165. }
  166. };
  167.  
  168. // Identify who sits to the left and to the right
  169. if (playerId == this.mainPlayer) {
  170. this.players[playerId].left = playerOrder[this.playersCount - 1];
  171. } else {
  172. this.players[playerId].left = playerOrder[i - 1];
  173. }
  174. if (playerId == playerOrder[this.playersCount - 1]) {
  175. this.players[playerId].right = this.mainPlayer;
  176. } else {
  177. this.players[playerId].right = playerOrder[i + 1];
  178. }
  179.  
  180. this.renderPythiaContainers(playerId);
  181. }
  182.  
  183. this.renderPythiaMenu();
  184. this.setStyles();
  185.  
  186. // Configure Pythia according to settings
  187. this.togglePythiaSettingPlayerCardsDisplay(false);
  188. this.togglePythiaSettingWarScoresDisplay(this.settings.enableWarScores);
  189. this.togglePythiaSettingRichBoardsDisplay(this.settings.enableRichBoards);
  190. this.togglePythiaSettingCardPointsDisplay(this.settings.enableCardPoints);
  191. this.togglePythiaSettingHDDisplay(this.settings.enableHD);
  192. this.togglePythiaSettingFullwidthDisplay(this.settings.enableFullwidth);
  193.  
  194. // Connect event handlers to follow game progress
  195. this.dojo.subscribe("newHand", this, "recordHand");
  196. this.dojo.subscribe("cardsPlayed", this, "recordTurn");
  197. this.dojo.subscribe("coinDelta", this, "recordCoins");
  198. this.dojo.subscribe("discard", this, "recordDiscard");
  199. this.dojo.subscribe("newWonders", this, "recordWonderChoice");
  200. this.dojo.subscribe("wonderBuild", this, "recordWonderStage");
  201. this.dojo.subscribe("updateScore", this, "recordScoreUpdate");
  202. this.dojo.subscribe("warVictory", this, "recordWarResults");
  203. this.dojo.subscribe("newAge", this, "changeAge");
  204.  
  205. if (Enable_Logging) console.log("PYTHIA: My eyes can see everything!");
  206. return this;
  207. },
  208.  
  209. // Record which wonder each player has chosen
  210. recordWonderChoice: function(data) {
  211. if (Enable_Logging) console.log("PYTHIA: wonders chosen - I got", data);
  212.  
  213. // Input check
  214. if (!data || !data.args || !data.args.wonders || !this.game || !this.game.wonders) {
  215. return;
  216. }
  217.  
  218. const wonders = Object.keys(data.args.wonders);
  219. for (const playerId of wonders) {
  220. const wonderId = data.args.wonders[playerId]
  221. this.players[playerId].wonder = wonderId;
  222. this.players[playerId].maxWonderStages = Object.keys(this.game.wonders[wonderId].stages).length;
  223. }
  224. },
  225.  
  226. // Check what came to main player in the new hand
  227. recordHand: function(data) {
  228. if (Enable_Logging) console.log("PYTHIA: new hand - I got", data);
  229.  
  230. // Input check
  231. if (!data || !data.args || !data.args.cards) {
  232. return;
  233. }
  234.  
  235. // Rotate old hands and render cards
  236. if (!this.isFirstTurn()) {
  237. this.passCards();
  238. this.renderPlayerCards();
  239. this.renderPlayerCardTooltips();
  240. }
  241. // Save new hand to main player
  242. this.players[this.mainPlayer].hand = data.args.cards;
  243.  
  244. // Get card worth in victory points and coins and render it
  245. for (var cardId in data.args.cards) {
  246. const playedCard = data.args.cards[cardId];
  247. const cardWorth = this.calculateCardWorth(this.mainPlayer, playedCard.type);
  248. this.renderCardPoints(cardId, false, cardWorth.points, cardWorth.coins);
  249. }
  250. },
  251.  
  252. // Process all cards played by all players
  253. recordTurn: function(data) {
  254. if (Enable_Logging) console.log("PYTHIA: cards played - I got", data);
  255.  
  256. // Input check
  257. if (!data || !data.args || !data.args.cards) {
  258. return;
  259. }
  260.  
  261. var warPlayed = false;
  262.  
  263. // Cycle all played cards
  264. for (var cardId in data.args.cards) {
  265. const playedCard = data.args.cards[cardId];
  266. const originalCard = this.game.card_types[playedCard.type];
  267. const playerId = playedCard.location_arg;
  268. if (!originalCard.category) return; // Input check
  269.  
  270. const cardCategory = originalCard.category;
  271.  
  272. // Track if played card was military
  273. if (originalCard.shield) {
  274. warPlayed = true;
  275. this.players[playerId].shields += originalCard.shield;
  276. this.renderMilitaryPower(playerId);
  277. }
  278.  
  279. // Update played scientific symbols
  280. if (originalCard.science) {
  281. this.increaseScienceCounter(playerId, originalCard.science);
  282. }
  283.  
  284. // Update played card types array of the player
  285. this.players[playerId].playedTypes[cardCategory] += 1;
  286.  
  287. // Track cards that we need for points worth feature
  288. this.increaseCardCounter(playerId, playedCard.type);
  289.  
  290. // Delete played card
  291. if (isObjectEmpty(this.players[playerId].hand)) {
  292. continue;
  293. }
  294. delete this.players[playerId].hand[cardId];
  295. }
  296. if (warPlayed) {
  297. this.calculateWarScores();
  298. }
  299. },
  300.  
  301. // If there was a trade, update how many coins each player has
  302. recordCoins: function(data) {
  303. if (Enable_Logging) console.log("PYTHIA: coins changed - I got", data);
  304.  
  305. // Input check
  306. if (!data || !data.args || !data.args.coinddelta) {
  307. return;
  308. }
  309.  
  310. this.players[data.args.player_id].coins += data.args.coinddelta;
  311. },
  312.  
  313. // If main player discarded - we know what card it was
  314. recordDiscard: function(data) {
  315. if (Enable_Logging) console.log("PYTHIA: card discarded - I got", data);
  316.  
  317. // Input check
  318. if (!data || !data.args || !data.args.card_id || !!data.channelorig) {
  319. return;
  320. }
  321.  
  322. var player = data.channelorig.substring(9);
  323. delete this.players[player].hand[data.args.card_id];
  324. },
  325.  
  326. // Record when a wonder stage was played
  327. recordWonderStage: function(data) {
  328. if (Enable_Logging) console.log("PYTHIA: wonder built - I got", data);
  329.  
  330. // Input check
  331. if (!data || !data.args || !data.args.step || !data.args.player_id) {
  332. return;
  333. }
  334.  
  335. const playerId = data.args.player_id;
  336. const stage = data.args.step;
  337. var wonderId = this.players[playerId].wonder;
  338.  
  339. // We might have corrupted data, refresh it from the game HTML
  340. if (wonderId == 0) {
  341. const board = this.dojo.byId("player_board_wonder_" + playerId);
  342. switch (board.style.backgroundPositionY) {
  343. case "0%":
  344. // Giza A
  345. wonderId = 1;
  346. break;
  347.  
  348. case "7.69231%":
  349. // Baby A
  350. wonderId = 2;
  351. break;
  352.  
  353. case "15.3846%":
  354. // Oly A
  355. wonderId = 3;
  356. break;
  357.  
  358. case "23.0769%":
  359. // Rho A
  360. wonderId = 4;
  361. break;
  362.  
  363. case "30.7692%":
  364. // Eph A
  365. wonderId = 5;
  366. break;
  367.  
  368. case "38.4615%":
  369. // Alex A
  370. wonderId = 6;
  371. break;
  372.  
  373. case "46.1538%":
  374. // Hali A
  375. wonderId = 7;
  376. break;
  377.  
  378. case "53.8462%":
  379. // Giza B
  380. wonderId = 8;
  381. break;
  382.  
  383. case "61.5385%":
  384. // Baby B
  385. wonderId = 9;
  386. break;
  387.  
  388. case "69.2308%":
  389. // Oly B
  390. wonderId = 10;
  391. break;
  392.  
  393. case "76.9231%":
  394. // Rho B
  395. wonderId = 11;
  396. break;
  397.  
  398. case "84.6154%":
  399. // Eph B
  400. wonderId = 12;
  401. break;
  402.  
  403. case "92.3077%":
  404. // Alex B
  405. wonderId = 13;
  406. break;
  407.  
  408. case "100%":
  409. // Hali B
  410. wonderId = 14;
  411. break;
  412. }
  413.  
  414. this.players[playerId].wonder = wonderId;
  415. }
  416.  
  417. this.players[playerId].wonderStages += 1; // increase a counter of built wonder stages
  418.  
  419. // Game correctness check
  420. if (!this.game.wonders[wonderId] || !this.game.wonders[wonderId].stages ||
  421. !this.game.wonders[wonderId].stages[stage]) {
  422. return;
  423. }
  424.  
  425. // If Rhodos built a stage - it could have shields
  426. if (this.game.wonders[wonderId].stages[stage].shield) {
  427. this.players[playerId].shields += this.game.wonders[wonderId].stages[stage].shield;
  428. this.calculateWarScores();
  429. this.renderMilitaryPower(playerId);
  430. }
  431.  
  432. // If Babylon built a stage - it could have science
  433. if (this.game.wonders[wonderId].stages[stage].science) {
  434. this.increaseScienceCounter(playerId, this.game.wonders[wonderId].stages[stage].science);
  435. }
  436.  
  437. // If Hali built a stage - it could have discard play
  438. if (this.game.wonders[wonderId].stages[stage].pickDiscarded) {
  439. this.calculateDiscardWorth(this);
  440. }
  441. },
  442.  
  443. // Update internal scores as well
  444. recordScoreUpdate: function(data) {
  445. if (Enable_Logging) console.log("PYTHIA: scores updated - I got", data);
  446.  
  447. // Input check
  448. if (!data || !data.args || !data.args.scores) {
  449. return;
  450. }
  451.  
  452. const scores = Object.keys(data.args.scores);
  453. for (const playerId of scores) {
  454. this.players[playerId].bgaScore = data.args.scores[playerId];
  455. this.renderPlayerScore(playerId);
  456. }
  457.  
  458. // Update leader & runnerup positions
  459. this.renderLeaderRunnerup();
  460. },
  461.  
  462. // Process war results
  463. recordWarResults: function(data) {
  464. if (Enable_Logging) console.log("PYTHIA: war battle happened - I got", data);
  465.  
  466. // Input check
  467. if (!data || !data.args || !data.args.points || !data.args.neighbour_id) {
  468. return;
  469. }
  470.  
  471. // Save defeat tokens
  472. if (data.args.points > 0) {
  473. this.players[data.args.neighbour_id].defeats += 1;
  474. }
  475.  
  476. // If this is the last war - do cleanup
  477. if (this.currentAge == 3) {
  478. this.finishGame();
  479. }
  480. },
  481.  
  482. // Calculate additional score from shields
  483. calculateWarScores: function() {
  484. var currentPlayerId = this.mainPlayer;
  485. var i = 0;
  486. while (i < this.playersCount) {
  487. var thisPlayer = this.players[currentPlayerId];
  488. thisPlayer.warScore = 0;
  489.  
  490. // Check battles with right neighbour
  491. var rightPlayer = this.players[thisPlayer.right];
  492. if (thisPlayer.shields > rightPlayer.shields) {
  493. this.increaseWarScore(currentPlayerId, this.currentAge);
  494. } else if (thisPlayer.shields < rightPlayer.shields) {
  495. this.decreaseWarScore(currentPlayerId, this.currentAge);
  496. }
  497.  
  498. // Check battles with left neighbour
  499. var leftPlayer = this.players[thisPlayer.left];
  500. if (thisPlayer.shields > leftPlayer.shields) {
  501. this.increaseWarScore(currentPlayerId, this.currentAge);
  502. } else if (thisPlayer.shields < leftPlayer.shields) {
  503. this.decreaseWarScore(currentPlayerId, this.currentAge);
  504. }
  505.  
  506. currentPlayerId = thisPlayer.right;
  507. i++;
  508. }
  509. },
  510.  
  511. calculateDiscardWorth: function(that, counter = 1) {
  512. const discardWrapper = that.dojo.byId('discarded_wrap');
  513.  
  514. // Check that we can see discard div, else wait
  515. if (discardWrapper.style.display == "block") {
  516. const discarded = that.dojo.query("#discarded div");
  517. for (var i in discarded) {
  518. const card = discarded[i];
  519. if (!card.style) {
  520. continue;
  521. }
  522.  
  523. // Calculate card position - the only way to find which card is actually in discard
  524. var posX, posY;
  525. if (that.isNewEdition) {
  526. posX = -255 * parseInt(card.style.backgroundPositionX) / 100;
  527. posY = -393 * parseInt(card.style.backgroundPositionY) / 100;
  528. } else {
  529. posX = -parseInt(card.style.width) * parseInt(card.style.backgroundPositionX) / 100;
  530. posY = -parseInt(card.style.height) * parseInt(card.style.backgroundPositionY) / 100;
  531. }
  532.  
  533. var cardType = null;
  534. for (var j in that.game.card_types) {
  535. const originalCard = that.game.card_types[j];
  536. if (originalCard.backx == posX && originalCard.backy == posY) {
  537. cardType = j;
  538. break;
  539. }
  540. }
  541.  
  542. if (cardType) {
  543. const cardWorth = that.calculateCardWorth(that.mainPlayer, cardType);
  544. that.renderCardPoints(card.id.substr(15), true, cardWorth.points, cardWorth.coins);
  545. }
  546. }
  547. } else {
  548. if (counter > 10) {
  549. clearTimeout();
  550. return;
  551. }
  552.  
  553. setTimeout(that.calculateDiscardWorth, 2000, that, counter + 1);
  554. }
  555. },
  556.  
  557. // Get how many coins and victory points this card will bring to this player
  558. calculateCardWorth: function(playerId, cardType) {
  559. const leftPlayerId = this.players[playerId].left;
  560. const rightPlayerId = this.players[playerId].right;
  561.  
  562. var worth = {
  563. points: null,
  564. coins: null,
  565. };
  566.  
  567. switch (parseInt(cardType)) {
  568. // Raw materials
  569. case 1:
  570. case 2:
  571. case 3:
  572. case 4:
  573. case 5:
  574. case 6:
  575. case 7:
  576. case 8:
  577. case 9:
  578. case 10:
  579. case 28:
  580. case 28:
  581. case 30:
  582. case 31:
  583. if (this.players[playerId].playedCards.haven) {
  584. worth.points += 1;
  585. }
  586. if (this.players[playerId].playedCards.shipownersGuild) {
  587. worth.points += 1;
  588. }
  589. break;
  590.  
  591.  
  592. // Manufactured goods
  593. case 11:
  594. case 12:
  595. case 13:
  596. case 32:
  597. case 33:
  598. case 34:
  599. case 39:
  600. case 40:
  601. if (this.players[playerId].playedCards.shipownersGuild) {
  602. worth.points += 1;
  603. }
  604. if (this.players[playerId].playedCards.chamberOfCommerce) {
  605. worth.points += 2;
  606. }
  607. break;
  608.  
  609. // Commercial cards
  610. case 18:
  611. case 19:
  612. case 20:
  613. case 21:
  614. if (this.players[playerId].playedCards.lighthouse) {
  615. worth.points += 1;
  616. }
  617. break;
  618.  
  619. // Military cards
  620. case 22:
  621. case 23:
  622. case 24:
  623. case 43:
  624. case 44:
  625. case 45:
  626. case 46:
  627. case 70:
  628. case 71:
  629. case 72:
  630. case 73:
  631. case 80:
  632. if (!this.game.card_types[cardType] || !this.game.card_types[cardType].shield) {
  633. break;
  634. }
  635. worth.points = this.calculateMilitaryCardWorth(playerId, parseInt(this.game.card_types[cardType].shield));
  636. if (this.players[playerId].playedCards.ludus) {
  637. worth.points += 1;
  638. }
  639. break;
  640.  
  641. // Scientific symbols
  642. case 25:
  643. case 26:
  644. case 27:
  645. case 47:
  646. case 48:
  647. case 49:
  648. case 50:
  649. case 58:
  650. case 74:
  651. case 75:
  652. case 76:
  653. case 77:
  654. case 78:
  655. if (!this.game.card_types[cardType] || !this.game.card_types[cardType].science) {
  656. break;
  657. }
  658. worth.points = this.calculateScienceCardWorth(playerId, this.game.card_types[cardType].science);
  659. break;
  660.  
  661. // Age 2 commerce
  662. case 41: // Vineyard - coins for brown cards
  663. worth.coins = this.players[leftPlayerId].playedTypes["raw"] + this.players[rightPlayerId].playedTypes["raw"] +
  664. this.players[playerId].playedTypes["raw"];
  665. if (this.players[playerId].playedCards.lighthouse) {
  666. worth.points += 1;
  667. }
  668. break;
  669.  
  670. case 42: // Bazaar - coins for grey cards
  671. worth.coins = (this.players[leftPlayerId].playedTypes["man"] + this.players[rightPlayerId].playedTypes["man"] +
  672. this.players[playerId].playedTypes["man"]) * 2;
  673. if (this.players[playerId].playedCards.lighthouse) {
  674. worth.points += 1;
  675. }
  676. break;
  677.  
  678. // Age 3 guilds
  679. case 51: // Workers guild - brown cards
  680. worth.points = this.players[leftPlayerId].playedTypes["raw"] + this.players[rightPlayerId].playedTypes["raw"];
  681. if (this.players[playerId].playedCards.shipownersGuild) {
  682. worth.points += 1;
  683. }
  684. break;
  685. case 52: // Craftsmens guild - grey cards
  686. worth.points = 2 * (this.players[leftPlayerId].playedTypes["man"] + this.players[rightPlayerId].playedTypes["man"]);
  687. if (this.players[playerId].playedCards.shipownersGuild) {
  688. worth.points += 1;
  689. }
  690. break;
  691. case 53: // Traders guild - yellow cards
  692. worth.points = this.players[leftPlayerId].playedTypes["com"] + this.players[rightPlayerId].playedTypes["com"];
  693. if (this.players[playerId].playedCards.shipownersGuild) {
  694. worth.points += 1;
  695. }
  696. break;
  697. case 54: // Philosopehrs guild - yellow cards
  698. worth.points = this.players[leftPlayerId].playedTypes["sci"] + this.players[rightPlayerId].playedTypes["sci"];
  699. if (this.players[playerId].playedCards.shipownersGuild) {
  700. worth.points += 1;
  701. }
  702. break;
  703. case 55: // Spies guild - red cards
  704. worth.points = this.players[leftPlayerId].playedTypes["mil"] + this.players[rightPlayerId].playedTypes["mil"];
  705. if (this.players[playerId].playedCards.shipownersGuild) {
  706. worth.points += 1;
  707. }
  708. break;
  709. case 56:
  710. if (this.isNewEdition) {
  711. // Decorators guild - if all stages are built then 7 points
  712. worth.points = this.players[playerId].wonderStages == this.players[playerId].maxWonderStages ? 7 : 0;
  713. } else {
  714. // Strategist guild - defeat tokens
  715. worth.points = this.players[leftPlayerId].defeats + this.players[rightPlayerId].defeats;
  716. }
  717. if (this.players[playerId].playedCards.shipownersGuild) {
  718. worth.points += 1;
  719. }
  720. break;
  721. case 57: // Shipowners guild - own brown grey purple cards
  722. worth.points = this.players[playerId].playedTypes["raw"] + this.players[playerId].playedTypes["man"] +
  723. this.players[playerId].playedTypes["gui"] + 1;
  724. break;
  725. case 59: // Magistrates guild - blue cards
  726. worth.points = this.players[leftPlayerId].playedTypes["civ"] + this.players[rightPlayerId].playedTypes["civ"];
  727. if (this.players[playerId].playedCards.shipownersGuild) {
  728. worth.points += 1;
  729. }
  730. break;
  731. case 60: // Builders guild - wonder stages]
  732. worth.points = this.players[playerId].wonderStages + this.players[leftPlayerId].wonderStages +
  733. this.players[rightPlayerId].wonderStages;
  734. if (this.players[playerId].playedCards.shipownersGuild) {
  735. worth.points += 1;
  736. }
  737. break;
  738.  
  739. // Age 3 commerce
  740. case 66: // Haven - coins and points for own brown cards
  741. worth.coins = this.players[playerId].playedTypes["raw"];
  742. worth.points = this.players[playerId].playedTypes["raw"];
  743. if (this.players[playerId].playedCards.lighthouse) {
  744. worth.points += 1;
  745. }
  746. break;
  747. case 67: // Lighthouse - coins and points for own yellow cards
  748. worth.coins = this.players[playerId].playedTypes["com"] + 1;
  749. worth.points = this.players[playerId].playedTypes["com"] + 1;
  750. break;
  751. case 68: // Chamber of commerce - coins and points for own grey cards
  752. worth.coins = this.players[playerId].playedTypes["man"] * 2;
  753. worth.points = this.players[playerId].playedTypes["man"] * 2;
  754. if (this.players[playerId].playedCards.lighthouse) {
  755. worth.points += 1;
  756. }
  757. break;
  758. case 69: // Arena - coins and points for own wonder stages
  759. worth.coins = this.players[playerId].wonderStages * 3;
  760. worth.points = this.players[playerId].wonderStages;
  761. if (this.players[playerId].playedCards.lighthouse) {
  762. worth.points += 1;
  763. }
  764. break
  765. case 79: // Ludus - coins and points for own military stages
  766. worth.coins = this.players[playerId].playedTypes["mil"] * 3;
  767. worth.points = this.players[playerId].playedTypes["mil"];
  768. if (this.players[playerId].playedCards.lighthouse) {
  769. worth.points += 1;
  770. }
  771. break;
  772. }
  773.  
  774. return worth;
  775. },
  776.  
  777. // How will war score change if a player gets extra shields
  778. calculateMilitaryCardWorth: function(playerId, extraShields) {
  779. // Input check
  780. if (!playerId || !this.players[playerId]) {
  781. return 0;
  782. }
  783.  
  784. var thisPlayer = this.players[playerId];
  785. var newWarScore = 0;
  786.  
  787. // Check battles with right neighbour
  788. var rightPlayer = this.players[thisPlayer.right];
  789. if ((thisPlayer.shields + extraShields) > rightPlayer.shields) {
  790. newWarScore += War_Points_Per_Age[this.currentAge];
  791. } else if ((thisPlayer.shields + extraShields) < rightPlayer.shields) {
  792. newWarScore -= 1;
  793. }
  794.  
  795. // Check battles with left neighbour
  796. var leftPlayer = this.players[thisPlayer.left];
  797. if ((thisPlayer.shields + extraShields) > leftPlayer.shields) {
  798. newWarScore += War_Points_Per_Age[this.currentAge];
  799. } else if ((thisPlayer.shields + extraShields) < leftPlayer.shields) {
  800. newWarScore -= 1;
  801. }
  802.  
  803. return newWarScore - thisPlayer.warScore;
  804. },
  805.  
  806. // How many points will this science card bring to a player?
  807. calculateScienceCardWorth: function(playerId, newSymbol) {
  808. // Input check
  809. if (!playerId || !this.players[playerId] || !this.players[playerId].science) {
  810. return;
  811. }
  812.  
  813. const playerScience = this.players[playerId].science;
  814. const sciencePointsNow = this.calculateSciencePoints(playerScience.gears, playerScience.tablets,
  815. playerScience.compasses, playerScience.jokers);
  816. var sciencePointsAfter = null;
  817.  
  818. switch (newSymbol) {
  819. case 1:
  820. sciencePointsAfter = this.calculateSciencePoints(playerScience.gears + 1,
  821. playerScience.tablets, playerScience.compasses, playerScience.jokers);
  822. break;
  823.  
  824. case 2:
  825. sciencePointsAfter = this.calculateSciencePoints(playerScience.gears,
  826. playerScience.tablets + 1, playerScience.compasses, playerScience.jokers);
  827. break;
  828.  
  829. case 3:
  830. sciencePointsAfter = this.calculateSciencePoints(playerScience.gears,
  831. playerScience.tablets, playerScience.compasses + 1, playerScience.jokers);
  832. break;
  833.  
  834. case "?":
  835. sciencePointsAfter = this.calculateSciencePoints(playerScience.gears,
  836. playerScience.tablets, playerScience.compasses, playerScience.jokers + 1);
  837. break;
  838.  
  839. default:
  840. break;
  841. }
  842.  
  843. return sciencePointsAfter - sciencePointsNow;
  844. },
  845.  
  846. // How many points will a science set bring?
  847. calculateSciencePoints: function(gears, tablets, compasses, jokers) {
  848. // Joker can be any symbol, calculate each option with recursion if we have them
  849. if (jokers > 0) {
  850. const pointsWithJokerGear = this.calculateSciencePoints(gears + 1, tablets, compasses, jokers - 1);
  851. const pointsWithJokerTablet = this.calculateSciencePoints(gears, tablets + 1, compasses, jokers - 1);
  852. const pointsWithJokerCompass = this.calculateSciencePoints(gears, tablets, compasses + 1, jokers - 1);
  853.  
  854. return Math.max(pointsWithJokerGear, pointsWithJokerTablet, pointsWithJokerCompass);
  855. } else {
  856. // No jokers - calculate according to the rules
  857. var points = gears * gears + tablets * tablets + compasses * compasses; // individual symbols
  858. points += 7 * Math.min(gears, tablets, compasses); // set of 3
  859.  
  860. return points;
  861. }
  862. },
  863.  
  864. // Cleanup things between ages
  865. changeAge: function(data) {
  866. if (Enable_Logging) console.log("PYTHIA: new age - I got", data);
  867.  
  868. this.currentAge += 1;
  869.  
  870. // Recalculate war scores for the new age
  871. this.calculateWarScores();
  872.  
  873. const keys = Object.keys(this.players);
  874. for (const playerId of keys) {
  875. // Clean player hands and update total scores
  876. this.players[playerId].hand = {};
  877. this.renderPlayerScore(playerId);
  878. this.renderLeaderRunnerup();
  879. }
  880.  
  881. // Clean rendered cards from previous age
  882. this.dojo.query("." + Player_Cards_Div_Class).forEach(this.dojo.empty);
  883. },
  884.  
  885. // Cleanup Pythia when the game is done
  886. finishGame: function() {
  887. this.isFinished = true;
  888. this.togglePythiaSettingRichBoardsDisplay(false);
  889. this.togglePythiaSettingWarScoresDisplay(false);
  890. this.togglePythiaSettingPlayerCardsDisplay(false);
  891. },
  892.  
  893. // Add war scores based on the age
  894. increaseWarScore: function(playerId, age) {
  895. this.players[playerId].warScore += War_Points_Per_Age[age];
  896. },
  897. // Decrase war scores
  898. decreaseWarScore: function(playerId, age) {
  899. this.players[playerId].warScore -= 1;
  900. },
  901.  
  902. // Add a counter to played science symbols
  903. increaseScienceCounter: function(player, symbol) {
  904. switch (symbol) {
  905. case 1:
  906. this.players[player].science.gears += 1;
  907. break;
  908. case 2:
  909. this.players[player].science.tablets += 1;
  910. break;
  911. case 3:
  912. this.players[player].science.compasses += 1;
  913. break;
  914. case "?":
  915. this.players[player].science.jokers += 1;
  916. break;
  917. default:
  918. break;
  919. }
  920. },
  921.  
  922. // Track if certain card was player by hte player, needed for Card Worth function
  923. increaseCardCounter: function(playerId, cardType) {
  924. switch (parseInt(cardType)) {
  925. case 57: // Shipowners guild - own brown grey purple cards
  926. this.players[playerId].playedCards.shipownersGuild = true;
  927. break;
  928. case 66: // Haven - coins and points for own brown cards
  929. this.players[playerId].playedCards.haven = true;
  930. break;
  931. case 67: // Lighthouse - coins and points for own yellow cards
  932. this.players[playerId].playedCards.lighthouse = true;
  933. break;
  934. case 68: // Chamber of commerce - coins and points for own grey cards
  935. this.players[playerId].playedCards.chamberOfCommerce = true;
  936. break;
  937. case 79: // Ludus - coins and points for own military stages
  938. this.players[playerId].playedCards.ludus = true;
  939. break;
  940. }
  941. },
  942.  
  943. // Move cards unplayed cards between players
  944. passCards: function() {
  945. // This should be counter to age direction, because
  946. // Pythia always passes starting from the last player
  947. var direction = this.currentAge == 2 ? "right" : "left";
  948. var currentPlayerId = this.mainPlayer;
  949. var i = 0;
  950. while (i < this.playersCount) {
  951. var neighborId = this.players[currentPlayerId][direction];
  952. this.players[neighborId].hand = this.players[this.players[neighborId][direction]].hand;
  953. currentPlayerId = neighborId;
  954. i++;
  955. }
  956. },
  957.  
  958. // Render player containers
  959. renderPythiaContainers: function(playerId) {
  960. // Insert war score container in scores table
  961. if (!this.dojo.byId(Player_Score_Id_Prefix + playerId)) {
  962. this.dojo.place(
  963. "<span id='" + Player_Score_Id_Prefix + playerId + "'" +
  964. "class='player_score_value " + Player_Score_Span_Class + "'> (1)</span>",
  965. BGA_Player_Score_Id_Prefix + playerId,
  966. "after");
  967. }
  968.  
  969. // Insert military power container on player board
  970. if (!this.dojo.byId(Player_Military_Power_Id_Prefix + playerId)) {
  971. const refNode = this.dojo.query("#" + BGA_Player_Board_Id_Prefix + playerId + " .sw_coins");
  972. if (refNode && refNode[0]) {
  973. this.dojo.place(
  974. "<div id='" + Player_Military_Power_Id_Prefix + playerId + "' class='pythia_player_military_power'>" +
  975. "<img src='" + Military_Power_Icon + "'/><span>0</span></div>",
  976. refNode[0],
  977. "last");
  978. }
  979. }
  980.  
  981. // Insert war score container on player board
  982. if (!this.dojo.byId(Player_War_Score_Id_Prefix + playerId)) {
  983. const refNode = this.dojo.query("#" + BGA_Player_Board_Id_Prefix + playerId + " .sw_coins");
  984. if (refNode && refNode[0]) {
  985. this.dojo.place(
  986. "<div id='" + Player_War_Score_Id_Prefix + playerId + "' class='pythia_player_war_score'>" +
  987. "<i class='fa fa-star'></i><span>1 (1)</span></div>",
  988. refNode[0],
  989. "first");
  990. }
  991. }
  992.  
  993. // Skip card container for main player and if already rendered
  994. if (playerId == this.mainPlayer || this.dojo.byId(Player_Cards_Id_Prefix + playerId)) {
  995. return;
  996. }
  997. // Insert card container on player board
  998. this.dojo.place("<div id='" + Player_Cards_Id_Prefix + playerId + "'" +
  999. " class='" + Player_Cards_Div_Class + "'></div>",
  1000. BGA_Player_Board_Id_Prefix + playerId,
  1001. "first");
  1002. },
  1003.  
  1004. // Render player hands
  1005. renderPlayerCards: function() {
  1006. const keys = Object.keys(this.players);
  1007. for (const playerId of keys) {
  1008. if (playerId == this.mainPlayer || isObjectEmpty(this.players[playerId].hand)) {
  1009. continue;
  1010. }
  1011.  
  1012. var cardsHTML = "";
  1013. var left = 1;
  1014. for (var card in this.players[playerId].hand) {
  1015. var playedCard = this.game.card_types[this.players[playerId].hand[card].type];
  1016. var posX = -playedCard.backx;
  1017. var posY = -playedCard.backy;
  1018. const cardHtmlId = Player_Hand_Card_Id_Prefix + card;
  1019.  
  1020. cardsHTML += "<div id='" + cardHtmlId + "' style='left: " + left + "px;'>"
  1021. cardsHTML += "<div style='background-position: " + posX + "px " + posY + "px;'>";
  1022. cardsHTML += "<span>" + playedCard.nametr + "</span></div></div>";
  1023.  
  1024. left += 79;
  1025. }
  1026. this.dojo.place(cardsHTML, Player_Cards_Id_Prefix + playerId, "only");
  1027. }
  1028. },
  1029.  
  1030.  
  1031. // Render tooltips for cards in player hands
  1032. renderPlayerCardTooltips: function() {
  1033. const keys = Object.keys(this.players);
  1034. for (const playerId of keys) {
  1035. if (playerId == this.mainPlayer || isObjectEmpty(this.players[playerId].hand)) {
  1036. continue;
  1037. }
  1038. for (var card in this.players[playerId].hand) {
  1039. const tooltipId = "player_hand_item_" + card;
  1040.  
  1041. // Game correctness check
  1042. if (!window.parent.gameui.addTooltipHtml || !window.parent.gameui.tooltips ||
  1043. !window.parent.gameui.tooltips[tooltipId] || !window.parent.gameui.tooltips[tooltipId].label) {
  1044. continue;
  1045. }
  1046.  
  1047. window.parent.gameui.addTooltipHtml(Player_Hand_Card_Id_Prefix + card, window.parent.gameui.tooltips[tooltipId].label);
  1048. }
  1049. }
  1050. },
  1051.  
  1052. // Update total player score
  1053. renderPlayerScore: function(playerId, score = 0) {
  1054. if (this.isFinished) {
  1055. return;
  1056. }
  1057.  
  1058. var playerScore = this.dojo.byId(Player_Score_Id_Prefix + playerId);
  1059. if (playerScore) {
  1060. const totalScore = this.players[playerId].bgaScore + this.players[playerId].warScore;
  1061. playerScore.innerHTML = " (" + totalScore + ")";
  1062. this.dojo.query("#" + Player_War_Score_Id_Prefix + playerId + " span")[0]
  1063. .innerHTML = this.players[playerId].bgaScore + " (" + totalScore + ")";
  1064. }
  1065. },
  1066.  
  1067. // Add border and position of leader and runnerup players
  1068. renderLeaderRunnerup: function() {
  1069. if (!this.settings.enableRichBoards || this.isFinished) {
  1070. return;
  1071. }
  1072.  
  1073. // Clean previous leader & runnerup
  1074. this.dojo.query("." + Player_Leader_Class + ", ." + Player_Runnerup_Class)
  1075. .removeClass([Player_Leader_Class, Player_Runnerup_Class]);
  1076.  
  1077. // Find leader and runner ups
  1078. var totalScores = [];
  1079. const keys = Object.keys(this.players);
  1080. for (const playerId of keys) {
  1081. totalScores.push(
  1082. [playerId,
  1083. this.players[playerId].bgaScore + this.players[playerId].warScore,
  1084. this.players[playerId].coins
  1085. ]);
  1086. }
  1087.  
  1088. totalScores.sort(function(a, b) {
  1089. return b[1] - a[1] || b[2] - a[2];;
  1090. });
  1091.  
  1092. // Mark new ones
  1093. this.dojo.addClass(BGA_Player_Board_Id_Prefix + totalScores[0][0], Player_Leader_Class);
  1094. this.dojo.addClass(BGA_Player_Board_Id_Prefix + totalScores[1][0], Player_Runnerup_Class);
  1095. },
  1096.  
  1097. // Add laurel wreath and coins icons on top of the card container
  1098. renderCardPoints: function(cardId, isDiscard = false, pointsWorth = null, coinsWorth = null) {
  1099. // Leave if card has no worth info
  1100. if (pointsWorth === null && coinsWorth === null) {
  1101. return;
  1102. }
  1103.  
  1104. // Clean up previous worth in case we saw this card already
  1105. const containerId = isDiscard ? Discard_Card_Worth_Id_Prefix + cardId : Card_Worth_Id_Prefix + cardId;
  1106. this.dojo.destroy(containerId);
  1107.  
  1108. // Build HTML
  1109. const extraClass = this.settings.enableCardPoints ? "" : "pythia_hidden";
  1110. var html = "<span id='" + containerId + "' class='" + Card_Worth_Class + " " + extraClass + "'>";
  1111. if (coinsWorth !== null) {
  1112. html += "<img class='" + Card_Worth_Coins_Class + "' src='" + Coins_Image[coinsWorth] + "' />";
  1113. }
  1114. if (pointsWorth !== null) {
  1115. html += "<img src='" + Victory_Points_Image[pointsWorth] + "' />";
  1116. }
  1117. html += "</span>";
  1118.  
  1119. if (isDiscard) {
  1120. this.dojo.place(html, "discarded_item_" + cardId, "only");
  1121. } else {
  1122. this.dojo.place(html, "cardmenu_" + cardId, "after");
  1123. }
  1124. },
  1125.  
  1126. // Render shields icon next to player coins
  1127. renderMilitaryPower: function(playerId) {
  1128. if (!playerId || !this.players[playerId] || this.isFinished) {
  1129. return;
  1130. }
  1131.  
  1132. var container = this.dojo.query("#" + Player_Military_Power_Id_Prefix + playerId + " span");
  1133. if (container[0]) {
  1134. container[0].innerHTML = this.players[playerId].shields;
  1135. }
  1136. },
  1137.  
  1138. // Render Pythia menu
  1139. renderPythiaMenu: function() {
  1140. var menuHtml = "<div id='pythia_menu'>";
  1141. menuHtml += "<div class='menu_header'>";
  1142. menuHtml += "<h3>PYTHIA v" + GM_info.script.version + "</h3>";
  1143.  
  1144. // Show or hide Pythia menu based on the setting
  1145. if (this.settings.showMenu) {
  1146. menuHtml += "<div id='pythia_menu_list_toggle' class='toggle_container'><a href='javascript:void(0)' class='collapser' data-target='#pythia_menu .menu_content'>(collapse menu)</a>";
  1147. menuHtml += "<a href='javascript:void(0)' class='expander pythia_hidden' data-target='#pythia_menu .menu_content'>(expand menu)</a>";
  1148. menuHtml += "</div></div>";
  1149. menuHtml += "<div class='menu_content'>";
  1150. } else {
  1151. menuHtml += "<div id='pythia_menu_list_toggle' class='toggle_container'><a href='javascript:void(0)' class='collapser pythia_hidden' data-target='#pythia_menu .menu_content'>(collapse menu)</a>";
  1152. menuHtml += "<a href='javascript:void(0)' class='expander' data-target='#pythia_menu .menu_content'>(expand menu)</a>";
  1153. menuHtml += "</div></div>";
  1154. menuHtml += "<div class='menu_content pythia_hidden'>";
  1155. }
  1156.  
  1157. // Card Points setting
  1158. menuHtml += "<div id='pythia_menu_cardpoints' class='menu_item'><span class='title'>Cards Worth:</span>";
  1159. menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";
  1160.  
  1161. // Rich Boards setting
  1162. menuHtml += "<div id='pythia_menu_richboards' class='menu_item'><span class='title'>Rich Boards:</span>";
  1163. menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";
  1164.  
  1165. // War scores setting
  1166. menuHtml += "<div id='pythia_menu_warscores' class='menu_item'><span class='title'>War Scores:</span>";
  1167. menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";
  1168.  
  1169. // HD images setting
  1170. menuHtml += "<div id='pythia_menu_hd' class='menu_item'><span class='title'>HD Images:</span>";
  1171. menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";
  1172.  
  1173. // Fullwidth setting
  1174. menuHtml += "<div id='pythia_menu_fullwidth' class='menu_item'><span class='title'>Full Width:</span>";
  1175. menuHtml += "<span class='status'>Enabled</span><button type='button'>Disable</button></div>";
  1176.  
  1177. menuHtml += "</div>";
  1178. menuHtml += "</div>";
  1179. this.dojo.place(menuHtml, "logs_wrap", "before");
  1180.  
  1181. // Set correct texts based on settings
  1182. this.togglePythiaSettingText("pythia_menu_warscores", this.settings.enableWarScores);
  1183. this.togglePythiaSettingText("pythia_menu_richboards", this.settings.enableRichBoards);
  1184. this.togglePythiaSettingText("pythia_menu_cardpoints", this.settings.enableCardPoints);
  1185. this.togglePythiaSettingText("pythia_menu_hd", this.settings.enableHD);
  1186. this.togglePythiaSettingText("pythia_menu_fullwidth", this.settings.enableFullwidth);
  1187.  
  1188. // Connect event handlers
  1189. this.dojo.connect(this.dojo.query("button", "pythia_menu_warscores")[0], "onclick", this, "togglePythiaSettingWarScores");
  1190. this.dojo.connect(this.dojo.query("button", "pythia_menu_richboards")[0], "onclick", this, "togglePythiaSettingRichBoards");
  1191. this.dojo.connect(this.dojo.query("button", "pythia_menu_cardpoints")[0], "onclick", this, "togglePythiaSettingCardPoints");
  1192. this.dojo.connect(this.dojo.query("button", "pythia_menu_hd")[0], "onclick", this, "togglePythiaSettingHD");
  1193. this.dojo.connect(this.dojo.query("button", "pythia_menu_fullwidth")[0], "onclick", this, "togglePythiaSettingFullwidth");
  1194.  
  1195. // Expand & collapse menu handlers
  1196. this.dojo.connect(this.dojo.query(".menu_header .collapser")[0], "onclick", this, "togglePythiaSettingMenu");
  1197. this.dojo.connect(this.dojo.query(".menu_header .expander")[0], "onclick", this, "togglePythiaSettingMenu");
  1198. this.dojo.connect(this.dojo.query(".menu_header .collapser")[0], "onclick", this, "toggleCollapserExpander");
  1199. this.dojo.connect(this.dojo.query(".menu_header .expander")[0], "onclick", this, "toggleCollapserExpander");
  1200. },
  1201.  
  1202. // Enable or disable display of cards in player hands
  1203. togglePythiaSettingPlayerCardsDisplay: function(pleaseShow) {
  1204. if (pleaseShow) {
  1205. this.dojo.query("." + Player_Cards_Div_Class).removeClass("pythia_hidden");
  1206. this.dojo.query(".sw_coins", "boardspaces").addClass("pythia_player_cards_enabled");
  1207. } else {
  1208. this.dojo.query("." + Player_Cards_Div_Class).addClass("pythia_hidden");
  1209. this.dojo.query(".sw_coins", "boardspaces").removeClass("pythia_player_cards_enabled");
  1210. }
  1211. },
  1212.  
  1213. // Enable or disable display of war scores
  1214. togglePythiaSettingWarScores: function(event) {
  1215. this.settings.enableWarScores = !this.settings.enableWarScores;
  1216. localStorage.setItem("pythia-seetings-warscores", this.settings.enableWarScores);
  1217. this.togglePythiaSettingWarScoresDisplay(this.settings.enableWarScores);
  1218. this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableWarScores);
  1219. },
  1220. togglePythiaSettingWarScoresDisplay: function(pleaseShow) {
  1221. if (pleaseShow) {
  1222. this.dojo.query("." + Player_Score_Span_Class).removeClass("pythia_hidden");
  1223. } else {
  1224. this.dojo.query("." + Player_Score_Span_Class).addClass("pythia_hidden");
  1225. }
  1226. },
  1227.  
  1228. // Enable or disable display of leader and runnerup positions
  1229. togglePythiaSettingRichBoards: function(event) {
  1230. this.settings.enableRichBoards = !this.settings.enableRichBoards;
  1231. localStorage.setItem("pythia-seetings-richboards", this.settings.enableRichBoards);
  1232. this.togglePythiaSettingRichBoardsDisplay(this.settings.enableRichBoards);
  1233. this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableRichBoards);
  1234. },
  1235. togglePythiaSettingRichBoardsDisplay: function(pleaseShow) {
  1236. if (pleaseShow) {
  1237. this.renderLeaderRunnerup();
  1238. this.dojo.query(".pythia_player_war_score").removeClass("pythia_hidden");
  1239. this.dojo.query(".pythia_player_military_power").removeClass("pythia_hidden");
  1240. } else {
  1241. this.dojo.query("." + Player_Leader_Class + ", ." + Player_Runnerup_Class)
  1242. .removeClass([Player_Leader_Class, Player_Runnerup_Class]);
  1243.  
  1244. this.dojo.query(".pythia_player_war_score").addClass("pythia_hidden");
  1245. this.dojo.query(".pythia_player_military_power").addClass("pythia_hidden");
  1246. }
  1247. },
  1248.  
  1249. // Enable or disable display of cards points worth
  1250. togglePythiaSettingCardPoints: function(event) {
  1251. this.settings.enableCardPoints = !this.settings.enableCardPoints;
  1252. localStorage.setItem("pythia-seetings-cardpoints", this.settings.enableCardPoints);
  1253. this.togglePythiaSettingCardPointsDisplay(this.settings.enableCardPoints);
  1254. this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableCardPoints);
  1255. },
  1256. togglePythiaSettingCardPointsDisplay: function(pleaseShow) {
  1257. if (pleaseShow) {
  1258. this.dojo.query("." + Card_Worth_Class).removeClass("pythia_hidden");
  1259. } else {
  1260. this.dojo.query("." + Card_Worth_Class).addClass("pythia_hidden");
  1261. }
  1262. },
  1263.  
  1264. // Enable or disable HD graphics
  1265. togglePythiaSettingHD: function(event) {
  1266. this.settings.enableHD = !this.settings.enableHD;
  1267. localStorage.setItem("pythia-seetings-hd", this.settings.enableHD);
  1268. this.togglePythiaSettingHDDisplay(this.settings.enableHD);
  1269. this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableHD);
  1270. },
  1271. togglePythiaSettingHDDisplay: function(pleaseShow) {
  1272. this.dojo.query("body").toggleClass("pythia_hd", pleaseShow);
  1273. },
  1274.  
  1275. // Hide or show elements for fullwidth mode
  1276. togglePythiaSettingFullwidth: function(event) {
  1277. this.settings.enableFullwidth = !this.settings.enableFullwidth;
  1278. localStorage.setItem("pythia-seetings-fullwidth", this.settings.enableFullwidth);
  1279. this.togglePythiaSettingFullwidthDisplay(this.settings.enableFullwidth);
  1280. this.togglePythiaSettingText(event.target.parentNode.id, this.settings.enableFullwidth);
  1281. },
  1282. togglePythiaSettingFullwidthDisplay: function(pleaseShow) {
  1283. this.dojo.query("body").toggleClass("pythia_fullwidth", pleaseShow);
  1284. this.dojo.toggleClass("right-side-first-part", "pythia_hidden", pleaseShow);
  1285. this.dojo.toggleClass("logs_wrap", "pythia_hidden", pleaseShow);
  1286. },
  1287.  
  1288. togglePythiaSettingMenu: function(event) {
  1289. this.settings.showMenu = event.target.className == "expander";
  1290. localStorage.setItem("pythia-seetings-showmenu", this.settings.showMenu);
  1291. },
  1292.  
  1293. // Switch enable/disable text in Pythia settings
  1294. togglePythiaSettingText: function(parentId, isEnabled) {
  1295. if (isEnabled) {
  1296. this.dojo.query(".status", parentId)
  1297. .addClass("enabled")
  1298. .removeClass("disabled")[0]
  1299. .innerHTML = "Enabled";
  1300. this.dojo.query("button", parentId)[0].innerHTML = "Disable";
  1301. } else {
  1302. this.dojo.query(".status", parentId)
  1303. .addClass("disabled")
  1304. .removeClass("enabled")[0]
  1305. .innerHTML = "Disabled";
  1306. this.dojo.query("button", parentId)[0].innerHTML = "Enable";
  1307. }
  1308. },
  1309.  
  1310. // Toggle hide / show of any visual element given data-target
  1311. toggleCollapserExpander: function(event) {
  1312. this.dojo.query(".collapser, .expander", event.target.parentNode).toggleClass("pythia_hidden");
  1313. this.dojo.query(event.target.getAttribute('data-target')).toggleClass("pythia_hidden");
  1314. },
  1315.  
  1316. // Is this the first turn of the age?
  1317. isFirstTurn: function() {
  1318. return isObjectEmpty(this.players[this.mainPlayer].hand);
  1319. },
  1320.  
  1321. // Set Pythia CSS styles
  1322. setStyles: function() {
  1323. this.dojo.query("body").addClass("pythia_enabled");
  1324. this.dojo.place(
  1325. "<style type='text/css' id='Pythia_Styles'>" +
  1326. // Generic settings
  1327. ".pythia_player_cards_enabled.sw_coins { top: 50px; } " +
  1328. ".pythia_enabled.arena_mode .player_elo_wrap { visibility: visible; }" +
  1329. ".pythia_enabled #player_board_wrap_" + this.mainPlayer + " .sw_coins { top: 0px; } " +
  1330. ".pythia_enabled #player_hand_wrap { padding-top: 58px; } " +
  1331. ".pythia_enabled #discarded_wrap h3 { padding-bottom: 60px; } " +
  1332. ".pythia_enabled #howto_tutorial { display: none; } " +
  1333. ".pythia_enabled .pythia_hidden { display: none; } " +
  1334. ".pythia_enabled .toggle_container { display: inline; } " +
  1335.  
  1336. // Pythia menu
  1337. "#pythia_menu { font-size: 14px; padding: 10px; position: relative; background-color: #e9d6bf; border: 1px solid black; border-radius: 5px; } " +
  1338. "#pythia_menu .menu_header h3 { display: inline; } " +
  1339. "#pythia_menu .menu_header a { margin-left: 5px; } " +
  1340. "#pythia_menu .menu_content { margin-top: 10px; } " +
  1341. "#pythia_menu .menu_item { height: 26px; } " +
  1342. "#pythia_menu .menu_item span.title { width: 90px; display: inline-block;} " +
  1343. "#pythia_menu .menu_item span.status { text-align: center; width: 55px; display: inline-block; } " +
  1344. "#pythia_menu .menu_item span.status.enabled { color: green; } " +
  1345. "#pythia_menu .menu_item span.status.disabled { color: red; } " +
  1346. "#pythia_menu .menu_item button { width: 60px; padding: 3px; border-radius: 5px; margin-left: 10px; } " +
  1347.  
  1348. // Player cards
  1349. "." + Player_Cards_Div_Class + " { display: block; height: 50px; } " +
  1350. "." + Player_Cards_Div_Class + " div { position: absolute; top: 11px; } " +
  1351. "." + Player_Cards_Div_Class + " div div { background-image: url(" + Cards_Image + "); width: 128px; height: 45px; zoom: 0.6; } " +
  1352. "." + Player_Cards_Div_Class + " div div span { width: 100%; text-align: center; position: absolute; left: 0; top: -25px; font-size: 18px; color: black; cursor: default; } " +
  1353.  
  1354. // Rich boards
  1355. "." + Player_Leader_Class + ", ." + Player_Runnerup_Class + " { border: 5px solid; } " +
  1356. "." + Player_Leader_Class + " { border-color: green; } " +
  1357. "." + Player_Runnerup_Class + " { border-color: red; border-style: inset; } " +
  1358. "." + Player_Leader_Class + " h3::before, ." + Player_Runnerup_Class + " h3::before { float: left; margin-top: -4px; white-space: pre; }" +
  1359. "." + Player_Leader_Class + " h3::before { content: '(Leader) '; color: green; }" +
  1360. "." + Player_Runnerup_Class + " h3::before { content: '(Runner up) '; color: red; }" +
  1361. ".pythia_player_military_power { display: inline; position: relative; top: 3px; }" +
  1362. ".pythia_player_military_power img { width: 30px; padding: 0 4px; }" +
  1363. ".pythia_player_military_power span { position: relative; top: -7px; }" +
  1364. ".pythia_player_war_score { display: inline; padding-right: 4px; position: relative; top: -1px; }" +
  1365. ".pythia_player_war_score i { font-size: 32px; }" +
  1366. ".pythia_player_war_score span { padding-left: 4px; position: relative; top: -3px; }" +
  1367.  
  1368. // Cards worth
  1369. "." + Card_Worth_Class + " { position: absolute; top: -53px; left: 6px; width: 128px; text-align: center; }" +
  1370. "." + Card_Worth_Class + " img { width: 48px; }" +
  1371. "." + Card_Worth_Class + " img." + Card_Worth_Coins_Class + " { position: relative; top: -3px; }" +
  1372.  
  1373. // War scores
  1374. "." + Player_Score_Span_Class + " { display: inline; }" +
  1375.  
  1376. // Fullwidth view
  1377. ".pythia_fullwidth #left-side { margin-right: 0px; } " +
  1378.  
  1379. // New edition styles
  1380. ".new_edition ." + Player_Cards_Div_Class + " div div { background-image: url(" + Cards_Image_V2 + "); width: 255px; height: 110px; zoom: 0.3; text-align: center;} " +
  1381. ".new_edition ." + Player_Cards_Div_Class + " div div span { font-size: 18px; top: 7px; } " +
  1382. ".new_edition .last_board_item { padding-right: 2px; } " +
  1383. ".new_edition .last_board_item .board_item { border-width: 4px; margin: -2px 0 0 -2px; border-color: greenyellow; border-style: outset; } " +
  1384. ".new_edition .last_step_item { border-width: 4px; margin: -4px 0 0 -4px; border-color: greenyellow; border-style: outset; } " +
  1385.  
  1386. // New edition HD boards
  1387. "#pythia_menu_hd { display: none; } " +
  1388. ".new_edition #pythia_menu_hd { display: block; } " +
  1389. ".new_edition.pythia_hd .wonder_face, .new_edition.pythia_hd .player_board_wonder { background-size: 450px 3122px; background-image: url(" + HD_Boards + "); }" +
  1390.  
  1391. "</style>", "sevenwonder_wrap", "last");
  1392. }
  1393. };
  1394.  
  1395. function sleep(ms) {
  1396. return new Promise(resolve => setTimeout(resolve, ms));
  1397. }
  1398.  
  1399. function isObjectEmpty(object) {
  1400. return typeof(object) == "undefined" ||
  1401. (Object.keys(object).length === 0 && object.constructor === Object);
  1402. }
  1403.  
  1404. // Everything starts here
  1405.  
  1406. // Everything starts here
  1407. var onload = async function() {
  1408. if (Is_Inside_Game) {
  1409. await sleep(3000); // Wait for BGA to load dojo and 7W scripts
  1410. if (!window.parent || !window.parent.gameui || !window.parent.gameui.game_name ||
  1411. window.parent.gameui.game_name != "sevenwonders") {
  1412. return;
  1413. }
  1414.  
  1415. // Prevent multiple launches
  1416. if (window.parent.isPythiaStarted) {
  1417. return;
  1418. } else {
  1419. if (Enable_Logging) console.log("PYTHIA: I have come to serve you");
  1420. window.parent.isPythiaStarted = true;
  1421. window.parent.pythia = pythia.init();
  1422. }
  1423. }
  1424. };
  1425.  
  1426. if (document.readyState === "complete") {
  1427. onload();
  1428. } else {
  1429. (addEventListener || attachEvent).call(window, addEventListener ? "load" : "onload", onload);
  1430. }