Klanowicze online

Klanowicze online - zmiany w CSS by Vigellal

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Klanowicze online
// @author       Reskiezis
// @author       Vigellal
// @description  Klanowicze online - zmiany w CSS by Vigellal
// @version      2.0.3
// @match        *://*.margonem.pl/
// @match        *://*.margonem.com/
// @run-at       document-idle
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=margonem.pl
// @namespace https://greasyfork.org/users/937248
// ==/UserScript==

/*
  - - -
  KLANOWICZE ONLINE
  AUTORSTWA RESKIEZISA aka PERSKIEGO KOTA
  WERSJA DLA NOWEGO I STAREGO INTERFEJSU
  
  ZMIANY W CSS BY VIGELLAL
  - - -

  - - - - - - -
  GARMORY ZNOWU POPSULO DODATEK?
  POPROS SWOJEGO DODATKOPISARZA O NAPRAWE!

  Garmory czesto cos zmienia, ale dzieki temu mozna przewidziec co sie zepsulo.
  1. Najczestszy problem - zmiana struktury listy krotek (zlaczonych tablic zawierajacych id gracza, imie itd...)
     PATRZ linia 105
  2. Nowa automatycznie wykonywana funkcja po wywolaniu _g('clan&a=members') lub zmiana w nazewnictwie funkcji/elementow UI, ktore sa wykorzystywane do ominiecia automatycznego wywolania tej funkcji
     PATRZ metody ApplicationSI.prototype.fetchMembers lub ApplicationNI.prototype.fetchMembers
  3. Zmiana nazwy wlasciwosci w obiekcie zwracanym przez _g('clan&a=members').
     (kiedys wlasciwosc members nazywala sie members2)

  Otworzenie konsoli w Chrome - CTRL+SHIFT+J
*/

;(function(){
  'use strict';

  // czy gracz gra na Nowym Interfejsie?
  var isNewInterface = typeof window.Engine !== 'undefined' && typeof window.Engine.hero !== 'undefined'

  /*
    \/ \/ \/
    SEKCJA UI START
    Wyjatek: metody renderMembers i setBattleInfo sa wykorzystywane z poziomu klasy Application
  */
  var STORAGE_KEY = 'klanowicze_online'

  // Enum - przyjmuje jedna z dwoch wartosci
  // SizeEnum.NORMAL albo SizeEnum.COMPRESSED
  var SizeEnum = {
    NORMAL: 0,
    COMPRESSED: 1
  }

  function Popup(events){
    /*
      Metody z klasy Application obslugujace zdarzenia.
      events: {
        startFetchingInIntervals(),
        stopFetchingInIntervals(),
        addToGroup(),
        sendMessageTo()
      }
    */
    this.events = events

    // stan UI komponentu
    this.state = {
      hidden: false,
      top: 10,
      left: 10,
      size: SizeEnum.NORMAL
    }

    // zaladuj poprzedni stan UI komponentu z dysku, o ile istnieje
    this.loadStateFromDisk()

    // elementy HTML
    this.kobox = null
    this.title = null
    this.expandButton = null
    this.membersTable = null
    this.hideButton = null

    // stworz strukture, przypisz elementy html do obiektu i nasluchuj zdarzenia
    this.build()

    // upewnij sie, ze okienko jest widoczne w przegladarce
    this.noOverflow()

    // dopasuj wyglad w zaleznosci od this.state.size
    this.implementStateSize()
  }

  Popup.prototype.renderMembers = function(members){
    this.title.removeAttribute('data-battleinfo')

    var tbody = document.createElement('tbody')

    var includesHero = false
    var count = 0
    var MEMBERS_TUPLE_LENGTH = 10

    /*
      tablica members to ciag zlaczonych tablic (krotek) typu:
      [ id, nick, lvl, prof, map, x, y, ?, loggedTimeAgo, icon ]
      rozmar jednej takiej tablicy przechowywany jest w stalej MEMBERS_TUPLE_LENGTH

      > > > UWAGA! < < <
      PRAWDOPODOBNIE COS SIE KIEDYS ZMIENI W STRUKTURZE TEJ TABLICY
      PRZY TESTOWANIU WARTO JA WYPISAC Z console.log(members)

      Zmiany w przeszlosci:
      - dodano 10 element, czyli sciezke do wygladu postaci (icon)
      - loggedTimeAgo (9 element) przechowywal wartosc 'online' gdy gracz byl zalogowany
    */

    for(var j = 0; j <= members.length; j += MEMBERS_TUPLE_LENGTH){
      // jezeli dany gracz jest zalogowany to loggedTimeAgo (dziewiaty element krotki) jest rowny zero
      if(members[j+8] !== 0)
        continue

      count++

      // nie pokazuj wlasnej postaci na liscie zalogowanych klanowiczow
      var nick = members[j+1]
      if(isNewInterface ? nick === window.Engine.hero.d.nick : nick === hero.nick){
        includesHero = true
        continue
      }

      var id = members[j]
      var lvl = members[j+2]
      var prof = members[j+3]
      var map = members[j+4]
      var x = members[j+5]
      var y = members[j+6]

      var row = tbody.insertRow()
      row.classList.add('ko-row')

      var addToGroupCell = row.insertCell()
      addToGroupCell.textContent = '+'
      if(isNewInterface) addToGroupCell.dataset.tip = 'Dodaj do grupy'
      else addToGroupCell.setAttribute('tip', 'Dodaj do grupy')
      addToGroupCell.classList.add('ko-add-to-group-cell')
      addToGroupCell.addEventListener('click', this.events.addToGroup.bind(this, id))

      var nickCell = row.insertCell()
      nickCell.textContent = `${nick} (${lvl}${prof})`
      nickCell.classList.add('ko-nick-cell')
      nickCell.addEventListener('click', this.events.sendMessageTo.bind(this, nick))

      var locationTip = `${map} (${x},${y})`
      if(this.state.size == SizeEnum.COMPRESSED){
        if(isNewInterface) nickCell.dataset.tip = locationTip
        else  nickCell.setAttribute('tip', locationTip)
      } else {
        var mapCell = row.insertCell()
        mapCell.textContent = map
        mapCell.classList.add('ko-map-cell')
        if(isNewInterface) mapCell.dataset.tip = locationTip
        else mapCell.setAttribute('tip', locationTip)
      }
    }

    if(!includesHero)
      count++

    if(this.state.size == SizeEnum.COMPRESSED)
      this.title.textContent = `Online: ${count}`
    else
      this.title.textContent = `Klanowicze online: ${count}`

    var titleTipText = count === 1
      ? 'Jesteś tylko ty'
      : `${count} klanowiczów (łącznie z tobą)`

    if(isNewInterface) this.title.dataset.tip = titleTipText
    else this.title.setAttribute('tip', titleTipText)

    if(this.membersTable.tBodies.length === 0){
      this.membersTable.appendChild(tbody)
      return
    }

    this.membersTable.replaceChild(tbody, this.membersTable.tBodies[0])
  }

  Popup.prototype.setBattleInfo = function(){
    this.title.textContent = this.state.size === SizeEnum.COMPRESSED
      ? 'Walka'
      : 'Gracz uczestniczy w walce'

    if(isNewInterface) this.title.dataset.tip = 'Dodatek aktywuje się po zakończeniu walki'
    else this.title.setAttribute('tip', 'Dodatek aktywuje się po zakończeniu walki')

    this.title.setAttribute('data-battleinfo', '1')
  }

  Popup.prototype.handleHideButtonClick = function(){
    var newHidden = !this.state.hidden
    this.state.hidden = newHidden
    this.membersTable.hidden = this.state.hidden
    this.saveStateToDisk()
    if(this.state.hidden){
      this.hideButton.textContent = 'Rozwiń'
      this.events.stopFetchingInIntervals()
    } else {
      this.hideButton.textContent = 'Zwiń'
      this.events.startFetchingInIntervals()
    }
  }

  Popup.prototype.implementStateSize = function(){
    // aktualizacja klasy
    if(this.state.size === SizeEnum.COMPRESSED){
      this.kobox.classList.add('compressed')
    } else {
      this.kobox.classList.remove('compressed')
    }

    // aktualizacja tekstu
    if(this.title.getAttribute('data-battleinfo')){
      // wyswietlono wczesniej informacje o walce, nie zmieniaj
      this.setBattleInfo()
    } else {
      var lastOnline = this.title.textContent.split(': ')[1]
      if(lastOnline === undefined)
        lastOnline = '-'

      if(this.state.size === SizeEnum.COMPRESSED){
        this.title.textContent = `Online: ${lastOnline}`
      } else {
        this.title.textContent = `Klanowicze online: ${lastOnline}`
      }
    }
  }

  Popup.prototype.handleExpandButtonClick = function(){
    var nextSize = (this.state.size + 1) % 2
    this.state.size = nextSize
    this.saveStateToDisk()
    this.implementStateSize()
  }

  Popup.prototype.loadStateFromDisk = function(){
    try {
      var state = JSON.parse(
        localStorage.getItem(STORAGE_KEY)
      )

      if(state.areMembersHidden !== undefined || state.wasMembersHidden !== undefined)
        throw 'Stary sposób zapisu'

      if(state.hidden !== undefined && state.top !== undefined && state.left !== undefined && state.size !== undefined)
        this.state = state
    } catch(error) {
      console.log('Klanowicze online: błędna konfiguracja, reset. Powód:', error)
      localStorage.removeItem(STORAGE_KEY)
    }
  }

  Popup.prototype.saveStateToDisk = function(){
    // funkcja w setTimeout tworzy nowy this
    var self = this

    // nie zatrzymuj petli zdarzen
    setTimeout(function(){
      localStorage.setItem(STORAGE_KEY, JSON.stringify(self.state))
    }, 0)
  }

  // ogranicz pozycje okna do widzialnej czesci ekranu
  Popup.prototype.noOverflow = function(){
    var { top, left, width } = this.kobox.getBoundingClientRect()

    var oneThird = Math.ceil(1/3*width)

    if(top < 0)
      this.kobox.style.top = `0px`
    else if(top > window.innerHeight - 18)
      this.kobox.style.top = `${window.innerHeight - 18}px`

    if(left < 0 - oneThird*2)
      this.kobox.style.left = `${0 - oneThird*2}px`
    else if(left > window.innerWidth - oneThird)
      this.kobox.style.left = `${window.innerWidth - oneThird}px`

    // zapisz zmiany
    if(this.state.top !== top || this.state.left !== left){
      this.state.top = top
      this.state.left = left
      this.saveStateToDisk()
    }
  }

  Popup.prototype.build = function(){
    // struktura HTML
    $(document.body).append(`
      <div id="kobox">
        <div class="header">
          <span ctip="t_npc"></span>
          <img class="expand" tip="Zmień wielkość" ctip="t_npc" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE5IDEyaC0ydjNoLTN2Mmg1di01ek03IDloM1Y3SDV2NWgyVjl6bTE0LTZIM2MtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxOGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDNWNWgxOFYxOXoiLz48L3N2Zz4=">
        </div>
        <table></table>
        <div class="hide">Zwiń</div>
        <div class="corner1"></div>
        <div class="corner2"></div>
      </div>
    `);

    // przypisz elementy do obiektu
    this.kobox = document.querySelector('#kobox')
    this.title = this.kobox.querySelector('.header span')
    this.expandButton = this.kobox.querySelector('.header img')
    this.membersTable = this.kobox.querySelector('table')
    this.hideButton = this.kobox.querySelector('.hide')

    // zaktualizuj wyglad
    this.kobox.style.left = `${this.state.left}px`
    this.kobox.style.top = `${this.state.top}px`
    this.hideButton.textContent = this.state.hidden
      ? 'Rozwiń'
      : 'Zwiń'
    this.membersTable.hidden = this.state.hidden

    if(isNewInterface) this.expandButton.dataset.tip = "Zmień wielkość"
    else this.expandButton.setAttribute('tip', 'Zmień wielkość')

    // obsluz zdarzenia
    this.hideButton.addEventListener('click', this.handleHideButtonClick.bind(this))
    this.expandButton.addEventListener('click', this.handleExpandButtonClick.bind(this))

    var self = this
    // przeciaganie okienka
    $(this.kobox).draggable({
      cancel: 'table, .hide, .expand',
      start: function(){
        if(!isNewInterface) g.lock.add('ko')
      },
      stop: function(){
        if(!isNewInterface) g.lock.remove('ko')
        self.noOverflow()
      }
    })

    // style
    var stylesheet = document.createElement('style')
    stylesheet.appendChild(document.createTextNode(`
      #kobox {
        font-family: verdana;
        box-sizing: border-box;
        position: absolute !important;
        border: 1px solid #545454;
        color:#fff;
        font-weight:300;
        background: rgba(0,0,0,.6);
        z-index: 500;
        font-size: 14px;
        width: 12em;
        box-shadow:0 2px 5px 0 rgba (0,0,0,.5);
        border-radius: 5px;
      }

      #kobox.compressed {
        width: 12em;
      }

      @media (min-width: 1500px) {
        #kobox {
          font-size: 16px;
        }
      }

      @media (min-width: 1700px) {
        #kobox {
          font-size: 17px;
          width: 24em;
        }
        #kobox.compressed {
          width: 12em;
        }
      }

      #kobox > .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 3px;
        font-size: 1em;
        text-align: center;
        font-weight: bold;
        border-bottom: 1px solid white;
        z-index: 1;
        background: rgba(0,0,0,.6);
      }

      #kobox > .header {
        cursor: move;
        cursor: -webkit-grab;
        cursor:    -moz-grab;
        cursor:         grab;
      }
      #kobox > .header:active {
        cursor: -webkit-grabbing;
        cursor:    -moz-grabbing;
        cursor:         grabbing;
      }

      #kobox > .header > span {
        pointer-events: none;
      }

      #kobox > .header > .expand {
        height: 1em;
        cursor: pointer;
        opacity: 0.7;
      }
      #kobox > .header > .expand:hover {
        opacity: 0.9;
      }

      #kobox > table {
        font-size: 0.7em;
        width: 100%;
        border-collapse: collapse;
        table-layout: fixed;
      }

      #kobox > .hide {
        font-size: 0.8em;
        margin: 1px;
        text-align: center;
        cursor: pointer;
        border-top: 1px solid white;
        z-index: 1;
        user-select: none;
      }

      #kobox > .corner1, .corner2 {
        position: absolute;
        width: 35px;
        height: 23px;
        z-index: -1;
      }
      #kobox > .corner1 {
        background: url(img/tip-cor.png) no-repeat 0px 0px;
        top: -6px;
        left: -6px;
      }
      #kobox > .corner2 {
        background: url(img/tip-cor.png) no-repeat -35px 0px;
        bottom: -6px;
        right: -6px;
      }

      #kobox > table > tbody > .ko-row {
        border: solid;
        border-width: 1px 0;
        border-color: #5d5006;
        height: 1.6em;
      }
      #kobox > table > tbody > .ko-row:hover {
        background: #3c3c16;
      }
      #kobox > table > tbody > .ko-row:first-child {
        border-top: none;
      }
      #kobox > table > tbody > .ko-row:last-child {
        border-bottom: none;
      }

      #kobox > table > tbody > .ko-row > .ko-add-to-group-cell, .ko-nick-cell {
        cursor: pointer;
        user-select: none;
      }
      #kobox > table > tbody > .ko-row > .ko-add-to-group-cell:hover, .ko-nick-cell:hover {
        color: #eaeb74;
      }

      #kobox > table > tbody > .ko-row > .ko-add-to-group-cell {
        text-align: center;
        width: 12px;
      }

      #kobox > table > tbody > .ko-row > .ko-map-cell {
        text-align: right;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
    `))
    document.body.appendChild(stylesheet)
  }
  /*
    SEKCJA UI KONIEC
    /\ /\ /\
  */

  /*
    \/ \/ \/
    KLASA APPLICATION
  */
  // pomocnicza funkcja do deklaracji metod abstrakcyjnych (ktore musza zostac nadpisane przez dzieci)
  var abstractMethod = function(){
    throw new Error('Klanowicze online: wywolanie metody abstrakcyjnej')
  }

  function Application(){
    // konstruktor

    this.interval = null

    this.popup = new Popup({
      startFetchingInIntervals: this.startFetchingInIntervals.bind(this),
      stopFetchingInIntervals: this.stopFetchingInIntervals.bind(this),
      addToGroup: this.addToGroup.bind(this),
      sendMessageTo: this.sendMessageTo.bind(this)
    })

    if(!this.popup.hidden)
      this.startFetchingInIntervals()
  }
  // metody:
  Application.prototype.startFetchingInIntervals = function(){
    this.fetchMembers()
    this.interval = setInterval(this.fetchMembers.bind(this), 10000)
  }
  Application.prototype.stopFetchingInIntervals = function(){
    if(this.interval !== null){
      clearInterval(this.interval)
      this.interval = null
    }
  }
  // metody abstrakcyjne (musza byc nadpisane przez dzieci):
  Application.prototype.fetchMembers = abstractMethod
  Application.prototype.addToGroup = abstractMethod
  Application.prototype.sendMessageTo = abstractMethod
  Application.prototype.checkIfIsInBattle = abstractMethod

  /*
    \/ \/ \/
    DZIECI KLASY APPLICATION (z nadpisanymi metodami pod Nowy Interfejs i Stary Interfejs)
  */

  // Stary Interfejs
  function ApplicationSI(){
    var self = this

    // ostatnia pobrana lista klanowiczow
    var lastFetchedMembers = null

    // gracz otworzyl okno z klanowiczami
    var isOpenedMembersWindow = false
    document.querySelector('#clanmenu span[name="Klanowicze"]').parentElement.addEventListener('click', () => {
      isOpenedMembersWindow = true
    })

    var parseInput = window.parseInput
    window.parseInput = function(d, callback, xhr){
      if(d.w && (d.w.toString().startsWith('Zapytanie odrzucone') || d.w.toString().startsWith('Odrzucono stare zapytanie')))
        delete d.w

      if(!d.members2 && !d.members)
        return parseInput(d, callback, xhr)

      if(isOpenedMembersWindow){
        // gracz otworzyl okno z klanowiczami
        isOpenedMembersWindow = false
      } else {
        // lista klanowiczow przechwycona przez dodatek
        if(d.members) lastFetchedMembers = d.members.slice()
        delete d.members2
        delete d.members
      }

      return parseInput(d, callback, xhr)
    }

    this.fetchMembers = function(){
      // pierwsze zaladowanie strony - wyswietl info o walce
      if(self.checkIfIsInBattle() && lastFetchedMembers === null){
        self.popup.setBattleInfo()
      }

      _g('clan&a=members', function(){
        if(lastFetchedMembers)
          self.popup.renderMembers(lastFetchedMembers)
      })
    }

    this.checkIfIsInBattle = function(){
      return Boolean(g.battle)
    }

    this.addToGroup = function(id){
      window._g(`party&a=inv&id=${id}`)
    }

    this.sendMessageTo = function(nick){
      window.chatTo(nick)
    }

    Application.call(this)
  }

  // Nowy Interfejs
  function ApplicationNI(){
    var self = this

    // jesli gracz nie ma klanu to wyjdz
    if(!window.Engine.hero.d.clan)
      return

    const NO_CHAT_INPUT_WARN = 'Klanowicze online: chatInputElement ma wartosc null - potrzebny jest nowy selektor okienka tekstowego chatu.\nSkontaktuj sie z dodatkopisarzem.'

    var chatInputElement = document.querySelector('.chat-tpl input')
    if(chatInputElement === null){
      console.warn(NO_CHAT_INPUT_WARN);
    }

    var fetchedMembersBefore = false

    this.fetchMembers = function(){
      if(self.checkIfIsInBattle() && !fetchedMembersBefore){
        self.popup.setBattleInfo()
      }

      // nie przeszkadzaj gdy gracz zmienia postac lub pisze wiadomosc
      if(Engine.logOff || document.activeElement === chatInputElement)
        return

      var clan = Engine.clan ? { ...Engine.clan } : Engine.clan
      if(!clan)
        Engine.clan = {
          updateMembers(){}
        }

      _g(`clan&a=members`, function({ members }){
        Engine.clan = clan

        if(members){
          self.popup.renderMembers(members)

          if(!fetchedMembersBefore)
            fetchedMembersBefore = true
        }
      })
    }

    this.checkIfIsInBattle = function(){
      return window.Engine.battle && window.Engine.battle.show
    }

    this.addToGroup = function(id){
      window._g(`party&a=inv&id=${id}`)
    }

    this.sendMessageTo = function(nick){
      var chatInput = document.querySelector('.chat-tpl .input-wrapper input')
      if(chatInput === null){
        console.warn(NO_CHAT_INPUT_WARN)
      } else {
        chatInput.value = `@${nick.replace(/ /g, '_')} `
        chatInput.focus()
      }
    }

    Application.call(this)
  }

  // dziedziczenie (NIE RUSZAJ TEGO)
  ApplicationSI.prototype = Object.create(Application.prototype);
  ApplicationSI.prototype.constructor = ApplicationSI
  ApplicationNI.prototype = Object.create(Application.prototype);
  ApplicationNI.prototype.constructor = ApplicationNI

  // funkcja pomocnicza, ktora czeka az funkcja "check" zwroci prawde i wtedy wywola funkcje "then"
  var waitFor = function(check, then){
    if(!check())
      setTimeout(waitFor, 1000, check, then)
    else
      then()
  }

  if(isNewInterface){
    waitFor(function(){
      // czekaj na pelne zaladowanie gry
      return window.Engine && window.Engine.allInit
    }, function(){
       new ApplicationNI()
    })
  } else {
    waitFor(function(){
      // czasem zdarzy sie, ze TamperMonkey wykona sie przed skryptem Margonem i zmienna g jest niezainicjowana - czekaj na zaladowanie
      return window.g !== undefined
    }, function(){
      window.g.loadQueue.push({ fun: function(){
        new ApplicationSI()
      } })
    })
  }
})();