Transformania Time Main Page Remake

Reshapes, restyles the Transformania Time main page a bit.

// ==UserScript==
// @name         Transformania Time Main Page Remake
// @namespace   http://steamcommunity.com/id/siggo/
// @version      1.0.6
// @description  Reshapes, restyles the Transformania Time main page a bit.
// @author       Prios
// @match        https://www.transformaniatime.com/
// @match        https://test.transformaniatime.com/
// @copyright 2019, Prios (https://openuserjs.org/users/Prios)
// @grant        none
// @license      MIT
// jshint multistr: true
// ==/UserScript==

// PROBLEM: Browser widths between 1200px and 1400px look terrible. All things considered, best to let the components on the left shrink a little.

/*
function linkifyLogEntries( i ) {

  var logActionsMatch = /\b([\wü]*)(?: '?.*')?( .*?)(?: entered from | left toward | cleansed here\.| meditated here\.| searched here\.| cast | dropped | picked up | threw a | shouted |, a | tamed | released a | consumed a )/;
  var logSubject = $( this ).text().match(logActionsMatch); // [1] is first name, [2] is anything beyond the quote-designated nickname (if any) but before the matched action

  if ( logSubject !== null ) {
        logSubject = logSubject.slice(1).join(''); // squash the two subgroups together
    // if ( ( logSubject != 'You' )) {
      $(this).wrapInner('<a style="font-weight:normal;color:inherit !important;" href="https://www.transformaniatime.com/PvP/LookAtPlayerItem?vicname=' + logSubject + '"></a>');
    // }
  }
  if ( i >= 300 ) { return false; }
}
*/

function pseudoShoutClass() { // just simulates a:hover color:black, a color:white
    var $this = $(this);
    var whichEvent = event.type;
    if (whichEvent === 'mouseover') {
        $this.attr('style', $this.attr('style').replace('white', 'black')); // Using .attr because .css doesn't support !important
    }
    else if (whichEvent === 'mouseout') {
        $this.attr('style', $this.attr('style').replace('black', 'white'));
    }
}

// constructor
// storage: key used by TfT to store the setting in localStorage; required
// span: the span tag itself as a jquery object; required
// friendlyName: string, the public 'end user' name for the setting; required
// switches: an object with 'true' and 'false' key entries, and corresponding span tag display text as paired values; optional, uses 'ON' and 'OFF' as default
function DynamicMenuItem(storage, span, friendlyName, switches) {
    this.storageKey = storage;
    this.$spanJQO = span;
    this.myFriendlyName = friendlyName;
    this.switchSettings = switches || {
        'true': 'ON',
        'false': 'OFF'
    };
    this.updateSpan = function () {
        var curSetting = localStorage.getItem(this.storageKey) || 'false';
        this.$spanJQO.html(this.switchSettings[curSetting]);
    };
    this.alertStatus = function () {
        var curSetting = localStorage.getItem(this.storageKey) || 'false';
        alert(this.myFriendlyName + ' now ' + this.switchSettings[curSetting] + '!');
    };
    this.flipSwitch = function () {
        var locStoKey = this.storageKey;
        var curSetting = localStorage.getItem(locStoKey) || 'false';
        if (curSetting === 'false') { // this setting is stored as a string in localStorage by TfT
            localStorage.setItem(locStoKey, 'true');
        }
        else {
            localStorage.setItem(locStoKey, 'false');
        }
        this.updateSpan();
        this.alertStatus(); // curSetting will be determined all over again twice but that's okay, this isn't really a super time sensitive operation
        // Plus we'll find out if .setItem did something unexpected, instead of assuming it worked exactly as desired
    };
    this.updateSpan(); // initialize
}

$(function () {

    // var startTime = Date.now();
    'use strict';

    // ACTION BUTTONS - DETACH, AND CANCEL SCRIPT IF NOT PRESENT
    var $playerActions = $('#playerActionBox').detach(); // The original action buttons
    if ($playerActions.length === 0) return; // Stop the script right here if we're not on the right kind of page

    // MAJOR COMPONENT EXTRACTION

    var $tftSite = $('body').detach(); // Detaching the page to avoid making lots of updates to the DOM itself. Counterintuitively, it's fastest and least intensive to do .finds() from here, when possible.
    var $siteTop = $tftSite.find('#siteTop'); // everything in the body except the footer
    var $siteFooter = $tftSite.find('footer'); // footer tag inside a container-class div

    // SUBCOMPONENT VARIABLES

    var $settingsWrenchListitem = $tftSite.find('.glyphicon-wrench').parent().parent();
    var $newSettingsMenu = $('<li id="settingsMenuItem" class="dropdown">\
<a id="settingsMenuAnchor" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Settings <span class="caret"></span></a>\
<ul id="settingsMenuList" class="dropdown-menu">\
<li><a id="settingsTurnAudio" href="#">Turn Alerts are <span id="dynTurnSpan">UNKNOWN (error!)</span></a></li>\
<li><a id="settingsAttackAudio" href="#">Attack Alerts are <span id="dynAttackSpan">UNKNOWN (error!)</span></a></li>\
<li><a id="settingsMsgAudio" href="#">Message Alerts are <span id="dynMsgSpan">UNKNOWN (error!)</span></a></li>\
<li><a id="settingsPopups" href="#">Popup Notifications are <span id="dynPopSpan">UNKNOWN (error!)</span></a></li>\
<li><a id="settingsBioItem" href="/Settings/SetBio">Update Bio</a></li>\
<li><a id="settingsBlacklistItem" href="/Settings/MyBlacklistEntries">Manage Blacklist</a></li>\
<li><a id="settingsReserveNameItem" href="/PvP/ReserveName">Reserve Name</a></li>\
<li><a id="settingsNicknameItem" href="/Settings/SetNickname">Set your Nickname</a></li>\
<li><a id="settingsFullPageItem" href="/Settings/Settings">More Settings...</a></li>\
</ul>\
</li>'); // ids make finding these elements much faster

    var $specialBox = $tftSite.find('.specialBox'); // stats on people playing, boss announcements, chaos round announcement

    var $containerInner = $tftSite.find('.containerInner'); //  Area with: Avatar info, self portrait, stat bars, action buttons, movement grid, area description

    var $innerRows = $containerInner.find('.row'); // Row two starts before the movement grid, row one ends before the action buttons (action buttons are not in a row)
    var $frontOuters = $tftSite.find('.frontOuter'); // the three columns in first row, avatar info/portrait/stat bars
    var $avatarText = $tftSite.find('.avatarText'); // First column's single child, Name/Form/Covenant avatar info box
    var $actionCounts = $frontOuters.find('.avatarCount').slice(0, 2); // first and second, this is number/max of attacks and restorations

    var $allIcons = $tftSite.find('.col-sm-12').find('.icon'); // The attack, meditation, and money icons
    var $attackIcon = $tftSite.find('.icon-timesattacking');
    var $recoverIcon = $tftSite.find('.icon-cleansemeditate');
    var $moneyIcon = $tftSite.find('.icon-money');

    var $moveGrid = $tftSite.find('.tableLines'); // Movement grid
    var $moveGridCells = $moveGrid.find('td');

    var $knownSpells = $tftSite.find('.knownspells'); // spells known in this area

    var $covController = $tftSite.find('.covController'); // The sentence stating which covenant has enchanted the area

    var $activityLog = $tftSite.find('#RecentActivityLog'); // Original log of room's events, just above footer
    var $activityLogWide; // Widescreen-only clone to place on right side -- cloned later, after some alterations to original

    var $lindellaLatest = $activityLog.find('li:contains(Lindella the Soul Vendor )').first();
    var $wuffieLatest = $activityLog.find('li:contains(Wüffie the Soul Pet Vendor )').first();
    var lindellaTrail;
    var wuffieTrail;

    // UTILITY VARIABLES

    var selfLookURL = $tftSite.find('a:contains(Look at Yourself)').attr('href');

    var busStop = $tftSite.find('.bus').length; // 1/true if at a bus stop, 0/false if not
    var covSafeground = $tftSite.find('.covSafeground').length; // 1/true if a safeground, 0/false if not

    var barButtonsData = [{
        'target': 'Cleanse',
        'text': '',
        'isBlocked': false
    },
                          {
                              'target': 'Meditate',
                              'text': '',
                              'isBlocked': false
                          },
                          {
                              'target': 'Search',
                              'text': '',
                              'isBlocked': false
                          }
                         ];

    // creating objects associated with the on/off settings menu items, for setting and updating them
    var switchTurnSound = new DynamicMenuItem('play_updateSoundEnabled', $newSettingsMenu.find('#dynTurnSpan'), 'Audible Turn Alerts');
    var switchAttackSound = new DynamicMenuItem('play_AttackSoundEnabled', $newSettingsMenu.find('#dynAttackSpan'), 'Audible Attack Alerts');
    var switchMsgSound = new DynamicMenuItem('play_MessageSoundEnabled', $newSettingsMenu.find('#dynMsgSpan'), 'Audible Message Alerts');
    var switchPopups = new DynamicMenuItem('play_html5PushEnabled', $newSettingsMenu.find('#dynPopSpan'), 'HTML5 Popup Notifications');

    // -----------
    // MAIN SCRIPT

    // attaching click listeners to dynamic settings menu items
    $newSettingsMenu.find('#settingsTurnAudio').click(function () {
        switchTurnSound.flipSwitch();
        playUpdateSound = localStorage.getItem(switchTurnSound.storageKey) === 'true'; // converting the 'true' or 'false' string into a real boolean
    });
    $newSettingsMenu.find('#settingsAttackAudio').click(function () {
        switchAttackSound.flipSwitch();
        playAttackSound = localStorage.getItem(switchAttackSound.storageKey) === 'true';
    });
    $newSettingsMenu.find('#settingsMsgAudio').click(function () {
        switchMsgSound.flipSwitch();
        playMessageSound = localStorage.getItem(switchMsgSound.storageKey) === 'true';
    });
    $newSettingsMenu.find('#settingsPopups').click(function () {
        switchPopups.flipSwitch();
        notificationsEnabled = localStorage.getItem(switchPopups.storageKey) === 'true';
    });

    // Replacing the wrench with the new settings menu
    $settingsWrenchListitem.replaceWith($newSettingsMenu);

    // populate barButtonsData[0-2].text with actionButton texts
    $playerActions.find('.actionButton').each(function (i) {
        barButtonsData[i].text = $(this).text();
    });

    // Checking whether player has insufficient AP for these actions
    if ((cleanseCost > ap) || (playerMana < 3.0)) {
        barButtonsData[0].isBlocked = true;
    }
    if (meditateCost > ap) {
        barButtonsData[1].isBlocked = true;
    }
    if (searchCost > ap) {
        barButtonsData[2].isBlocked = true;
    }

    // General Activity Log alterations
    if ($lindellaLatest.length) { // if Lindella has at least one log entry
        $lindellaLatest.css({
            'color': 'cyan'
        });
        lindellaTrail = $lindellaLatest.text();
        if (lindellaTrail.includes(" left toward ")) { // check that it's a leaving entry
            lindellaTrail = lindellaTrail.match(/Lindella the Soul Vendor left toward (.*)\n/)[1]; // Now make it just the location she's leaving towards
        }
        else {
            lindellaTrail = null;
        }
        // alert('lindellaTrail is ' + lindellaTrail);
    }

    if ($wuffieLatest.length) { // if Wüffie has at least one log entry
        $wuffieLatest.css({
            'color': 'deeppink'
        });
        wuffieTrail = $wuffieLatest.text();
        if (wuffieTrail.includes(" left toward ")) { // check that it's a leaving entry
            wuffieTrail = wuffieTrail.match(/Wüffie the Soul Pet Vendor left toward (.*)\n/)[1]; // Now make it just the location she's leaving towards
        }
        else {
            wuffieTrail = null;
        }
        // alert('wuffieTrail is ' + wuffieTrail);
    }

    // $activityLog.find('li').each( linkifyLogEntries );

    $activityLogWide = $activityLog.clone();

    // Activity Log alterations (original only, post-cloning)
    $activityLog
        .addClass('hidden-lg');

    // Activity Log alterations (sidebar only)
    $activityLogWide
        .attr('id', 'RecentActivityLogSide') // giving the clone a unique ID, unfortunately this removes the id-targeted style rules and forces me to re-add them with the .css method
        .addClass('visible-lg-block') // Bootstrap class thingy that makes this only show up when the page is at least 1200px wide.
        .css({
        'background': '#C2A9AF',
        'text-align': 'justify',
        'overflow-y': 'scroll',
        'padding': 10,
        'width': '31%',
        'position': 'fixed',
        'resize': 'none',
        'height': 'initial',
        'top': 15,
        'bottom': 15
    }) // the 31% width is a kludge.
    ;

    // $specialBox
    //  .css({'font-size': 'small'})
    // ;

    $siteFooter
        .unwrap() // stripping the footer's container div, footer will be moved into another container later
    ;

    $tftSite.find('.offlinePlayersWrapperBG') // this is the inventory row
        .css({
        'border-bottom': 0,
        'background': '#ebe8e0'
    });

    /*
  $tftSite.find('.formerPlayer') // links to player profiles of ground items
    .wrap(function() {
      var $this = $(this);
      var victimName = $this.text().slice(10, -1);
      return ('<a href="https://www.transformaniatime.com/PvP/LookAtPlayerItem?vicname=' + victimName + '">');
    })
  ;
  */

    $actionCounts
        .each(function (i) {
        var $this = $(this);
        if ($this.text() === ' 3 / 3') {
            $this.css({
                'color': 'red',
                'font-weight': 'bold'
            }); // warning when tapped out on limited-per-turn actions
            if (i === 1) { // if it's the second one, i.e. recoveries. Kind of confusing and clunky.
                barButtonsData[0].isBlocked = true; // blocking both cleansing and meditation
                barButtonsData[1].isBlocked = true;
            }
        }
    })
        .css({
        'font-size': 'larger'
    });

    $allIcons
        .css({
        'font-size': 'x-large'
    })
        .removeClass();

    $attackIcon
        .html('⚔️');

    $recoverIcon
        .html('🔮');

    $moneyIcon
        .html('💰');

    // preventing style rule freakout
    $containerInner
        .css({
        'background': '#ebe8e0'
    });

    // gross rearrangement of page layout -- this unfortunately causes a marked visual 'jump' once the page is reattached
    $siteTop
        .attr('class', 'container-fluid')

        .children()

        .wrapAll('<div class="col-lg-8">') // wrap the ENTIRE existing contents in new column
        .parent() // traverse to newly-made column
        .wrap('<div class="row">') // wrap the new column in a new row
        .append($specialBox)
        .append($siteFooter) // move footer to bottom of column

        .after('<div class="col-lg-4">') // add another column to the row
        .next() // traverse to newly-made column
        .append($activityLogWide) // add side-log to the new column

    ;

    $moveGrid // direct alteration before moving to new spot
        .css({
        'width': 'inherit',
        'height': 'inherit',
        'box-shadow': '-5px 5px 5px gray',
        'table-layout': 'fixed'
    })

        .find('td')
        .css({
        'font-size': 12,
    })
        .end()

        .find('tr')
        .css({
        'height': '33%'
    })

        .find('a')
        .css({
        'display': 'table',
        'width': '100%',
        'height': '100%'
    }) // expands the anchor to fill the cell; this requires removing the display:table-cell property, which unfortunately messes up the text's vertical alignment
        .append(function () { // moves the anchor's text to a newly-created nested div element which itself has display:table-cell, fixing the vertical alignment
        var $myText = $(this).text();
        $(this).text('');
        return '<div style="display: table-cell; vertical-align: middle">' + $myText + '</div>';
    })
        .end()

        .find('div:contains(' + wuffieTrail + ')') // if wuffieTrail is null, this finds nothing and nothing happens
        .each(function () {
        // alert('text is ' + $(this).text());
        if ($(this).text() === wuffieTrail) { // ensures a precise match, not just containing
            $(this).css({
                'color': 'deeppink'
            });
            return false; // stops looping
        }
    })

        .end()

        .find('div:contains(' + lindellaTrail + ')') // if lindellaTrail is null, this finds nothing and nothing happens
    // will overwrite Wuffie's highlighting if they both coincide. A little wasteful
        .each(function () {
        // alert('text is ' + $(this).text());
        if ($(this).text() === lindellaTrail) { // ensures a precise match, not just containing
            $(this).css({
                'color': 'cyan'
            });
            return false; // stops looping
        }
    })
        .end();

    $moveGridCells // this part is a little repetitious, but not too much so, and the individual parts might change in the future

        .eq(8) // ninth cell
        .css({
        'background': '#A16969'
    })

        .append('<a href="/pvp/EnchantLocation" style="font-weight: bold; font-size: larger; position: initial; color: white !important">Enchant</a>') // I don't have a better alternative than !important here unfortunately
        .find('a') // selects just-created anchor
        .hover(pseudoShoutClass)
        .end()

        .end()

        .eq(2) // third cell
        .css({
        'background': '#A16969'
    })

        .append('<a href="/pvp/shout" class="shout" style="font-weight: bold; font-size: larger; position: initial; color: white !important">Shout</a>')
        .find('a') // selects just-created anchor
        .hover(pseudoShoutClass)
        .end()

        .end()

        .eq(6) // seventh cell
        .css({
        'background': '#A16969'
    })

        .append($knownSpells.html()) // simply sticks the html for $knownSpells into the cell unmodified
    ;

    if (busStop) {
        $moveGridCells

            .eq(0)
            .css({
            'background': '#A16969'
        })

            .append('<a href="/pvp/Bus" style="font-weight: bold; font-size: larger; position: initial; color: white !important">Take Bus</a>')
            .find('a') // selects just-created anchor
            .hover(pseudoShoutClass);
    }

    $avatarText // direct alteration before moving
        .css({
        'margin-bottom': 10,
        'display': 'inline-block'
    });

    $innerRows.eq(0) // Row with player portrait and stats
        .css({
        'margin-bottom': 20,
        'height': 301
    })

        .find($frontOuters) // the three columns
        .css({
        'border': 0,
        'height': 301
    })

        .eq(0) // left column, original location of player name and form name, new location of moveGrid
        .css({
        'width': 301
    })
        .prepend($moveGrid) // moves it from its original location, already has shadow set
        .end()

        .eq(1) // center column, location of player portrait
        .css({
        'width': 301
    })

        .find('.portraitFront')
        .css({
        'border': 0,
        'width': 'inherit',
        'height': 'inherit',
        'box-shadow': '1px 5px 5px gray'
    })
        .wrap('<a href="' + selfLookURL + '" style="font-weight: normal; width: inherit; height: inherit"></a>')
        .end()

        .end()

        .eq(2) // right column, location of player stats

        .find('.avatarBars')
        .css({
        'background': '#d9d9d9',
        'box-shadow': '5px 5px 5px gray'
    })
        .prepend($avatarText) // pulled over here from left column

        .find('.barWrapper') // 'backgrounds' of the bars, empty bars
        .css({
        'height': 35
    })

        .find('.barText') // what it says on the tin
        .css({
        'line-height': '35px'
    })
        .each(function (i) { // this will break if the order of the vital stat bars is changed
        var $this = $(this);
        $this
            .wrap('<a href="/PvP/' + barButtonsData[i].target + '" style="font-weight: normal"></a>') // inherits .barWrapper size
            .hover(
            function () {
                $this // mouseenter
                    .data('initialText', $this.text())
                    .text(barButtonsData[i].text)
                    .css({
                    'color': '#e4e0d4',
                    'font-weight': 'bold'
                });
                if (barButtonsData[i].isBlocked && (secondsToUpdate > 0)) {
                    $this
                        .css({
                        'text-decoration': 'line-through'
                    })
                        .parent()
                        .attr('href', null);
                }
            },
            function () {
                $this // mouseleave
                    .text($this.data('initialText'))
                    .css({
                    'color': '',
                    'font-weight': '',
                    'text-decoration': ''
                })
                    .parent()
                    .attr('href', '/PvP/' + barButtonsData[i].target);
            });
    })
        .end()

        .find('.barData') // 'fullness' of bars, widths are percentages of bar wrappers
        .css({
        'height': 'inherit'
    })

        .filter('.barWP') // possibly I should be doing this with an array-like object instead?
        .css({
        'background': 'linear-gradient(red, darkred)'
    })
        .end()

        .filter('.barMP')
        .css({
        'background': 'linear-gradient(blue, darkblue)'
    })
        .end()

        .filter('.barAP')
        .css({
        'background': 'linear-gradient(orange, darkorange)'
    })
        .end()

        .end()

        .end()

        .find('.avatarXPAmt')
        .css({
        'background': 'linear-gradient(violet, purple)'
    })
        .wrap('<a href="/PvP/MyPerks"></a>') // inherits size from .avatarXPWrapper
    ;

    $innerRows.eq(1) // second row, with now-empty movegrid column and room description

        .find('.col-md-4') // former movegrid column
        .remove()
        .end()

        .find('.covenDescription') // room description and title
        .removeClass('col-md-8');

    if (covSafeground) {

        $covController

            .css({
            'color': '#a13d2d'
        })

            .contents()

            .first() // The text that says 'Enchanted by the '
            .replaceWith('SAFEGROUND for the ');
    }

    // Finally update the real site with the changes
    $('head').after($tftSite);

    // console.log(Date.now() - startTime);

});