Quest Runner — Mapper

Veri işleme katmanı

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Quest Runner — Mapper
// @namespace    popmundo-qr
// @description  Veri işleme katmanı
// @version      0.1
// @author       luke-james-gibson
// @license      MIT
// @match        https://*.popmundo.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    function waitEngine(fn) {
        if (window.QR?.ready) { fn(); return; }
        setTimeout(() => waitEngine(fn), 150);
    }
    waitEngine(init);

    function init() {
        const QR = window.QR;
        const S  = QR.S;

        // DIR MAP
        const DIR = {
            N:'North', NE:'NorthEast', NW:'NorthWest',
            S:'South', SE:'SouthEast', SW:'SouthWest',
            E:'East',  W:'West', UP:'Up', DOWN:'Down',
            NORTH:'North', NORTHEAST:'NorthEast', NORTHWEST:'NorthWest',
            SOUTH:'South', SOUTHEAST:'SouthEast', SOUTHWEST:'SouthWest',
            EAST:'East',   WEST:'West', UPWARD:'Up', DOWNWARD:'Down',
            K:'North', KD:'NorthEast', KB:'NorthWest',
            G:'South', GD:'SouthEast', GB:'SouthWest',
            D:'East',  B:'West', Y:'Up', A:'Down',
            KUZEY:'North', KUZEYDOGU:'NorthEast', KUZEYBATI:'NorthWest',
            GUNEY:'South', GUNEYDOGU:'SouthEast', GUNEYBATI:'SouthWest',
            DOGU:'East',   BATI:'West', YUKARI:'Up', ASAGI:'Down',
            L:'East', O:'West', NO:'NorthWest', SO:'SouthWest', NL:'NorthEast', SL:'SouthEast',
            NORTE:'North', SUL:'South', LESTE:'East', OESTE:'West',
            NORDESTE:'NorthEast', NOROESTE:'NorthWest', SUDESTE:'SouthEast', SUDOESTE:'SouthWest',
        };

        const DIR_ARROWS = {
            North:'↑', NorthEast:'↗', East:'→', SouthEast:'↘',
            South:'↓', SouthWest:'↙', West:'←', NorthWest:'↖',
            Up:'⬆', Down:'⬇',
        };

        function cleanToken(t) {
            return t.toUpperCase()
                .replace(/Ğ/g,'G').replace(/Ş/g,'S').replace(/İ/g,'I').replace(/ı/g,'I')
                .replace(/Ü/g,'U').replace(/Ö/g,'O').replace(/Ç/g,'C')
                .replace(/[^A-Z]/g,'');
        }

        QR.dirArrow  = d => DIR_ARROWS[d] || d;
        QR.resolveDir = t => DIR[cleanToken(t)] || null;

        // PARSER
        const norm = s => s.trim().toLowerCase().replace(/\s+/g,' ');

        function parseUseItem(raw) {
            const body = raw.slice(9).trim();
            if (!body) return { item:null, location:null };
            const at = body.indexOf('@');
            if (at >= 0) {
                const rawItem = body.slice(0, at).trim();
                return { item:(!rawItem || rawItem==='?') ? null : norm(rawItem), location:norm(body.slice(at+1)) };
            }
            const rawItem = body.trim();
            return { item:(!rawItem || rawItem==='?') ? null : norm(rawItem), location:null };
        }

        function trySimpleLine(line, steps) {
            // "Use <Location>" → use_item ? @ <Location>
            if (/^use\s+\S/i.test(line) && !/^use_item/i.test(line)) {
                const loc = line.slice(4).trim();
                if (loc) { steps.push({ type:'use_item', item:null, location:norm(loc) }); return true; }
            }
            // "<N> minutes/min/dk"
            const minM = line.match(/^(\d+)\s*(?:minutes?|min|dk)$/i);
            if (minM) { steps.push({ type:'passage', minutes:parseInt(minM[1]) }); return true; }
            // "Arrival <Label>"
            if (/^arrival\s+/i.test(line)) {
                const label = line.slice(8).trim().replace(/\s+/g,'_').toUpperCase();
                steps.push({ type:'checkpoint', label:'ARRIVAL_'+label }); return true;
            }
            return false;
        }

        function parseDirToken(token, errors) {
            if (!token) return null;
            const bm = token.match(/^[\[\(](.+)[\]\)]$/);
            if (bm) {
                const dirs = bm[1].split('|').map(QR.resolveDir).filter(Boolean);
                if (dirs.length) return { type:'move', dirs };
                errors && errors.push('Bad branch: '+token); return null;
            }
            const dir = QR.resolveDir(token);
            if (dir) return { type:'move', dirs:[dir] };
            if (token.length>1 && !/^\d+$/.test(token)) errors && errors.push('Unknown dir: "'+token+'"');
            return null;
        }

        function extractMin(line) {
            const m = line.match(/(\d+)\s*(min|dk)/i);
            return m ? parseInt(m[1]) : null;
        }

        function parseText(text) {
            const steps=[], errors=[];
            for (const raw of text.split('\n')) {
                const line = raw.trim();
                if (!line || /^(#|\/)/.test(line)) continue;
                if (/^move\s/i.test(line)) {
                    line.slice(5).trim().split(/[\s,\-]+/).forEach(t => {
                        const s=parseDirToken(t,errors); if(s) steps.push(s);
                    });
                } else if (/^use_item/i.test(line)) {
                    steps.push({ type:'use_item', ...parseUseItem(line) });
                } else if (/^passage/i.test(line)) {
                    steps.push({ type:'passage', minutes:extractMin(line) });
                } else if (/^wait/i.test(line)) {
                    steps.push({ type:'wait', minutes:extractMin(line) });
                } else if (/^checkpoint/i.test(line)) {
                    steps.push({ type:'checkpoint', label:line.slice(10).trim()||null });
                } else if (trySimpleLine(line, steps)) {
                    // handled
                } else {
                    const tokens = line.split(/[\s,\-]+/).filter(Boolean);
                    let any=false;
                    tokens.forEach(t => { const s=parseDirToken(t,errors); if(s){steps.push(s);any=true;} });
                    if (!any && tokens.length) errors.push('Unknown: "'+line.slice(0,40)+'"');
                }
            }
            return { steps, errors };
        }
        QR.parseText = parseText;

        // QUEST STORE
        const QUEST_KEY = 'qr_quests';
        const QStore = {
            load:   ()         => S.get(QUEST_KEY,{}),
            save:   db         => S.set(QUEST_KEY,db),
            get:    id         => S.get(QUEST_KEY,{})[id]||null,
            delete: id         => { const db=S.get(QUEST_KEY,{}); delete db[id]; S.set(QUEST_KEY,db); },
            upsert: q          => {
                const db=S.get(QUEST_KEY,{});
                if (db[q.id]) q.versions=[...(db[q.id].versions||[]),{ts:Date.now(),text:db[q.id].text}];
                else q.versions=q.versions||[];
                db[q.id]=q; S.set(QUEST_KEY,db);
            },
            new: (name,text)   => { const {steps,errors}=parseText(text); return {id:'q_'+Date.now(),name,text,sequence:steps,versions:[],builtin:false,errors}; },
            copy: (srcId,name) => { const src=S.get(QUEST_KEY,{})[srcId]; return src?{...src,id:'q_'+Date.now(),name,versions:[],builtin:false}:null; },
            createVariant: (srcId,stepIdx,newItem) => {
                const src=S.get(QUEST_KEY,{})[srcId]; if(!src) return null;
                const varNum=(src.variants||0)+1;
                const seq=src.sequence.map((s,i)=>(i===stepIdx&&s.type==='use_item')?{...s,item:newItem}:s);
                const db=S.get(QUEST_KEY,{});
                if(db[srcId]) db[srcId].variants=varNum;
                const q={...src,id:'q_'+Date.now(),name:src.name+' v'+varNum,sequence:seq,versions:[],builtin:false,variantOf:srcId};
                db[q.id]=q; S.set(QUEST_KEY,db); return q;
            },
        };
        QR.quests = QStore;

        // MAPPER GRAPH
        const MAP_KEY = 'qr_mapdata';
        const Mapper = {
            load:  ()   => S.get(MAP_KEY,{rooms:{},edges:[]}),
            save:  data => S.set(MAP_KEY,data),
            clear: ()   => S.del(MAP_KEY),

            recordRoom: (name,dirs,items,detailed) => {
                if(!name) return;
                const data=Mapper.load(), ex=data.rooms[name]||{};
                data.rooms[name]={ name, dirs, items, visits:(ex.visits||0)+1, lastSeen:Date.now(), detailed:detailed||ex.detailed||false };
                Mapper.save(data);
            },
            recordEdge: (from,dir,to) => {
                if(!from||!dir||!to||from===to) return;
                const data=Mapper.load();
                const ex=data.edges.find(e=>e.from===from&&e.dir===dir);
                if(ex){ex.to=to;ex.ts=Date.now();}
                else data.edges.push({from,dir,to,ts:Date.now()});
                Mapper.save(data);
            },
            snapshot: () => {
                const name=QR.dom?.locName?.(); if(!name) return null;
                return { name, dirs:QR.dom.activeDirections(), items:QR.dom.getItems().map(i=>i.name) };
            },
            exportJSON:  () => JSON.stringify(Mapper.load(),null,2),
            exportDraft: () => {
                const data=Mapper.load(), edges=[...data.edges].sort((a,b)=>a.ts-b.ts);
                if(!edges.length) return '# Harita verisi yok';
                const lines=['# Quest taslağı — haritadan otomatik']; let prev=null;
                for(const e of edges) {
                    if(e.from!==prev) lines.push('\n# '+e.from);
                    lines.push('move '+e.dir);
                    prev=e.to;
                    const room=data.rooms[e.to];
                    room?.items?.forEach(item=>lines.push('# use_item '+item+' @ '+e.to));
                }
                return lines.join('\n');
            },
            buildMapHTML: () => {
                const data=Mapper.load(), rooms=Object.values(data.rooms);
                if(!rooms.length) return '<div style="opacity:.5;padding:10px;text-align:center;">Henüz harita verisi yok</div>';
                const listRows=rooms.sort((a,b)=>b.lastSeen-a.lastSeen).map(r=>{
                    const dirBtns=r.dirs.map(d=>{
                        const edge=data.edges.find(e=>e.from===r.name&&e.dir===d);
                        const lc=edge?'data-to="'+edge.to+'"':'';
                        return '<button class="qr-map-go" '+lc+' data-dir="'+d+'" '+
                            'style="background:#1565C0;color:white;border:none;border-radius:4px;padding:2px 5px;font-size:11px;cursor:pointer;margin:1px;">'+
                            QR.dirArrow(d)+' '+d+'</button>';
                    }).join('');
                    return '<tr style="border-bottom:1px solid rgba(255,255,255,.06);">'+
                        '<td style="padding:4px 6px;font-size:11px;">'+r.name+'</td>'+
                        '<td style="padding:4px 6px;">'+dirBtns+'</td>'+
                        '<td style="padding:4px 6px;font-size:10px;color:#FFD600;">'+(r.items.join(', ')||'—')+'</td>'+
                        '<td style="padding:4px 6px;font-size:9px;opacity:.4;">×'+r.visits+'</td></tr>';
                }).join('');
                const edgeLines=data.edges.slice(-30).map(e=>
                    '<div style="font-size:10px;padding:1px 0;">'+
                    '<span style="opacity:.5;">'+e.from+'</span> '+
                    '<span style="color:#FFD600;">'+QR.dirArrow(e.dir)+' '+e.dir+'</span> → '+e.to+'</div>'
                ).join('');
                return '<div style="max-height:180px;overflow-y:auto;margin-bottom:8px;">'+
                    '<table style="width:100%;border-collapse:collapse;">'+
                    '<thead><tr style="font-size:9px;opacity:.4;border-bottom:1px solid rgba(255,255,255,.15);">'+
                    '<th style="padding:3px;text-align:left;">Oda</th><th style="padding:3px;text-align:left;">Çıkışlar</th>'+
                    '<th style="padding:3px;text-align:left;">Item</th><th></th></tr></thead>'+
                    '<tbody>'+listRows+'</tbody></table></div>'+
                    '<div style="max-height:120px;overflow-y:auto;border-top:1px solid rgba(255,255,255,.1);padding-top:6px;">'+
                    '<div style="font-size:9px;opacity:.4;margin-bottom:3px;">Kenarlar (son 30)</div>'+edgeLines+'</div>';
            },
        };
        QR.mapper = Mapper;

        // NORTH POLE SEED
        (function(){
            const db=QStore.load(); if(db['north-pole']) return;
            const NP=[
                '# ICY CAVERNS - Giriş','checkpoint ICY_ENTRANCE',
                'move North SouthEast East NorthEast','passage 16min','',
                '# ICY CAVERNS - Yol','checkpoint ICY_PATH_START',
                'move East SouthEast South SouthEast SouthEast Down South SouthWest South SouthEast South South SouthEast South SouthWest South SouthEast SouthWest SouthEast East SouthEast SouthWest West',
                'use_item ? @ Icy Caverns','',
                '# ICY CAVERNS - Dönüş','checkpoint ICY_RETURN',
                'move East NorthEast NorthWest West NorthWest NorthEast NorthWest North NorthEast North NorthWest North North NorthWest North NorthEast North Up NorthWest NorthWest North NorthWest West',
                'passage 16min','',
                '# ICEPEAK MOUNTAIN - Giriş','checkpoint ICEPEAK_ENTRANCE',
                'move SouthWest West NorthWest West SouthWest West','passage 16min','',
                '# ICEPEAK MOUNTAIN - Yol','checkpoint ICEPEAK_PATH_START',
                'move NorthWest Up NorthWest Up NorthEast Up NorthEast Down SouthEast East Down NorthEast North Up NorthEast Up West Down NorthWest Up North Up Up',
                'use_item ? @ Icepeak Mountain','',
                '# ICEPEAK MOUNTAIN - Dönüş','checkpoint ICEPEAK_RETURN',
                'move Down Down South Down SouthEast Up East Down SouthWest Down South SouthWest Up West NorthWest Up SouthWest Down SouthWest Down SouthEast Down SouthEast',
                'passage 16min','',
                '# BLINDING BLIZZARD - Giriş','checkpoint BLIZZARD_ENTRANCE',
                'move East NorthEast East North NorthEast East NorthEast North','passage 16min','',
                '# BLINDING BLIZZARD - Yol','checkpoint BLIZZARD_PATH_START',
                'move North North East North East North East North East North East North East North East',
                'use_item ? @ Blinding Blizzard','move North West','passage 16min','',
                '# FIREPLACE','checkpoint FIREPLACE',
                'move South SouthWest West North','use_item ? @ Fireplace','',
                '# TREE','checkpoint TREE',
                'move South SouthWest NorthWest North','use_item ? @ Tree',
            ].join('\n');
            const {steps}=parseText(NP);
            db['north-pole']={id:'north-pole',name:'North Pole',text:NP,sequence:steps,versions:[],builtin:true};
            QStore.save(db);
        })();

        // MAPPER: sayfa yüklenince oda kaydet
        let _prevRoom=null, _prevDir=null;
        function mapperOnLoad() {
            if(!QR.page.isCompass()) return;
            const snap=Mapper.snapshot(); if(!snap) return;
            const char=QR.char.current();
            const mapping=char?S.get('qr_map_mode_'+char,false):false;
            Mapper.recordRoom(snap.name,snap.dirs,snap.items,mapping);
            if(_prevRoom&&_prevDir) Mapper.recordEdge(_prevRoom,_prevDir,snap.name);
            _prevRoom=snap.name; _prevDir=null;
            QR.emit('map_updated',snap,Mapper.load());
        }
        QR.on('moved',       (c,dir)=>{ _prevRoom=QR.dom?.locName?.(); _prevDir=dir; });
        QR.on('manual_move', (c,dir)=>{ _prevRoom=QR.dom?.locName?.(); _prevDir=dir; });
        mapperOnLoad();

        QR.mapperReady=true;
        QR.emit('mapper_ready');
        console.log('[QR] Mapper v0.1 ready');
    }
})();