import differenceInDays from 'date-fns/differenceInDays'; import getTranslations from './i18n/extendedTribeProfile'; import requestCreator from './libs/requestCreator'; import { setPage, getPage, generatePaginationItems, getContainerStyles, } from './utils/pagination'; import renderTodaysStats from './common/renderTodaysStats'; import showEnnoblementsPopup from './common/showEnnoblementsPopup'; import showHistoryPopup from './common/showHistoryPopup'; import showPopup from './utils/showPopup'; import getIDFromURL from './utils/getIDFromURL'; import getCurrentServer from './utils/getCurrentServer'; import { setItem, getItem } from './utils/localStorage'; import { formatDate } from './utils/date'; import getServerVersionCode from './utils/getServerVersionCode'; import * as twstatsutils from './utils/twstats'; import * as twhelputils from './utils/twhelp'; import * as twutils from './utils/tribalwars'; // ==UserScript== // @name Extended tribe profile // @namespace https://github.com/tribalwarshelp/scripts // @updateURL https://raw.githubusercontent.com/tribalwarshelp/scripts/master/dist/extendedTribeProfile.js // @downloadURL https://raw.githubusercontent.com/tribalwarshelp/scripts/master/dist/extendedTribeProfile.js // @version 1.1.6 // @description Extended tribe profile // @author Kichiyaki https://dwysokinski.me/ // @match *://*/game.php*screen=info_ally* // @grant none // @run-at document-end // ==/UserScript== const SERVER = getCurrentServer(); const VERSION = getServerVersionCode(SERVER); const TRIBE_ID = getIDFromURL(window.location.search); const LOCAL_STORAGE_KEY = 'kichiyaki_extended_tribe_profile' + TRIBE_ID; const TRIBE_QUERY = ` query tribe( $server: String! $id: Int! $dailyTribeStatsSort: [String!] $dailyTribeStatsLimit: Int $playersLimit: Int $playersSort: [String!] $playerFilter: PlayerFilter! $dailyTribeStatsFilter: DailyTribeStatsFilter! ) { tribe(server: $server, id: $id) { id bestRank bestRankAt mostPoints mostPointsAt mostVillages mostVillagesAt createdAt dominance } dailyTribeStats( server: $server limit: $dailyTribeStatsLimit sort: $dailyTribeStatsSort filter: $dailyTribeStatsFilter ) { items { rank rankAtt rankDef rankTotal points scoreAtt scoreAtt scoreDef scoreTotal villages members } } players(server: $server, sort: $playersSort, filter: $playerFilter, limit: $playersLimit) { items { id rankAtt rankDef rankSup rankTotal scoreAtt scoreAtt scoreDef scoreSup scoreTotal dailyGrowth } } } `; const ENNOBLEMENTS_QUERY = ` query ennoblements($server: String!, $limit: Int, $offset: Int, $sort: [String!], $filter: EnnoblementFilter!) { ennoblements(server: $server, limit: $limit, offset: $offset, sort: $sort, filter: $filter) { total items { village { id name x y } oldOwner { id name } oldOwnerTribe { id tag } newOwner { id name } newOwnerTribe { id tag } ennobledAt } } } `; const ENNOBLEMENTS_PER_PAGE = 15; const TRIBE_HISTORY_AND_TRIBE_DAILY_STATS_QUERY = ` query tribeHistoryAndTribeDailyStats($server: String!, $tribeHistoryFilter: TribeHistoryFilter!, $dailyTribeStatsFilter: DailyTribeStatsFilter!, $sort: [String!], $offset: Int, $limit: Int) { tribeHistory(server: $server, sort: $sort, limit: $limit, offset: $offset, filter: $tribeHistoryFilter) { total items { totalVillages points rank scoreAtt rankAtt scoreDef rankDef scoreTotal rankTotal createDate totalMembers } } dailyTribeStats(server: $server, sort: $sort, limit: $limit, offset: $offset, filter: $dailyTribeStatsFilter) { items { points scoreAtt scoreDef scoreTotal villages createDate members } } } `; const TRIBE_HISTORY_PER_PAGE = 15; const TRIBE_MEMBERS_DAILY_STATS_QUERY = ` query tribeMembersDailyStats($server: String!, $filter: DailyPlayerStatsFilter!, $limit: Int, $sort: [String!]) { dailyPlayerStats(server: $server, limit: $limit, sort: $sort, filter: $filter) { items { player { id name } points scoreAtt scoreDef scoreSup scoreTotal villages createDate } } } `; let MEMBERS_GROWTH_MODE = 'points'; const TRIBE_CHANGES_QUERY = ` query tribeChanges($server: String!, $limit: Int, $offset: Int, $sort: [String!], $filter: TribeChangeFilter!) { tribeChanges(server: $server, offset: $offset, limit: $limit, sort: $sort, filter: $filter) { total items { player { id name } newTribe { id tag } createdAt } } } `; const TRIBE_CHANGES_PAGINATION_CONTAINER_ID = 'tribeChangesPagination'; const TRIBE_CHANGES_PER_PAGE = 15; const profileInfoTBody = document.querySelector( '#content_value > table:nth-child(3) > tbody > tr > td:nth-child(1) > table > tbody' ); const actionContainer = profileInfoTBody; const otherElementsContainer = document.querySelector( '#content_value > table:nth-child(3) > tbody > tr > td:nth-child(2)' ); const membersContainer = document .querySelector('#content_value h3') .nextElementSibling.querySelector('tbody'); const translations = getTranslations(); const loadDataFromCache = () => { return getItem(LOCAL_STORAGE_KEY); }; const cacheTribeData = (data = {}) => { setItem(LOCAL_STORAGE_KEY, data); }; const getMemberIDs = () => { const ids = []; membersContainer.querySelectorAll('a').forEach(a => { const href = a.getAttribute('href'); if (href.includes('info_player')) { ids.push(getIDFromURL(href)); } }); return ids; }; const getMemberNames = () => { const ids = []; membersContainer.querySelectorAll('a').forEach(a => { if (a.getAttribute('href').includes('info_player')) { ids.push(a.innerText.trim()); } }); return ids; }; const loadData = async () => { const memberIDs = getMemberIDs(); const data = await requestCreator({ query: TRIBE_QUERY, variables: { server: SERVER, id: TRIBE_ID, dailyTribeStatsSort: ['createDate DESC'], dailyTribeStatsLimit: 1, dailyTribeStatsFilter: { tribeID: [TRIBE_ID], }, playersSort: ['rank ASC'], playersLimit: memberIDs.length, playerFilter: { id: memberIDs, }, }, }); cacheTribeData(data); return data; }; const renderTr = ({ title, data, id }) => { let tr = document.querySelector('#' + id); if (!tr) { tr = document.createElement('tr'); tr.id = id; tr.appendChild(document.createElement('td')); tr.appendChild(document.createElement('td')); profileInfoTBody.append(tr); } tr.children[0].innerHTML = title; tr.children[1].innerHTML = data; }; const extendMembersData = players => { membersContainer.parentElement.style.width = '100%'; const heading = membersContainer.querySelector('tr:first-child'); if (heading.children.length !== 11) { [ translations.oda, translations.odd, translations.ods, translations.od, translations.dailyGrowth, translations.playerLinks, ].forEach(v => { const th = document.createElement('th'); th.innerHTML = v; heading.appendChild(th); }); } membersContainer.querySelectorAll('tr').forEach(tr => { const a = tr.querySelector('a'); if (!a) { return; } const playerID = getIDFromURL(a.getAttribute('href')); const player = players.items.find(p => p.id === playerID); if (player) { [ [player.scoreAtt, player.rankAtt], [player.scoreDef, player.rankDef], [player.scoreSup, player.rankSup], [player.scoreTotal, player.rankTotal], player.dailyGrowth, [ { link: twhelputils.buildPlayerURL(VERSION, SERVER, player.id), label: 'TWHelp', }, { link: twstatsutils.buildPlayerURL(SERVER, player.id), label: 'TWStats', }, ], ].forEach((data, index) => { let td = tr.children[5 + index]; if (!td) { td = document.createElement('td'); tr.appendChild(td); } if (Array.isArray(data)) { if (typeof data[0] === 'number') { td.innerHTML = `${data[0].toLocaleString()} (${ data[1] })`; } else if (data[0].link) { td.innerHTML = data .map( ({ link, label }) => `${label}` ) .join('
'); } } else if (typeof data === 'number') { td.innerHTML = data.toLocaleString(); } }); } }); }; const render = ({ tribe, dailyTribeStats, players }) => { [ { title: translations.createdAt + ':', data: formatDate(tribe.createdAt), id: 'created_at', }, { title: translations.dominance + ':', data: tribe.dominance.toFixed(2) + '%', id: 'dominance', }, { title: translations.bestRank + ':', data: tribe.bestRank + ' ' + `(${formatDate(tribe.bestRankAt)})`, id: 'best_rank', }, { title: translations.mostPoints + ':', data: tribe.mostPoints.toLocaleString() + ' ' + `(${formatDate(tribe.mostPointsAt)})`, id: 'most_points', }, { title: translations.mostVillages + ':', data: tribe.mostVillages + ' ' + `(${formatDate(tribe.mostVillagesAt)})`, id: 'most_villages', }, ].forEach(data => { renderTr(data); }); if (dailyTribeStats && dailyTribeStats.items.length > 0) { renderTodaysStats(otherElementsContainer, dailyTribeStats.items[0]); } if (players && players.items.length > 0) { extendMembersData(players); } }; const handleShowTribeEnnoblementsClick = async e => { e.preventDefault(); const page = getPage(e.target); if (!isNaN(page)) { const data = await requestCreator({ query: ENNOBLEMENTS_QUERY, variables: { filter: { or: { oldOwnerTribeID: [TRIBE_ID], newOwnerTribeID: [TRIBE_ID], }, }, offset: ENNOBLEMENTS_PER_PAGE * (page - 1), limit: ENNOBLEMENTS_PER_PAGE, sort: ['ennobledAt DESC'], server: SERVER, }, }); showEnnoblementsPopup(e, data.ennoblements, { currentPage: page, limit: ENNOBLEMENTS_PER_PAGE, onPageChange: handleShowTribeEnnoblementsClick, }); } }; const handleShowTribeHistoryClick = async e => { e.preventDefault(); const page = getPage(e.target); if (!isNaN(page)) { try { const filter = { tribeID: [TRIBE_ID], }; const { tribeHistory, dailyTribeStats } = await requestCreator({ query: TRIBE_HISTORY_AND_TRIBE_DAILY_STATS_QUERY, variables: { server: SERVER, offset: TRIBE_HISTORY_PER_PAGE * (page - 1), limit: TRIBE_HISTORY_PER_PAGE, sort: ['createDate DESC'], tribeHistoryFilter: filter, dailyTribeStatsFilter: filter, }, }); showHistoryPopup(e, tribeHistory, dailyTribeStats, { currentPage: page, limit: TRIBE_HISTORY_PER_PAGE, tribe: true, onPageChange: handleShowTribeHistoryClick, }); } catch (error) { console.log('cannot load tribe history', error); } } }; const getMembersGrowthTdStyle = value => { const statIncreaseStyle = 'color: #000; background-color: #0f0'; const statDecreaseStyle = 'color: #000; background-color: #f00'; const defaultStyle = 'color: #000; background-color: #808080'; return value > 0 ? statIncreaseStyle : value < 0 ? statDecreaseStyle : defaultStyle; }; const mapMembersGrowthTdValue = i => { switch (MEMBERS_GROWTH_MODE) { case 'points': return i.points; case 'villages': return i.villages; case 'od': return i.scoreTotal; case 'oda': return i.scoreAtt; case 'odd': return i.scoreDef; case 'ods': return i.scoreSup; default: return 0; } }; const buildMembersGrowthTBody = stats => { const dates = [ ...new Set(stats.items.map(item => item.createDate)), ].reverse(); return ` ${translations.player} ${dates .map(date => { return `${formatDate(date, { year: 'numeric', month: '2-digit', day: '2-digit', })}`; }) .join('')} ${translations.total} ${getMemberIDs() .map(id => { const filtered = stats.items .filter(item => item.player && item.player.id === id) .reverse(); let player = undefined; if (filtered.length > 0) { player = filtered[0].player; } const tds = []; let total = 0; for (let date of dates) { const i = filtered.find(i => i.createDate === date); let val = 0; if (i) { val = mapMembersGrowthTdValue(i); } total += val; tds.push( `${val.toLocaleString()}` ); } return ` ${ player ? `${player.name}` : '-' } ${tds.join('')} ${total.toLocaleString()} `; }) .join('')} `; }; const MEMBERS_GROWTH_TABLE_ID = 'membersGrowth'; const MEMBERS_GROWTH_FORM = MEMBERS_GROWTH_TABLE_ID + 'Form'; const createChangeTypeHandler = stats => e => { e.preventDefault(); MEMBERS_GROWTH_MODE = e.target[0].value; document.querySelector('#' + MEMBERS_GROWTH_TABLE_ID).innerHTML = buildMembersGrowthTBody(stats); }; const renderMembersGrowthPopup = (e, stats) => { const formOptions = [ ['points', translations.points], ['villages', translations.villages], ['od', translations.opponentsDefeated], ['oda', translations.opponentsDefeatedAsAttacker], ['odd', translations.opponentsDefeatedAsDefender], ['ods', translations.opponentsDefeatedAsSupporter], ].map( v => `` ); const html = `
${buildMembersGrowthTBody(stats)}
`; showPopup({ e, title: translations.membersGrowth, id: 'mg', html, }); document .querySelector('#' + MEMBERS_GROWTH_FORM) .addEventListener('submit', createChangeTypeHandler(stats)); }; const loadMembersGrowthData = async ({ createDateLTE, createDateGT } = {}) => { const memberIDs = getMemberIDs(); const limit = memberIDs.length * differenceInDays(createDateLTE, createDateGT); const filter = { playerID: memberIDs, createDateLTE, createDateGT, }; const data = await requestCreator({ query: TRIBE_MEMBERS_DAILY_STATS_QUERY, variables: { filter, limit, sort: ['createDate DESC'], server: SERVER, }, }); return data; }; const handleShowMembersGrowthClick = async e => { e.preventDefault(); const createDateGT = new Date(); createDateGT.setDate(createDateGT.getDate() - 7); const data = await loadMembersGrowthData({ createDateLTE: new Date(), createDateGT, }); renderMembersGrowthPopup(e, data.dailyPlayerStats); }; const renderTribeChanges = (e, currentPage, tribeChanges) => { const paginationItems = generatePaginationItems({ total: tribeChanges.total, limit: TRIBE_CHANGES_PER_PAGE, currentPage, }); const html = `
${paginationItems.join('')}
${tribeChanges.items .map(tribeChange => { let rowHTML = '' + ``; if (tribeChange.player) { rowHTML += ``; } else { rowHTML += ''; } rowHTML += ``; return rowHTML + ''; }) .join('')}
${translations.date} ${translations.player} ${translations.act}
${formatDate(tribeChange.createdAt)}${tribeChange.player.name}-${ tribeChange.newTribe && tribeChange.newTribe.id === TRIBE_ID ? translations.joined : translations.left }
`; showPopup({ e, title: translations.tribeChanges, id: 'tribeChanges', html, }); document .querySelectorAll('#' + TRIBE_CHANGES_PAGINATION_CONTAINER_ID + ' a') .forEach(el => { el.addEventListener('click', handleShowTribeChangesClick); }); }; const handleShowTribeChangesClick = async e => { e.preventDefault(); const page = getPage(e.target); if (!isNaN(page)) { const data = await requestCreator({ query: TRIBE_CHANGES_QUERY, variables: { filter: { or: { oldTribeID: [TRIBE_ID], newTribeID: [TRIBE_ID], }, }, offset: TRIBE_CHANGES_PER_PAGE * (page - 1), limit: TRIBE_CHANGES_PER_PAGE, sort: ['createdAt DESC'], server: SERVER, }, }); renderTribeChanges(e, page, data.tribeChanges); } }; const handleGenerateMailingListClick = e => { e.preventDefault(); const members = getMemberNames(); const chunks = []; for (let i = 0; i < members.length; i += 50) { chunks.push(members.slice(i, i + 50)); } let html = ''; chunks.forEach((names, index) => { html += `

${index + 1}.

`; }); Dialog.show('mailinglist', html); }; const loadVillages = async (variables, total = false) => { try { const data = await requestCreator({ variables, query: ` query villages($server: String!, $sort: [String!], $limit: Int, $offset: Int, $filter: VillageFilter!) { villages(server: $server, sort: $sort, limit: $limit, offset: $offset, filter: $filter) { ${total ? 'total' : ''} items { id x y } } } `, }); if (data && data.villages && Array.isArray(data.villages.items)) { return data.villages; } } catch (error) { console.log('load villages', error); } return { total: 0, items: [], }; }; const showLoadingDialog = (current = 0, total = 0) => { if (!current || !total) { return Dialog.show('loading', 'Loading...'); } return Dialog.show( 'loading', `Loaded: ${current}/${total}` ); }; const handleExportTribeVillagesFormSubmit = async e => { e.preventDefault(); let limit = parseInt(e.target[4].value); const variables = { filter: { xLTE: parseInt(e.target[0].value), xGTE: parseInt(e.target[1].value), yLTE: parseInt(e.target[2].value), yGTE: parseInt(e.target[3].value), playerID: getMemberIDs(), }, limit: isNaN(limit) || !limit ? 0 : limit, sort: ['id ASC'], server: SERVER, }; showLoadingDialog(); let { total, items } = await loadVillages(variables, true); const length = items.length; if (limit !== 0 && limit < total) { total = limit; } if (isNaN(limit) || !limit || limit > length) { for (let offset = length; offset < total; offset += length) { showLoadingDialog(offset, total); const more = await loadVillages({ ...variables, filter: { ...variables.filter, }, offset, }); items = [...items, ...more.items]; } } Dialog.show( 'exportTribeVillages', ` ` ); }; const handleExportTribeVillagesClick = e => { e.preventDefault(); const FORM_ID = 'etvForm'; const html = `
`; Dialog.show('exportTribeVillages', html); document .querySelector('#' + FORM_ID) .addEventListener('submit', handleExportTribeVillagesFormSubmit); }; const wrapAction = action => { const actionWrapperTd = document.createElement('td'); actionWrapperTd.colSpan = '2'; actionWrapperTd.append(action); const actionWrapperTr = document.createElement('tr'); actionWrapperTr.appendChild(actionWrapperTd); return actionWrapperTr; }; const renderActions = () => { const linkToTWHelp = document.createElement('a'); linkToTWHelp.href = twhelputils.buildTribeURL(VERSION, SERVER, TRIBE_ID); linkToTWHelp.innerHTML = translations.action.linkToTWHelp; actionContainer.appendChild(wrapAction(linkToTWHelp)); const showEnnoblements = document.createElement('a'); showEnnoblements.href = '#'; setPage(showEnnoblements, '1'); showEnnoblements.innerHTML = translations.action.showEnnoblements; showEnnoblements.addEventListener('click', handleShowTribeEnnoblementsClick); actionContainer.appendChild(wrapAction(showEnnoblements)); const showHistory = document.createElement('a'); showHistory.href = '#'; setPage(showHistory, '1'); showHistory.innerHTML = translations.action.showHistory; showHistory.addEventListener('click', handleShowTribeHistoryClick); actionContainer.appendChild(wrapAction(showHistory)); const showTribeChanges = document.createElement('a'); showTribeChanges.href = '#'; setPage(showTribeChanges, '1'); showTribeChanges.innerHTML = translations.action.showTribeChanges; showTribeChanges.addEventListener('click', handleShowTribeChangesClick); actionContainer.appendChild(wrapAction(showTribeChanges)); const showMembersGrowth = document.createElement('a'); showMembersGrowth.href = '#'; showMembersGrowth.innerHTML = translations.action.showMembersGrowth; showMembersGrowth.addEventListener('click', handleShowMembersGrowthClick); actionContainer.appendChild(wrapAction(showMembersGrowth)); const generateMailingList = document.createElement('a'); generateMailingList.href = '#'; generateMailingList.innerHTML = translations.action.generateMailingList; generateMailingList.addEventListener('click', handleGenerateMailingListClick); actionContainer.appendChild(wrapAction(generateMailingList)); const exportVillages = document.createElement('a'); exportVillages.href = '#'; exportVillages.innerHTML = translations.action.exportVillages; exportVillages.addEventListener('click', handleExportTribeVillagesClick); actionContainer.appendChild(wrapAction(exportVillages)); }; (async function () { try { renderActions(); const dataFromCache = loadDataFromCache(); if (dataFromCache && dataFromCache.tribe) { render(dataFromCache); } const dataFromAPI = await loadData(); if (dataFromAPI) { render(dataFromAPI); } } catch (error) { console.log('extended tribe profile', error); } })();