Klanowicze online

Dodatek do gry Margonem

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Klanowicze online
// @author       Reskiezis
// @description  Dodatek do gry Margonem
// @version      2.0.3
// @match        *://*.margonem.pl/
// @match        *://*.margonem.com/
// @run-at       document-idle
// @grant        none
// @namespace    https://greasyfork.org/users/233329
// ==/UserScript==
 
/*
  - - -
  KLANOWICZE ONLINE
  AUTORSTWA RESKIEZISA aka PERSKIEGO KOTA
  WERSJA DLA NOWEGO I STAREGO INTERFEJSU
  - - -
 
  - - - - - - -
  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: Helvetica;
        box-sizing: border-box;
        position: absolute !important;
        border: 3px gold double;
        color: #eeeeee;
        background: black;
        z-index: 500;
        font-size: 14px;
        width: 12em;
      }
 
      #kobox.compressed {
        width: 8em;
      }
 
      @media (min-width: 1500px) {
        #kobox {
          font-size: 16px;
        }
      }
 
      @media (min-width: 1700px) {
        #kobox {
          font-size: 17px;
          width: 16em;
        }
        #kobox.compressed {
          width: 10em;
        }
      }
 
      #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 gold;
        z-index: 1;
      }
 
      #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 gold;
        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){
            getEngine().chatController.getChatInputWrapper().setPrivateMessageProcedure(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){
            getEngine().chatController.getChatInputWrapper().setPrivateMessageProcedure(nick)
        }
 
        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()
            } })
        })
    }
})();