Popmundo quest guide — guide mode, step navigation, log and timer.
// ==UserScript==
// @name 📜 Guide
// @name:en 📜 Guide
// @name:pt-BR 📜 Guide
// @namespace popmundo.guide
// @version 2.1
// @description Popmundo quest rehberi — guide modu, adım takibi, log ve zamanlayıcı.
// @description:en Popmundo quest guide — guide mode, step navigation, log and timer.
// @description:pt-BR Guia de quests do Popmundo — modo guia, navegação por etapas, log e timer.
// @author luke-james-gibson
// @license MIT
// @id 568413
// @match https://*.popmundo.com/*
// @homepageURL https://rentry.org/questrunner
// @supportURL https://rentry.org/kobequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
// ─── POPCONTROL DISABLE CHECK ───────
try { const _ppc = JSON.parse(localStorage.getItem('ppc_enabled')||'{}'); if (_ppc['guide'] === false) return; } catch {}
window.QR = {};
const QR = window.QR;
// EVENTS
const _ev = {};
QR.on = (e,fn) => { (_ev[e]=_ev[e]||[]).push(fn); };
QR.off = (e,fn) => { if(_ev[e]) _ev[e]=_ev[e].filter(f=>f!==fn); };
QR.emit = (e,...a) => { (_ev[e]||[]).forEach(fn=>{ try{fn(...a);}catch(err){} }); };
// STORAGE
const S = {
get: (k,d) => { try { const v=GM_getValue(k,null); return v!==null?JSON.parse(v):d; } catch(e){return d;} },
set: (k,v) => { try { GM_setValue(k,JSON.stringify(v)); } catch(e){} },
del: k => { try { GM_deleteValue(k); } catch(e){} },
keys: p => { try { return (GM_listValues?GM_listValues():[]).filter(k=>k.startsWith(p)); } catch(e){return[];} },
};
QR.S = S;
// LANG
const _getLang = () => {
try { const m=document.cookie.match(/ppm_lang=([^;]+)/); return m?m[1]:'TR'; } catch(e){return 'TR';}
};
const _setLang = code => { document.cookie=`ppm_lang=${code};domain=.popmundo.com;path=/;max-age=31536000`; };
const LANG = _getLang();
const _i18n = {
TR: {
tabChars:'Karakterler', tabQuests:'Questler', tabLog:'Log', tabSettings:'Ayarlar',
readMe:'📖 Beni Oku', kobe:'🗺️ Kobe', otherQuests:'🗺️ Diğer Questler',
questDb:'📋 Quest Veritabanı',
close:'Kapat',
langLabel:'Dil',
},
EN: {
tabChars:'Characters', tabQuests:'Quests', tabLog:'Log', tabSettings:'Settings',
readMe:'📖 Read Me', kobe:'🗺️ Kobe', otherQuests:'🗺️ Other Quests',
questDb:'📋 Quest Database',
close:'Close',
langLabel:'Language',
},
PT: {
tabChars:'Personagens', tabQuests:'Quests', tabLog:'Log', tabSettings:'Configurações',
readMe:'📖 Leia-me', kobe:'🗺️ Kobe', otherQuests:'🗺️ Outros Quests',
questDb:'📋 Banco de Quests',
close:'Fechar',
langLabel:'Idioma',
},
};
const _clMA = (() => { try { const v = localStorage.getItem('ppc_lc_guide'); return v ? JSON.parse(v) : null; } catch { return null; } })();
const t = k => (_clMA && _clMA[k]) ? _clMA[k] : ((_i18n[LANG]||_i18n.TR)[k] || (_i18n.TR)[k] || k);
// LOGGING
const LOG_KEY = c => 'qr_log_' + c;
const norm = s => s.trim().toLowerCase().replace(/\s+/g,' ');
function addLog(char, type, msg, loc) {
const log = S.get(LOG_KEY(char), []);
log.push({ ts:Date.now(), type, msg, loc:loc||QR.dom?.locName?.()||'' });
if (log.length>500) log.splice(0, log.length-500);
S.set(LOG_KEY(char), log);
QR.emit('log', char, { type, msg });
}
const addDLog = () => {};
QR.log = addLog;
QR.dlog = addDLog;
QR.getLog = c => S.get(LOG_KEY(c), []);
QR.clearLog = c => S.del(LOG_KEY(c));
// PAGE / CHAR
QR.page = {
isCompass: () => /\/Locale\/Compass/i.test(location.href),
isLocale: () => /\/Locale/i.test(location.href),
isMobile: () => window.innerWidth<=768||/Android|iPhone|iPad/i.test(navigator.userAgent),
};
QR.char = {
current: () => { const d=document.querySelector('[id$="ucCharacterBar_ddlCurrentCharacter"]'); return d?d.options[d.selectedIndex].text.trim():null; },
all: () => { const d=document.querySelector('[id$="ucCharacterBar_ddlCurrentCharacter"]'); return d?[...d.options].map(o=>o.text.trim()).filter(Boolean):[]; },
};
// CHARACTER STATE
const ck = (c,k) => 'qr_c_'+c+'_'+k;
const C = {
getStep: c => S.get(ck(c,'step'),0),
setStep: (c,n) => S.set(ck(c,'step'),n),
activeQuestId: c => S.get(ck(c,'questId'),null)||S.get(ck(c,'assigned'),null),
getQuestId: c => S.get(ck(c,'questId'),null),
setQuestId: (c,q) => S.set(ck(c,'questId'),q),
getAssigned: c => S.get(ck(c,'assigned'),null),
setAssigned: (c,q) => S.set(ck(c,'assigned'),q),
getStopMode: c => S.get(ck(c,'stopmode'),'ask'),
setStopMode: (c,v) => S.set(ck(c,'stopmode'),v),
getPassageTs: c => S.get(ck(c,'passageTs'),null),
setPassageTs: (c,v) => S.set(ck(c,'passageTs'),v),
getPassageMin: c => S.get(ck(c,'passageMin'),null),
setPassageMin: (c,v) => S.set(ck(c,'passageMin'),v),
getQuestStartTs: c => S.get(ck(c,'questStartTs'),null),
setQuestStartTs: (c,v) => S.set(ck(c,'questStartTs'),v),
getCheckpoints: c => S.get(ck(c,'checkpoints'),[]),
addCheckpoint: (c,d) => { const cps=S.get(ck(c,'checkpoints'),[]); cps.push({...d,ts:Date.now()}); S.set(ck(c,'checkpoints'),cps); },
resumeFromCheckpoint: (c,labelOrIdx) => {
const quest=QR.quests?.get(C.activeQuestId(c)); if(!quest) return false;
let target=-1, cpCount=0;
quest.sequence.forEach((s,i) => {
if(s.type!=='checkpoint') return;
if(typeof labelOrIdx==='number'?cpCount===labelOrIdx:s.label===labelOrIdx) target=i+1;
cpCount++;
});
if(target<0) return false;
C.setStep(c,target); QR.emit('resume',c,target,labelOrIdx); return true;
},
reset: c => ['step','questId','passageTs','passageMin','checkpoints','questStartTs'].forEach(k=>S.del(ck(c,k))),
getNote: c => S.get(ck(c,'note'),''),
setNote: (c,v) => S.set(ck(c,'note'),v),
all: () => { const s=new Set(); S.keys('qr_c_').forEach(k=>{ const m=k.match(/^qr_c_(.+)_\w+$/); if(m) s.add(m[1]); }); return [...s]; },
};
QR.C = C;
// DOM — SVG + onCompassLoad fallback
const VALID_DIRS = new Set(['North','NorthEast','NorthWest','South','SouthEast','SouthWest','East','West','Up','Down']);
function parseCompassScript() {
const result = { active:{}, inactive:[] };
for (const s of document.querySelectorAll('script')) {
if (!s.textContent.includes('onCompassLoad')) continue;
const activeRe = /activateCompassObject\("([^"]+)",\s*"([^"]+)"/g;
const inactiveRe = /deactivateCompassObject\("([^"]+)"/g;
let m;
while ((m=activeRe.exec(s.textContent))!==null) result.active[m[1]]=m[2];
while ((m=inactiveRe.exec(s.textContent))!==null) result.inactive.push(m[1]);
}
return result;
}
let _compassCache = null;
function getCompassData() { if (!_compassCache) _compassCache=parseCompassScript(); return _compassCache; }
QR.on('map_updated', () => { _compassCache=null; });
QR.dom = {
moveEl: dir => {
const svgEl=document.querySelector('#compass g#'+dir+'.fe-move');
if (svgEl) return svgEl;
const data=getCompassData();
if (data.active[dir]) return { getAttribute:(attr)=>attr==='data-lc'?data.active[dir]:null, _synthetic:true };
return null;
},
activeDirections: () => {
const svgDirs=[...document.querySelectorAll('#compass g.fe-move')].map(el=>el.id).filter(id=>VALID_DIRS.has(id));
if (svgDirs.length) return svgDirs;
return Object.keys(getCompassData().active).filter(d=>VALID_DIRS.has(d));
},
getItems: () => {
const items=[];
document.querySelectorAll('[id$="lnkItem"]').forEach(link => {
const row=link.closest('tr'); if(!row) return;
const useBtn=row.querySelector('[id$="btnUse"]'); if(!useBtn) return;
const name=link.textContent.trim();
const subEl=row.querySelector('.cText_Light em');
const subtitle=subEl?subEl.textContent.trim():'';
const fullLabel=subtitle?name+' — '+subtitle:name;
items.push({ name, subtitle, fullLabel, useBtn });
});
return items;
},
isTraveling: () => /seyahat|varis|traveling|arriving/i.test(document.body?.innerText||''),
locName: () => { const e=document.querySelector('h1'); return e?e.textContent.trim():null; },
};
// DIRECTION CLICK
function clickDirection(char, dir) {
const el=QR.dom.moveEl(dir), lc=el?.getAttribute('data-lc'), loc=QR.dom.locName();
if (el && !el._synthetic) {
try {
el.dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true,view:unsafeWindow}));
addLog(char,'move',(QR.dirArrow?.(dir)||dir)+' '+dir,loc);
if(mapMode(char)) mapAppend(char,{type:'move',dirs:[dir]});
return true;
} catch(e) {}
}
if (lc) {
addLog(char,'move',(QR.dirArrow?.(dir)||dir)+' '+dir+' (nav)',loc);
if(mapMode(char)) mapAppend(char,{type:'move',dirs:[dir]});
location.href='/World/Popmundo.aspx/Locale/'+lc;
return true;
}
return false;
}
// ITEM MATCHING
function matchItem(step, items) {
const loc=QR.dom.locName(), normLoc=norm(loc||'');
const isWild=!step.item||step.item==='?';
if (items.length===1 && isWild) return {result:'match',item:items[0]};
if (!isWild) {
const needle=norm(step.item);
const matched=items.filter(i=>{
const ni=norm(i.name), ns=norm(i.subtitle||'');
return ni.includes(needle)||needle.includes(ni)||ns.includes(needle)||needle.includes(ns);
});
if (matched.length===1) return {result:'match',item:matched[0]};
if (matched.length>1) return {result:'ambiguous',items:matched};
}
if (step.location) {
const locOk=normLoc.includes(norm(step.location))||norm(step.location).includes(normLoc);
if (!locOk) return {result:'wrong_location'};
}
if (items.length===1) return {result:'match',item:items[0]};
if (items.length>1) return {result:'ambiguous',items};
return {result:'no_items'};
}
// ALERT
const Alert = {
_ac:null, _muteUntil:0, _intervals:{},
_ctx: () => { if(!Alert._ac) Alert._ac=new(window.AudioContext||window.webkitAudioContext)(); return Alert._ac; },
sound: () => {
if (Date.now()<Alert._muteUntil) return;
try {
const ac=Alert._ctx();
[440,550,660,880,1100].forEach((f,i) => {
const osc=ac.createOscillator(), gain=ac.createGain();
osc.connect(gain); gain.connect(ac.destination);
osc.frequency.value=f; osc.type='sine';
const t=ac.currentTime+i*0.18;
gain.gain.setValueAtTime(0,t);
gain.gain.linearRampToValueAtTime(0.4,t+0.05);
gain.gain.linearRampToValueAtTime(0,t+0.16);
osc.start(t); osc.stop(t+0.18);
});
} catch(e){}
},
notify: (title,body) => {
if (Notification.permission==='granted') new Notification(title,{body,icon:'/Static/Icons/popmundo.ico'});
else if (Notification.permission!=='denied') Notification.requestPermission().then(p=>{ if(p==='granted') new Notification(title,{body}); });
},
fire: (char,msg) => {
Alert._stopRepeat(char); Alert._ring(char,msg);
let count=0;
Alert._intervals[char]=setInterval(()=>{ count++; if(count>=5||Date.now()<Alert._muteUntil){Alert._stopRepeat(char);return;} Alert._ring(char,msg); },60000);
},
_ring: (char,msg) => {
Alert.sound(); Alert.notify('Guide',msg);
S.set('qr_alert_pulse',{ts:Date.now(),msg});
QR.emit('alert_pulse',msg); addLog(char,'alert',msg);
},
_stopRepeat: (char) => { if(Alert._intervals[char]){clearInterval(Alert._intervals[char]);delete Alert._intervals[char];} },
mute: (char) => { Alert._muteUntil=Date.now()+60*60*1000; Alert._stopRepeat(char); QR.emit('alert_muted',char); },
isMuted: () => Date.now()<Alert._muteUntil,
};
QR.alert = Alert;
window.addEventListener('storage', e => {
if(e.key==='qr_alert_pulse') { Alert.sound(); QR.emit('alert_pulse',JSON.parse(e.newValue||'{}').msg); }
});
// ── MAPPER ──────────────────────────────────────────────────────────────
// 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',
CIMA:'Up', ACIMA:'Up', BAIXO:'Down', ABAIXO:'Down',
};
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
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 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 trySimpleLine(line, steps) {
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;}
}
const minM=line.match(/^(\d+)\s*(?:minutes?|min|dk)$/i);
if (minM) { steps.push({type:'passage',minutes:parseInt(minM[1])});return true; }
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 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 (/^try_item/i.test(line)) {
const body=line.slice(9).trim(); const base={type:'try_item'};
if (!body) { steps.push({...base,item:null,location:null}); continue; }
const at=body.indexOf('@');
if (at>=0) { const rawItem=body.slice(0,at).trim(); steps.push({...base,item:(!rawItem||rawItem==='?')?null:norm(rawItem),location:norm(body.slice(at+1))}); }
else { const rawItem=body.trim(); steps.push({...base,item:(!rawItem||rawItem==='?')?null:norm(rawItem),location:null}); }
} 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 (/^require\s/i.test(line)) {
const body=line.slice(8).trim(), pipe=body.indexOf('|');
const item=pipe>=0?body.slice(0,pipe).trim():body;
const description=pipe>=0?body.slice(pipe+1).trim():'';
steps.push({type:'require',item,description});
} else if (/^info\s/i.test(line)) {
const body=line.slice(5).trim(), pipe=body.indexOf('|');
const text=pipe>=0?body.slice(0,pipe).trim():body;
const link=pipe>=0?body.slice(pipe+1).trim():'';
steps.push({type:'info',text,link});
} else if (/^goto\s/i.test(line)) {
const body=line.slice(5).trim(), pipe=body.lastIndexOf('|');
const label=pipe>=0?body.slice(0,pipe).trim():body;
const locId=pipe>=0?body.slice(pipe+1).trim():'';
steps.push({type:'goto',label,locId});
} else if (/^branch\s/i.test(line)) {
// branch <Dir> -> <CHECKPOINT>
const m=line.match(/^branch\s+(\S+)\s*->\s*(.+)$/i);
if (m) {
const dir=QR.resolveDir(m[1].trim()), cp=m[2].trim();
if (dir) steps.push({type:'branch',dir,checkpoint:cp});
else errors.push('branch: bad dir "'+m[1]+'"');
} else errors.push('branch: bad syntax "'+line.slice(0,40)+'"');
} 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; },
};
QR.quests = QStore;
// NORTH POLE SEED
(function(){
const db=QStore.load();
const npSeedVersion=2;
if(db['north-pole']&&(db['north-pole'].seedVersion||1)>=npSeedVersion) 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 çukur @ 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 nehir @ 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,seedVersion:2};
QStore.save(db);
})();
// BUILTIN QUESTS SEED
(function(){
const db=QStore.load();
const seeds=[
{
id:'antenora',
name:'☠️ Antenora',
text:[
'# Başlamadan önce envanterinde Gümüş Sikke olduğundan emin ol',
'checkpoint BASLANGIÇ',
'use_item Gümüş Sikke',
'passage 5min',
'# The Eternal Darkness',
'checkpoint ETERNAL_DARKNESS',
'move K K D G G D',
'# The Dark Pit',
'use_item Çukur (Siyah)',
'passage 5min',
'# Bottom of the Pit',
'checkpoint BOTTOM_OF_PIT',
'move K D K D G G D G G',
'# The Ferry — Feribot süren dolunca kullan',
'checkpoint THE_FERRY',
'use_item Feribot',
'passage 5min',
'# The Gates to Hades — Eastern Path to Antenora (8 adım)',
'checkpoint GATES_TO_HADES',
'move D D D D',
'# The Eastern Crossroads',
'move K D G G',
'# Antenora',
].join('\n'),
},
{
id:'derinligin-sirlari',
name:'🌊 Derinliğin Sırları',
seedVersion:2,
text:[
'require Dalış Ekipmanı | Spor Malzemeleri kategorisinden satın alın (%50 yer kaplar)',
'info Dalış serisi sırasıyla tamamlanmalı: 1. Derinliğin Sırları → 2. Unholy Diver → 3. Nautilus/Kraken Takımı | /Forum/Popmundo.aspx/Thread/2209668.1',
'# Başlangıç: Tromso Halk Plajı',
'checkpoint BASLANGIÇ',
'use_item Denizaltı',
'passage 12min',
'# The Helm of the Nautilus',
'checkpoint HELM',
'use_item Gemi Dümeni North Atlantic',
'passage 3min',
'# The Nautilus - North Atlantic',
'checkpoint NORTH_ATLANTIC_NAUTILUS',
'use_item Basınç Odası',
'passage 4min',
'# The North Atlantic',
'checkpoint NORTH_ATLANTIC',
'move K D D',
'# Mysterious Cave Opening',
'use_item Mağara Ağzı',
'passage 2min',
'# Underwater Cavern',
'checkpoint UNDERWATER_CAVERN',
'move G B B K B KB K KB K KB B B KB K K KD K K KB K KB K K K KD KD K KD D D GD GD D GD GD D GD GD G G',
'use_item Mağara Ağzı',
'passage 5min',
'# Dakkar\'s Camp',
'checkpoint DAKKARS_CAMP',
'move K GD D',
'# Dakkar\'s Emergency Supplies — Kutu 4 saatte bir kullanılabilir',
'# NOT: Yanında asistan/çocuk olması önerilir',
'use_item kutu',
'checkpoint OKSIJEN_ALINDI',
'move G G',
'use_item Oksijen Kaynağı',
'move GD',
'use_item Mağara Ağzı',
'passage 4min',
'# Dark Waters',
'checkpoint DARK_WATERS',
'move D K KB B K KB B K KD D D KD D',
'use_item Mağara Ağzı',
'passage 4min',
'# Dakkar\'s Abyss — UYARI: Oksijen hızla azalıyor! En az 2 Acil Durum Oksijen Tüpü hazır olsun',
'checkpoint DAKKARS_ABYSS',
'move A A A A A A A A A A A A A A A A A A A A',
'# Cold Abyss',
'checkpoint COLD_ABYSS',
'move A A A A A A A A A A A A A A A',
'# Dakkar\'s Ledge',
'checkpoint DAKKARS_LEDGE',
'move A',
'# Dakkar\'s Infinite Abyss',
'checkpoint DAKKARS_INFINITE_ABYSS',
'move A A A A A A A A A A A A A A A A A A A A A A A A A A A A A',
'# Dakkar\'s Point of no Return',
'checkpoint POINT_OF_NO_RETURN',
'move A',
'# Dakkar\'s Fall',
'checkpoint DAKKARS_FALL',
'move A A A A A A A A A A A A A A A A A A A A A A A A A A',
'# Dakkar\'s Landing',
'checkpoint DAKKARS_LANDING',
'move D G G B B GB B GB G GD G D KD KD D D KD D GD D D',
'# The Sunken Monolith',
'use_item Monolith',
].join('\n'),
},
{
id:'unholy-diver',
name:'🤿 Unholy Diver',
seedVersion:2,
text:[
'require Batık Monolitin Sırları | Derinliğin Sırları questinden elde edilir',
'info Dalış serisi: 2. Unholy Diver | /Forum/Popmundo.aspx/Thread/2212309.1',
'# Başlangıç: Tromso Halk Plajı',
'checkpoint BASLANGIÇ',
'use_item Denizaltı',
'passage 16min',
'# The Helm of the Nautilus',
'checkpoint HELM',
'use_item Gemi Dümeni Desert Island',
'passage 16min',
'# The Nautilus - Desert Island',
'checkpoint NAUTILUS_DESERT_ISLAND',
'use_item Desert Island',
'passage 9min',
'# Castaway Beach — Potkal alma',
'checkpoint CASTAWAY_BEACH',
'move GB G B K K KB K KD',
'# Abandoned Shelter',
'use_item Çalışma Masası',
'checkpoint POTKAL_ALINDI',
'# Zıpkın alma',
'move GB KB K KB K KD KD K K KD KD G',
'# Abandoned Raft',
'use_item Sal',
'checkpoint ZIPKIN_ALINDI',
'# Alyans alma',
'move K GB GB G G GB D KD KD D KD GD',
'# Dakkar\'s End',
'use_item mezar taşı',
'checkpoint ALYANS_ALINDI',
'# Black Lagoon\'a git',
'move KB KB D KD KD GD D',
'# To The Black Lagoon',
'use_item Patika',
'passage 5min',
'# Black Lagoon — Fener alma',
'checkpoint BLACK_LAGOON',
'move K KB KB K K K B G',
'# Dead Scuba Diver',
'use_item ceset',
'checkpoint FENER_ALINDI',
'# Sonraki patikaya git',
'move K D G G G GD GD G',
'use_item Patika',
'passage 5min',
'# Castaway Beach\'e dön',
'checkpoint PATIKA_2_BITTI',
'move B KB GB GB B GB GB GB GD G G GB GB G G D K KD',
'# Castaway Beach',
'use_item Denizaltı',
'passage 16min',
'# The Nautilus - Desert Island',
'checkpoint NAUTILUS_RETURN',
'use_item Basınç Odası',
'passage 5min',
'# The Shipwreck\'e git',
'checkpoint BASIN_SONRASI',
'move K D K D K D K D K D K D K D K D K',
'# The Shipwreck',
'use_item Merdiven Lower deck',
'passage 5min',
'move D D',
'use_item Kapı',
'passage 5min',
'# Captain\'s Quarters',
'use_item kutu',
'move K',
'use_item yatak',
'# Tebrikler! Üç Dişli Mızrak ve başarı!',
].join('\n'),
},
{
id:'nautilus-takimi',
name:'⚙️ Nautilus Takımı',
seedVersion:2,
text:[
'info Atlantis\'in Tacı başarısı tekrar alınabilir | /Forum/Popmundo.aspx/Thread/2339858.1',
'# Başlangıç: Telegrafbukta Beach',
'checkpoint BASLANGIÇ',
'use_item Denizaltı',
'passage 16min',
'# The Helm of the Nautilus',
'checkpoint HELM_OF_NAUTILUS',
'use_item Gemi Dümeni Kraken',
'passage 5min',
'# Helm of Nautilus — giriş: Torpido Yuvası (1. numara)',
'checkpoint HELM_GIRIS',
'move K B',
'# Torpedo Bay',
'use_item Torpido Yuvası',
'# Gemi Dümeni (FIRE TORPEDO) için 2. numaraya git',
'move D G',
'use_item Gemi Dümeni',
'checkpoint TORPEDO_ATESLENDI',
'# Nautilus Cargo Hold — İngiliz Anahtarı (3. numara)',
'move G G D',
'use_item kutu',
'checkpoint ANAHTAR_ALINDI',
'# Gear Mechanism — Batı sonra sürekli Güney (yaklaşık 15-20 adım)',
'# Gear Mechanism\'e varınca İleri → basabilirsin',
'checkpoint GEAR_ARAMA',
'move B G G G G G G G G G G G G G G G G G G G G',
'# Gear Mechanism',
'use_item İngiliz Anahtarı',
'# NOT: Başarı gelmezse tekrar kullan (20dk süresi var)',
'checkpoint ANAHTAR_KULLANILDI',
'# Nautilus Tower (5. numara)',
'move K K K K B',
'checkpoint NAUTILUS_TOWER',
'# MANUEL: Haritada 5. numara için belirtilen adımı uygula, sonra İleri → bas',
'# Neverending Caverns beneath the Sea',
'checkpoint NEVERENDING_CAVERNS',
'move K KD KB K B K K KB KD K K D GD D D KD KD K KB B KB K KD KB KD KD KD D K B K D KD K D',
'use_item Oksijen Kaynağı',
'move B K KB KB KD GD D KD K KB KB K B K KB KB K KD KB K KD KD KD KB K D',
'use_item Çukur',
'# Tebrikler! Atlantis\'in Tacı başarısı!',
].join('\n'),
},
{
id:'kraken-takimi',
name:'🦑 Kraken Takımı',
seedVersion:2,
text:[
'info Atlantis\'in Tacı başarısı tekrar alınabilir | /Forum/Popmundo.aspx/Thread/2339858.1',
'# Başlangıç: Telegrafbukta Beach',
'checkpoint BASLANGIÇ',
'use_item Denizaltı',
'passage 16min',
'# The Helm of the Nautilus',
'checkpoint HELM_OF_NAUTILUS',
'use_item Gemi Dümeni Kraken',
'passage 5min',
'# Helm of Nautilus — giriş: Nautilus Pipe Organ',
'checkpoint HELM_GIRIS',
'move B',
'# Nautilus Pipe Organ',
'use_item Org',
'checkpoint ORG_KULLANILDI',
'# Crew Quarters — Takunyalar',
'move D K D',
'use_item kutu',
'checkpoint TAKUNYALAR_ALINDI',
'# Gear Mechanism — Batı sonra sürekli Güney (yaklaşık 15-20 adım)',
'# Gear Mechanism\'e varınca İleri → basabilirsin',
'checkpoint GEAR_ARAMA',
'move B G G G G G G G G G G G G G G G G G G G G',
'# Gear Mechanism',
'use_item Takunyalar',
'# NOT: Başarı gelmezse tekrar kullan (10dk süresi var)',
'checkpoint TAKUNYALAR_KULLANILDI',
'# Nautilus Tower (5. numara)',
'move K K K K B',
'checkpoint NAUTILUS_TOWER',
'# MANUEL: Haritada 5. numara için belirtilen adımı uygula, sonra İleri → bas',
'# Neverending Caverns beneath the Sea',
'checkpoint NEVERENDING_CAVERNS',
'move K KD KB K B K K KB KD K K D GD D D KD KD K KB B KB K KD KB KD KD KD D K B K D KD K D',
'use_item Oksijen Kaynağı',
'move B K KB KB KD GD D KD K KB KB K B K KB KB K KD KB K KD KD KD KB K D',
'use_item Çukur',
'# Tebrikler! Atlantis\'in Tacı başarısı!',
].join('\n'),
},
{
id:'mozaik-mucizeler',
name:'🏛️ Mozaik Mucizeler',
text:[
'# Başlangıç: Telegrafbukta Beach',
'checkpoint BASLANGIÇ',
'use_item Denizaltı (The Nautilus)',
'passage 12min',
'# The Helm of the Nautilus',
'checkpoint HELM',
'use_item Gemi Dümeni (Secrets of the Atlantic)',
'passage 12min',
'# The Nautilus - Atlantic Ocean',
'checkpoint NAUTILUS_ATLANTIC',
'use_item Sandal (Atlantic Ocean)',
'passage 12min',
'# Atlantic Ocean — SARKAÇ NAVIGASYONU BAŞLIYOR',
'checkpoint ATLANTIC_OCEAN',
'# MANUEL: Sarkaç\'ı kullanarak Ancient Buoy\'a ulaş',
'# Sarkaç\'ın gösterdiği yönde 50-60 adım git, tekrar kullan',
'# "Varış noktasına ulaştım" mesajı çıkınca Ancient Buoy\'dasin',
'# Ancient Buoy\'a varınca Merdiven\'i kullan — script otomatik devam eder',
'use_item Merdiven',
'passage 10min',
'# Sunken Ruins',
'checkpoint SUNKEN_RUINS',
'# Hece 1: Ἀτ',
'move D D K',
'use_item Mozaik Zemin (Ἀτ)',
'checkpoint AT_TAMAM',
'# Hece 2: λαν',
'move G B B B K K KB K KB',
'use_item Mozaik Zemin (λαν)',
'checkpoint LAN_TAMAM',
'# Hece 3: τίς',
'move GD G GD G G G GB GB G GD GB',
'use_item Mozaik Zemin (τίς)',
'checkpoint TIS_TAMAM',
'# Hece 4: νῆσ',
'move KD D GD D K K KD D KD K K K K KD D',
'use_item Mozaik Zemin (νῆσ)',
'checkpoint NIS_TAMAM',
'# Hece 5: ος',
'move B GB KB B B G GB G B B GB G G',
'use_item Mozaik Zemin (ος)',
'checkpoint OS_TAMAM',
'# Geçit — Atlantis Nesos',
'move K K KD D D K KB K KD K KD',
'use_item Geçit (Atlantis Nesos)',
'# Tebrikler! Mozaik Mucizeler başarısı!',
].join('\n'),
},
{
id:'nec-plus-ultra',
name:'🔱 Nec Plus Ultra',
text:[
'# Ön koşul: Mozaik Mucizeler tamamlanmış olmalı',
'# Mozaik\'in son adımından (Geçit Atlantis Nesos) devam',
'checkpoint BASLANGIÇ',
'use_item Patika (Nec Plus Ultra)',
'move D',
'use_item Denizaltı (The Ancient Mariner)',
'passage 30min',
'# The Traveller\'s Cave Entrance',
'checkpoint CAVE_ENTRANCE',
'move GD D KD K',
'use_item Patika',
'passage 60min',
'checkpoint PATIKA_1_BITTI',
'move B KB K D KD',
'use_item Patika',
'passage 60min',
'checkpoint PATIKA_2_BITTI',
'move GB GB',
'use_item Patika',
'passage 180min',
'checkpoint PATIKA_3_BITTI',
'# The Waiting Traveller — Frank Blomdahl bekleniyor',
'move B G',
'checkpoint FRANK_BEKLENIYOR',
'# MANUEL: Frank 6 saatte bir gelir (TR: 08:19 / 14:19 / 20:19 / 06:19)',
'# Frank gelince Garip Kontrol Manivelası verir — script otomatik devam eder',
'use_item Garip Kontrol Manivelası',
'checkpoint MAKINE_YANINDA',
'# MANUEL: Makineden şu sırayla bas: 7 → 9 → - → 2 → 9 → START',
'# Karanlık Patika (Plato\'s Cave) belirince İleri → bas',
'checkpoint MAKINE_KOD1',
'use_item Karanlık Patika (Plato\'s Cave)',
'passage 10min',
'# Plato\'s Cave',
'checkpoint PLATOS_CAVE',
'use_item Pranga',
'checkpoint MÜZIK_ANISI_BEKLENIYOR',
'# MANUEL: 6 saatte bir Müzik Anısı gelir, otomatik çıkarsın — sonra İleri → bas',
'use_item Buton (RETURN)',
'passage 60min',
'checkpoint MAKINE_GERI',
'# MANUEL: Makineden şu sırayla bas: - → 1 → 1 → 0 → - → 3 → 1 → START',
'# Island of Atlantis\'e ışınlanınca İleri → bas',
'checkpoint MAKINE_KOD2',
'# Island of Atlantis',
'checkpoint ISLAND_OF_ATLANTIS',
'move K KB K KD KB KB K KD',
'use_item Patika',
'passage 60min',
'checkpoint ATLANTIS_PATIKA_1',
'move KD GD GD GB',
'use_item Patika',
'passage 180min',
'checkpoint ATLANTIS_PATIKA_2',
'move K KD D KD K KB B KB KD KB',
'use_item Patika (Atlantis Mountain Path)',
'passage 240min',
'# Throne of Poseidon',
'checkpoint ATLANTIS_MOUNTAIN',
'move K K',
'use_item Taht',
'passage 5min',
'checkpoint THRONE_OF_POSEIDON',
'use_item Org (Poseidon\'s Pipes)',
'# Org Anahtarı 05-SOL envanterine eklendi',
'checkpoint ORG_ANAHTARI_ALINDI',
'# Geri dönüş: Taht → G-G → Patika (4sa) → GD-GB-GD-D-GD-G-GB-B-GB-G → Patika (3sa)',
'# → KD-KB-KB-GB → Patika (1sa) → GB-G-GD-GD-GB-G-GD-G → Buton RETURN (1sa)',
'# → K-KB → Denizaltı The Ancient Mariner (30dk) → Underwater Cliff',
'use_item Taht',
'passage 5min',
'checkpoint DONUŞ_BAŞLADI',
'move G G',
'use_item Patika',
'passage 240min',
'checkpoint DONUŞ_PATIKA_1',
'move GD GB GD D GD G GB B GB G',
'use_item Patika',
'passage 180min',
'checkpoint DONUŞ_PATIKA_2',
'move KD KB KB GB',
'use_item Patika',
'passage 60min',
'checkpoint DONUŞ_PATIKA_3',
'move GB G GD GD GB G GD G',
'use_item Buton (RETURN)',
'passage 60min',
'checkpoint MAKINE_DONUŞ',
'move K KB',
'use_item Denizaltı (The Ancient Mariner)',
'passage 30min',
'# Underwater Cliff — UYARI: Oksijen hemen azalmaya başlar!',
'checkpoint UNDERWATER_CLIFF',
'move B',
'# Sunken Temple of Gods — HİÇBİR ŞEYE TIKLAMA',
'checkpoint SUNKEN_TEMPLE',
'move K K K',
'# Poseidon\'s Broken Pipes — Org tuşları sırasıyla',
'use_item Org (09-RE)',
'use_item Org (10-MI)',
'use_item Org (08-DO)',
'use_item Org (01-DO)',
'use_item Org Anahtarı (05-SOL)',
'# Tebrikler! Nec Plus Ultra başarısı, 4dp ve Mars Haritası!',
].join('\n'),
},
];
let changed=false;
seeds.forEach(({id,name,text,seedVersion})=>{
const ex=db[id];
if(ex&&ex.builtin&&(ex.seedVersion||1)>=(seedVersion||1)) return;
const {steps}=parseText(text);
db[id]={id,name,text,sequence:steps,versions:[],builtin:true,seedVersion:seedVersion||1};
changed=true;
});
if(changed) QStore.save(db);
})();
// ── ENGINE ───────────────────────────────────────────────────────────────
function runStep(char) {
const qid=C.activeQuestId(char); if(!qid) return;
const quest=QStore.get(qid); if(!quest) return;
const idx=C.getStep(char);
if (idx>=quest.sequence.length) {
addLog(char,'complete','Quest tamamlandı: '+quest.name);
QR.emit('complete',char); return;
}
const step=quest.sequence[idx];
QR.emit('step',char,step,idx);
if (step.type==='move') _execMove(char,step,idx);
else if (step.type==='use_item') _execUseItem(char,step,idx);
else if (step.type==='try_item') _execTryItem(char,step,idx);
else if (step.type==='passage') _execPassage(char,step,idx);
else if (step.type==='wait') _execWait(char,step,idx);
else if (step.type==='checkpoint') _execCheckpoint(char,step,idx);
else if (step.type==='branch') _execBranch(char,step,idx);
else if (step.type==='goto') _execGoto(char,step,idx);
else if (step.type==='require') _execRequire(char,step,idx);
else if (step.type==='info') _execInfo(char,step,idx);
}
function _execMove(char,step,idx) {
const activeDirs=QR.dom.activeDirections();
let moved=false;
for (const dir of step.dirs) {
if (!activeDirs.includes(dir)) continue;
if (clickDirection(char,dir)) { C.setStep(char,idx+1); moved=true; break; }
}
if (!moved) {
const msg='Yön yok: '+step.dirs.map(d=>(QR.dirArrow?.(d)||d)+' '+d).join('|')+' | Aktif: '+(activeDirs.length?activeDirs.join(', '):'hiçbiri');
addLog(char,'error',msg); QR.emit('error',char,step,idx,msg);
}
}
function _execUseItem(char,step,idx) {
const items=QR.dom.getItems();
if (!C.getQuestStartTs(char)) { C.setQuestStartTs(char,Date.now()); addLog(char,'timer_start','Sayaç başladı'); }
const r=matchItem(step,items);
if (r.result==='match') {
C.setStep(char,idx+1); r.item.useBtn.click();
addLog(char,'use_item','✓ '+r.item.name); QR.emit('used_item',char,r.item.name,idx);
} else if (r.result==='wrong_location') {
addLog(char,'error','Yanlış lokasyon: '+(step.location||'?')+' ≠ '+(QR.dom.locName()||'?'));
QR.emit('use_item_ambiguous',char,step,idx,items.map(i=>i.fullLabel||i.name));
} else {
const label=r.result==='no_items'?'Sayfada item yok':'Item eşleşmedi: '+(step.item||'?');
addLog(char,'error',label+' @ '+(QR.dom.locName()||'?'));
QR.emit('use_item_ambiguous',char,step,idx,(r.items||items).map(i=>i.fullLabel||i.name));
}
}
function _execTryItem(char,step,idx) {
const items=QR.dom.getItems();
if (!C.getQuestStartTs(char)) C.setQuestStartTs(char,Date.now());
const r=matchItem(step,items);
if (r.result==='match') {
C.setStep(char,idx+1); r.item.useBtn.click();
addLog(char,'use_item','✓ (try) '+r.item.name); QR.emit('used_item',char,r.item.name,idx);
} else if (r.result==='ambiguous'||r.result==='no_match') {
QR.emit('use_item_ambiguous',char,step,idx,(r.items||[]).map(i=>i.name));
} else {
addLog(char,'use_item','⊘ try_item — item yok, geçildi');
C.setStep(char,idx+1);
}
}
function _execBranch(char,step,idx) {
const activeDirs=QR.dom.activeDirections();
if (activeDirs.includes(step.dir)) {
const ok=C.resumeFromCheckpoint(char,step.checkpoint);
if (ok) {
addLog(char,'branch','Dal: '+step.dir+' → '+step.checkpoint);
QR.emit('checkpoint',char,step,idx,QR.dom.locName());
} else {
addLog(char,'error','Branch checkpoint bulunamadı: '+step.checkpoint);
QR.emit('error',char,step,idx,'branch_checkpoint_missing');
}
} else {
C.setStep(char,idx+1);
}
}
function _execGoto(char,step,idx) {
if (!step.locId) { C.setStep(char,idx+1); buildCompass(); return; }
addLog(char,'move','🗺️ → '+(step.label||step.locId));
C.setStep(char,idx+1);
location.href='/World/Popmundo.aspx/Locale/MoveToLocale/'+step.locId;
}
QR.resolveItemChoice = (char,stepIdx,chosenName) => {
const items=QR.dom.getItems();
const found=items.find(i=>norm(i.fullLabel||i.name)===norm(chosenName)||norm(i.name)===norm(chosenName));
if (!found) { QR.emit('error',char,null,stepIdx,'Seçilen item yok: '+chosenName); return; }
C.setStep(char,stepIdx+1); found.useBtn.click();
addLog(char,'use_item','✓ (seçim) '+found.name); QR.emit('used_item',char,found.name,stepIdx);
};
function _execPassage(char,step,idx) {
if (!C.getQuestStartTs(char)) C.setQuestStartTs(char,Date.now());
C.setStep(char,idx+1); C.setPassageTs(char,Date.now()); C.setPassageMin(char,step.minutes);
if (!QR.dom.isTraveling()) {
const items=QR.dom.getItems();
if (items.length) items[0].useBtn.click();
}
addLog(char,'passage','Passage: '+(step.minutes||'?')+'dk');
QR.emit('passage_start',char,step);
}
function _execWait(char,step,idx) {
C.setStep(char,idx+1); C.setPassageTs(char,Date.now()); C.setPassageMin(char,step.minutes);
addLog(char,'wait','Bekleme: '+(step.minutes||'?')+'dk'); QR.emit('passage_start',char,step);
}
function _execCheckpoint(char,step,idx) {
const loc=QR.dom.locName();
C.addCheckpoint(char,{label:step.label,stepIdx:idx,locName:loc}); C.setStep(char,idx+1);
addLog(char,'checkpoint','📍 '+(step.label||'#'+idx)+' @ '+(loc||'?'));
QR.emit('checkpoint',char,step,idx,loc);
}
function _execRequire(char,step,idx) {
const key='qr_req_'+char+'_'+(C.activeQuestId(char)||'')+'_'+idx;
if (sessionStorage.getItem(key)) { C.setStep(char,idx+1); return; }
showRequirePopup(step,()=>{ sessionStorage.setItem(key,'1'); C.setStep(char,idx+1); buildCompass(); });
}
function _execInfo(char,step,idx) {
const qid=C.activeQuestId(char)||'';
const key='qr_info_'+qid+'_'+idx;
if (S.get(key,false)) { C.setStep(char,idx+1); buildCompass(); return; }
S.set(key,true);
const quest=QStore.get(qid);
const infoSteps=quest?quest.sequence.filter(s=>s.type==='info'):[step];
showInfoModal(infoSteps.length?infoSteps:[step]);
C.setStep(char,idx+1); buildCompass();
}
// PASSAGE WATCHER
function watchPassage(char) {
if (QR.dom.isTraveling()) return;
const ts=C.getPassageTs(char); if(!ts) return;
const min=C.getPassageMin(char)||0;
const elapsed=Date.now()-ts;
const early=elapsed<min*60000;
C.setPassageTs(char,null); C.setPassageMin(char,null);
addLog(char,'passage_end','Seyahat tamamlandı'+(early?' (erken varış)':''));
if(!early){
const quest=QStore.get(C.activeQuestId(char));
const msg=char+(quest?' — '+quest.name+' questine':'')+' devam edebilirsin';
Alert.fire(char,msg);
}
QR.emit('passage_end',char,{early});
}
QR.engine = { runStep, watchPassage, clickDirection };
// MANUAL MOVE HOOK
// Sayfa yenilenmeden önce sessionStorage'a yönü yaz, yeni sayfada oku
(function(){
const key='qr_pending_manual';
const pending=sessionStorage.getItem(key);
if(pending){
sessionStorage.removeItem(key);
try{
const {char,dir}=JSON.parse(pending);
if(char&&dir) {
addLog(char,'move',(QR.dirArrow?.(dir)||dir)+' '+dir+' (manuel)');
QR.emit('manual_move',char,dir);
// harita modu aktifse kayıt al
if(S.get('qr_mapmode_'+char,false)) mapAppend(char,{type:'move',dirs:[dir]});
}
}catch(e){}
}
})();
function hookManualMoves() {
if (!QR.page.isCompass()) return;
document.querySelectorAll('#compass g.fe-move').forEach(el => {
if (el._qrHooked) return;
el._qrHooked = true;
el.addEventListener('click', () => {
const char=QR.char.current(); if(!char) return;
const dir=el.id; if(!VALID_DIRS.has(dir)) return;
sessionStorage.setItem('qr_pending_manual',JSON.stringify({char,dir}));
}, true);
});
}
// MAIN LOOP — 3–6s random delay
function scheduleLoop() {
setTimeout(() => {
try {
if (QR.page.isLocale()) {
const char=QR.char.current();
if (char && C.getPassageTs(char)) watchPassage(char);
}
} catch(e){}
scheduleLoop();
}, 3000+Math.random()*3000);
}
scheduleLoop();
// ── UI ───────────────────────────────────────────────────────────────────
const BG = 'background:linear-gradient(160deg,#1a2f5e,#243d7a)';
const BDR = 'border:1.5px solid rgba(255,255,255,.22);border-radius:8px';
const FNT = 'font-family:Arial,sans-serif;font-size:12px;color:white';
const inp = (x='') => 'width:100%;box-sizing:border-box;background:rgba(255,255,255,.12);color:white;border:1px solid rgba(255,255,255,.25);border-radius:6px;padding:6px;font-size:11px;'+x;
const sec = (x='') => 'background:rgba(255,255,255,.07);border-radius:6px;padding:8px;margin-bottom:6px;'+x;
const row = (x='') => 'display:flex;gap:6px;margin-bottom:6px;align-items:center;'+x;
const btn = (bg,x='') => 'background:'+bg+';color:white;border:none;border-radius:6px;padding:6px 10px;font-size:11px;cursor:pointer;'+x;
function mkEl(tag, style, html) {
const e=document.createElement(tag);
if(style) e.style.cssText=style;
if(html!==undefined) e.innerHTML=html;
return e;
}
// TOAST
function notify(msg, color) {
const n=mkEl('div',
'position:fixed;top:18px;left:50%;transform:translateX(-50%);'+
'background:'+(color||'#1a2f5e')+';color:white;padding:9px 16px;'+
'border-radius:8px;font-weight:bold;font-size:13px;z-index:100002;'+
'box-shadow:0 4px 12px rgba(0,0,0,.5);pointer-events:none;',msg);
document.body.appendChild(n); setTimeout(()=>n.remove(),4000);
}
function stepLabel(s) {
if(!s) return '—';
if(s.type==='move') return (QR.dirArrow?.(s.dirs[0])||'')+' '+s.dirs.join(' | ');
if(s.type==='use_item') return '🎒 '+(s.item||'?')+(s.location?' @ '+s.location:'');
if(s.type==='try_item') return '🎒? '+(s.item||'?')+(s.location?' @ '+s.location:'');
if(s.type==='passage') return '⛵ '+(s.minutes||'?')+'dk';
if(s.type==='wait') return '⏳ '+(s.minutes||'?')+'dk';
if(s.type==='checkpoint') return '📍 '+(s.label||'');
if(s.type==='goto') return '🗺️ → '+(s.label||s.locId);
if(s.type==='branch') return '🔀 '+s.dir+' → '+s.checkpoint;
if(s.type==='require') return '⚠️ '+s.item+(s.description?' — '+s.description:'');
if(s.type==='info') return 'ℹ️ '+s.text+(s.link?' ['+s.link+']':'');
return s.type;
}
function elapsedStr(char) {
const ts=C.getQuestStartTs(char); if(!ts) return null;
const d=Date.now()-ts, h=Math.floor(d/3600000), m=Math.floor((d%3600000)/60000);
return h?h+'s '+m+'dk':m+'dk';
}
// PASSAGE TIMER
let _timerInterval=null;
function startTimer(char) {
if(_timerInterval) clearInterval(_timerInterval);
_timerInterval=setInterval(()=>{
const el=document.getElementById('qrTimer'); if(!el){clearInterval(_timerInterval);_timerInterval=null;return;}
const ts=C.getPassageTs(char), min=C.getPassageMin(char);
if(!ts||!min){el.innerHTML='';clearInterval(_timerInterval);_timerInterval=null;return;}
const left=ts+min*60000-Date.now();
if(left<=0){el.innerHTML='<span style="color:#8BC34A;font-weight:bold;">✓ Hazır!</span>';clearInterval(_timerInterval);_timerInterval=null;return;}
const m2=Math.floor(left/60000), s=Math.floor((left%60000)/1000);
const eta=new Date(ts+min*60000);
el.innerHTML='<span style="color:#FFD600;">⏳ '+m2+'m '+s.toString().padStart(2,'0')+'s → '+
eta.getHours().toString().padStart(2,'0')+':'+eta.getMinutes().toString().padStart(2,'0')+'</span>';
},1000);
}
// PULSE STYLE
if (!document.getElementById('qrPulseStyle')) {
const st=document.createElement('style'); st.id='qrPulseStyle';
st.textContent='@keyframes qrPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.2)}}';
document.head.appendChild(st);
}
QR.on('alert_pulse',()=>{
const fb=document.getElementById('qrFloat'); if(!fb) return;
fb.style.boxShadow='0 0 0 4px #f44336'; fb.style.animation='qrPulse 0.6s ease-in-out 4';
setTimeout(()=>{if(fb){fb.style.boxShadow='';fb.style.animation='';}},3000);
});
// FLOATING BUTTON
function createFloat() {
if(document.getElementById('qrFloat')) return;
const b=mkEl('button',
'position:fixed;bottom:18px;right:14px;'+
'background:linear-gradient(135deg,#1a2f5e,#2a5298);color:white;'+
'border:2px solid rgba(255,255,255,.35);border-radius:50%;'+
'width:46px;height:46px;font-size:20px;cursor:pointer;'+
'z-index:9999;box-shadow:0 2px 10px rgba(0,0,0,.5);');
b.id='qrFloat'; b.title='Guide'; b.textContent='🗺️';
b.onclick=()=>{
_dismissed=false;
const p=document.getElementById('qrCompass');
if(p) p.remove();
else if(QR.page.isLocale()) buildCompass();
else openAdmin('quests');
};
document.body.appendChild(b);
}
// DRAGGABLE
function makeDraggable(panel, handle) {
let ox,oy,drag=false;
handle.addEventListener('mousedown',e=>{drag=true;ox=e.clientX-panel.offsetLeft;oy=e.clientY-panel.offsetTop;e.preventDefault();});
document.addEventListener('mousemove',e=>{
if(!drag) return;
panel.style.left=Math.max(0,Math.min(window.innerWidth-panel.offsetWidth,e.clientX-ox))+'px';
panel.style.top=Math.max(0,Math.min(window.innerHeight-panel.offsetHeight,e.clientY-oy))+'px';
});
document.addEventListener('mouseup',()=>{
if(!drag) return; drag=false;
S.set('qr_panel_pos',{x:parseInt(panel.style.left),y:parseInt(panel.style.top)});
});
}
// DISMISSED FLAG — × ile kapanınca setInterval yeniden açmaz
let _dismissed = false;
// HARITA MODU
const mapKey = c => 'qr_maplog_'+c;
const mapMode = c => S.get('qr_mapmode_'+c,false);
function mapStart(char) {
S.set('qr_mapmode_'+char,true);
S.set(mapKey(char),[]);
addLog(char,'map','🗺 Harita kaydı başladı');
buildCompass();
}
function mapStop(char) {
S.set('qr_mapmode_'+char,false);
buildCompass();
}
function mapAppend(char, step) {
const log=S.get(mapKey(char),[]);
// move adımlarını birleştir
if(step.type==='move' && log.length && log[log.length-1].type==='move') {
log[log.length-1].dirs=[...log[log.length-1].dirs,...step.dirs];
} else {
log.push(step);
}
S.set(mapKey(char),log);
}
function mapToText(log) {
return log.map(s=>{
if(s.type==='move') return 'move '+s.dirs.join(' ');
if(s.type==='use_item') return 'use_item '+(s.item||'?')+(s.location?' @ '+s.location:'');
if(s.type==='passage') return 'passage '+(s.minutes||'?')+'min';
if(s.type==='checkpoint') return 'checkpoint '+(s.label||'MAP_CP');
return '';
}).filter(Boolean).join('\n');
}
function mapSaveAsQuest(char) {
const log=S.get(mapKey(char),[]); if(!log.length){notify('Harita boş','#f44336');return;}
const name=prompt('Quest adı:'); if(!name) return;
const text=mapToText(log);
const {steps}=parseText(text);
if(!steps.length){notify('Parse başarısız','#f44336');return;}
QStore.upsert({...QStore.new(name,text),sequence:steps});
S.set('qr_mapmode_'+char,false);
S.del(mapKey(char));
notify('"'+name+'" kaydedildi — '+steps.length+' adım','#4CAF50');
buildCompass();
}
// Harita modunda her move/use_item'ı kaydet
QR.on('manual_move',(char,dir)=>{ if(mapMode(char)) mapAppend(char,{type:'move',dirs:[dir]}); });
QR.on('used_item',(char,name,idx)=>{
if(!mapMode(char)) return;
const loc=QR.dom.locName();
mapAppend(char,{type:'use_item',item:name,location:loc});
});
QR.on('passage_start',(char,step)=>{ if(mapMode(char)) mapAppend(char,{type:'passage',minutes:step.minutes}); });
// COMPACT MODE
const getCompact = () => S.get('qr_compact', false);
const setCompact = v => S.set('qr_compact', v);
// QUEST EDIT OVERLAY
function openQuestEdit(qid) {
document.getElementById('qrQEOv')?.remove();
const quest=QStore.get(qid); if(!quest) return;
let parsed=null;
const ov=mkEl('div','position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:100003;display:flex;align-items:center;justify-content:center;');
ov.id='qrQEOv';
const box=mkEl('div',BG+';'+BDR+';'+FNT+';width:92%;max-width:520px;max-height:90vh;overflow-y:auto;padding:14px;');
box.innerHTML=
'<div style="font-weight:bold;font-size:13px;margin-bottom:8px;">✏️ Düzenle: '+quest.name+'</div>'+
'<input id="qrQEName" style="'+inp('margin-bottom:6px;')+'" value="'+quest.name.replace(/"/g,'"')+'">'+
'<textarea id="qrQEText" rows="12" style="'+inp('resize:vertical;font-family:monospace;font-size:11px;margin-bottom:6px;')+'">'+(quest.text||'')+'</textarea>'+
'<div id="qrQEOut" style="display:none;background:rgba(0,0,0,.25);border-radius:6px;padding:6px;font-size:10px;max-height:100px;overflow-y:auto;font-family:monospace;margin-bottom:6px;"></div>'+
'<div style="display:flex;gap:6px;">'+
'<button id="qrQEParse" style="'+btn('#607D8B','flex:1;padding:7px;font-size:11px;')+'">Parse</button>'+
'<button id="qrQESave" style="'+btn('#4CAF50','flex:1;padding:7px;font-size:11px;')+'">Kaydet</button>'+
'<button id="qrQECancel" style="'+btn('#888','padding:7px 14px;font-size:11px;')+'">İptal</button></div>';
ov.appendChild(box); document.body.appendChild(ov);
ov.onclick=e=>{if(e.target===ov)ov.remove();};
box.querySelector('#qrQECancel').onclick=()=>ov.remove();
box.querySelector('#qrQEParse').onclick=()=>{
const {steps,errors}=QR.parseText(box.querySelector('#qrQEText').value); parsed=steps;
const out=box.querySelector('#qrQEOut'); out.style.display='block';
const LC2={use_item:'#FFD600',try_item:'#FFD600',passage:'#90CAF9',wait:'#90CAF9',checkpoint:'#CE93D8',branch:'#A5D6A7'};
out.innerHTML=steps.map((s,i)=>'<div style="color:'+(LC2[s.type]||'white')+';padding:1px 0;">'+(i+1)+'. '+stepLabel(s)+'</div>').join('')+
(errors.length?errors.map(e=>'<div style="color:#f44336;">⚠ '+e+'</div>').join(''):'')+
'<div style="border-top:1px solid rgba(255,255,255,.15);margin-top:4px;padding-top:4px;opacity:.7;">'+steps.length+' adım</div>';
};
box.querySelector('#qrQESave').onclick=()=>{
const name=box.querySelector('#qrQEName').value.trim(), text=box.querySelector('#qrQEText').value.trim();
if(!name||!text){notify('Ad ve adımlar gerekli','#f44336');return;}
if(!parsed){const r=QR.parseText(text);parsed=r.steps;}
if(!parsed.length){notify('Geçerli adım yok','#f44336');return;}
QStore.upsert({...QStore.get(qid),name,text,sequence:parsed});
notify('"'+name+'" güncellendi','#4CAF50'); parsed=null; ov.remove(); buildCompass();
};
}
// COMPASS / LOCALE PANEL
function buildCompass() {
if(!QR.page.isLocale()) return;
document.getElementById('qrCompass')?.remove();
const char=QR.char.current(); if(!char) return;
const quest=QStore.get(C.activeQuestId(char));
const stepIdx=C.getStep(char), total=quest?.sequence.length||0;
const stopmode=C.getStopMode(char);
const inPass=!!C.getPassageTs(char), complete=quest&&stepIdx>=total;
const pct=total?Math.min(100,Math.round(stepIdx/total*100)):0;
const pos=S.get('qr_panel_pos',{x:10,y:10});
const elapsed=elapsedStr(char);
const activeDirs=QR.dom.activeDirections(), muted=Alert.isMuted();
const isMapping=mapMode(char);
const mapLog=isMapping?S.get(mapKey(char),[]):[];
const onCompass=QR.page.isCompass();
const isCompact=getCompact();
const infoSteps=quest?quest.sequence.filter(s=>s.type==='info'):[];
const nextSteps=quest&&!complete?quest.sequence.slice(stepIdx,stepIdx+6):[];
const curStep=nextSteps[0];
// Big action button content
let actionInner, actionClickable=!!curStep;
if (curStep?.type==='move') {
if (onCompass) {
const wantedDirs=curStep.dirs;
const fnDirs=wantedDirs.filter(d=>!activeDirs.includes(d));
const fpDirs=activeDirs.filter(d=>!wantedDirs.includes(d));
const dirsHtml=wantedDirs.map(d=>{
const ok=activeDirs.includes(d);
return '<span style="display:inline-flex;align-items:center;gap:4px;background:'+(ok?'rgba(46,125,50,.85)':'rgba(183,28,28,.85)')+
';border-radius:6px;padding:5px 10px;margin:2px;white-space:nowrap;">'+
'<span style="font-size:22px;line-height:1;">'+(QR.dirArrow?.(d)||d)+'</span>'+
'<span style="font-size:12px;">'+d+'</span>'+
'<span style="font-size:11px;opacity:.8;">'+(ok?'✓':'✗')+'</span></span>';
}).join('');
const fpHtml=fpDirs.length
?'<div style="margin-top:5px;padding-top:5px;border-top:1px solid rgba(255,255,255,.12);">'+
'<div style="font-size:9px;opacity:.4;margin-bottom:2px;">DİĞER YÖNLER</div>'+
fpDirs.map(d=>'<span style="display:inline-flex;align-items:center;gap:3px;background:rgba(230,81,0,.8);border-radius:5px;padding:3px 8px;font-size:11px;margin:1px;">'+
(QR.dirArrow?.(d)||d)+' '+d+'</span>').join('')+'</div>'
:'';
actionInner='<div style="display:flex;flex-wrap:wrap;gap:1px;">'+dirsHtml+'</div>'+fpHtml;
} else {
actionInner='<div style="text-align:center;padding:6px 0;">'+
'<div style="font-size:11px;opacity:.6;margin-bottom:6px;">🧭 Yön adımı — pusulaya git</div>'+
'<div>'+curStep.dirs.map(d=>'<span style="margin:2px;padding:4px 10px;background:rgba(255,255,255,.15);border-radius:5px;font-size:13px;display:inline-block;">'+(QR.dirArrow?.(d)||d)+' '+d+'</span>').join('')+'</div>'+
'</div>';
actionClickable=false;
}
} else if (curStep?.type==='goto') {
actionInner='<div style="text-align:center;padding:4px 0;">'+
'<div style="font-size:11px;opacity:.6;margin-bottom:4px;">📍 Konum</div>'+
'<div style="font-size:14px;font-weight:bold;">🗺️ → '+(curStep.label||curStep.locId)+'</div></div>';
} else if (curStep?.type==='require') {
actionInner='<div style="text-align:center;padding:4px 0;">'+
'<div style="color:#FF9800;font-size:12px;font-weight:bold;margin-bottom:5px;">⚠️ Gerekli Eşya</div>'+
'<div style="font-size:14px;font-weight:bold;padding:6px 10px;background:rgba(255,152,0,.18);border-radius:6px;margin-bottom:'+(curStep.description?'4px':'0')+';">'+curStep.item+'</div>'+
(curStep.description?'<div style="font-size:10px;opacity:.7;">'+curStep.description+'</div>':'')+
'</div>';
} else if (curStep?.type==='info') {
actionInner='<div style="text-align:center;padding:4px 0;">'+
'<div style="color:#42A5F5;font-size:12px;font-weight:bold;margin-bottom:5px;">ℹ️ Bilgi</div>'+
'<div style="font-size:11px;opacity:.85;">'+curStep.text+'</div>'+
'</div>';
} else if (curStep) {
actionInner='<div style="font-size:14px;font-weight:bold;text-align:center;padding:4px 0;">'+stepLabel(curStep)+'</div>';
} else {
actionInner='<div style="text-align:center;opacity:.35;font-size:11px;padding:6px 0;">Quest yok veya tamamlandı</div>';
actionClickable=false;
}
const upcomingHtml=nextSteps.slice(1,6).map((s,i)=>
'<div style="font-size:10px;opacity:.6;padding:2px 0;">'+(i+2)+'. '+stepLabel(s)+'</div>'
).join('');
const recentLog=QR.getLog(char).slice(-20).reverse();
const LC={error:'#f44336',use_item:'#FFD600',checkpoint:'#CE93D8',passage:'#90CAF9',wait:'#90CAF9',move:'rgba(255,255,255,.55)',manual_move:'#80CBC4'};
const recentHtml=recentLog.length
?recentLog.map(e=>'<div style="font-size:10px;padding:1px 0;color:'+(LC[e.type]||'rgba(255,255,255,.7)')+';">'+e.msg+'</div>').join('')
:'<div style="font-size:10px;opacity:.3;">—</div>';
const allQ=QStore.load(), qid=C.activeQuestId(char);
const questOpts='<option value="">— quest seç —</option>'+
'<option value="__map__"'+(isMapping?' selected':'')+'>🗺 Yeni Harita</option>'+
Object.values(allQ).map(q=>'<option value="'+q.id+'"'+(qid===q.id?' selected':'')+'>'+q.name+'</option>').join('');
let cpOpts='<option value="">— checkpoint —</option>';
if(quest){let ci=0;quest.sequence.forEach((s,i)=>{if(s.type==='checkpoint'){cpOpts+='<option value="'+i+'">'+(s.label||'#'+ci)+' ('+i+')</option>';ci++;}});}
const timerHtml=inPass?'<div style="'+sec('text-align:center;margin-bottom:6px;')+'"><span id="qrTimer">⏳ Yükleniyor...</span></div>':'';
const actionStyle=actionClickable
?btn('#1a3a6e','width:100%;text-align:left;padding:8px 10px;margin-bottom:6px;border:1.5px solid rgba(255,255,255,.2);')
:'width:100%;padding:8px 10px;margin-bottom:6px;background:rgba(255,255,255,.05);border-radius:6px;border:1px solid rgba(255,255,255,.1);';
const panel=mkEl('div');
panel.id='qrCompass';
panel.style.cssText=BG+';'+BDR+';'+FNT+';position:fixed;left:'+pos.x+'px;top:'+pos.y+'px;width:'+(isCompact?'200':'300')+'px;z-index:9998;box-shadow:0 4px 20px rgba(0,0,0,.6);';
panel.innerHTML=
'<div id="qrDrag" style="padding:8px 12px;cursor:move;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid rgba(255,255,255,.12);">'+
'<span style="font-size:13px;font-weight:bold;">📜 Guide '+GM_info.script.version+'</span>'+
'<div style="display:flex;gap:4px;align-items:center;">'+
(elapsed?'<span style="font-size:10px;color:#FFD600;">⏱'+elapsed+'</span>':'')+
(!isCompact?'<span style="font-size:10px;opacity:.5;">'+char+'</span>':'')+
(!isCompact&&infoSteps.length?'<button id="qrInfoBtn" style="'+btn('#1565C0','width:22px;height:22px;padding:0;font-size:13px;border-radius:50%;')+'" title="Bilgi">ℹ️</button>':'')+
(!isCompact&&quest?'<button id="qrQEdit" style="'+btn('#37474F','width:22px;height:22px;padding:0;font-size:12px;border-radius:50%;')+'" title="Quest Düzenle">✏️</button>':'')+
'<button id="qrCmpToggle" style="'+btn('rgba(255,255,255,.15)','width:22px;height:22px;padding:0;font-size:12px;')+'" title="'+(isCompact?'Normal Mod':'Kompakt Mod')+'">'+(isCompact?'⛶':'⊡')+'</button>'+
'<button id="qrClose" style="'+btn('rgba(255,255,255,.15)','width:20px;height:20px;padding:0;font-size:12px;')+'">×</button>'+
'</div>'+
'</div>'+
(isCompact
? '<div style="padding:7px 8px;">'+
'<div style="'+row('margin-bottom:5px;')+'">'+
'<select id="qrQSel" style="'+inp('flex:1;font-size:10px;padding:4px;')+'">'+questOpts+'</select>'+
'<span style="font-size:10px;color:#8BC34A;white-space:nowrap;margin-left:4px;">'+stepIdx+'/'+total+'</span>'+
'</div>'+
timerHtml+
(actionClickable
?'<button id="qrNext" style="'+btn('#1a3a6e','width:100%;text-align:center;padding:7px 6px;margin-bottom:5px;border:1.5px solid rgba(255,255,255,.2);font-size:13px;')+'">'+actionInner+'</button>'
:'<div style="width:100%;padding:7px 6px;margin-bottom:5px;background:rgba(255,255,255,.05);border-radius:6px;border:1px solid rgba(255,255,255,.1);font-size:11px;text-align:center;">'+actionInner+'</div>')+
'<div style="'+row('margin-bottom:0;')+'">'+
'<button id="qrPrev" style="'+btn('#37474F','flex:1;font-size:11px;padding:5px;')+'">← Geri</button>'+
'<button id="qrFwd" style="'+btn('#37474F','flex:1;font-size:11px;padding:5px;')+'">İleri →</button>'+
'</div>'+
'</div>'
: '<div style="padding:10px 12px;">'+
'<select id="qrQSel" style="'+inp('margin-bottom:6px;')+'">'+questOpts+'</select>'+
'<div style="position:relative;background:rgba(255,255,255,.15);border-radius:6px;height:14px;margin-bottom:8px;">'+
'<div style="background:linear-gradient(90deg,#4CAF50,#8BC34A);height:100%;width:'+pct+'%;border-radius:6px;transition:width .3s;"></div>'+
'<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;text-shadow:0 0 4px rgba(0,0,0,.9);">'+stepIdx+'/'+total+'</div>'+
'</div>'+
timerHtml+
(isMapping?
'<div style="'+sec('margin-bottom:6px;border:1.5px solid #80CBC4;')+'">'+
'<div style="font-size:9px;color:#80CBC4;margin-bottom:3px;">🗺 HARITA KAYDI — '+mapLog.length+' adım</div>'+
'<div style="display:flex;gap:4px;">'+
'<button id="qrMapSave" style="'+btn('#00897B','flex:1;font-size:11px;padding:5px;')+'">💾 Quest Olarak Kaydet</button>'+
'<button id="qrMapStop" style="'+btn('#607D8B','font-size:11px;padding:5px;')+'">✖ İptal</button>'+
'</div></div>'
:'')+
(actionClickable
?'<button id="qrNext" style="'+actionStyle+'">'+actionInner+'</button>'
:'<div style="'+actionStyle+'">'+actionInner+'</div>')+
(upcomingHtml?'<div style="'+sec('padding:5px 8px;margin-bottom:6px;')+'">'+upcomingHtml+'</div>':'')+
'<div style="'+row('margin-bottom:6px;')+'">'+
'<button id="qrPrev" style="'+btn('#37474F','flex:1;font-size:11px;padding:6px;')+'">← Geri</button>'+
'<button id="qrFwd" style="'+btn('#37474F','flex:1;font-size:11px;padding:6px;')+'">İleri →</button>'+
'<button id="qrStop" style="'+btn(stopmode==='ask'?'#FF9800':'#607D8B','flex:1;')+'">'+(stopmode==='ask'?'Dur':'Geç')+'</button>'+
'</div>'+
'<div style="'+row('margin-bottom:6px;')+'">'+
'<select id="qrCpSel" style="'+inp('flex:1;')+'">'+cpOpts+'</select>'+
'<button id="qrCpGo" style="'+btn('#9C27B0','white-space:nowrap;')+'">↩ Git</button>'+
'</div>'+
'<div style="'+sec('margin-bottom:6px;')+'">'+
'<div style="font-size:9px;opacity:.4;margin-bottom:3px;">SON 20 OLAY</div>'+
'<div id="qrRecentLog" style="max-height:120px;overflow-y:auto;">'+recentHtml+'</div>'+
'</div>'+
'<div style="'+row('margin-bottom:0;')+'">'+
(muted?'<button id="qrUnmute" style="'+btn('#607D8B','flex:1;font-size:10px;padding:5px;')+'">🔇 Sessiz</button>':'')+
'<button id="qrAdm" style="'+btn('#1565C0','flex:1;font-size:10px;padding:5px;')+'">Admin</button>'+
'<button id="qrRes" style="'+btn('#e65100','flex:1;font-size:10px;padding:5px;')+'">Reset</button>'+
'</div>'+
'</div>'
);
document.body.appendChild(panel);
panel.querySelector('#qrCmpToggle').onclick=()=>{setCompact(!isCompact);buildCompass();};
panel.querySelector('#qrQEdit')?.addEventListener('click',()=>openQuestEdit(C.activeQuestId(char)));
// item pick aktifse panel içine listele
if(_itemPick&&_itemPick.char===char) {
const body=panel.querySelector('div[style*="padding:10px"]');
if(body) renderItemPick(body,_itemPick.char,_itemPick.step,_itemPick.idx,_itemPick.names);
}
makeDraggable(panel,panel.querySelector('#qrDrag'));
panel.querySelector('#qrClose').onclick = ()=>{ _dismissed=true; panel.remove(); };
panel.querySelector('#qrInfoBtn')?.addEventListener('click',()=>showInfoModal(infoSteps));
panel.querySelector('#qrQSel').onchange = e=>{
if(e.target.value==='__map__'){mapStart(char);return;}
if(e.target.value){C.setQuestId(char,e.target.value);C.setStep(char,0);}
buildCompass();
};
const mapSaveBtn=panel.querySelector('#qrMapSave');
const mapStopBtn=panel.querySelector('#qrMapStop');
if(mapSaveBtn) mapSaveBtn.onclick=()=>mapSaveAsQuest(char);
if(mapStopBtn) mapStopBtn.onclick=()=>mapStop(char);
panel.querySelector('#qrStop')?.addEventListener('click',()=>{ C.setStopMode(char,stopmode==='ask'?'continue':'ask'); buildCompass(); });
panel.querySelector('#qrPrev').onclick = ()=>{ C.setStep(char,Math.max(0,stepIdx-1)); QR.log(char,'manual','← Geri ('+(Math.max(0,stepIdx-1))+')'); buildCompass(); };
panel.querySelector('#qrFwd').onclick = ()=>{ C.setStep(char,stepIdx+1); QR.log(char,'manual','→ İleri ('+(stepIdx+1)+')'); buildCompass(); };
panel.querySelector('#qrAdm')?.addEventListener('click',()=>openAdmin('chars'));
panel.querySelector('#qrRes')?.addEventListener('click',()=>{ if(confirm('Reset: '+char+'?')){C.reset(char);buildCompass();} });
panel.querySelector('#qrCpGo')?.addEventListener('click',()=>{
const sel=panel.querySelector('#qrCpSel'); if(!sel||!sel.value) return;
C.setStep(char,parseInt(sel.value)+1);
QR.log(char,'resume','↩ '+sel.options[sel.selectedIndex].text);
notify('↩ '+sel.options[sel.selectedIndex].text,'#9C27B0'); buildCompass();
});
const nb=panel.querySelector('#qrNext'); if(nb) nb.onclick=()=>{ QR.engine.runStep(char); buildCompass(); };
const um=panel.querySelector('#qrUnmute'); if(um) um.onclick=()=>{ Alert._muteUntil=0; buildCompass(); };
if(onCompass) hookManualMoves();
if(inPass) startTimer(char);
}
// ENGINE EVENTS
QR.on('complete', () => { notify('✅ Quest tamamlandı!','#4CAF50'); buildCompass(); });
QR.on('error', () => buildCompass());
QR.on('checkpoint', () => buildCompass());
QR.on('passage_start', () => buildCompass());
QR.on('passage_end', (char,opts) => { if(!opts?.early) showPassageDialog(char); else buildCompass(); });
QR.on('used_item', () => buildCompass());
QR.on('step', () => buildCompass());
QR.on('log', () => {
const el=document.getElementById('qrCompass'); if(!el) return;
const char=QR.char.current(); if(!char) return;
const log=QR.getLog(char).slice(-20).reverse();
const LC={error:'#f44336',use_item:'#FFD600',checkpoint:'#CE93D8',passage:'#90CAF9',wait:'#90CAF9'};
const recEl=el.querySelector('#qrRecentLog');
if(recEl) recEl.innerHTML=log.map(e=>'<div style="font-size:10px;padding:1px 0;color:'+(LC[e.type]||'rgba(255,255,255,.7)')+';">'+e.msg+'</div>').join('');
});
// PASSAGE DIALOG
function showPassageDialog(char) {
document.getElementById('qrPassDlg')?.remove();
const ov=mkEl('div','position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:100001;display:flex;align-items:center;justify-content:center;');
ov.id='qrPassDlg';
const box=mkEl('div',BG+';'+BDR+';'+FNT+';padding:20px;width:280px;text-align:center;',
'<div style="font-size:15px;font-weight:bold;margin-bottom:8px;">⛵ Seyahat Tamamlandı</div>'+
'<div style="margin-bottom:16px;opacity:.8;">'+char+' — sonraki adıma geçebilirsin.</div>'+
'<div style="display:flex;gap:8px;justify-content:center;">'+
'<button id="qrPOk" style="'+btn('#4CAF50','padding:8px 24px;font-size:13px;')+'">Tamam</button>'+
'<button id="qrPM" style="'+btn('#607D8B','padding:8px 14px;font-size:13px;')+'">🔇</button></div>');
ov.appendChild(box); document.body.appendChild(ov);
ov.onclick=e=>{if(e.target===ov){ov.remove();buildCompass();}};
box.querySelector('#qrPOk').onclick=()=>{ov.remove();buildCompass();};
box.querySelector('#qrPM').onclick =()=>{Alert.mute(char);notify('Alarm susturuldu','#607D8B');ov.remove();buildCompass();};
}
// ITEM PICK — panel içi tıklanabilir liste
let _itemPick=null; // {char, step, idx, names}
function showItemPick(char,step,idx,names) {
_itemPick={char,step,idx,names};
notify('Item seç — panelden tıkla','#FF9800');
buildCompass();
}
function renderItemPick(panel,char,step,idx,names) {
const wrap=mkEl('div',sec('margin-bottom:6px;border:1.5px solid #FF9800;'));
wrap.innerHTML='<div style="font-size:9px;opacity:.6;margin-bottom:4px;">KULLANILACAK İTEM — Seç</div>'+
names.map((n,i)=>'<button data-i="'+i+'" class="qrIP" style="'+btn('#1565C0','display:block;width:100%;text-align:left;margin-bottom:3px;padding:5px 8px;font-size:11px;')+'">'+n+'</button>').join('')+
'<button id="qrIPSkip" style="'+btn('#607D8B','width:100%;font-size:10px;padding:4px;margin-top:2px;')+'">Atla</button>';
panel.appendChild(wrap);
wrap.querySelectorAll('.qrIP').forEach(b=>b.onclick=()=>{
const name=names[parseInt(b.dataset.i)];
const items=QR.dom.getItems(), found=items.find(i=>norm(i.name)===norm(name));
_itemPick=null;
if(!found){notify('Item artık sayfada yok','#f44336');buildCompass();return;}
C.setStep(char,idx+1); found.useBtn.click();
const msg='✓ (seçim) '+found.name+' @ '+(QR.dom.locName()||'?');
addLog(char,'use_item',msg);
addLog(char,'use_item_pick','🎯 '+found.name+' — adım '+(idx+1));
QR.emit('used_item',char,found.name,idx);
buildCompass();
});
wrap.querySelector('#qrIPSkip').onclick=()=>{
_itemPick=null; C.setStep(char,idx+1);
addLog(char,'skip','Atlandı — adım '+(idx+1)); buildCompass();
};
}
QR.on('use_item_ambiguous',(char,step,idx,names)=>showItemPick(char,step,idx,names));
// REQUIRE POPUP
function showRequirePopup(step, onConfirm) {
document.getElementById('qrReqDlg')?.remove();
const ov=mkEl('div','position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:100003;display:flex;align-items:center;justify-content:center;');
ov.id='qrReqDlg';
const box=mkEl('div',BG+';border:2px solid #FF9800;border-radius:10px;'+FNT+';padding:22px 20px;width:300px;text-align:center;');
box.innerHTML=
'<div style="font-size:28px;margin-bottom:6px;">⚠️</div>'+
'<div style="font-size:13px;font-weight:bold;color:#FF9800;margin-bottom:10px;">Başlamadan önce hazırla!</div>'+
'<div style="font-size:16px;font-weight:bold;padding:10px;background:rgba(255,152,0,.18);border-radius:8px;margin-bottom:'+(step.description?'8px':'12px')+';">'+step.item+'</div>'+
(step.description?'<div style="font-size:11px;opacity:.75;margin-bottom:12px;">'+step.description+'</div>':'')+
'<div style="display:flex;gap:8px;justify-content:center;">'+
'<button id="qrReqOk" style="'+btn('#4CAF50','padding:9px 22px;font-size:13px;font-weight:bold;')+'">✓ Elimde var</button>'+
'<button id="qrReqNo" style="'+btn('#607D8B','padding:9px 14px;font-size:12px;')+'">Geri</button></div>';
ov.appendChild(box); document.body.appendChild(ov);
box.querySelector('#qrReqOk').onclick=()=>{ ov.remove(); onConfirm(); };
box.querySelector('#qrReqNo').onclick=()=>ov.remove();
}
// INFO MODAL
function showInfoModal(infoSteps) {
if(!infoSteps||!infoSteps.length) return;
document.getElementById('qrInfoDlg')?.remove();
const ov=mkEl('div','position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:100003;display:flex;align-items:center;justify-content:center;');
ov.id='qrInfoDlg';
const box=mkEl('div',BG+';border:2px solid #1565C0;border-radius:10px;'+FNT+';padding:20px;width:320px;max-height:80vh;overflow-y:auto;');
const content=infoSteps.map(s=>{
const threadId=s.link?(s.link.match(/Thread\/(\S+)$/)?.[1]||s.link):'';
const linkHtml=threadId?'<div style="margin-top:6px;"><a data-href="'+s.link+'" href="#" style="color:#42A5F5;font-size:11px;text-decoration:none;">📋 Forum: '+threadId+'</a></div>':'';
return '<div style="background:rgba(255,255,255,.07);border-radius:6px;padding:10px;margin-bottom:8px;text-align:left;">'+
'<div style="font-size:12px;line-height:1.5;">'+s.text+'</div>'+linkHtml+'</div>';
}).join('');
box.innerHTML=
'<div style="font-size:14px;font-weight:bold;color:#42A5F5;margin-bottom:12px;text-align:center;">ℹ️ Bilgi</div>'+
content+
'<button id="qrInfoClose" style="'+btn('#1565C0','width:100%;padding:8px;margin-top:4px;font-size:12px;')+'">Tamam</button>';
ov.appendChild(box); document.body.appendChild(ov);
ov.onclick=e=>{if(e.target===ov)ov.remove();};
box.querySelector('#qrInfoClose').onclick=()=>ov.remove();
box.querySelectorAll('[data-href]').forEach(a=>{
a.onclick=e=>{e.preventDefault();window.location.href=location.origin+a.dataset.href;};
});
}
// ADMIN PANEL
let _tab='chars';
function openAdmin(tab) {
const ex=document.getElementById('qrAdmin'); if(ex){ex.remove();return;}
if(tab) _tab=tab;
const ov=mkEl('div','position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:99999;display:flex;align-items:center;justify-content:center;');
ov.id='qrAdmin';
const box=mkEl('div',BG+';'+BDR+';'+FNT+';width:92%;max-width:640px;max-height:88vh;overflow-y:auto;padding:14px;');
ov.appendChild(box); document.body.appendChild(ov);
ov.onclick=e=>{if(e.target===ov)ov.remove();};
const TABS={chars:t('tabChars'),quests:t('tabQuests'),log:t('tabLog'),settings:t('tabSettings')};
function render(){
box.innerHTML=
'<div style="font-size:14px;font-weight:bold;text-align:center;margin-bottom:8px;">📜 Guide</div>'+
'<div style="display:flex;gap:2px;margin-bottom:10px;">'+
Object.keys(TABS).map(k=>'<button data-t="'+k+'" style="flex:1;padding:7px;font-size:11px;cursor:pointer;border:none;border-radius:6px 6px 0 0;'+
'background:rgba(255,255,255,'+(_tab===k?'.18':'.06')+');color:white;font-weight:'+(_tab===k?'bold':'normal')+';">'+TABS[k]+'</button>').join('')+
'</div><div id="tabBody"></div>'+
'<button id="qrAClose" style="background:rgba(255,255,255,.15);color:white;border:none;border-radius:6px;padding:6px;width:100%;font-size:11px;cursor:pointer;margin-top:8px;">'+t('close')+'</button>';
box.querySelectorAll('[data-t]').forEach(b=>b.onclick=()=>{_tab=b.dataset.t;render();});
box.querySelector('#qrAClose').onclick=()=>ov.remove();
const body=box.querySelector('#tabBody');
if(_tab==='chars') renderChars(body);
if(_tab==='quests') renderQuests(body);
if(_tab==='log') renderLog(body);
if(_tab==='settings') renderSettings(body);
}
render();
}
function renderSettings(body) {
const lw=mkEl('div',sec('display:flex;gap:4px;flex-wrap:wrap;margin-bottom:8px;'));
lw.innerHTML=
'<button id="qrLR" style="'+btn('#1565C0','flex:1;font-size:10px;padding:5px;')+'">'+t('readMe')+'</button>'+
'<button id="qrLK" style="'+btn('#607D8B','flex:1;font-size:10px;padding:5px;')+'">'+t('kobe')+'</button>'+
'<button id="qrLP" style="'+btn('#607D8B','flex:1;font-size:10px;padding:5px;')+'">'+t('otherQuests')+'</button>'+
'<button id="qrDB" style="'+btn('#2E7D32','flex:1;font-size:10px;padding:5px;')+'">'+t('questDb')+'</button>';
body.appendChild(lw);
lw.querySelector('#qrLR').onclick=()=>window.open('https://rentry.org/questrunner','_blank');
lw.querySelector('#qrLK').onclick=()=>window.open('https://rentry.org/kobequest','_blank');
lw.querySelector('#qrLP').onclick=()=>window.open('https://rentry.org/popquest','_blank');
lw.querySelector('#qrDB').onclick=()=>window.open('https://rentry.org/questrunnerdb','_blank');
// LANGUAGE SELECTOR
const langSec=mkEl('div',sec('font-size:10px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.5px;margin:10px 0 4px;'));
langSec.textContent=t('langLabel');
body.appendChild(langSec);
const langRow=mkEl('div',sec('display:flex;gap:4px;margin-bottom:6px;'));
[['TR','🇹🇷 Türkçe'],['EN','🇬🇧 English'],['PT','🇧🇷 Português']].forEach(([code,label])=>{
const b=mkEl('button',btn(LANG===code?'#6f42c1':'#607D8B','flex:1;font-size:10px;padding:5px;border-radius:4px;cursor:pointer;border:none;font-weight:'+(LANG===code?'bold':'normal')+';'));
b.textContent=label;
b.onclick=()=>{_setLang(code);location.reload();};
langRow.appendChild(b);
});
body.appendChild(langRow);
}
// CHARS TAB
function renderChars(body){
const chars=QR.char.all(), allQ=QStore.load();
const opts='<option value="">— none —</option>'+Object.values(allQ).map(q=>'<option value="'+q.id+'">'+q.name+'</option>').join('');
const rows=chars.map(c=>{
const asgn=C.getAssigned(c)||C.getQuestId(c)||'', step=C.getStep(c), total=allQ[asgn]?.sequence.length||0;
const cps=C.getCheckpoints(c), lastCp=cps.length?(cps[cps.length-1].label||'#'+(cps.length-1)):'—';
const note=C.getNote(c)||'';
// passage kalan süre
const pts=C.getPassageTs(c), pmin=C.getPassageMin(c);
let passLeft='';
if(pts&&pmin){
const elapsed=Math.floor((Date.now()-pts)/60000);
const left=Math.max(0,Math.round(pmin-elapsed));
passLeft='<br><span style="color:#90CAF9;font-size:9px;">⏳ ~'+left+'dk kaldı</span>';
}
return '<tr style="border-bottom:1px solid rgba(255,255,255,.08);">'+
'<td style="padding:5px 4px;font-size:11px;white-space:nowrap;">'+c+'</td>'+
'<td style="padding:5px 4px;"><select data-c="'+c+'" class="qrCQ" style="'+inp()+'">'+opts.replace('value="'+asgn+'"','value="'+asgn+'" selected')+'</select>'+
'<input data-c="'+c+'" class="qrCN" placeholder="Not..." style="'+inp('margin-top:3px;font-size:10px;')+'" value="'+note.replace(/"/g,'"')+'"></td>'+
'<td style="padding:5px 4px;font-size:10px;text-align:center;">'+step+'/'+total+'<br>'+
'<span style="color:#CE93D8;font-size:9px;">📍'+lastCp+'</span><br>'+
'<span style="color:#FFD600;font-size:9px;">⏱'+(elapsedStr(c)||'—')+'</span>'+passLeft+'</td>'+
'<td style="padding:5px 4px;"><button data-c="'+c+'" class="qrCR" style="'+btn('#FF9800','font-size:10px;padding:3px 7px;')+'">Reset</button></td></tr>';
}).join('');
body.innerHTML='<table style="width:100%;border-collapse:collapse;"><thead><tr style="font-size:9px;opacity:.5;border-bottom:1px solid rgba(255,255,255,.15);">'+
'<th style="padding:4px;text-align:left;">Karakter</th><th style="padding:4px;text-align:left;">Quest / Not</th>'+
'<th style="padding:4px;text-align:center;">Durum</th><th></th></tr></thead>'+
'<tbody>'+(rows||'<tr><td colspan="4" style="padding:10px;opacity:.5;text-align:center;">Karakter yok</td></tr>')+'</tbody></table>'+
'<div style="'+sec('margin-top:8px;')+'"><div style="font-size:10px;opacity:.6;margin-bottom:4px;">Toplu ata</div>'+
'<div style="display:flex;gap:6px;"><select id="qrBulkQ" style="'+inp()+'flex:1;">'+opts+'</select>'+
'<button id="qrBulk" style="'+btn('#4CAF50','padding:6px 10px;font-size:11px;')+'">Ata</button></div></div>';
body.querySelectorAll('.qrCQ').forEach(s=>s.onchange=e=>{C.setAssigned(e.target.dataset.c,e.target.value);C.setStep(e.target.dataset.c,0);});
body.querySelectorAll('.qrCN').forEach(i=>i.onblur=e=>{C.setNote(e.target.dataset.c,e.target.value.trim());});
body.querySelectorAll('.qrCR').forEach(b=>b.onclick=e=>{const c=e.target.dataset.c;if(confirm('Reset: '+c+'?')){C.reset(c);renderChars(body);}});
body.querySelector('#qrBulk').onclick=()=>{
const qid=body.querySelector('#qrBulkQ').value; if(!qid) return;
QR.char.all().forEach(c=>{C.setAssigned(c,qid);C.setStep(c,0);});
notify('"'+(QStore.get(qid)?.name||qid)+'" → '+QR.char.all().length+' karakter'); renderChars(body);
};
}
// QUESTS TAB
let _editId=null;
function renderQuests(body){
const allQ=QStore.load(), editing=_editId?QStore.get(_editId):null;
const list=Object.values(allQ).map(q=>
'<div style="'+sec('display:flex;justify-content:space-between;align-items:center;')+'">'+
'<div><div style="font-weight:bold;font-size:12px;">'+q.name+'</div>'+
'<div style="font-size:10px;opacity:.6;">'+q.sequence.length+' adım'+(q.builtin?' · builtin':'')+'</div></div>'+
'<div style="display:flex;gap:4px;">'+
'<button data-q="'+q.id+'" class="qrEd" style="'+btn('#2196F3','font-size:10px;padding:4px 8px;')+'">Düzenle</button>'+
'<button data-q="'+q.id+'" class="qrCp" style="'+btn('#9C27B0','font-size:10px;padding:4px 8px;')+'">Kopyala</button>'+
'<button data-q="'+q.id+'" class="qrDl" style="'+btn('#f44336','font-size:10px;padding:4px 8px;')+'">Sil</button>'+
'</div></div>'
).join('')||'<div style="opacity:.5;font-size:11px;margin-bottom:8px;">Quest yok.</div>';
let parsed=null;
body.innerHTML=list+
'<div style="border-top:1px solid rgba(255,255,255,.15);padding-top:10px;margin-top:4px;">'+
'<div style="font-size:11px;font-weight:bold;margin-bottom:6px;">'+(_editId?'Düzenle: '+editing?.name:'Yeni Quest')+'</div>'+
'<input id="qrQN" placeholder="Quest adı" style="'+inp('margin-bottom:6px;')+'"'+(editing?' value="'+editing.name+'"':'')+'>'+
'<textarea id="qrQT" rows="8" placeholder="North South East Use Icy Caverns 16 minutes Arrival Label # veya: move North use_item ? @ Icy Caverns passage 16min checkpoint LABEL branch West -> CHECKPOINT try_item ? @ Location" style="'+inp('resize:vertical;font-family:monospace;font-size:11px;margin-bottom:6px;')+'">'+((editing?.text)||'')+'</textarea>'+
'<div id="qrPOut" style="display:none;background:rgba(0,0,0,.25);border-radius:6px;padding:6px;font-size:10px;max-height:120px;overflow-y:auto;font-family:monospace;margin-bottom:6px;"></div>'+
'<div style="display:flex;gap:6px;">'+
'<button id="qrParse" style="'+btn('#607D8B','flex:1;font-size:11px;padding:6px;')+'">Parse</button>'+
'<button id="qrSaveQ" style="'+btn('#4CAF50','flex:1;font-size:11px;padding:6px;')+'">Kaydet</button>'+
'<button id="qrImpBtn" style="'+btn('#2196F3','flex:1;font-size:11px;padding:6px;')+'">Import</button>'+
'<button id="qrExpBtn" style="'+btn('#607D8B','flex:1;font-size:11px;padding:6px;')+'">Export</button>'+
'<button id="qrCancel" style="'+btn('#888','font-size:11px;padding:6px;')+'">İptal</button></div></div>';
body.querySelectorAll('.qrEd').forEach(b=>b.onclick=e=>{_editId=e.target.dataset.q;renderQuests(body);});
body.querySelectorAll('.qrCp').forEach(b=>b.onclick=e=>{const n=prompt('Kopya adı:');if(!n)return;const c=QStore.copy(e.target.dataset.q,n);if(c){QStore.upsert(c);renderQuests(body);notify('Kopyalandı: "'+n+'"');}});
body.querySelectorAll('.qrDl').forEach(b=>b.onclick=e=>{const q=allQ[e.target.dataset.q];if(confirm('Sil: "'+q?.name+'"?')){QStore.delete(e.target.dataset.q);_editId=null;renderQuests(body);}});
body.querySelector('#qrParse').onclick=()=>{
const {steps,errors}=QR.parseText(body.querySelector('#qrQT').value); parsed=steps;
const out=body.querySelector('#qrPOut'); out.style.display='block';
const LC2={use_item:'#FFD600',try_item:'#FFD600',passage:'#90CAF9',wait:'#90CAF9',checkpoint:'#CE93D8',branch:'#A5D6A7'};
out.innerHTML=steps.map((s,i)=>'<div style="color:'+(LC2[s.type]||'white')+';padding:1px 0;">'+(i+1)+'. '+stepLabel(s)+'</div>').join('')+
(errors.length?errors.map(e=>'<div style="color:#f44336;">⚠ '+e+'</div>').join(''):'')+
'<div style="border-top:1px solid rgba(255,255,255,.15);margin-top:4px;padding-top:4px;opacity:.7;">'+steps.length+' adım</div>';
};
body.querySelector('#qrSaveQ').onclick=()=>{
const name=body.querySelector('#qrQN').value.trim(), text=body.querySelector('#qrQT').value.trim();
if(!name||!text){notify('Ad ve adımlar gerekli','#f44336');return;}
if(!parsed){const r=QR.parseText(text);parsed=r.steps;}
if(!parsed.length){notify('Geçerli adım yok','#f44336');return;}
if(_editId){const ex=QStore.get(_editId);QStore.upsert({...ex,name,text,sequence:parsed});notify('"'+name+'" güncellendi');}
else{QStore.upsert({...QStore.new(name,text),sequence:parsed});notify('"'+name+'" kaydedildi — '+parsed.length+' adım');}
parsed=null;_editId=null;renderQuests(body);
};
body.querySelector('#qrCancel').onclick=()=>{_editId=null;renderQuests(body);};
body.querySelector('#qrImpBtn').onclick=openImport;
body.querySelector('#qrExpBtn').onclick=exportAll;
}
// LOG TAB
function renderLog(body){
const chars=QR.char.all(); let selChar=QR.char.current()||chars[0]||'';
function draw(){
const log=QR.getLog(selChar).slice(-150).reverse();
const LC={error:'#f44336',checkpoint:'#CE93D8',manual:'#90CAF9',manual_move:'#80CBC4',resume:'#A5D6A7',variant:'#FFCC02',complete:'#8BC34A',alert:'#FF9800',move:'rgba(255,255,255,.5)'};
const t=e=>{const d=new Date(e.ts);return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');};
const cOpts=chars.map(c=>'<option value="'+c+'"'+(c===selChar?' selected':'')+'>'+c+'</option>').join('');
body.innerHTML=
'<div style="'+row('margin-bottom:8px;')+'">'+
'<select id="qrLC" style="'+inp('flex:1;')+'"><option value="">— karakter —</option>'+cOpts+'</select>'+
'<button id="qrLCl" style="'+btn('#f44336','font-size:10px;padding:5px 8px;')+'">Sil</button>'+
'<button id="qrLCopy" style="'+btn('#1565C0','font-size:10px;padding:5px 8px;')+'">📋</button></div>'+
'<div style="max-height:320px;overflow-y:auto;"><table style="width:100%;border-collapse:collapse;">'+
'<thead><tr style="font-size:9px;opacity:.4;border-bottom:1px solid rgba(255,255,255,.1);">'+
'<th style="padding:2px 4px;text-align:left;">Saat</th><th style="padding:2px 4px;text-align:left;">Mesaj</th><th style="padding:2px 4px;text-align:left;">Yer</th></tr></thead>'+
'<tbody>'+(log.map(e=>'<tr style="border-bottom:1px solid rgba(255,255,255,.05);color:'+(LC[e.type]||'white')+';">'+
'<td style="padding:2px 6px;font-size:9px;opacity:.6;white-space:nowrap;">'+t(e)+'</td>'+
'<td style="padding:2px 6px;font-size:10px;">'+e.msg+'</td>'+
'<td style="padding:2px 6px;font-size:9px;opacity:.4;">'+(e.loc||'')+'</td></tr>').join('')||
'<tr><td colspan="3" style="padding:8px;opacity:.4;text-align:center;">Boş</td></tr>')+'</tbody></table></div>';
body.querySelector('#qrLC').onchange=e=>{selChar=e.target.value;draw();};
body.querySelector('#qrLCl').onclick=()=>{if(confirm('Log sil?')){QR.clearLog(selChar);draw();}};
body.querySelector('#qrLCopy').onclick=()=>{
const rows=QR.getLog(selChar).slice(-150).reverse().map(e=>t(e)+'\t'+e.msg+'\t'+(e.loc||''));
navigator.clipboard.writeText('Saat\tMesaj\tYer\n'+rows.join('\n')).then(()=>notify('Kopyalandı','#4CAF50')).catch(()=>notify('Hata','#f44336'));
};
}
draw();
}
// IMPORT / EXPORT
function exportAll(){
const db=QStore.load();
const plain=Object.values(db).map(q=>'# '+q.name+'\n'+(q.text||q.sequence.map(stepLabel).join('\n'))).join('\n\n---\n\n');
document.getElementById('qrTM')?.remove();
const ov=mkEl('div','position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:100000;display:flex;align-items:center;justify-content:center;');
ov.id='qrTM';
const box=mkEl('div',BG+';'+BDR+';'+FNT+';width:92%;max-width:580px;max-height:80vh;overflow-y:auto;padding:14px;');
box.innerHTML='<div style="font-weight:bold;margin-bottom:8px;">Export</div>'+
'<div style="display:flex;gap:8px;margin-bottom:8px;">'+
'<div style="flex:1;"><div style="font-size:10px;opacity:.6;margin-bottom:3px;">Plain text</div><textarea rows="10" readonly style="'+inp('resize:vertical;font-family:monospace;font-size:10px;')+'">'+plain+'</textarea></div>'+
'<div style="flex:1;"><div style="font-size:10px;opacity:.6;margin-bottom:3px;">JSON DB</div><textarea rows="10" readonly style="'+inp('resize:vertical;font-family:monospace;font-size:10px;')+'">'+JSON.stringify(db,null,2)+'</textarea></div></div>'+
'<button id="qrEC" style="'+btn('#607D8B','width:100%;padding:7px;')+'">Kapat</button>';
ov.appendChild(box); document.body.appendChild(ov);
ov.onclick=e=>{if(e.target===ov)ov.remove();};
box.querySelector('#qrEC').onclick=()=>ov.remove();
}
function openImport(){
document.getElementById('qrIM')?.remove();
const ov=mkEl('div','position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:100000;display:flex;align-items:center;justify-content:center;');
ov.id='qrIM';
const box=mkEl('div',BG+';'+BDR+';'+FNT+';width:90%;max-width:480px;padding:14px;');
box.innerHTML='<div style="font-weight:bold;margin-bottom:8px;">Import</div>'+
'<div style="display:flex;gap:6px;margin-bottom:8px;">'+
['Plain text','JSON DB'].map(l=>'<label style="flex:1;text-align:center;padding:6px;background:rgba(255,255,255,.1);border-radius:6px;cursor:pointer;font-size:11px;">'+
'<input type="radio" name="qrIFmt" value="'+(l==='JSON DB'?'json':'plain')+'"'+(l==='Plain text'?' checked':'')+'>'+' '+l+'</label>').join('')+
'</div><input id="qrIN" placeholder="Quest adı (plain text için)" style="'+inp('margin-bottom:6px;')+'">'+
'<textarea id="qrIT" rows="8" style="'+inp('resize:vertical;font-family:monospace;font-size:11px;margin-bottom:8px;')+'"></textarea>'+
'<div style="display:flex;gap:6px;">'+
'<button id="qrDoI" style="'+btn('#4CAF50','flex:1;padding:7px;')+'">Import</button>'+
'<button id="qrIC" style="'+btn('#607D8B','flex:1;padding:7px;')+'">İptal</button></div>';
ov.appendChild(box); document.body.appendChild(ov);
ov.onclick=e=>{if(e.target===ov)ov.remove();};
box.querySelector('#qrIC').onclick=()=>ov.remove();
box.querySelector('#qrDoI').onclick=()=>{
const text=box.querySelector('#qrIT').value.trim(); if(!text){notify('Bir şey yapıştır','#f44336');return;}
const fmt=box.querySelector('[name="qrIFmt"]:checked').value;
if(fmt==='json'){
try{const db=JSON.parse(text);const ex=QStore.load();Object.assign(ex,db);QStore.save(ex);notify(Object.keys(db).length+' quest import edildi');ov.remove();}
catch(e){notify('Geçersiz JSON','#f44336');}
} else {
const name=box.querySelector('#qrIN').value.trim()||'Quest '+Date.now();
const q=QStore.new(name,text);QStore.upsert(q);
notify('"'+name+'" import edildi — '+q.sequence.length+' adım');ov.remove();
}
};
}
// ── INIT ────────────────────────────────
createFloat();
if (QR.page.isCompass()) { buildCompass(); hookManualMoves(); }
// PopControl hazır olana kadar bekler
// EN strings for PopControl export
window.__ppcStrGuide = {"tabChars": "Characters", "tabQuests": "Quests", "tabLog": "Log", "tabSettings": "Settings", "readMe": "📖 Read Me", "kobe": "🗺️ Kobe", "otherQuests": "🗺️ Other Quests", "questDb": "📋 Quest Database", "close": "Close", "langLabel": "Language"};
function _waitPC(cb,n){n=n||0;if(unsafeWindow.PopControl){cb();return;}if(n<20)setTimeout(function(){_waitPC(cb,n+1);},300);}
// Auto-connect to PopControl if available
_waitPC(function() {
unsafeWindow.PopControl.register({
id:'guide', icon:'📜', label:'Guide',
strings: window.__ppcStrGuide || {},
buttons:[{icon:'📜', label:'Guide', onClick:function(){ _dismissed=false; var p=document.getElementById('qrCompass'); if(p)p.remove(); else if(QR.page.isLocale())buildCompass(); else openAdmin('quests'); }}],
onUndo:function(){ S.set('qr_pc_connected',false); if(!document.getElementById('qrFloat')) createFloat(); }
});
document.getElementById('qrFloat')?.remove();
S.set('qr_pc_connected',true);
});
setInterval(()=>{
if(!S.get('qr_pc_connected',false) && !document.getElementById('qrFloat')) createFloat();
if(QR.page.isCompass()) {
hookManualMoves();
if(!_dismissed && !document.getElementById('qrCompass')) buildCompass();
}
},5000);
QR.ui = { buildCompass, openAdmin, notify, stepLabel };
QR.ready = true;
QR.emit('ready');
console.log('[QR] v'+GM_info.script.version+' ready');
})();