Travian Kingdoms Tools

Travian Kingdoms Tools: Search for valleys and oases

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name Travian Kingdoms Tools
// @description Travian Kingdoms Tools: Search for valleys and oases
// @match https://*.kingdoms.com/*
// @version 1.0.1
// @namespace https://greasyfork.org/users/563852
// ==/UserScript==

(async () => {
    'use strict';

    let session = undefined;
    let searching = false;

    (function(request) {
        XMLHttpRequest.prototype.send = function() {
            const argument = arguments[0];

            if (typeof argument === 'string' || argument instanceof String) {
                if (argument.includes('session')) {
                    session = JSON.parse(argument).session;
                }
            }

            request.apply(this, arguments);
        };
    })(XMLHttpRequest.prototype.send);

    setInterval(async () => {
        const node = document.querySelector('.travian-kingdoms-tools');

        if (window.location.href.includes('page:map')) {
            if (!node) {
                const { text, node, nodeOne, nodeTwo } = createFooter();

                node.addEventListener('click', async () => {
                    const timestamp = new Date();

                    if (searching) {
                        text.innerText = 'Search for valleys and oases';
                        nodeOne.style.display = 'none';
                        nodeTwo.style.display = 'none';
                    } else {
                        text.innerText = 'Searching will take a moment...';

                        const { valleys: mapValleys, oases: mapOases, names } = await processMap();
                        const { valleys: oasesValleys, oases } = await processOases(session, mapValleys, mapOases, names);
                        const valleys = await processValleys(oasesValleys, oases);

                        const { valleyTable, oasisTable } = createTables();
                        oases.map((oasis, index) => createRow(oasisTable, index + 1, createOasisContent(oasis)));
                        valleys.map((valley, index) => createRow(valleyTable, index + 1, createValleyContent(valley)));

                        text.innerText = `Searching was successfully completed in ${parseInt(((new Date() - timestamp) / 1000).toString())} seconds!`;
                        nodeOne.appendChild(valleyTable);
                        nodeTwo.appendChild(oasisTable);
                        nodeOne.style.display = 'block';
                        nodeTwo.style.display = 'block';
                    }

                    searching = !searching;
                });
            }
        } else {
            if (node) {
                node.remove();
                document.querySelector('.travian-kingdoms-tools-table-one').remove();
                document.querySelector('.travian-kingdoms-tools-table-two').remove();
            }
        }
    }, 1000);
})();

const processMap = async () => {
    const mapVillageId = /villId:(\d+)/.exec(window.location.toString());
    const { response: { privateApiKey } } = await (await fetch(`https://${window.location.hostname}/api/external.php?action=requestApiKey&[email protected]&siteName=Example&siteUrl=https://example.com&public=0`)).json();
    const { response: { map: { cells }, players } } = await (await fetch(`https://${window.location.hostname}/api/external.php?action=getMapData&privateApiKey=${privateApiKey}`)).json();
    const valleys = [];
    const oases = [];
    let villageX = 0;
    let villageY = 0;

    if (mapVillageId) {
        players.map(({ villages }) => {
            villages.map(({ villageId, x, y }) => {
                if (villageX === 0 && villageY === 0 && villageId === mapVillageId[1]) {
                    villageX = parseInt(x);
                    villageY = parseInt(y);
                }
            });
        });
    }

    const names = (await Promise.all(cells.map(async ({ id, x, y, resType, oasis }) => {
        if (['10', '11', '20', '21', '30', '31', '40', '41'].includes(oasis)) {
            oases.push({
                externalId: parseInt(id),
                type: parseInt(oasis),
                x: parseInt(x),
                y: parseInt(y),
                distance: parseInt(Math.sqrt(Math.pow(x - villageX, 2) + Math.pow(y - villageY, 2)).toString()),
                elephant: 0,
                tiger: 0,
                crocodile: 0,
                bear: 0,
                wolf: 0,
                boar: 0,
                bat: 0,
                snake: 0,
                spider: 0,
                rat: 0,
            });

            return `MapDetails:${id}`;
        }

        if (['11115', '3339'].includes(resType)) {
            valleys.push({
                externalId: parseInt(id),
                type: parseInt(resType),
                x: parseInt(x),
                y: parseInt(y),
                bonus: 0,
                distance: parseInt(Math.sqrt(Math.pow(x - villageX, 2) + Math.pow(y - villageY, 2)).toString()),
                occupied: false,
                oases: [],
            });

            return `MapDetails:${id}`;
        }
    }))).filter(content => content !== undefined);

    return { valleys, oases, names };
};

const processOases = async (session, valleys, oases, names) => {
    const maximum = 999;
    const { cache } = await (await fetch(`https://${window.location.hostname}/api/?c=cache&a=get`, {
        method: 'POST',
        body: JSON.stringify({ controller: 'cache', action: 'get', params: { names, session } }),
    })).json();

    cache.map(({ name, data: { isOasis, isHabitable, oasisStatus, hasVillage, hasNPC, troops: { units } = { units: {} } } }) => {
        const innerExternalId = parseInt(name.split(':')[1]);

        if (isHabitable) {
            const valley = valleys.filter(({ externalId }) => externalId === innerExternalId)[0];
            valley.occupied = parseInt(hasVillage) === 1 || parseInt(hasNPC) === 1;
        }

        if (isOasis && !['1'].includes(oasisStatus)) {
            const oasis = oases.filter(({ externalId }) => externalId === innerExternalId)[0];

            if (units[10]) {
                oasis.elephant = parseInt(units[10]);
                oasis.elephant = oasis.elephant > maximum ? maximum : oasis.elephant;
            }

            if (units[9]) {
                oasis.tiger = parseInt(units[9]);
                oasis.tiger = oasis.tiger > maximum ? maximum : oasis.tiger;
            }

            if (units[8]) {
                oasis.crocodile = parseInt(units[8]);
                oasis.crocodile = oasis.crocodile > maximum ? maximum : oasis.crocodile;
            }

            if (units[7]) {
                oasis.bear = parseInt(units[7]);
                oasis.bear = oasis.bear > maximum ? maximum : oasis.bear;
            }

            if (units[6]) {
                oasis.wolf = parseInt(units[6]);
                oasis.wolf = oasis.wolf > maximum ? maximum : oasis.wolf;
            }

            if (units[5]) {
                oasis.boar = parseInt(units[5]);
                oasis.boar = oasis.boar > maximum ? maximum : oasis.boar;
            }

            if (units[4]) {
                oasis.bat = parseInt(units[4]);
                oasis.bat = oasis.bat > maximum ? maximum : oasis.bat;
            }

            if (units[3]) {
                oasis.snake = parseInt(units[3]);
                oasis.snake = oasis.snake > maximum ? maximum : oasis.snake;
            }

            if (units[2]) {
                oasis.spider = parseInt(units[2]);
                oasis.spider = oasis.spider > maximum ? maximum : oasis.spider;
            }

            if (units[1]) {
                oasis.rat = parseInt(units[1]);
                oasis.rat = oasis.rat > maximum ? maximum : oasis.rat;
            }
        }
    });

    oases.sort((one, two) => {
        let sortation = undefined;

        ['elephant', 'tiger', 'crocodile', 'bear', 'wolf', 'boar', 'bat', 'snake', 'spider', 'rat'].map(animal => {
            if (one[animal] !== two[animal] && !sortation) {
                sortation = one[animal] > two[animal] ? -1 : 1;
            }
        });

        return sortation ? sortation : one.distance > two.distance ? 1 : -1;
    });

    return { valleys, oases };
};

const processValleys = (valleys, oases) => {
    const innerValleys = valleys.map(valley => {
        const bonuses = [];

        oases
            .filter(({ x, y }) => x > valley.x - 4 && x < valley.x + 4 && y > valley.y - 4 && y < valley.y + 4)
            .map(({ type }) => {
                switch (type) {
                    case 41:
                        bonuses.push(50);
                        break;
                    case 40:
                    case 31:
                    case 21:
                    case 11:
                        bonuses.push(25);
                        break;
                    default:
                        bonuses.push(0);
                        break;
                }

                valley.oases.push({ type });
            });

        bonuses.sort((one, two) => two - one);
        valley.oases.sort(({ type: oneType }, { type: twoType }) => {
            if ([41, 31, 21, 11].includes(twoType)) {
                return [41, 31, 21, 11].includes(oneType) ? twoType - oneType : 1;
            } else {
                return [41, 31, 21, 11].includes(oneType) ? -1 : twoType - oneType;
            }
        });

        return { ...valley, bonus: bonuses.slice(0, 3).reduce((accumulator, bonus) => accumulator + bonus, 0) };
    });

    innerValleys.sort((
            { type: oneType, bonus: oneBonus, distance: oneDistance, occupied: oneOccupied },
            { type: twoType, bonus: twoBonus, distance: twoDistance, occupied: twoOccupied },
        ) => {
            if (oneOccupied !== twoOccupied) {
                return oneOccupied - twoOccupied;
            } else {
                if (oneType !== twoType) {
                    return twoType - oneType;
                } else {
                    return oneBonus !== twoBonus ? twoBonus - oneBonus : oneDistance - twoDistance;
                }
            }
        },
    );

    return innerValleys;
};

const createFooter = () => {
    const body = document.querySelector('body');

    const text = document.createElement('div');
    text.innerText = 'Search for valleys and oases';

    const node = document.createElement('div');
    node.style.position = 'fixed';
    node.style.bottom = '0';
    node.style.right = '0';
    node.style.left = '0';
    node.style.backgroundColor = '#000000';
    node.style.color = '#FFFFFF';
    node.style.lineHeight = '25px';
    node.style.textAlign = 'center';
    node.style.cursor = 'pointer';
    node.style.zIndex = '10000';
    node.classList.add('travian-kingdoms-tools');

    const nodeOne = document.createElement('div');
    nodeOne.style.display = 'none';
    nodeOne.style.float = 'left';
    nodeOne.style.width = '50%';
    nodeOne.style.position = 'fixed';
    nodeOne.style.bottom = '25px';
    nodeOne.style.right = '50%';
    nodeOne.style.left = '0';
    nodeOne.style.height = '279px';
    nodeOne.style.maxHeight = '279px';
    nodeOne.style.backgroundColor = '#000000';
    nodeOne.style.color = '#FFFFFF';
    nodeOne.style.lineHeight = '25px';
    nodeOne.style.textAlign = 'center';
    nodeOne.style.cursor = 'pointer';
    nodeOne.style.zIndex = '10000';
    nodeOne.style.border = '1px solid #000000';
    nodeOne.style.overflowY = 'scroll';
    nodeOne.classList.add('travian-kingdoms-tools-table-one');

    const nodeTwo = document.createElement('div');
    nodeTwo.style.display = 'none';
    nodeTwo.style.float = 'right';
    nodeTwo.style.width = '50%';
    nodeTwo.style.position = 'fixed';
    nodeTwo.style.bottom = '25px';
    nodeTwo.style.right = '0';
    nodeTwo.style.left = '50%';
    nodeTwo.style.height = '279px';
    nodeTwo.style.maxHeight = '279px';
    nodeTwo.style.backgroundColor = '#000000';
    nodeTwo.style.color = '#FFFFFF';
    nodeTwo.style.lineHeight = '25px';
    nodeTwo.style.textAlign = 'center';
    nodeTwo.style.cursor = 'pointer';
    nodeTwo.style.zIndex = '10000';
    nodeTwo.style.border = '1px solid #000000';
    nodeTwo.style.overflowY = 'scroll';
    nodeTwo.classList.add('travian-kingdoms-tools-table-two');

    node.appendChild(text);
    body.appendChild(node);
    body.appendChild(nodeOne);
    body.appendChild(nodeTwo);

    return { text, node, nodeOne, nodeTwo };
};

const createTables = () => {
    const valleyTable = document.createElement('table');
    valleyTable.style.color = '#000000';
    valleyTable.style.border = '0';
    valleyTable.style.cursor = 'default';

    const oasisTable = document.createElement('table');
    oasisTable.style.color = '#000000';
    oasisTable.style.border = '0';
    oasisTable.style.cursor = 'default';

    return { valleyTable, oasisTable };
};

const createRow = (table, contentOne, contentTwo) => {
    const row = table.insertRow();

    const cellOne = row.insertCell();
    cellOne.style.width = '25px';
    cellOne.style.borderRight = '1px solid #000000';
    cellOne.style.borderBottom = '1px solid #000000';
    cellOne.style.textAlign = 'center';
    cellOne.innerHTML = contentOne.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '&nbsp;');

    const cellTwo = row.insertCell();
    cellTwo.style.height = '25px';
    cellTwo.style.borderRight = '1px solid #000000';
    cellTwo.style.borderBottom = '1px solid #000000';
    cellTwo.innerHTML = contentTwo;
};

const createValleyContent = ({ externalId, type, oases, bonus, distance, occupied }) => {
    const mapper = {
        41: { name: '50%', position: '-286px -376px', opacity: '1.0' },
        40: { name: '25%', position: '-286px -376px', opacity: '0.5' },
        31: { name: '25% + 25%', position: '-132px -376px', opacity: '1.0' },
        30: { name: '25%', position: '-132px -376px', opacity: '0.5' },
        21: { name: '25% + 25%', position: '-242px -376px', opacity: '1.0' },
        20: { name: '25%', position: '-242px -376px', opacity: '0.5' },
        11: { name: '25% + 25%', position: '-198px -376px', opacity: '1.0' },
        10: { name: '25%', position: '-198px -376px', opacity: '0.5' },
    };

    const content = oases.map(oasis => {
        const { name, position, opacity } = mapper[oasis.type];

        return `<div title='${name}' style='display: inline-block; width: 22px; height: 22px; margin: 0 auto; vertical-align: -4px; background-image: url("./layout/images/sprites/general.png"); background-position: ${position}; opacity: ${opacity}'></div>`;
    }).join('&nbsp');

    return `<a href="https://${window.location.hostname}/#/page:map/window:mapCellDetails/cellId:${externalId}/centerId:${externalId}" style="color: ${occupied ? '#FF0000' : '#008800'}">The 
            ${type.toString().padStart(5, '0')} valley</a> is 
            ${distance.toString().padStart(3, '0')} fields away with 
            ${bonus.toString().padStart(3, '0')}% ${content}`;
};

const createOasisContent = oasis => {
    const { externalId, distance } = oasis;

    const content = [
        { key: 'elephant', value: 'Elephant', position: '-40px -80px' },
        { key: 'tiger', value: 'Tiger', position: '-20px -100px' },
        { key: 'crocodile', value: 'Crocodile', position: '0 -100px' },
        { key: 'bear', value: 'Bear', position: '-100px -80px' },
        { key: 'wolf', value: 'Wolf', position: '-100px -60px' },
        { key: 'boar', value: 'Boar', position: '-100px -40px' },
        { key: 'bat', value: 'Bat', position: '0 0' },
        { key: 'snake', value: 'Snake', position: '-100px 0' },
        { key: 'spider', value: 'Spider', position: '-80px -80px' },
        { key: 'rat', value: 'Rat', position: '-60px -80px' },
    ].map(({ key, value, position }) => oasis[key] ? `${oasis[key].toString().padStart(3, '0')} 
        <div title='${value}' style='display: inline-block; width: 20px; height: 20px; margin: 0 auto; vertical-align: -4px; background-image: url("./layout/images/sprites/unit/small/unit/small.png"); background-position: ${position};'></div>` : undefined,
    ).filter(content => content !== undefined).join('&nbsp;');

    return `<a href="https://${window.location.hostname}/#/page:map/window:mapCellDetails/cellId:${externalId}/centerId:${externalId}" style="color: ${content ? '#008800' : '#FF0000'}">The oasis</a> is 
        ${distance.toString().padStart(3, '0')} 
        fields away ${content ? `with ${content}` : ''}`;
};