Repository navigation
Expand file tree
/
Copy pathdock.html
More file actions
732 lines (702 loc) · 50.9 KB
/
Copy pathdock.html
File metadata and controls
732 lines (702 loc) · 50.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SEQO — Dock</title>
<style>
* { box-sizing: border-box; margin: 0; }
body { background: #16161d; color: #ddd; font-family: 'Segoe UI', sans-serif; font-size: 13px;
display: flex; flex-direction: column; height: 100vh; padding: 8px 10px 10px;
border: 1px solid rgba(255,215,102,.25); border-top: none; }
/* ---- top bar (shared) ---- */
#topbar { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; flex: none;
-webkit-app-region: drag; }
#topbar > * { -webkit-app-region: no-drag; }
#mode-title { color: #ffd766; font-weight: 600; font-size: 14px; -webkit-app-region: drag; }
#pin, #close { cursor: pointer; color: #999; font-size: 14px; padding: 0 4px; }
#pin:hover, #close:hover { color: #ffd766; }
#pin.detached { color: #7dffa0; }
button { background: #2a2a35; color: #eee; border: 1px solid #555; border-radius: 5px;
padding: 4px 10px; font-size: 12px; cursor: pointer; }
button:hover { background: #3a3a48; }
button.gold { border-color: #b99a3e; color: #ffd766; }
select, input[type=text] { background: #22222b; color: #eee; border: 1px solid #444;
border-radius: 5px; padding: 4px 8px; font-size: 12px; }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-thumb { background: rgba(255,215,102,.35); border-radius: 4px; }
.scroll { flex: 1; overflow-y: auto; padding-right: 4px; min-height: 0; }
/* ---- cleanup mode ---- */
#cleanup-root { display: none; flex-direction: column; flex: 1; min-height: 0; }
#cleanup-root.active { display: flex; }
#chead { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; flex: none; flex-wrap: wrap; }
#status { color: #8a8; font-size: 11px; margin-left: auto; }
#runcmd { display: flex; gap: 8px; align-items: center; background: rgba(255,215,102,.08);
border: 1px solid rgba(255,215,102,.3); border-radius: 6px; padding: 4px 9px;
margin-bottom: 6px; font-size: 11.5px; color: #cfc9a8; flex: none; }
#runcmd code { background: rgba(0,0,0,.4); padding: 1px 7px; border-radius: 4px;
color: #ffd766; font-family: Consolas, monospace; user-select: all; }
.divider { color: #ffd766; font-weight: 600; font-size: 12.5px; margin: 10px 0 4px;
border-bottom: 1px solid rgba(255,215,102,.3); padding-bottom: 2px; }
.divider small { color: #999; font-weight: 400; }
.row { display: flex; gap: 8px; align-items: baseline; padding: 3px 4px; border-radius: 4px; font-size: 12px; }
.row:hover { background: rgba(255,255,255,.05); }
.row .nm { cursor: pointer; text-decoration: underline dotted rgba(255,255,255,.25); }
.row .nm:hover { color: #ffd766; }
.row .cnt { color: #888; flex: none; }
.row .why { font-size: 11px; margin-left: auto; text-align: right; max-width: 58%; white-space: normal; }
.need .why { color: #7dffa0; }
.done .why { color: #cfa96a; }
.unk .why { color: #999; }
.gear .why { color: #9db4e8; }
.row .junkbtn { flex: none; cursor: pointer; opacity: .4; font-size: 12px; }
.row .junkbtn:hover { opacity: 1; }
.qchip { display: inline-block; margin: 0 4px 2px 0; padding: 0 6px; border-radius: 8px;
background: rgba(125,167,217,.15); color: #7da7d9; cursor: pointer; font-size: 11px; }
.qchip:hover { background: rgba(125,167,217,.35); }
#empty { color: #777; text-align: center; margin-top: 40px; line-height: 1.6; }
#qdetail { flex: none; display: none; background: rgba(125,167,217,.1);
border: 1px solid rgba(125,167,217,.35); border-radius: 6px;
padding: 6px 9px; margin-top: 6px; font-size: 12px; line-height: 1.55;
max-height: 34%; overflow-y: auto; }
#qdetail b { color: #7da7d9; }
#qdetail .rw { color: #7dffa0; cursor: pointer; text-decoration: underline dotted; margin-right: 8px; }
#qdetail .chain { color: #ffb27d; }
#qdetail .close2 { float: right; cursor: pointer; color: #888; }
#cfoot { flex: none; color: #776; font-size: 10px; margin-top: 5px; line-height: 1.4; }
/* ---- item mode ---- */
#item-root { display: none; flex-direction: column; flex: 1; min-height: 0; }
#item-root.active { display: flex; }
#ihead { display: flex; gap: 8px; align-items: center; margin-bottom: 5px; flex: none; flex-wrap: wrap; }
#ititle { color: #ffd766; font-weight: 700; font-size: 14px; }
#iwish { cursor: pointer; }
#itier-row { display: none; gap: 8px; align-items: center; margin-bottom: 5px; flex: none; }
#itier-row input { flex: 1; }
#itierval { color: #ffd766; font-weight: 700; width: 30px; }
#istats { white-space: pre-wrap; font-family: Consolas, monospace; font-size: 12px; color: #cfe3cf;
background: rgba(255,255,255,.05); border-radius: 6px; padding: 7px 9px; margin-bottom: 6px; flex: none; }
.fx { background: rgba(125,167,217,.1); border: 1px solid rgba(125,167,217,.3); border-radius: 6px;
padding: 5px 9px; margin-bottom: 5px; font-size: 12px; line-height: 1.5; flex: none; }
.fx b { color: #7da7d9; cursor: pointer; text-decoration: underline dotted; }
.fx b:hover { color: #ffd766; }
.sect-h { color: #ffd766; font-size: 11.5px; font-weight: 600; margin: 8px 0 3px; }
.sect-b { white-space: pre-wrap; font-size: 11.5px; line-height: 1.5; color: #ccc;
font-family: Consolas, monospace; }
.chiprow { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 5px; }
.chip { padding: 0 7px; border-radius: 8px; background: rgba(255,255,255,.08); color: #bcd;
cursor: pointer; font-size: 11px; line-height: 18px; }
.chip:hover { background: rgba(255,215,102,.2); color: #ffd766; }
.cached { color: #666; font-size: 10px; margin-top: 8px; }
/* ---- guide mode (wiki page text) ---- */
#guide-root { display: none; flex-direction: column; flex: 1; min-height: 0; }
#guide-root.active { display: flex; }
#gtext { white-space: pre-wrap; font-size: 12px; line-height: 1.55; color: #ccd;
background: rgba(255,255,255,.04); border-radius: 6px; padding: 8px 11px; user-select: text; }
#gfoot { flex: none; display: flex; gap: 10px; align-items: center; color: #776; font-size: 10.5px; margin-top: 5px; }
#gfoot .chip { font-size: 11px; }
/* Large text setting: the fine print grows a step everywhere */
body.large { font-size: 14.5px; }
body.large .row { font-size: 13.5px; }
body.large .row .why { font-size: 12.5px; }
body.large #gtext { font-size: 13.5px; }
body.large .sect-b, body.large #istats { font-size: 13px; }
body.large .fx { font-size: 13px; }
</style>
</head>
<body>
<div id="topbar">
<span id="back" title="Back" style="display:none;cursor:pointer;color:#7da7d9;font-size:15px;padding:0 4px">←</span>
<span id="mode-title">SEQO</span>
<span style="flex:1"></span>
<span id="pin" title="Detach from the overlay (float free) / re-attach">📌</span>
<span id="close" title="Close this panel">✕</span>
</div>
<!-- ================= CLEANUP MODE ================= -->
<div id="cleanup-root">
<div id="runcmd">
Run <code>/outputfile inventory</code> in game first, so the list matches your real bags.
<button id="copycmd" title="Copy the command to paste into the game">📋 copy</button>
<span id="copied" style="color:#7dffa0;display:none">copied ✓</span>
</div>
<div id="chead">
<input type="text" id="filter" placeholder="filter…" style="width:110px">
<select id="sort" title="Sort order inside each location section">
<option value="name">sort: name</option>
<option value="category">sort: category</option>
<option value="count">sort: count</option>
</select>
<button id="scan" class="gold" title="Unknowns look themselves up automatically — this forces a re-run by hand">🔎 Scan</button>
<button id="refresh" title="Rebuild the list from your latest inventory file">⟳</button>
<span id="status"></span>
</div>
<div id="chips" style="display:flex;gap:5px;flex-wrap:wrap;margin-bottom:5px;flex:none"></div>
<div id="list" class="scroll"><div id="empty">Waiting for the overlay…<br>Make sure you've run /outputfile inventory in game.</div></div>
<div id="qdetail"></div>
<div id="cfoot">Ground truth is your inventory file. Rows leave the list live as you destroy or vendor items.
🗑 flags an item junk forever (it drops into Job done) · click an item name for its full details · click a quest for its rewards and what they lead to.</div>
</div>
<!-- ================= GUIDE MODE (wiki page text) / HELP ================= -->
<div id="guide-root">
<div class="scroll"><div id="gtoc" style="display:none"></div><div id="gtext">loading…</div></div>
<div id="gfoot">
<span id="gage"></span>
<span style="flex:1"></span>
<button id="grefresh" title="Re-fetch this page from the wiki">⟳</button>
<span class="chip" id="gopen" title="Open the full page in your browser">open full page ↗</span>
</div>
</div>
<!-- ================= ITEM MODE ================= -->
<div id="item-root">
<div id="ihead">
<span id="ititle">—</span>
<span id="iwish" title="Wish list: loud alert whenever this drops">☆</span>
<span style="flex:1"></span>
<button id="iupdate" title="Re-fetch this item from eqlwiki + eqlegendstools">⟳ Update</button>
</div>
<div class="scroll" id="ibody">
<div id="itier-row">
<span style="color:#999;font-size:11px">Upgrade tier</span>
<input type="range" id="itier" min="0" max="10" value="0" step="1">
<span id="itierval" title="Exact tier values, rounded down like the game. In-game items part-way to the next tier read slightly higher.">+0</span>
</div>
<div id="istats" style="display:none"></div>
<div id="ifx"></div>
<div id="idrops" class="chiprow"></div>
<div id="isect"></div>
<div id="isources" class="chiprow"></div>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
let mode = null;
// back navigation: cleanup → item → item → … and back out again
const navStack = [];
function pushNav() {
if (mode === 'cleanup') navStack.push({ mode: 'cleanup' });
else if (mode === 'item' && itemName) navStack.push({ mode: 'item', name: itemName });
else if (mode === 'guide' && guidePayload && guidePayload.helpTopic !== undefined)
navStack.push({ mode: 'helptopic', i: guidePayload.helpTopic });
else if (mode === 'guide' && guidePayload) navStack.push({ mode: 'guide', payload: guidePayload });
else if (mode === 'guide') navStack.push({ mode: 'help' });
$('back').style.display = navStack.length ? '' : 'none';
}
// ---------- top bar ----------
let detached = false;
$('close').addEventListener('click', () => window.overlay.closeDock());
$('back').addEventListener('click', goBack);
$('pin').addEventListener('click', () => {
detached = !detached;
$('pin').classList.toggle('detached', detached);
$('pin').title = detached ? 'Re-attach to the overlay' : 'Detach from the overlay (float free)';
window.overlay.dockDetach(detached);
});
function goBack() {
const prev = navStack.pop();
$('back').style.display = navStack.length ? '' : 'none';
if (!prev) return;
if (prev.mode === 'cleanup') { setMode('cleanup'); render(); }
else if (prev.mode === 'guide') showGuide(prev.payload, false, true);
else if (prev.mode === 'help') showHelp(true);
else if (prev.mode === 'helptopic') showHelpTopic(prev.i, true);
else showItem(prev.name, false, true);
}
function setMode(m) {
mode = m;
$('cleanup-root').classList.toggle('active', m === 'cleanup');
$('item-root').classList.toggle('active', m === 'item');
$('guide-root').classList.toggle('active', m === 'guide');
$('mode-title').textContent = m === 'cleanup' ? '🧹 Cleanup' : m === 'guide' ? '📖 Guide' : '🔎 Item detail';
}
// =========================================================================
// HELP — how every feature works, shipped inside the app (works offline)
// =========================================================================
const HELP = [
['Getting started', 'Pick your character’s log file in ⚙ Settings → General — everything SEQO shows comes from that log plus your /outputfile exports.\n\nThe overlay has two modes. EDIT mode: title bar visible, drag to move, resize from the corner. LOCKED mode (🔒 or Ctrl+Alt+O): clicks pass through to the game, and controls work on mouse-over only — hover and the tabs light up. Ctrl+Alt+H hides/shows the whole overlay.\n\nThe 😴 button in the title bar is the AFK watch: turn it on when you step away and SEQO sirens if anything attacks you. In locked mode 😴 and 🔓 float at the top-right.'],
['Game exports (/outputfile)', 'Three in-game commands feed SEQO’s trackers — paste each into the game’s chat:\n\n/outputfile inventory — bags, bank, worn gear. Drives Cleanup, epic checkmarks, rep counters.\n/outputfile achievements — unlock status.\n/outputfile faction — faction progress bars.\n\nSEQO finds the files next to your log and picks up a fresh one within a second, no clicks. Views that read an export show a 📋 reminder bar whenever their file is missing or stale. On a fresh login a popup lists the commands with copy buttons and ticks each one off as its new file lands — it only asks for the exports your features actually use (turn it off in ⚙ Alerts & Sounds).'],
['Meter', 'The ⚔ Meter shows the current fight; Σ Session shows the whole session’s damage by source — the old Spells tab. Click any player bar to see their damage by spell; the 🐾 button on a bar marks them as your pet (pets fold into your own number when "Merge pet damage" is on).\n\n📋 chat copies a one-line summary for in-game chat, 📋 full copies a multi-line breakdown for Discord — "top N" picks how many players are included.\n\nSpell timers: define "Spell = seconds" lines in ⚙ Alerts & Sounds and casting starts a countdown chip visible on every tab.'],
['Bottom bar: runs & XP', 'The bar at the overlay’s bottom edge is on every tab.\n\n▶ Start run begins a run (instance clear, camp, farm session): loot, coin, motes and rare kills are tracked until you press ⏹ End run — the button carries a live clock while a run is going. The Loot tab’s button is the same run.\n\nNext to it is the session XP/AA readout: percent gained, kills, per-hour pace, position in your level and time-to-ding once a level-up has been seen. At level 50 it shows AA only. ↺ reset starts the pace over (kills, %, per-hour) and keeps your level position and AA total.'],
['Loot tab', 'Every drop SEQO witnesses, with a verdict: 🌟 WISH (starred), KEEP (a quest, epic, grind or your own flag wants it), SPARE / SELLABLE / OTHER CLASS for Plane of Sky pieces, nothing for the rest. Click a verdict chip on a drop to cycle your own keep/junk flag.\n\nIf your advanced-loot filter AUTO-SELLS something you still need, SEQO shouts — fix the filter, the item is gone.\n\nThe run box shows the live run: coin, items, ✨ motes with per-hour rate, rares down vs known. 📋 copies a one-liner, 📋+ the full report.\n\n🧹 Cleanup and 🎯 Needs open their windows. (The Wiki export button lives in ⚙ Advanced with the wiki account setup.)'],
['Cleanup window', 'Judges every stored item: can this go?\n\nRows sit in strict location sections — 🎒 Bags, 📦 Character storage (the depot), 🏦 Bank (shared bank folded in, tagged) — name-sorted, with a chip row to filter by verdict: Needed / Gear / Unknown / Job done. An item split across places shows "+N elsewhere"; hover for the exact split.\n\nUnknown items look themselves up on the wiki automatically (each only ever once — answers are cached and synced). Quest rows show every quest plus the chain: "↳ quest → reward → needed for: …", so a worthless-looking fang that feeds your epic says so right on the row. 🗑 flags junk forever. Ground truth is your inventory export — run /outputfile inventory before a big cleanout.'],
['Zone needs (tab & panel)', 'Everything you still need that drops where you\'re standing: ⭐ starred gear, ⚔ epic pieces (with true counts), 🔓 unlock turn-ins with live counters, 🧙 tracked spell-quest pieces. Each row names the mobs that drop it; click a row for full item details. Counters tick the moment the loot line hits your log, and rows flash green when their item just dropped.\n\nIt lives in two places with the same list: the 🎯 Needs TAB in the overlay, and the slim pop-out strip (⧉ Pop-out, or 🎯 Needs on the Loot tab) that docks under the overlay and stays visible while you play. The pop-out is persistent — it follows you between zones and reopens where you left it after a restart; — collapses it to a one-line bar.'],
['Lookup tab', 'Search box: local caches first (your loot DB, cached wiki pages), Sources ⟳ asks eqlwiki and eqlegendstools. Click any result for the full item panel in the attached window: stats with the +1…+10 upgrade slider, effects explained, drop sources, your own drop rates, and deep links to the community databases.\n\nThe zone block lists the current zone’s named mobs and their drops — with your personal kill counts and drop rates next to the wiki’s word. ☆ on any row stars it onto your wish list.'],
['Quests tab', 'The chip row picks the view: 📜 Quests (zone quests + the Plane of Sky tracker), 🔓 Unlocks, 💀 Slayer, 🧙 Spells, ⚔ Epics, ✨ Exalts. (Best-in-slot gear moved to the ⚔ Class tab’s Gear advisor.)\n\nPoS: pick your classes, and quest items check off automatically as you loot; the summary strip counts turned-in / ready / missing-one. Items you already own check off from your inventory export. Click a quest’s ✓ when you hand it in.\n\nOther zones list the wiki’s quests for wherever you are — the zone dropdown follows you as you zone.'],
['Slayer achievements', 'The 💀 Slayer view reads your achievements export: four groups (General meta-achievements, Conquest family counters, Special, Skill), each with live progress bars, sorted closest-to-done first. In General, chain rows are clickable — jump straight to the achievement they require.\n\n🧭 on any target opens a route read-out in this window: where that family hunts, and what the kills COST you — race targets list the exact factions their race unlock needs, with YOUR current standing, so you never torch a grind you\'re mid-way through by accident.\n\nCounts come from /outputfile achievements — run it again after a hunt to update. Zone suggestions are classic-era knowledge; Legends may differ.'],
['Class tab: gear advisor', 'The 🛡 Gear view scores every wearable item on the wiki for YOUR trio and shows, per slot: 👤 what you wear, 🎒 the best you own, 🌍 the best obtainable with where it comes from. ☆ stars an upgrade onto your wish list so zone needs flags where it drops.\n\nPick a Purpose (Balanced, Tank, Melee ST, AoE, Caster DPS, Healer) — each is just a stat-weight table, and ⚙ weights lets you edit every number (saved per purpose). The weapon-type filter keeps a sword-and-board build from being told a two-hander is an upgrade; the obtained-by chips (Drop/Quest/Craft/Vendor) match how you actually play.\n\nFirst use: ⬇ Build catalog reads every item page off the wiki once (a few minutes, progress shown). After that it\'s cached, synced between your computers, and instant. "+N" upgrades score as their base item.'],
['Class tab: buffs & stacking', 'The 🧪 Buffs view runs on the GAME\'S OWN spell files (read from your install, refreshed automatically each patch) — not wiki guesses.\n\nON YOU NOW tracks every lasting effect the moment its landing line hits your log, with countdowns: exact for your own casts, ~estimated when someone else\'s level decides the duration. Fade lines are matched exactly, so fade alerts name the real spell.\n\nYOUR LINEUP builds itself: set your spell-gem count (AAs raise it) and SEQO picks the strongest non-conflicting buff set your trio can memorize at YOUR level — balanced for fighting by default, no setup needed. Tap 🎯 Focus to lean it toward specific stats (Resists means all five). Every pick shows its real numbers from the game data — including the price on mixed spells like Dark Temptation (ATK +25 · HP/tick −3) — and hover any pick for full details and swap options. Stacking is checked the way the game checks it, slot by slot plus the stacking-group table: same-stat buffs that genuinely stack (Grim Aura + Dark Temptation) are both picked, because their values add.\n\n⏳ LONG BUFFS trades a little stat strength for durations you don\'t babysit — and shows the exact trade in one line (STA −5 · DEX −1, avg duration 12m → 40m). Songs are unaffected: Symphonic Aura keeps them running either way, which is also why bard songs sit at the BOTTOM of the lineup as the optional block.\n\n⚔ DISCIPLINES lists every /disc combat ability your trio can use — Focused Will, Evasive, Deadeye and the rest — with their real decoded effects (crit %, riposte, mitigation, attack speed…). They\'re activated, one at a time, and never spend a spell gem.\n\nUTILITY lists invisibility, levitate, see invisible, water breathing and night vision separately — cast those when needed, they never spend a lineup gem.\n\nALL BUFFS (collapsed by default) lists everything your trio can cast, and uses the game\'s own stacking table to call out groups that DON\'T stack — cast the ✔ one, skip the overridden. And when a cast fails with "did not take hold", SEQO names the blocking buff and says whether yours was stronger.\n\nYour trio fills in automatically from your own /who row; the three dropdowns let you preplan any other makeup and ☆ gear for it.'],
['Spell quests', 'The 🧙 Spells view tracks the optional class spell quests (Tashania, Ice Comet, Divine Might, Life Leech, Call of Flame). ⭐ Track one and its hunted pieces join the zone-needs list — walk into Nagafen\'s Lair and the needs panel reminds you the stone spiders there drop your Crystalline Orb.\n\nCheckmarks fill from your inventory export and live loot; anything the auto-match misses, click the box by hand. Each quest card carries the full route, turn-in by turn-in, with the gotchas (temporary containers, don\'t-hand-in-invisible, fights that spawn on turn-in) called out.\n\nSome quests have a clicky-vs-spell choice — the card says when you can stop early and keep the clicky instead.'],
['Unlocks', 'Live status from your achievements export, faction bars from the faction export, rep-item counters from your inventory — all three commands are on the view’s 📋 reminder bar when stale.\n\nWhere the guide gives real alternatives, routes show as Option A / Option B, each with its own counters, so 437/1200 Gnoll Fangs only counts against the route that eats fangs. 🌙 marks evil-race-specific routes.\n\n📖 on a race, faction or deity opens the relevant guide text right here in this window — cached, works offline, with a link to the full page.'],
['Epics', 'The prep checklist understands how epics are actually built. Own a crafted reward (say, Darkforge Breastplate) and its ingredients show ✔ covered — you don’t need rotted pieces for armor you’re wearing. Chains work (Decrepit Sheath → Corrupted Ghoulbane), owning the finished epic silences the whole class, and shared items show true totals: if two of your classes each need one, the row reads 1/2 until you have both.\n\nThe game can’t show items you already handed to an NPC, so click a row’s checkbox to tick off a turn-in yourself. ☆ stars a piece onto your wish list for loud drop alerts.'],
['BiS drop alerts', 'Best-in-slot browsing lives on the ⚔ Class tab now — the Gear advisor scores every item for YOUR trio with editable weights and filters, which the old 🏆 BiS list never could.\n\nWhat remains here is the alert option: "Flag best-in-slot drops" in ⚙ Alerts & Sounds. Off (the default), nothing happens. On, drops that appear on the community BiS list for your class toast a 🏆 and get KEEP verdicts in the loot log. The better habit is still to ☆ the exact pieces you want — the Gear advisor\'s 🌍 rows have the star right there.'],
['Exaltations', '✨ Exalts lists every gear effect (Focus, Worn, Proc, Clicky…) from the wiki. Pick an effect to see every item that carries it, with ✓ on ones you own. Search finds "Mana Preservation II" style effects fast. Item pages link back here from their Effect lines.\n\nExaltation items in your bags (e.g. "… (Exaltation)") are always judged keep-worthy in Cleanup — they’re transferable effects.'],
['Timers & respawns', 'Kill a mob and SEQO starts timing it. ★ pins a camp: due alerts fire, and the chip bar shows the countdown on every tab.\n\nTimers are learned from YOUR kill-to-kill gaps — several cycles vote, outliers are dropped, and nothing is learned inside instanced copies or across a zone line. Mobs with varying spawns get a WINDOW ("15–30m"): an alert when the window opens, another when it’s past.\n\nThe mark after a countdown says how much to trust it: ⟲ learned from 3+ cycles · ~ tentative (1–2 cycles) · ~wiki zone-wide default · ✎ set by you. Click ✎ to set a timer yourself — "6:40", "25m", or "15-30" for a window; empty puts it back on automatic.\n\n+PH links a placeholder: its deaths reset (and teach) the named’s cycle.'],
['Map', 'The world map shows zone connections with your current zone glowing green. Pick a destination (or click one) for a route; scroll zooms, drag pans. It follows your zoning automatically.'],
['Notes', '📝 is a free-form scratchpad — special commands, camp coordinates, trade prices. It saves as you type, survives forever, and syncs between computers through the shared data folder.'],
['Alerts & AFK', 'Every alert kind has a distinct sound; volume and flash are in ⚙ Alerts & Sounds, and 🔔 Test all alerts plays each one.\n\nBuff fades can alert (rename cryptic fade lines to spell names in the fade watch). Spammy alerts you’ve seen show in the mute list — tick one to silence it forever.\n\n😴 AFK watch (title bar): loud repeating siren if you’re attacked while away. In locked mode it floats top-right with the 🔓.'],
['Syncing between computers', 'Point ⚙ Game Data & Sync → Shared data folder at Dropbox/Drive/OneDrive and SEQO’s databases (loot history, item knowledge, notes, timers…) follow you between computers. One computer at a time — camp before switching.\n\nSynced game files mirror chosen game files (spell loadouts, UI layouts) through that folder, newest wins, with .seqo.bak backups. The status under Sync now never leaves you guessing: it names what was pushed and pulled, what failed and why, and WHICH computer wrote last — "picked up changes from ED-DESKTOP" means the other machine’s data arrived.'],
['FAQ', 'Q: It says I need an item I already turned in.\nA: The inventory export only shows what you hold. On the Epics view, click the row’s checkbox to tick the turn-in off yourself.\n\nQ: My counters aren’t moving.\nA: Loot ticks live; totals ground in your last /outputfile inventory. Run it again — SEQO picks it up in a second.\n\nQ: Why did a respawn timer say ~tentative?\nA: It’s been learned from only one or two of your kill cycles. More cycles = solid ⟲. Or set it yourself with ✎.\n\nQ: A rare list / zone page looks wrong.\nA: Zone data comes from the wiki — ⟳ Update on the Lookup tab refetches it. Click-ignore a wrongly-listed "rare" and SEQO remembers.\n\nQ: What do the loot colors mean?\nA: Green KEEP = something still needs it. Gold WISH = you starred it. Grey = no known use. Orange SELLABLE/SPARE = PoS quest already served.\n\nQ: Where did the 🏆 BiS view go?\nA: It grew into the ⚔ Class tab’s Gear advisor — scored for your whole trio, with your own gear in the comparison. The "Flag best-in-slot drops" alert option still works from ⚙ Alerts & Sounds.\n\nQ: Why does the Gear view say "no catalog"?\nA: Press ⬇ Build catalog once — it reads every item page off the wiki (a few minutes, progress shown). After that, ⟳ updates take seconds because only changed pages are re-read.\n\nQ: Why do some buff countdowns show a ~?\nA: That duration depends on the CASTER\'s level, which SEQO can\'t always know. Your own casts are exact; others\' are honest estimates.\n\nQ: What does "experimental" on the Class tab mean?\nA: The data is the real thing (the game\'s own spell files, the wiki\'s item catalog), but the recommendations are young and still being tuned from field reports. Trust the numbers, sanity-check the advice, and report anything odd.\n\nQ: Where does my data live?\nA: Locally (or in your shared folder) — SEQO only READS the wiki and tool sites, and never sends your data anywhere.']
];
function showHelp(noPush) {
if (!noPush && mode) pushNav();
setMode('guide');
guidePayload = null;
$('mode-title').textContent = '❓ Help';
$('gtext').style.display = 'none';
$('gfoot').style.display = 'none';
const toc = $('gtoc');
toc.style.display = 'block';
toc.innerHTML = '';
const intro = document.createElement('div');
intro.style.cssText = 'color:#998;font-size:11.5px;margin-bottom:6px';
intro.textContent = 'Pick a topic — short and practical, written for SEQO v' + (window.__appv || '2.4') + '.';
toc.appendChild(intro);
HELP.forEach(([title], i) => {
const row = document.createElement('div');
row.className = 'row';
row.innerHTML = '<span class="nm"></span>';
row.querySelector('.nm').textContent = (i === HELP.length - 1 ? '💬 ' : '• ') + title;
row.addEventListener('click', () => showHelpTopic(i));
toc.appendChild(row);
});
}
function showHelpTopic(i, noPush) {
if (!noPush && mode) { navStack.push({ mode: 'help' }); $('back').style.display = ''; }
setMode('guide');
guidePayload = { helpTopic: i };
$('gtoc').style.display = 'none';
$('gfoot').style.display = 'none';
$('gtext').style.display = 'block';
$('mode-title').textContent = '❓ ' + HELP[i][0];
$('gtext').textContent = HELP[i][1];
}
// =========================================================================
// GUIDE MODE — a wiki page (or one section of it) as readable text, cached
// =========================================================================
let guidePayload = null;
async function showGuide(payload, refresh, noPush) {
if (!payload || !(payload.title || payload.localText)) return;
if (!noPush && mode) pushNav();
setMode('guide');
$('gtoc').style.display = 'none';
$('gtext').style.display = 'block';
$('gfoot').style.display = 'flex';
guidePayload = payload;
$('mode-title').textContent = '📖 ' + (payload.label || payload.title);
// overlay-composed read-out (e.g. a Slayer route): no wiki fetch at all
if (payload.localText) {
$('gtext').textContent = payload.localText;
$('gage').textContent = payload.credit || 'composed by SEQO from your own game files';
$('grefresh').style.display = 'none';
$('gopen').style.display = 'none';
return;
}
$('grefresh').style.display = '';
$('gopen').style.display = '';
$('gtext').textContent = 'loading…';
$('gage').textContent = '';
const d = await window.overlay.getGuidePage(payload.title, payload.anchor, !!refresh);
if (guidePayload !== payload) return; // user already moved on
if (d && d.error && payload.fallback) { showGuide(payload.fallback, refresh, true); return; }
if (!d || d.error) {
$('gtext').textContent = '⚠ ' + ((d && d.error) || 'could not load the page') +
'\n\nThe wiki may be unreachable right now — the browser link below still works.';
$('gopen').onclick = () => window.overlay.openExternal('https://eqlwiki.com/index.php?search=' + encodeURIComponent(payload.title));
return;
}
$('mode-title').textContent = '📖 ' + (payload.label || d.title) + (d.anchor ? ' · ' + d.anchor : '');
$('gtext').textContent = d.text || '(the page is empty)';
$('gage').textContent = 'cached ' + new Date(d.updated).toLocaleDateString() + ' · eqlwiki.com — all credit to its editors';
$('gopen').onclick = () => window.overlay.openExternal(d.url);
$('grefresh').onclick = () => showGuide(payload, true, true);
}
// =========================================================================
// CLEANUP MODE — rows pushed by the overlay; quest detail fetched here
// =========================================================================
let rows = [];
// strict location sections: where the item actually sits
const SECTIONS = [
['bags', '🎒 Bags'],
['depot', '📦 Character storage'],
['bank', '🏦 Bank']
];
// verdict filter chips across all sections
const CHIPS = [
['all', 'All', ''],
['need', '✅ Needed', 'quests, epics, tradeskills, grinds or your own flags want these'],
['gear', '🛡 Gear', 'wearable equipment with no quest use'],
['unk', '❓ Unknown', 'not in any database yet — the auto-scan is on it'],
['done', '🗑 Job done', 'nothing needs these anymore — vendor or destroy at will']
];
let pileFilter = 'all';
(function buildChips() {
const box = $('chips');
for (const [key, label, hint] of CHIPS) {
const c = document.createElement('span');
c.className = 'qchip';
c.dataset.chip = key;
c.textContent = label;
if (hint) c.title = hint;
c.addEventListener('click', () => { pileFilter = key; render(); });
box.appendChild(c);
}
})();
// how many of this row's copies sit in a given section — shared bank folds
// into Bank, and the storage's gear tab (trailing Equipment section) folds
// into Character storage alongside the Personal-Depot materials
const inSection = (r, sec) => {
const by = r.by || {};
if (sec === 'bank') return (by.bank || 0) + (by.shared || 0);
if (sec === 'depot') return (by.depot || 0) + (by.stored || 0);
return by[sec] || 0;
};
// category rank for the "category" sort: what kind of reason a row has
function catOf(r) {
const s = (r.reason || '').toLowerCase();
if (s.startsWith('pos:')) return '1 PoS quest';
if (s.startsWith('epic')) return '2 epic';
if (s.startsWith('bis')) return '3 BiS';
if (r.quests && r.quests.length) return '4 wiki quest';
if (s.includes('tradeskill')) return '5 tradeskill';
if (s.includes('faction')) return '6 faction grind';
if (s.includes('exaltation')) return '7 exaltation';
if (s.includes('wish') || s.includes('keep list')) return '8 your flags';
if (s.includes('gear')) return '9 gear';
return '99 ' + s.slice(0, 12);
}
function sortRows(items) {
const by = $('sort').value;
return items.sort((a, b) =>
by === 'count' ? (b.count - a.count) || a.name.localeCompare(b.name)
: by === 'category' ? catOf(a).localeCompare(catOf(b)) || a.name.localeCompare(b.name)
: a.name.localeCompare(b.name));
}
function render() {
{ const _el = $('list'), _st = _el ? _el.scrollTop : 0; if (_st) queueMicrotask(() => { _el.scrollTop = _st; }); } // rebuilds must not snap the scroll to the top
// highlight the active verdict chip
for (const c of $('chips').children) {
const on = c.dataset.chip === pileFilter;
c.style.background = on ? 'rgba(255,215,102,.25)' : '';
c.style.color = on ? '#ffd766' : '';
}
const list = $('list');
list.innerHTML = '';
const q = $('filter').value.trim().toLowerCase();
let shown = q ? rows.filter(r => r.name.toLowerCase().includes(q) || (r.reason || '').toLowerCase().includes(q)) : rows;
if (pileFilter !== 'all') shown = shown.filter(r => r.pile === pileFilter);
if (!shown.length) {
list.innerHTML = '<div id="empty">' + (rows.length ? 'Nothing matches the filter.' :
'Waiting for the overlay…<br>Make sure you\'ve run /outputfile inventory in game.') + '</div>';
return;
}
for (const [sec, label] of SECTIONS) {
const items = shown.filter(r => inSection(r, sec) > 0);
if (!items.length) continue;
const needHere = items.filter(r => r.pile === 'need').length;
const d = document.createElement('div');
d.className = 'divider';
d.innerHTML = '<span></span> <small>· ' + items.length + ' item' + (items.length === 1 ? '' : 's') +
(needHere ? ' · ' + needHere + ' still needed' : '') + '</small>';
d.querySelector('span').textContent = label;
list.appendChild(d);
let lastCat = null;
for (const r of sortRows(items)) {
if ($('sort').value === 'category') {
const c = catOf(r).replace(/^\d+ /, '');
if (c !== lastCat) {
lastCat = c;
const ch = document.createElement('div');
ch.style.cssText = 'color:#8a7a55;font-size:10.5px;margin:4px 0 1px';
ch.textContent = '— ' + c + ' —';
list.appendChild(ch);
}
}
const row = document.createElement('div');
row.className = 'row ' + r.pile;
row.innerHTML = '<span class="junkbtn" title="Flag as junk: it drops into Job done and SEQO remembers forever">🗑</span>' +
'<span class="nm" title="Open this item’s full details"></span><span class="cnt"></span><span class="why"></span>';
row.querySelector('.nm').textContent = r.name;
const here = inSection(r, sec);
const elsewhere = (r.count || 0) - here;
const sharedOnly = sec === 'bank' && (r.by && !r.by.bank && r.by.shared);
row.querySelector('.cnt').textContent =
(here > 1 ? '×' + here : '') +
(sharedOnly ? ' (shared)' : '') +
(elsewhere > 0 ? ' · +' + elsewhere + ' elsewhere' : '');
if (elsewhere > 0) {
const whereBits = [];
const by = r.by || {};
if (by.bags) whereBits.push(by.bags + ' in bags');
if (by.depot) whereBits.push(by.depot + ' in character storage');
if (by.stored) whereBits.push(by.stored + ' in the storage gear tab');
if (by.bank) whereBits.push(by.bank + ' in bank');
if (by.shared) whereBits.push(by.shared + ' in shared bank');
row.querySelector('.cnt').title = whereBits.join(' · ');
}
const why = row.querySelector('.why');
if (r.quests && r.quests.length) {
why.textContent = 'used in: ';
for (const qn of r.quests) {
const chip = document.createElement('span');
chip.className = 'qchip';
chip.textContent = qn;
chip.title = 'Click to see what ' + qn + ' rewards, and where those rewards lead';
chip.addEventListener('click', () => showQuest(qn));
why.appendChild(chip);
}
const extra = (r.reason || '').split(' · ')[1];
if (extra) why.appendChild(document.createTextNode(' · ' + extra));
} else {
why.textContent = r.reason || '';
why.title = r.reason || '';
}
row.querySelector('.nm').addEventListener('click', () => showItem(r.name));
row.querySelector('.junkbtn').addEventListener('click', () => {
window.overlay.cleanupCmd({ kind: 'junk', name: r.name });
r.pile = 'done'; r.reason = 'on your junk list'; delete r.quests; delete r.chain;
render();
});
list.appendChild(row);
if (r.chain && r.chain.length) {
// the quest chain, right on the page: reward → what it's needed for
for (const ch of r.chain) {
const cl = document.createElement('div');
cl.style.cssText = 'margin:0 4px 2px 26px;font-size:11px;color:#ffb27d;line-height:1.4';
cl.textContent = '↳ ' + ch;
list.appendChild(cl);
}
}
}
}
}
// quest detail WITH the chain: quest → rewards → what each reward is for
async function showQuest(questName) {
const qd = $('qdetail');
qd.style.display = 'block';
qd.innerHTML = '<span class="close2" title="close">✕</span><b></b> — fetching from the wiki…';
qd.querySelector('b').textContent = questName;
qd.querySelector('.close2').addEventListener('click', () => { qd.style.display = 'none'; });
const d = await window.overlay.updateItemData(questName);
qd.innerHTML = '<span class="close2" title="close">✕</span><b></b> ';
qd.querySelector('b').textContent = questName;
qd.querySelector('.close2').addEventListener('click', () => { qd.style.display = 'none'; });
if (!d || d.error || !d.pageTitle) {
qd.appendChild(document.createTextNode('— ⚠ ' + ((d && d.error) || 'no wiki page found')));
return;
}
const rewards = d.rewards || [];
if (!rewards.length) {
qd.appendChild(document.createTextNode('— the wiki page lists no reward (click the quest name on the overlay’s Lookup tab for the full text)'));
return;
}
qd.appendChild(document.createTextNode('rewards: '));
for (const rw of rewards.slice(0, 4)) {
const line = document.createElement('div');
const s = document.createElement('span');
s.className = 'rw';
s.textContent = rw;
s.title = 'Open ' + rw + '’s full details';
s.addEventListener('click', () => showItem(rw));
line.appendChild(s);
qd.appendChild(line);
// one link deeper: what is THIS reward needed for?
(async () => {
const rd = await window.overlay.updateItemData(rw);
const uses = (rd && !rd.error && rd.questUses || []).filter(u => u.toLowerCase() !== questName.toLowerCase());
if (uses.length) {
const c = document.createElement('span');
c.className = 'chain';
c.textContent = ' → needed for: ' + uses.join(', ');
line.appendChild(c);
}
})();
}
}
window.overlay.onCleanupData((p) => {
if (p.rows) { rows = p.rows; render(); }
if (p.status !== undefined) $('status').textContent = p.status;
if (p.scanning !== undefined) $('scan').disabled = p.scanning;
});
$('scan').addEventListener('click', () => window.overlay.cleanupCmd({ kind: 'scan' }));
$('refresh').addEventListener('click', () => window.overlay.cleanupCmd({ kind: 'refresh' }));
$('filter').addEventListener('input', render);
$('sort').addEventListener('change', render);
$('copycmd').addEventListener('click', () => {
window.overlay.copyText('/outputfile inventory');
$('copied').style.display = '';
setTimeout(() => { $('copied').style.display = 'none'; }, 2000);
});
// =========================================================================
// ITEM MODE — self-driven: fetches from the same caches the overlay uses
// =========================================================================
let itemName = '', itemStats = '', wished = false, lootDB = null;
// same math as the overlay (wiki Item Upgrade System page): round DOWN,
// stats +10%/tier, DMG +5%, ACCURACY +2.5%, +1 minimum, weight -10% floor .1
const SCALE_STATS = /\b(AC|HP|MANA|STR|STA|AGI|DEX|WIS|INT|CHA|ATK|HASTE|SV (?:FIRE|COLD|MAGIC|DISEASE|POISON))(:\s*\+?)(\d+)/gi;
function scaledStats(block, T) {
if (!T) return block;
let out = block.replace(SCALE_STATS, (a, st, sep, n) => st + sep + Math.max(Math.floor(+n * (1 + 0.10 * T)), +n + T));
out = out.replace(/\b(DMG)(:\s*\+?)(\d+)/gi, (a, st, sep, n) => st + sep + Math.max(Math.floor(+n * (1 + 0.05 * T)), +n + T));
out = out.replace(/\b(ACCURACY)(:\s*\+?)(\d+)/gi, (a, st, sep, n) => st + sep + Math.floor(+n * (1 + 0.025 * T)));
out = out.replace(/\b(WT)(:\s*)([\d.]+)/gi, (a, st, sep, n) => st + sep + Math.max(0.1, +(+n * (1 - 0.10 * T)).toFixed(1)));
return out;
}
$('itier').addEventListener('input', () => {
const T = +$('itier').value;
$('itierval').textContent = '+' + T;
$('istats').textContent = scaledStats(itemStats, T);
$('ititle').textContent = itemName + (T ? ' +' + T : '');
});
const ITEM_SOURCES = [
['eqlwiki', (q) => 'https://eqlwiki.com/index.php?search=' + encodeURIComponent(q)],
['EQLBase', (q) => 'https://eqlbase.com/items/?q=' + encodeURIComponent(q)],
['EQ Legends Tools', (q) => 'https://eqlegendstools.com/?s=' + encodeURIComponent(q)],
['Loadout Legends', (q) => 'https://www.loadoutlegends.com/database?search=' + encodeURIComponent(q)],
['Gnoll Guard', (q) => 'https://www.gnollguard.com/items?search=' + encodeURIComponent(q)]
];
async function showItem(name, refetch, noPush) {
if (!noPush && mode) pushNav();
setMode('item');
itemName = (name || '').replace(/\s*\+\d+$/, '');
$('ititle').textContent = itemName;
$('iwish').textContent = '☆';
$('istats').style.display = 'none';
$('itier-row').style.display = 'none';
$('ifx').innerHTML = '';
$('idrops').innerHTML = '';
$('isect').innerHTML = '<div class="sect-b">loading…</div>';
$('isources').innerHTML = '';
if (!lootDB) lootDB = await window.overlay.getLootDB().catch(() => null);
window.overlay.cleanupCmd({ kind: 'wishstate', name: itemName }); // overlay answers via cleanup-data? simpler: ask directly below
let cached = refetch ? null : await window.overlay.getItemData(itemName);
if (!cached || cached.error) {
$('isect').innerHTML = '<div class="sect-b">Fetching from the sources… (the first ever run also caches eqlegendstools — that one can take ~30s)</div>';
cached = await window.overlay.updateItemData(itemName);
}
if (!cached || cached.error) {
$('isect').innerHTML = '';
const e = document.createElement('div'); e.className = 'sect-b';
e.textContent = '⚠ ' + ((cached && cached.error) || 'nothing found');
$('isect').appendChild(e);
return;
}
renderItem(cached);
}
function renderItem(cached) {
$('ititle').textContent = cached.pageTitle || itemName;
// stats + tier slider
if (cached.statsblock) {
itemStats = cached.statsblock;
$('istats').style.display = 'block';
$('itier-row').style.display = 'flex';
$('itier').value = 0; $('itierval').textContent = '+0';
$('istats').textContent = itemStats;
} else if (cached.mobsheet) {
$('istats').style.display = 'block';
$('istats').textContent = cached.mobsheet;
}
// effects, each explained: pull the description from its wiki page
const fxBox = $('ifx');
fxBox.innerHTML = '';
const fxNames = [...new Set([...(cached.statsblock || '').matchAll(/Effect:\s*([^\n(]+)/gi)]
.map(m => m[1].trim()).filter(Boolean))];
for (const fx of fxNames) {
const d = document.createElement('div');
d.className = 'fx';
d.innerHTML = '✨ <b title="See every item that carries this effect (opens the overlay’s Exaltations view)"></b> <span style="color:#9ab">— looking up…</span>';
d.querySelector('b').textContent = fx;
d.querySelector('b').addEventListener('click', () => window.overlay.cleanupCmd({ kind: 'exaltview', name: fx }));
fxBox.appendChild(d);
window.overlay.getEffectItems(fx).then((ef) => {
const sp = d.querySelector('span');
if (ef && !ef.error && ef.desc) sp.textContent = '— ' + ef.desc;
else if (ef && !ef.error) sp.textContent = '— (no description on the wiki; click the name to see every item with it)';
else sp.textContent = '— could not fetch the effect page';
});
}
// known loot chips (mob pages)
const dd = $('idrops');
dd.innerHTML = '';
for (const it of cached.dropsList || []) {
const c = document.createElement('span');
c.className = 'chip';
c.textContent = it;
c.addEventListener('click', () => showItem(it));
dd.appendChild(c);
}
// sections: your kills first, then wiki text, then eqlegendstools rows
const sect = $('isect');
sect.innerHTML = '';
const add = (label, body) => {
const h = document.createElement('div'); h.className = 'sect-h'; h.textContent = label;
const b = document.createElement('div'); b.className = 'sect-b'; b.textContent = body;
sect.appendChild(h); sect.appendChild(b);
};
const mobLower = (cached.pageTitle || itemName).toLowerCase();
const obs = [];
for (const m of Object.values((lootDB && lootDB.mobs) || {})) {
if ((m.name || '').toLowerCase() !== mobLower || !m.kills) continue;
for (const d2 of Object.values(m.drops || {}))
obs.push(d2.name + ' ' + d2.count + '/' + m.kills +
' (' + (d2.count / m.kills * 100).toFixed(d2.count / m.kills >= 0.1 ? 0 : 1) + '%)' +
(m.zone ? ' — ' + m.zone : ''));
}
if (obs.length) add('Your own kills', obs.join('\n'));
if (cached.text) add('eqlwiki.com', (cached.dropsfrom ? 'Drops from: ' + cached.dropsfrom + '\n\n' : '') + cached.text);
for (const t of cached.tools || []) add('eqlegendstools.com · ' + t.source, t.lines.join('\n'));
const c2 = document.createElement('div');
c2.className = 'cached';
c2.textContent = 'cached ' + new Date(cached.updated || Date.now()).toLocaleDateString();
sect.appendChild(c2);
// deep links
const src = $('isources');
src.innerHTML = '';
for (const [n, fn] of ITEM_SOURCES) {
const b = document.createElement('span');
b.className = 'chip';
b.textContent = n + ' ↗';
b.title = 'Search "' + itemName + '" on ' + n + ' (opens your browser) — community database, all credit to its maintainers';
b.addEventListener('click', () => window.overlay.openExternal(fn(itemName)));
src.appendChild(b);
}
}
$('iupdate').addEventListener('click', () => showItem(itemName, true));
$('iwish').addEventListener('click', () => {
wished = !wished;
$('iwish').textContent = wished ? '🌟' : '☆';
window.overlay.cleanupCmd({ kind: 'wish', name: itemName, on: wished });
});
// wish-state answers ride the cleanup-data channel
window.overlay.onCleanupData((p) => {
if (p.wishState && p.wishState.name === itemName) {
wished = !!p.wishState.on;
$('iwish').textContent = wished ? '🌟' : '☆';
}
});
// ---------- mode dispatch ----------
window.overlay.onDockMode((m) => {
if (!m) return;
navStack.length = 0; // fresh entry from the overlay: history restarts
$('back').style.display = 'none';
if (m.mode === 'item' && m.payload && m.payload.name) showItem(m.payload.name, false, true);
else if (m.mode === 'guide' && m.payload) showGuide(m.payload, false, true);
else if (m.mode === 'help') showHelp(true);
else { setMode('cleanup'); render(); }
});
// Large-text setting from the overlay's settings
window.overlay.onUiScale((v) => document.body.classList.toggle('large', v === 'large'));
// locked overlay = this window goes click-through too: the title bar, rows,
// chips and readable text come alive on hover (click + scroll still work
// there), everywhere else your clicks reach the game underneath
let lockedMode = false, hoverLive = false;
const HOVER_SEL = '#topbar, #chips, .row, #gtext, #gtoc, #gfoot, button, input, select, .q-it';
window.overlay.onLockState((locked) => {
lockedMode = !!locked;
hoverLive = false;
});
window.addEventListener('mousemove', (e) => {
if (!lockedMode) return;
const el = document.elementFromPoint(e.clientX, e.clientY);
const want = !!(el && el.closest && el.closest(HOVER_SEL));
if (want !== hoverLive) { hoverLive = want; window.overlay.setInteractive(want); }
});
</script>
</body>
</html>