Files
onlyscavs/templates/quests.html
2026-03-01 21:49:32 +00:00

372 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# ══════════════════════════════════════════════
MACROS — must come before first use in Jinja2
══════════════════════════════════════════════ #}
{# Flow view: depth-indented vertical chain with connector lines #}
{% macro render_chain(qid, quest_by_id, children, visible, collector_prereqs, depth) %}
{% set q = quest_by_id[qid] %}
{% set visible_kids = [] %}
{% for cid in children.get(qid, []) %}
{% if cid in visible %}{% set _ = visible_kids.append(cid) %}{% endif %}
{% endfor %}
{% if depth > 0 %}<div class="fnode-connector" style="margin-left:{{ depth * 22 + 10 }}px"></div>{% endif %}
<div class="fnode-wrap" style="padding-left:{{ depth * 22 }}px">
<div class="fnode{% if q.done %} done{% endif %}{% if qid in collector_prereqs %} kappa-node{% endif %}"
data-id="{{ qid }}" data-done="{{ '1' if q.done else '0' }}">
<div class="fnode-top">
{% if qid in collector_prereqs %}<span class="kappa-star" title="Collector req"></span>{% endif %}
<span class="fnode-name">{{ q.name }}</span>
{% if q.wiki_link %}<a class="fnode-wiki" href="{{ q.wiki_link }}" target="_blank">wiki</a>{% endif %}
<button class="toggle-btn" onclick="toggle(this)">{{ '✓' if q.done else '○' }}</button>
</div>
</div>
</div>
{% for cid in visible_kids %}
{% set child = quest_by_id[cid] %}
{% if child.trader != q.trader %}
<div class="fnode-connector" style="margin-left:{{ (depth+1)*22 + 10 }}px"></div>
<div class="fnode-wrap" style="padding-left:{{ (depth+1)*22 }}px">
<div class="fnode{% if child.done %} done{% endif %}{% if cid in collector_prereqs %} kappa-node{% endif %}"
data-id="{{ cid }}" data-done="{{ '1' if child.done else '0' }}">
<div class="fnode-top">
{% if cid in collector_prereqs %}<span class="kappa-star"></span>{% endif %}
<span class="fnode-name">{{ child.name }}</span>
{% if child.wiki_link %}<a class="fnode-wiki" href="{{ child.wiki_link }}" target="_blank">wiki</a>{% endif %}
<button class="toggle-btn" onclick="toggle(this)">{{ '✓' if child.done else '○' }}</button>
</div>
<div class="fnode-meta"><span class="cross-badge">{{ child.trader }}</span></div>
</div>
</div>
{% else %}
{{ render_chain(cid, quest_by_id, children, visible, collector_prereqs, depth + 1) }}
{% endif %}
{% endfor %}
{% endmacro %}
{# List view: indented tree with ├── / └── connector lines.
open_stack: list of booleans — True = ancestor has more siblings (draw vert), False = last (draw blank).
is_last: whether this node is the last sibling among its parent's children. #}
{% macro render_list_item(qid, quest_by_id, children, visible, collector_prereqs, open_stack, is_last) %}
{% set q = quest_by_id[qid] %}
{% set visible_kids = [] %}
{% for cid in children.get(qid, []) %}
{% if cid in visible %}{% set _ = visible_kids.append(cid) %}{% endif %}
{% endfor %}
<div class="list-item">
<div class="list-indent">
{% for open in open_stack %}
<div class="list-indent-seg {{ 'vert' if open else 'blank' }}"></div>
{% endfor %}
{% if open_stack %}
<div class="list-indent-seg {{ 'elbow' if is_last else 'tee' }}"></div>
{% endif %}
</div>
<div class="list-row{% if q.done %} done{% endif %}"
data-id="{{ qid }}" data-done="{{ '1' if q.done else '0' }}">
{% if qid in collector_prereqs %}<span class="kappa-star"></span>{% endif %}
<span class="list-name">{{ q.name }}</span>
{% if q.wiki_link %}<a class="list-wiki" href="{{ q.wiki_link }}" target="_blank">wiki</a>{% endif %}
<button class="toggle-btn" onclick="toggle(this)">{{ '✓' if q.done else '○' }}</button>
</div>
</div>
{% set child_stack = open_stack + [not is_last] %}
{% for cid in visible_kids %}
{% set child = quest_by_id[cid] %}
{% set child_last = loop.last %}
{% if child.trader != q.trader %}
<div class="list-item">
<div class="list-indent">
{% for open in child_stack %}
<div class="list-indent-seg {{ 'vert' if open else 'blank' }}"></div>
{% endfor %}
<div class="list-indent-seg {{ 'elbow' if child_last else 'tee' }}"></div>
</div>
<div class="list-row{% if child.done %} done{% endif %}"
data-id="{{ cid }}" data-done="{{ '1' if child.done else '0' }}">
{% if cid in collector_prereqs %}<span class="kappa-star"></span>{% endif %}
<span class="list-name">{{ child.name }}</span>
{% if child.wiki_link %}<a class="list-wiki" href="{{ child.wiki_link }}" target="_blank">wiki</a>{% endif %}
<span class="cross-badge">{{ child.trader }}</span>
<button class="toggle-btn" onclick="toggle(this)">{{ '✓' if child.done else '○' }}</button>
</div>
</div>
{% else %}
{{ render_list_item(cid, quest_by_id, children, visible, collector_prereqs, child_stack, child_last) }}
{% endif %}
{% endfor %}
{% endmacro %}
<!doctype html>
<html>
<head>
<title>OnlyScavs Quest Trees</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--bg: #121212;
--panel: #1a1a1a;
--text: #eee;
--muted: #888;
--border: #2a2a2a;
--accent: #9ccfff;
--done-text: #6ec96e;
--done-bg: #1a2a1a;
--kappa: #f0c040;
--line: #333;
}
* { box-sizing: border-box; }
body { font-family: sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 16px; }
.page { max-width: 1100px; margin: 0 auto; }
nav { margin-bottom: 16px; font-size: 0.9rem; }
nav a { color: var(--accent); }
h1 { margin: 0 0 4px; }
/* toolbar */
.toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
.filter-btn {
background: var(--panel); border: 1px solid #444; color: var(--text);
border-radius: 6px; padding: 5px 14px; cursor: pointer; font-size: 0.85rem; text-decoration: none;
}
.filter-btn.active { border-color: var(--kappa); color: var(--kappa); }
.sep { color: var(--border); }
.legend { display: flex; gap: 14px; font-size: 0.78rem; color: var(--muted); flex-wrap: wrap; margin-left: auto; }
.legend span { display: flex; align-items: center; gap: 4px; }
/* trader sections */
.trader-section { margin-bottom: 8px; }
.trader-header {
display: flex; align-items: center; gap: 8px; padding: 9px 10px;
background: var(--panel); border: 1px solid var(--border); border-radius: 6px;
cursor: pointer; user-select: none;
}
.trader-header:hover { border-color: #444; }
.trader-name { font-weight: bold; font-size: 0.9rem; flex: 1; }
.trader-counts { font-size: 0.8rem; color: var(--muted); }
.chevron { color: var(--muted); font-size: 0.75rem; transition: transform 0.15s; }
.trader-section.collapsed .chevron { transform: rotate(-90deg); }
.trader-body { padding: 4px 0; }
.trader-section.collapsed .trader-body { display: none; }
/* ── FLOW VIEW ──
Quests rendered as a depth-indented vertical chain.
Parent → children flow top-to-bottom with indent + short vert connector. */
.flow-view .trader-body { padding: 8px 0 4px; }
.fnode-connector {
width: 1px; height: 10px; background: var(--line); flex-shrink: 0;
}
.fnode-wrap { display: flex; flex-direction: column; align-items: flex-start; width: 100%; }
.fnode {
width: calc(100% - 8px); margin: 0 4px; padding: 5px 8px;
border-radius: 5px; background: #141820; border: 1px solid #1e2535;
}
.fnode:hover { background: #1c2030; }
.fnode.done { background: var(--done-bg); border-color: #2a4a2a; }
.fnode.kappa-node { border-color: #5a4a10; }
.fnode-top { display: flex; align-items: center; gap: 5px; min-height: 20px; }
.fnode-name { font-size: 0.83rem; flex: 1; line-height: 1.3; word-break: break-word; }
.fnode.done .fnode-name { text-decoration: line-through; color: var(--done-text); }
.fnode-wiki { color: var(--accent); font-size: 0.7rem; text-decoration: none; flex-shrink: 0; }
.fnode-wiki:hover { text-decoration: underline; }
.fnode-meta { display: flex; align-items: center; gap: 5px; margin-top: 3px; }
.kappa-star { color: var(--kappa); font-size: 0.7rem; flex-shrink: 0; }
.cross-badge {
font-size: 0.65rem; color: var(--muted); font-style: italic;
background: #222; border-radius: 3px; padding: 1px 4px;
}
.toggle-btn {
background: transparent; border: 1px solid #444; color: var(--muted);
border-radius: 3px; padding: 1px 5px; cursor: pointer; font-size: 0.7rem;
flex-shrink: 0; margin-left: auto;
}
.fnode.done .toggle-btn { border-color: #3a6a3a; color: var(--done-text); }
/* ── LIST VIEW ──
Classic file-manager tree: ├── and └── connectors. */
.list-view .trader-body { padding: 4px 0; }
.list-tree { padding: 0; margin: 0; }
.list-item { display: flex; align-items: flex-start; }
.list-indent { display: flex; flex-shrink: 0; }
.list-indent-seg { width: 20px; flex-shrink: 0; position: relative; min-height: 30px; }
.list-indent-seg.vert::before {
content: ""; position: absolute; top: 0; bottom: 0; left: 9px; width: 1px; background: var(--line);
}
.list-indent-seg.tee::before {
content: ""; position: absolute; top: 0; bottom: 0; left: 9px; width: 1px; background: var(--line);
}
.list-indent-seg.tee::after {
content: ""; position: absolute; top: 50%; left: 9px; width: 11px; height: 1px; background: var(--line);
}
.list-indent-seg.elbow::before {
content: ""; position: absolute; top: 0; bottom: 50%; left: 9px; width: 1px; background: var(--line);
}
.list-indent-seg.elbow::after {
content: ""; position: absolute; top: 50%; left: 9px; width: 11px; height: 1px; background: var(--line);
}
/* blank: spacer only, no line */
.list-indent-seg.blank {}
.list-row {
flex: 1; display: flex; align-items: center; gap: 6px;
padding: 4px 6px 4px 2px; border-radius: 4px; margin: 1px 0;
background: transparent; min-height: 30px;
}
.list-row:hover { background: #1a1a1a; }
.list-row.done .list-name { text-decoration: line-through; color: var(--done-text); }
.list-name { font-size: 0.85rem; flex: 1; }
.list-wiki { color: var(--accent); font-size: 0.72rem; text-decoration: none; flex-shrink: 0; }
.list-wiki:hover { text-decoration: underline; }
.list-row .kappa-star { font-size: 0.72rem; }
.list-row .cross-badge { font-size: 0.7rem; }
.list-row .toggle-btn { font-size: 0.72rem; }
.list-row.done .toggle-btn { border-color: #3a6a3a; color: var(--done-text); }
.flow-view.hidden, .list-view.hidden { display: none; }
</style>
</head>
<body>
<div class="page">
<nav>
<a href="/">← Keys</a>
&nbsp;|&nbsp;
<a href="/collector">Collector Checklist</a>
&nbsp;|&nbsp;
<a href="/loadout">Loadout Planner</a>
&nbsp;|&nbsp;
<a href="/meds">Injectors</a>
</nav>
<h1>Quest Trees</h1>
<div class="toolbar">
<a class="filter-btn {% if not only_collector %}active{% endif %}"
href="/quests?view={{ view }}">All quests</a>
<a class="filter-btn {% if only_collector %}active{% endif %}"
href="/quests?collector=1&view={{ view }}">★ Collector only</a>
<span class="sep">|</span>
<a class="filter-btn {% if view != 'list' %}active{% endif %}"
href="/quests?{% if only_collector %}collector=1&{% endif %}view=flow">Flow</a>
<a class="filter-btn {% if view == 'list' %}active{% endif %}"
href="/quests?{% if only_collector %}collector=1&{% endif %}view=list">List</a>
<span class="sep">|</span>
<button class="filter-btn" type="button" onclick="setAllTradersCollapsed(true)">Collapse all</button>
<button class="filter-btn" type="button" onclick="setAllTradersCollapsed(false)">Expand all</button>
<div class="legend">
<span><span style="color:var(--kappa)"></span> Collector req</span>
<span><span style="color:var(--done-text)"></span> Done</span>
<span><span style="color:var(--muted);font-style:italic">cross</span> Other trader</span>
</div>
</div>
{# ── FLOW VIEW ── #}
<div class="flow-view {% if view == 'list' %}hidden{% endif %}">
{% for trader in traders %}
{% set roots = trader_roots[trader] %}
{% set total_t = namespace(n=0) %}
{% set done_t = namespace(n=0) %}
{% for qid in visible %}
{% if quest_by_id[qid].trader == trader %}
{% set total_t.n = total_t.n + 1 %}
{% if quest_by_id[qid].done %}{% set done_t.n = done_t.n + 1 %}{% endif %}
{% endif %}
{% endfor %}
<div class="trader-section" id="flow-{{ trader | replace(' ', '-') }}">
<div class="trader-header" onclick="toggleTrader(this)">
<span class="trader-name">{{ trader }}</span>
<span class="trader-counts">{{ done_t.n }} / {{ total_t.n }}</span>
<span class="chevron"></span>
</div>
<div class="trader-body">
{% for root_id in roots %}
{{ render_chain(root_id, quest_by_id, children, visible, collector_prereqs, 0) }}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{# ── LIST VIEW ── #}
<div class="list-view {% if view != 'list' %}hidden{% endif %}">
{% for trader in traders %}
{% set roots = trader_roots[trader] %}
{% set total_t = namespace(n=0) %}
{% set done_t = namespace(n=0) %}
{% for qid in visible %}
{% if quest_by_id[qid].trader == trader %}
{% set total_t.n = total_t.n + 1 %}
{% if quest_by_id[qid].done %}{% set done_t.n = done_t.n + 1 %}{% endif %}
{% endif %}
{% endfor %}
<div class="trader-section" id="list-{{ trader | replace(' ', '-') }}">
<div class="trader-header" onclick="toggleTrader(this)">
<span class="trader-name">{{ trader }}</span>
<span class="trader-counts">{{ done_t.n }} / {{ total_t.n }}</span>
<span class="chevron"></span>
</div>
<div class="trader-body">
<div class="list-tree">
{% for root_id in roots %}
{{ render_list_item(root_id, quest_by_id, children, visible, collector_prereqs, [], loop.last) }}
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
function toggleTrader(header) {
header.closest('.trader-section').classList.toggle('collapsed');
persistCollapsed();
}
function setAllTradersCollapsed(collapsed) {
document.querySelectorAll('.trader-section').forEach(s => s.classList.toggle('collapsed', collapsed));
persistCollapsed();
}
const COLLAPSE_KEY = 'quests.collapsedTraders2';
function persistCollapsed() {
const ids = Array.from(document.querySelectorAll('.trader-section.collapsed')).map(s => s.id);
localStorage.setItem(COLLAPSE_KEY, JSON.stringify(ids));
}
(function restoreCollapsed() {
try {
JSON.parse(localStorage.getItem(COLLAPSE_KEY) || '[]').forEach(id => {
const s = document.getElementById(id);
if (s) s.classList.add('collapsed');
});
} catch(e) {}
})();
function toggle(btn) {
const node = btn.closest('[data-id]');
const id = node.dataset.id;
const nowDone = node.dataset.done === '1' ? 0 : 1;
fetch('/collector/toggle', {
method: 'POST',
body: new URLSearchParams({ quest_id: id, done: nowDone })
})
.then(r => r.json())
.then(() => {
document.querySelectorAll(`[data-id="${id}"]`).forEach(n => {
n.dataset.done = nowDone;
const b = n.querySelector('.toggle-btn');
if (!b) return;
if (nowDone) { n.classList.add('done'); b.textContent = '✓'; }
else { n.classList.remove('done'); b.textContent = '○'; }
});
});
}
</script>
</body>
</html>