// ==UserScript==
// @name Better GamePress FGO
// @version 0.7.2
// @description Better servant pages.
// @author Rukako
// @namespace rukako
// @match https://grandorder.gamepress.gg/*
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/crel/4.0.1/crel.min.js
// ==/UserScript==
/* globals crel */
(function() {
'use strict';
if (window.BETTER_GAMEPRESS_FGO) return; // set flag in case script is loaded multiple times.
window.BETTER_GAMEPRESS_FGO = 1;
const $ = (a, b) => typeof b == 'undefined' ? document.querySelector(a) : a.querySelector(b);
const toArray = (x) => Array.prototype.slice.call(x, 0);
const $$ = (a, b) => toArray(typeof b == 'undefined' ? document.querySelectorAll(a) : a.querySelectorAll(b));
const log = console.log.bind(console);
const assert = console.assert.bind(console);
// Common functions.
// helper so crel correctly sets style attributes one at a time.
crel.attrMap.style = (el, style) => {
if (typeof style === 'string') {
el.setAttribute('style', style);
} else {
Object.entries(style).forEach(([k,v]) => { el.style[k] = v; });
}
}
// Run specific functions depending on URL.
if (/^\/servant\//.test(window.location.pathname)) {
removeDiscussion();
servantPageImprovements();
betterSkillUpgradeDisplay();
}
if ($('#gamepress-top-content')) {
backToTop();
}
removeFloatingHeader();
// Specific feature implementations.
function removeFloatingHeader() {
// fixes header bar at top of page.
$('.gamepress-top-menu .menu-container').style = 'position: inherit';
}
function removeDiscussion() {
// hides the discussion but doesn't remove it
// so running scripts don't error.
const disc = $('.view-discourse-block');
assert(disc, 'discussion node not present');
disc.style.display = 'none';
}
function servantPageImprovements() {
// converts text to a string usable as HTML ID or classname.
const toClassName = c => 'section-'+c.toLowerCase().replace(/[^A-Za-z0-9]/g, '-');
const pageTitle = $('#page-title');
// main table of contents under servant name.
const mainTOC = crel('div', {'class': 'main-toc hidden better-toc'});
const servantLayout = $('.servant-new-layout');
servantLayout.insertAdjacentHTML('beforebegin', '<h2 style="margin-top: 0" class="main-title hidden better-toc">Table of Contents</h2>');
servantLayout.insertAdjacentElement('beforebegin', mainTOC);
// a TOC row contains one category's buttons (profile, analysis, etc.)
const newTOCRow = () => crel('div', {'class': 'main-toc-section'});
const newTOCButton = (icon, text, link) => `<a href="#${link}"><span>` + (icon ? `<i class="fa ${icon}" aria-hidden="true"></i> ` : '') + text + `</span></a>`;
// define TOC categories.
const categories = {
'status': ['fa-user', 'Profile', 'table-of-contents-0'],
'analysis': ['fa-pie-chart', 'Analysis', 'section-overview'],
'profile': ['fa-file-text', 'Other', 'section-other-info']
};
// TABLE OF CONTENTS LINKS
['status', 'analysis', 'profile'].map(s => $('#'+s)).forEach(sec => {
const tabs = $(sec, '.servant-tabs');
assert(tabs, '.servant-tabs not found in', sec);
tabs.innerHTML += '<br><br><hr style="display:block;width: 100%;">';
const row = newTOCRow(); // add category to main TOC
mainTOC.insertAdjacentHTML('beforeend', newTOCButton(...categories[sec.id]));
$$(sec, '.main-title')
.map(title => [title, title.nextElementSibling])
.filter(([a,b]) => b && !b.classList.contains('servant-tabs'))
.forEach(([title, div]) => {
const text = title.textContent;
const id = title.id || toClassName(text);
title.id = id; // set ID of this h2 element to a usable ID.
// add button to current TOC category.
row.insertAdjacentHTML('beforeend', newTOCButton(null, text, id));
//tabs.innerHTML += `<li><a href="#${id}"><!--<i class="fa fa-user" aria-hidden="true"></i> -->${text}</a></li>`;
// create and insert the button in the inline tables of contents.
const li = crel('li', crel('a', {
href: '#'+id,
style: {
padding: '5px 6px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
}
}, text));
tabs.appendChild(li);
});
mainTOC.appendChild(row);
});
// skill analysis and materials links in servant skills section.
const levelUpSkillHref = toClassName('Level Up Skill Recommendation');
const skillMatsHref = toClassName('Skill Enhancement Materials');
const recHTML = `<ul class="servant-tabs" style="margin-bottom: 0 !important;">
<li><a href="#${levelUpSkillHref}" style="padding: 0;"><i class="fa fa-pie-chart" aria-hidden="true"></i> Skill Analysis</a></li>
<li><a href="#${skillMatsHref}" style="padding: 0;"><i class="fa fa-diamond" aria-hidden="true"></i> Materials</a></li>
</ul>`;
$('#skills').insertAdjacentHTML('afterbegin', recHTML);
// SKILL LINKS AND RECOMMENDATION ICONS
const skillIcons = [null, null, null];
// for each skill in 'level up skill recommendations'
Array.from($('.view-level-up-skill-recommendation tbody').children).forEach((tr, i) => {
if (tr.tagName != 'TR') return;
const a = $(tr, 'a[href^="/servant-skill/"]');
if (!a) return; // No link found. Empty skill row.
a.href = '#servant-skill-'+(i+1);
const iconsEl = $(tr, '.views-field-description__value'); // this contains the red/green icons.
assert(iconsEl, 'skill icons cell not found', tr);
const icons = Array.from(iconsEl.children);
// colours are from an inherited CSS property. store as style attribute in icon.
icons.forEach(icon => {icon.style.color = window.getComputedStyle(icon).color; });
assert(i < 3, 'too many skills in analysis', i);
skillIcons[i] = icons.map(x => x.cloneNode(true)); // clone icons.
});
// for each skill in 'servant skills'
$$('#skills > div > .field__item').forEach((el, i) => {
el.id = 'servant-skill-'+(i+1); // add ID to servant skill, so it can be linked to.
if (!skillIcons[i]) return; // no skill icons for this skill.
const title = $(el, 'a[href^="/servant-skill/"]');
const iconsSpan = crel('a',
{href: '#'+levelUpSkillHref,
title: 'Level up priority',
style: {marginLeft: '6px', fontSize: '16px'}},
skillIcons[i]);
// inserts skill icons after skill name.
title.insertAdjacentElement('afterend', iconsSpan);
});
// TIER LINK
// parses servant rarity number
let rarity = $('.taxonomy-term.vocabulary-stars').getAttribute('about').replace('/', '');
rarity = parseInt(rarity);
if (rarity <= 3) {
rarity = '1-3';
}
const tierListLink = crel('a', {href: `/${rarity}-star-tier-list`});
// wraps "Tier" heading in a link to the tier list.
const ratingBar = $('#overall-servant-rating');
const ref = ratingBar.nextElementSibling;
tierListLink.appendChild(ratingBar);
ref.parentNode.insertBefore(tierListLink, ref);
// number table of contents elements with counter.
let counterTOC = 0;
let currentTOC = null;
$$('.servant-new-layout h2.main-title').forEach((el, i) => {
if (el.textContent.trim().toLowerCase() === 'table of contents') {
el.id = el.id || ('table-of-contents-'+counterTOC);
currentTOC = '#'+el.id;
counterTOC++;
return;
}
});
// Fix tier number alignment
const tierDiv = $('#overall-servant-rating .overall-servant-rating-value');
if (tierDiv) {
tierDiv.style.lineHeight = tierDiv.clientHeight + 'px';
}
// add ID attributes for other h2.main-title elements as well.
$$('.main-title:not([id])').forEach(el => el.id = toClassName(el.textContent));
}
function backToTop() {
// BACK TO TOP
const toTopStyles = `
#to-top {
position: fixed;
bottom: 10px;
float: right;
right: 30px;
z-index: 2;
}
#to-top a:not(:hover) {
background-color: #f8f8f8;
}
#to-top a {
width: 45px;
height: 45px;
font-size: 20pt;
padding: 0;
}
`;
document.head.appendChild(crel('style', toTopStyles));
// global back to top button, located in bottom right corner.
$('.page').insertAdjacentHTML('beforeend', '<div id="to-top"><a class="featured-button" href="#"><i class="fa fa-chevron-up"></i></a></div>');
}
function betterSkillUpgradeDisplay() {
const skillStyles = `
.servant-new-layout .servant-skill-upgrade {
margin-top: 1px;
}
.servant-new-layout .field--name-field-servant-skills>.field__item {
border: #d1d8f0 solid 3px;
/*border-left: #585858 solid 5px;*/
}
.servant-new-layout .servant-skill-single {
/*border-left-style: none;*/
}
`;
// improves the border highlighting and grouping of skills
// to better group interlude-upgrade skills.
document.head.appendChild(crel('style', skillStyles));
}
})();