BGA Pythia - 7 Wonders Architects game helper

Visual aid that extends BGA game interface with useful information

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         BGA Pythia - 7 Wonders Architects game helper
// @description  Visual aid that extends BGA game interface with useful information
// @namespace    https://github.com/dpavliuchkov/bga-pythia
// @author       https://github.com/dpavliuchkov
// @version      1.2.6
// @license      MIT
// @include      *boardgamearena.com/*
// @grant        none
// ==/UserScript==

// System variables - don't edit
const Enable_Logging = false;
const Is_Inside_Game = /\?table=[0-9]*/.test(window.location.href);
const BGA_Player_Scoreboard_Id_Prefix = "overall_player_board_";
const BGA_Player_Score_Right_Id_Prefix = "player_name_";
const BGA_Player_Score_Main_Id_Prefix = "playerarea_";
const BGA_Progress_Id_Prefix = "pg_";
const Player_Score_Right_Id_Prefix = "pythia_score_right_";
const Player_Score_Main_Id_Prefix = "pythia_score_main_";
const Progress_Worth_Id_Prefix = "pythia_progress_worth_";
const Player_Score_Span_Class = "player_score_value";
const Player_Leader_Class = "pythia_leader";
const Progress_Worth_Class = "progress_worth";
const Cat_Card_Type_Id = 16;
const Politics_Progress_Type_Id = 2;
const Decor_Progress_Type_Id = 6;
const Strategy_Progress_Type_Id = 8;
const Education_Progress_Type_Id = 12;
const Culture_Progress_Type_Id = 13;
const Cat_Pawn_Type_Id = 17;
const Decor_Points = 4;

// progress tokens - type args
// 1 - science draw
// 2 - cat politics
// 3 - wood / clay
// 4 - ??
// 5 - double gold
// 6 - decor
// 7 - wonder stages
// 8 - victories
// 9 - two shields
// 10 - horns
// 11 - ??
// 12 - education
// 13 - culture
// 14 - engineering
// 15 - ??
// 16 - stone gold
// 17 - cat pawn

// big game https://boardgamearena.com/1/sevenwondersarchitects?table=227901083


// Main Pythia object
var pythia = {
    isStarted: false,
    isFinished: false,
    dojo: null,
    game: null,
    mainPlayerId: null,
    players: [],

    // Init Pythia
    init: function() {
        this.isStarted = true;
        // Check if the site was loaded correctly
        if (!window.parent || !window.parent.dojo || !window.parent.gameui.gamedatas) {
            return;
        }
        this.dojo = window.parent.dojo;
        this.game = window.parent.gameui.gamedatas;

        const playerOrder = this.game.playerorder;
        this.playersCount = playerOrder.length;
        this.mainPlayerId = playerOrder[0];

        // Prepare player objects and containers
        const keys = Object.keys(this.game.players);
        for (const playerId of keys) {
            this.players[playerId] = {
                bgaScore: 0,
                wonderStages: 0,
                totalCatCards: 0,
                totalWarVictories: 0,
                totalProgressTokens: 0,
                hasDecor: false,
                hasCulture: false,
                hasEducation: false,
            };
            this.renderPythiaContainers(playerId);
        }

        // Prepare progress tokens
        this.progressTokens = {};
        this.initProgressTokens();

        this.setStyles();

        // Connect event handlers to follow game progress
        this.dojo.subscribe("updateScore", this, "recordScoreUpdate");
        this.dojo.subscribe("getProgress", this, "recordProgressToken");
        this.dojo.subscribe("getCard", this, "recordGetCard");
        this.dojo.subscribe("flipWonderStage", this, "recordWonderStage");
        this.dojo.subscribe("conflictResult", this, "recordWarResult");
        this.dojo.subscribe("showProgress", this, "recordProgressShow");
        this.dojo.subscribe("victory", this, "recordVictory");

        if (Enable_Logging) console.log("PYTHIA: My eyes can see everything!");
        return this;
    },

    // Record new scores
    recordScoreUpdate: function(data) {
        if (Enable_Logging) console.log("PYTHIA: scores updated - I got", data);

        // Input check
        if (!data || !data.args) {
            return;
        }

        const playerId = data.args.player_id;
        this.players[playerId].bgaScore = data.args.score;

        const totalScore = this.getPlayerScore(playerId);
        this.renderPlayerScore(playerId, totalScore);

        const leaderId = this.getLeader();
        this.renderLeader(leaderId);
    },

    // Record which card a player got
    recordGetCard: function(data) {
        if (Enable_Logging) console.log("PYTHIA: player took a card - I got", data);

        // Input check
        if (!data || !data.args || !data.args.card) {
            return;
        }

        const playerId = data.args.player_id;
        var playerObjectChanged = false;

        // Increment if player drew a cat card
        if (data.args.card.type == Cat_Card_Type_Id) {
            this.players[playerId].totalCatCards++;
            playerObjectChanged = true;
        }

        // Update score values for progress tokens
        if (playerObjectChanged && playerId == this.mainPlayerId) {
            this.updateAllProgressWorth();
        }
    },

    // Record when a wonder stage was built
    recordWonderStage: function(data) {
        if (Enable_Logging) console.log("PYTHIA: wonder stage built - I got", data);

        // Input check
        if (!data || !data.args || !data.args.player_id) {
            return;
        }

        const playerId = data.args.player_id;
        this.players[playerId].wonderStages++; // increase a counter of built wonder stages

        // 5th wonder stage means the game is over
        if (this.players[playerId].wonderStages == 5) {
            this.isFinished = true;
        }
    },

    // Record which progress a player got
    recordProgressToken: function(data) {
        if (Enable_Logging) console.log("PYTHIA: player took a progress token - I got", data);

        // Input check
        if (!data || !data.args || !data.args.progress) {
            return;
        }

        // Skip movements of the cat pawn
        if (data.args.progress.type_arg == Cat_Pawn_Type_Id) {
            return;
        }

        const playerId = data.args.player_id;
        const token = data.args.progress;

        // Track progress tokens that can give victory points
        if (token.type_arg == Decor_Progress_Type_Id) {
            this.players[playerId].hasDecor = true;
        }
        if (token.type_arg == Culture_Progress_Type_Id) {
            this.players[playerId].hasCulture = true;
        }
        if (token.type_arg == Education_Progress_Type_Id) {
            this.players[playerId].hasEducation = true;
        }

        // Increment total progress tokens
        this.players[playerId].totalProgressTokens++;

        // Remove this token from the open list
        delete this.progressTokens[token.id];

        // Remove progress worth container
        this.dojo.destroy(Progress_Worth_Id_Prefix + token.id);

        // Update score values for progress tokens
        if (playerId == this.mainPlayerId) {
            this.updateAllProgressWorth();
        }
    },

    // Record war results
    recordWarResult: function(data) {
        if (Enable_Logging) console.log("PYTHIA: war has ended - I got", data);

        // Input check
        if (!data || !data.args || !data.args.score) {
            return;
        }

        // Update who got military win tokens
        const warResults = data.args.score;
        for (const playerId in warResults) {
            this.players[playerId].totalWarVictories += warResults[playerId].length;
        }

        // Update score values for progress tokens
        this.updateAllProgressWorth();
    },

    // Record which new progress token was shown
    recordProgressShow: function(data) {
        if (Enable_Logging) console.log("PYTHIA: new progress token displayed - I got", data);

        // Input check
        if (!data || !data.args || !data.args.progress) {
            return;
        }

        // Add this token to the open list
        const token = data.args.progress;
        this.progressTokens[token.id] = token.type_arg;

        // Update score values for progress tokens
        const progressWorth = this.getProgressWorth(token.type_arg, this.mainPlayerId);
        this.renderProgressWorth(token.id, progressWorth);
    },

    // Record that the game has ended
    recordVictory: function(data) {
        if (Enable_Logging) console.log("PYTHIA: game has finished - I got", data);

        // Input check
        if (!data || !data.args || !data.args.score) {
            return;
        }

        this.isFinished = true;
        const finalScores = data.args.score;

        const keys = Object.keys(finalScores);
        for (const playerId of keys) {
           this.renderPlayerScore(playerId, finalScores[playerId].total);
        }

        const leaderId = this.getLeader();
        this.renderLeader(leaderId);
    },

    renderProgressWorth: function(progressId, worth = 0) {
        // Clean previous value
        this.dojo.destroy(Progress_Worth_Id_Prefix + progressId);

        // Render progress worth
        this.dojo.place(
            "<span id='" + Progress_Worth_Id_Prefix + progressId + "'" +
                " class='" + Progress_Worth_Class + "'>⭐" + worth + "</span>",
            BGA_Progress_Id_Prefix + progressId,
            "first");
    },

    // Update total player score
    renderPlayerScore: function(playerId, score = 0) {
        var playerScore = this.dojo.byId(Player_Score_Main_Id_Prefix + playerId);
        if (playerScore) {
            this.dojo.query("#" + Player_Score_Main_Id_Prefix + playerId)[0]
                .innerHTML = "⭐" + score;
            this.dojo.query("#" + Player_Score_Right_Id_Prefix + playerId)[0]
                .innerHTML = "⭐" + score;
        }
    },

    // Add border and position of the leader player
    renderLeader: function(leaderId) {
        // Clean previous leader
        this.dojo.query("." + Player_Leader_Class).removeClass(Player_Leader_Class);

        // Mark new leader
        this.dojo.addClass(BGA_Player_Scoreboard_Id_Prefix + leaderId, Player_Leader_Class);
        this.dojo.addClass(BGA_Player_Score_Main_Id_Prefix + leaderId, Player_Leader_Class);
    },

    // Render player containers
    renderPythiaContainers: function(playerId) {
        // Insert war score container in scores table
        if (!this.dojo.byId(Player_Score_Main_Id_Prefix + playerId)) {
            const mainPlayerArea = this.dojo.query("#" + BGA_Player_Score_Main_Id_Prefix + playerId + " .stw_name_holder");
            this.dojo.place(
                "<span id='" + Player_Score_Main_Id_Prefix + playerId + "'" +
                "class='" + Player_Score_Span_Class + "'>⭐0</span>",
                mainPlayerArea[0],
                "first");

            this.dojo.place(
                "<span id='" + Player_Score_Right_Id_Prefix + playerId + "'" +
                "class='" + Player_Score_Span_Class + "'>⭐0</span>",
                BGA_Player_Score_Right_Id_Prefix + playerId,
                "first");
        }
    },

    // Called at the game start to detect which progress tokens were drawn
    initProgressTokens: function() {
        const tokens = this.dojo.query("#science .progress");
        for (const token of tokens) {
            if (!token.style || !token.id) {
                continue;
            }

            const tokenId = parseInt(token.id.substr(3));
            if (tokenId == 0) {
                continue;
            }

            // Calculate token background - to find which token is displayed
            const posX = Math.abs(parseInt(token.style.backgroundPositionX) / 100);
            const posY = Math.abs(parseInt(token.style.backgroundPositionY) / 100);

            var tokenType;
            // Relevant tokens
            if (posY == 0 && posX == 2) {
                tokenType = Politics_Progress_Type_Id;
            } else if (posY == 1 && posX == 2) {
                tokenType = Decor_Progress_Type_Id;
            } else if (posY == 2 && posX == 0) {
                tokenType = Strategy_Progress_Type_Id;
            } else if (posY == 3 && posX == 0) {
                tokenType = Education_Progress_Type_Id;
            } else if (posY == 3 && posX == 1) {
                tokenType = Culture_Progress_Type_Id;
            } else {
                // Not relevant token
                tokenType = 0;
            }

            this.progressTokens[tokenId] = tokenType;
            const progressWorth = this.getProgressWorth(tokenType, this.mainPlayerId);
            this.renderProgressWorth(tokenId, progressWorth);
        }
    },

    // Update worth of all visible progress tokens
    updateAllProgressWorth: function() {
        for (var tokenId in this.progressTokens) {
            const tokenType = this.progressTokens[tokenId];
            const progressWorth = this.getProgressWorth(tokenType, this.mainPlayerId);
            this.renderProgressWorth(tokenId, progressWorth);
        }
    },

    // Calculate how much this progress is worth for this player
    getProgressWorth: function(tokenType, playerId) {
        const player = this.players[playerId];
        var pointsWorth = 0;

        switch (parseInt(tokenType)) {
            case Politics_Progress_Type_Id:
                pointsWorth = player.totalCatCards;
                break;

            case Decor_Progress_Type_Id:
                pointsWorth = Decor_Points;
                break;

            case Strategy_Progress_Type_Id:
                pointsWorth = player.totalWarVictories;
                break;

            case Education_Progress_Type_Id:
                pointsWorth = 2 + player.totalProgressTokens * 2;
                break;

            case Culture_Progress_Type_Id:
                pointsWorth = 4;
                if (player.hasCulture) {
                    pointsWorth = 8;
                }
                break;
        }

        if (player.hasEducation) {
            pointsWorth += 2;
        }

        return pointsWorth;
    },

    // Get total player score at teh current moment of game
    getPlayerScore: function(playerId) {
        const player = this.players[playerId];
        var totalScore = player.bgaScore;

        // If game not finished - add 4 points for decor progress
        if (!this.isFinished && player.hasDecor) {
            totalScore += Decor_Points;
        }
        return totalScore;
    },

    // Calculate who is the current leader
    getLeader: function() {
        // Find leader
        var totalScores = [];
        const keys = Object.keys(this.players);
        for (const playerId of keys) {
            totalScores.push(
                [playerId,
                    this.getPlayerScore(playerId),
                    this.players[playerId].wonderStages
                ]);
        }
        totalScores.sort(function(a, b) {
            return b[1] - a[1] || b[2] - a[2];
        });

        return totalScores[0][0];
    },

    // Set Pythia CSS styles
    setStyles: function() {
        this.dojo.query("body").addClass("pythia_enabled");
        this.dojo.place(
            "<style type='text/css' id='Pythia_Styles'>" +
            "." + Player_Leader_Class + " .player-name" +
              ", ." + Player_Leader_Class + " a" +
              ", ." + Player_Leader_Class + " .stw_name_holder" +
              " { background-color: green; border-radius: 5px; color: white ! important; } " +
            ".stw_name_holder ." + Player_Score_Span_Class + " { margin-right: 10px; margin-top: -6px; } " +
            "#science ." + Progress_Worth_Class + "  { display: block; position: relative; top: -32px; left: 25%; height: 25px; width: 34px; background: #F5EDE6; padding-left: 4px; padding-bottom: 3px; border-radius: 10px; } " +
            "</style>", "game_play_area_wrap", "last");
    }
};

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function isObjectEmpty(object) {
    return typeof(object) == "undefined" ||
        (Object.keys(object).length === 0 && object.constructor === Object);
}

// Everything starts here
var onload = async function() {
    if (Is_Inside_Game) {
        await sleep(3000); // Wait for BGA to load dojo and 7W scripts
        if (!window.parent || !window.parent.gameui || !window.parent.gameui.game_name ||
            window.parent.gameui.game_name != "sevenwondersarchitects") {
            return;
        }

        // Prevent multiple launches
        if (window.parent.isPythiaStarted) {
            return;
        } else {
            if (Enable_Logging) console.log("PYTHIA: I have come to serve you");
            window.parent.isPythiaStarted = true;
            window.parent.pythia = pythia.init();
        }
    }
};

if (document.readyState === "complete") {
    onload();
} else {
    (addEventListener || attachEvent).call(window, addEventListener ? "load" : "onload", onload);
}