From 394c7ebde7abb10ac1cef54d28d4bcd5a633dbc8 Mon Sep 17 00:00:00 2001 From: serversdwn Date: Wed, 25 Feb 2026 05:53:16 +0000 Subject: [PATCH] fix: quest tree cleaned up a bit, Key's ratings not saving fixed --- app.py | 141 +++++++++-- import_keys.py | 15 +- tarkov.db | Bin 3944448 -> 3993600 bytes templates/collector.html | 490 ++++++++++++++++++++++++++++++++------- templates/quests.html | 423 +++++++++++++++++++++------------ 5 files changed, 814 insertions(+), 255 deletions(-) diff --git a/app.py b/app.py index e3197b0..8bcefe5 100644 --- a/app.py +++ b/app.py @@ -8,9 +8,66 @@ DB_PATH = "tarkov.db" def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") return conn +def _migrate_key_ids_and_maps(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = OFF") + + # Backfill missing key IDs with their api_id so ratings can join correctly. + conn.execute(""" + UPDATE keys + SET id = api_id + WHERE id IS NULL AND api_id IS NOT NULL + """) + + # If key_maps was created with INTEGER key_id, migrate to TEXT to match keys.id. + cols = conn.execute("PRAGMA table_info(key_maps)").fetchall() + key_id_type = None + if cols: + for col in cols: + if col["name"] == "key_id": + key_id_type = (col["type"] or "").upper() + break + if key_id_type and key_id_type != "TEXT": + conn.execute("ALTER TABLE key_maps RENAME TO key_maps_old") + conn.execute(""" + CREATE TABLE key_maps ( + key_id TEXT NOT NULL, + map_id INTEGER NOT NULL, + PRIMARY KEY (key_id, map_id), + FOREIGN KEY (key_id) REFERENCES keys(id), + FOREIGN KEY (map_id) REFERENCES maps(id) + ) + """) + conn.execute(""" + INSERT OR IGNORE INTO key_maps (key_id, map_id) + SELECT CAST(key_id AS TEXT), map_id + FROM key_maps_old + WHERE key_id IS NOT NULL + AND EXISTS (SELECT 1 FROM keys WHERE id = CAST(key_id AS TEXT)) + """) + conn.execute("DROP TABLE key_maps_old") + + # Remove orphaned ratings created with "None" or missing keys. + conn.execute(""" + DELETE FROM key_ratings + WHERE key_id IS NULL + OR key_id = 'None' + OR key_id NOT IN (SELECT id FROM keys) + """) + + conn.commit() + conn.execute("PRAGMA foreign_keys = ON") + conn.close() + + +_migrate_key_ids_and_maps() + + @app.route("/") def index(): conn = get_db() @@ -198,6 +255,7 @@ def rate_all(): def quests(): conn = get_db() only_collector = request.args.get("collector") == "1" + view = request.args.get("view", "flow") # "flow" or "list" # All quests + done state all_quests = conn.execute(""" @@ -243,7 +301,7 @@ def quests(): # Filter to collector-only if requested if only_collector: - visible = collector_prereqs | {collector_row["id"]} + visible = set(collector_prereqs) else: visible = set(quest_by_id.keys()) @@ -270,12 +328,14 @@ def quests(): visible=visible, collector_prereqs=collector_prereqs, only_collector=only_collector, + view=view, ) @app.route("/collector") def collector(): conn = get_db() + view = request.args.get("view", "flow") collector = conn.execute( "SELECT id FROM quests WHERE name = 'Collector'" ).fetchone() @@ -284,33 +344,76 @@ def collector(): conn.close() return "Run import_quests.py first to populate quest data.", 503 - # Recursive CTE: all transitive prerequisites, then keep only leaves - # (quests that are not themselves a dependency of another prereq) - prereqs = conn.execute(""" + # All quests + done state + all_quests = conn.execute(""" + SELECT q.id, q.name, q.trader, q.wiki_link, + COALESCE(qp.done, 0) AS done + FROM quests q + LEFT JOIN quest_progress qp ON q.id = qp.quest_id + ORDER BY q.trader, q.name + """).fetchall() + + # All dependency edges + all_deps = conn.execute("SELECT quest_id, depends_on FROM quest_deps").fetchall() + + # Collector prereq set (transitive) + rows = conn.execute(""" WITH RECURSIVE deps(quest_id) AS ( SELECT depends_on FROM quest_deps WHERE quest_id = ? UNION SELECT qd.depends_on FROM quest_deps qd JOIN deps d ON qd.quest_id = d.quest_id ) - SELECT q.id, q.name, q.trader, q.wiki_link, - COALESCE(qp.done, 0) AS done - FROM quests q - JOIN deps d ON q.id = d.quest_id - LEFT JOIN quest_progress qp ON q.id = qp.quest_id - WHERE q.id NOT IN ( - SELECT qd2.depends_on - FROM quest_deps qd2 - WHERE qd2.quest_id IN (SELECT quest_id FROM deps) - AND qd2.depends_on IN (SELECT quest_id FROM deps) - ) - ORDER BY q.trader, q.name + SELECT quest_id FROM deps """, (collector["id"],)).fetchall() + collector_prereqs = {r[0] for r in rows} conn.close() - total = len(prereqs) - done = sum(1 for q in prereqs if q["done"]) - return render_template("collector.html", quests=prereqs, total=total, done=done) + + # Build lookup structures + quest_by_id = {q["id"]: q for q in all_quests} + # children[parent_id] = [child_id, ...] (child depends_on parent) + children = {} + parents = {} + for dep in all_deps: + child, parent = dep["quest_id"], dep["depends_on"] + children.setdefault(parent, []).append(child) + parents.setdefault(child, []).append(parent) + # Sort each child list by quest name + for parent_id in children: + children[parent_id].sort(key=lambda i: quest_by_id[i]["name"] if i in quest_by_id else "") + + visible = set(collector_prereqs) + + # Root quests: in visible set and have no parents also in visible set + roots = [ + qid for qid in visible + if not any(p in visible for p in parents.get(qid, [])) + ] + + # Group roots by trader, sorted + trader_roots = {} + for qid in sorted(roots, key=lambda i: (quest_by_id[i]["trader"], quest_by_id[i]["name"])): + t = quest_by_id[qid]["trader"] + trader_roots.setdefault(t, []).append(qid) + + traders = sorted(trader_roots.keys()) + + total = len(collector_prereqs) + done = sum(1 for qid in collector_prereqs if qid in quest_by_id and quest_by_id[qid]["done"]) + return render_template( + "collector.html", + quest_by_id=quest_by_id, + children=children, + trader_roots=trader_roots, + traders=traders, + visible=visible, + collector_prereqs=collector_prereqs, + collector_id=collector["id"], + total=total, + done=done, + view=view, + ) @app.route("/collector/toggle", methods=["POST"]) diff --git a/import_keys.py b/import_keys.py index 3d48aaf..8b22474 100644 --- a/import_keys.py +++ b/import_keys.py @@ -62,9 +62,7 @@ def upsert_keys(conn, keys): continue cursor.execute( - """ - SELECT id FROM keys WHERE api_id = ? - """, + "SELECT id FROM keys WHERE api_id = ?", (api_id,) ) row = cursor.fetchone() @@ -73,19 +71,20 @@ def upsert_keys(conn, keys): cursor.execute( """ UPDATE keys - SET name = ?, short_name = ?, weight_kg = ?, uses = ?, wiki_url = ?, grid_image_url = ? + SET id = COALESCE(id, ?), + name = ?, short_name = ?, weight_kg = ?, uses = ?, wiki_url = ?, grid_image_url = ? WHERE api_id = ? """, - (name, short_name, weight, uses, wiki_url, grid_image_url, api_id) + (api_id, name, short_name, weight, uses, wiki_url, grid_image_url, api_id) ) updated += 1 else: cursor.execute( """ - INSERT INTO keys (api_id, name, short_name, weight_kg, uses, wiki_url, grid_image_url) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO keys (id, api_id, name, short_name, weight_kg, uses, wiki_url, grid_image_url) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, - (api_id, name, short_name, weight, uses, wiki_url, grid_image_url) + (api_id, api_id, name, short_name, weight, uses, wiki_url, grid_image_url) ) inserted += 1 diff --git a/tarkov.db b/tarkov.db index 3e8aef5690f360a882ee91ed2ad3866b7aa06d95..7d360cef148ae251b50391d4b644016307842c54 100644 GIT binary patch delta 25721 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 diff --git a/templates/collector.html b/templates/collector.html index d7146f0..227ae9c 100644 --- a/templates/collector.html +++ b/templates/collector.html @@ -1,3 +1,62 @@ +{# ══════════════════════════════════════════════ + MACROS — must come before first use in Jinja2 +══════════════════════════════════════════════ #} + +{# 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, collector_id) %} +{% 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 %} +
+
+ {% for open in open_stack %} +
+ {% endfor %} + {% if open_stack %} +
+ {% endif %} +
+
+ {% if qid in collector_prereqs %}{% endif %} + {{ q.name }} + {% if q.wiki_link %}wiki{% endif %} + {% if qid != collector_id %}{% endif %} +
+
+{% 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 %} +
+
+ {% for open in child_stack %} +
+ {% endfor %} +
+
+
+ {% if cid in collector_prereqs %}{% endif %} + {{ child.name }} + {% if child.wiki_link %}wiki{% endif %} + {{ child.trader }} + {% if cid != collector_id %}{% endif %} +
+
+ {% else %} + {{ render_list_item(cid, quest_by_id, children, visible, collector_prereqs, child_stack, child_last, collector_id) }} + {% endif %} +{% endfor %} +{% endmacro %} + @@ -8,12 +67,15 @@ --bg: #121212; --panel: #1a1a1a; --text: #eee; - --muted: #bbb; - --border: #333; + --muted: #888; + --border: #2a2a2a; --accent: #9ccfff; - --done-bg: #1a2a1a; --done-text: #6ec96e; + --done-bg: #1a2a1a; + --kappa: #f0c040; + --line: #333; } + * { box-sizing: border-box; } body { font-family: sans-serif; background: var(--bg); @@ -21,11 +83,29 @@ margin: 0; padding: 16px; } - .page { - max-width: 780px; - margin: 0 auto; + .page { max-width: 960px; margin: 0 auto; } + nav { margin-bottom: 16px; font-size: 0.9rem; } + nav a { color: var(--accent); } + h1 { margin: 0 0 4px; } + .toolbar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 12px; } - h1 { margin-bottom: 4px; } + .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); } .subtitle { color: var(--muted); margin: 0 0 16px; @@ -45,58 +125,166 @@ border-radius: 999px; transition: width 0.3s; } - .trader-group { margin-bottom: 8px; } - .trader-header { + .legend { + display: flex; + gap: 16px; font-size: 0.8rem; - font-weight: bold; - text-transform: uppercase; - letter-spacing: 0.08em; color: var(--muted); - padding: 12px 0 4px; - border-bottom: 1px solid var(--border); - margin-bottom: 4px; + flex-wrap: wrap; + margin-bottom: 16px; } - .quest-row { + .legend span { display: flex; align-items: center; gap: 5px; } + + /* Trader section */ + .trader-section { margin-bottom: 8px; } + .trader-header { display: flex; align-items: center; - gap: 10px; - padding: 8px 4px; - border-bottom: 1px solid #222; - border-radius: 4px; + gap: 8px; + padding: 10px 8px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + user-select: none; } - .quest-row.done { - background: var(--done-bg); - } - .quest-row.done .quest-name { - text-decoration: line-through; - color: var(--done-text); - } - .quest-name { - flex: 1; + .trader-header:hover { border-color: #444; } + .trader-name { + font-weight: bold; font-size: 0.95rem; + flex: 1; } - .quest-name a { - color: var(--accent); - font-size: 0.8rem; - margin-left: 6px; + .trader-counts { font-size: 0.8rem; color: var(--muted); } + .chevron { color: var(--muted); font-size: 0.8rem; transition: transform 0.15s; } + .trader-section.collapsed .chevron { transform: rotate(-90deg); } + .trader-body { padding: 6px 0 6px 8px; } + .trader-section.collapsed .trader-body { display: none; } + + /* Flow tree */ + .tree-root { + margin: 6px 0; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + } + .tree-children { + display: flex; + gap: 14px; + align-items: flex-start; + justify-content: center; + position: relative; + padding-top: 18px; + flex-wrap: wrap; + } + .tree-children:before { + content: ""; + position: absolute; + top: 8px; + left: 8px; + right: 8px; + height: 1px; + background: var(--border); + opacity: 0.9; + } + .tree-children > .tree-root { + padding-top: 8px; + } + .tree-children > .tree-root:before { + content: ""; + position: absolute; + top: 0; + left: 50%; + width: 1px; + height: 8px; + background: var(--border); + } + .quest-node { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 6px; + border-radius: 4px; + margin: 2px 0; + position: relative; + background: #141820; + border: 1px solid #1b2230; + } + .quest-node.has-children:after { + content: ""; + position: absolute; + left: 50%; + bottom: -8px; + width: 1px; + height: 8px; + background: var(--border); + } + .quest-node:hover { background: #1e1e1e; } + .quest-node.done .quest-label { text-decoration: line-through; color: var(--done-text); } + .quest-node.done { background: var(--done-bg); } + .quest-label { flex: 1; font-size: 0.9rem; } + .quest-label a { color: var(--accent); font-size: 0.75rem; margin-left: 6px; } + .kappa-star { color: var(--kappa); font-size: 0.75rem; flex-shrink: 0; } + .cross-trader { + font-size: 0.75rem; + color: var(--muted); + font-style: italic; + flex-shrink: 0; } .toggle-btn { - background: #2a2a2a; - color: var(--text); + background: transparent; border: 1px solid #444; - border-radius: 6px; - padding: 4px 10px; + color: var(--muted); + border-radius: 4px; + padding: 2px 7px; cursor: pointer; - font-size: 0.85rem; - white-space: nowrap; + font-size: 0.75rem; + flex-shrink: 0; } - .quest-row.done .toggle-btn { - background: #1e3a1e; + .quest-node.done .toggle-btn { border-color: #3a6a3a; color: var(--done-text); } - nav { margin-bottom: 20px; } - nav a { color: var(--accent); font-size: 0.9rem; } + + /* ── 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); + } + .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; } @@ -117,30 +305,131 @@
- {% set ns = namespace(current_trader=None) %} - {% for quest in quests %} - {% if quest.trader != ns.current_trader %} - {% if ns.current_trader is not none %}{% endif %} -
-
{{ quest.trader }}
- {% set ns.current_trader = quest.trader %} - {% endif %} +
+ Flow + List + | + + +
-
- - {{ quest.name }} - {% if quest.wiki_link %} - wiki - {% endif %} - +
+ Required for Collector + Marked done + ← Trader Cross-trader dependency +
+ +{% macro render_node(qid, quest_by_id, children, visible, collector_prereqs, collector_id, is_root=False) %} +{% 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 qid in collector_prereqs %}{% endif %} + + {{ q.name }} + {% if q.wiki_link %}wiki{% endif %} + + {% if qid != collector_id %} + {% endif %} +
+ {% if visible_kids %} +
+ {% for cid in visible_kids %} + {% set child = quest_by_id[cid] %} + {% if child.trader != q.trader %} +
+ {% if cid in collector_prereqs %}{% endif %} + + {{ child.name }} + {% if child.wiki_link %}wiki{% endif %} + + ← {{ child.trader }} + {% if cid != collector_id %} + + {% endif %} +
+ {% else %} + {{ render_node(cid, quest_by_id, children, visible, collector_prereqs, collector_id, false) }} + {% endif %} + {% endfor %} +
+ {% endif %} +
+{% endmacro %} + + {# ── FLOW VIEW ── #} +
+ {% for trader in traders %} + {% set roots = trader_roots[trader] %} + {% set total_trader = namespace(n=0) %} + {% set done_trader = namespace(n=0) %} + {% for qid in visible %} + {% if quest_by_id[qid].trader == trader and qid in collector_prereqs %} + {% set total_trader.n = total_trader.n + 1 %} + {% if quest_by_id[qid].done %}{% set done_trader.n = done_trader.n + 1 %}{% endif %} + {% endif %} + {% endfor %} + +
+
+ {{ trader }} + {{ done_trader.n }} / {{ total_trader.n }} + +
+
+ {% for root_id in roots %} + {{ render_node(root_id, quest_by_id, children, visible, collector_prereqs, collector_id, true) }} + {% endfor %} +
{% endfor %} - {% if ns.current_trader is not none %}
{% endif %} +
+ + {# ── LIST VIEW ── #} +
+ {% for trader in traders %} + {% set roots = trader_roots[trader] %} + {% set total_trader = namespace(n=0) %} + {% set done_trader = namespace(n=0) %} + {% for qid in visible %} + {% if quest_by_id[qid].trader == trader and qid in collector_prereqs %} + {% set total_trader.n = total_trader.n + 1 %} + {% if quest_by_id[qid].done %}{% set done_trader.n = done_trader.n + 1 %}{% endif %} + {% endif %} + {% endfor %} + +
+
+ {{ trader }} + {{ done_trader.n }} / {{ total_trader.n }} + +
+
+
+ {% for root_id in roots %} + {{ render_list_item(root_id, quest_by_id, children, visible, collector_prereqs, [], loop.last, collector_id) }} + {% endfor %} +
+
+
+ {% endfor %} +
+ diff --git a/templates/quests.html b/templates/quests.html index 9eaba34..15af0bb 100644 --- a/templates/quests.html +++ b/templates/quests.html @@ -1,3 +1,102 @@ +{# ══════════════════════════════════════════════ + 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 %}
{% endif %} +
+
+
+ {% if qid in collector_prereqs %}{% endif %} + {{ q.name }} + {% if q.wiki_link %}wiki{% endif %} + +
+
+
+{% for cid in visible_kids %} + {% set child = quest_by_id[cid] %} + {% if child.trader != q.trader %} +
+
+
+
+ {% if cid in collector_prereqs %}{% endif %} + {{ child.name }} + {% if child.wiki_link %}wiki{% endif %} + +
+
{{ child.trader }}
+
+
+ {% 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 %} +
+
+ {% for open in open_stack %} +
+ {% endfor %} + {% if open_stack %} +
+ {% endif %} +
+
+ {% if qid in collector_prereqs %}{% endif %} + {{ q.name }} + {% if q.wiki_link %}wiki{% endif %} + +
+
+{% 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 %} +
+
+ {% for open in child_stack %} +
+ {% endfor %} +
+
+
+ {% if cid in collector_prereqs %}{% endif %} + {{ child.name }} + {% if child.wiki_link %}wiki{% endif %} + {{ child.trader }} + +
+
+ {% else %} + {{ render_list_item(cid, quest_by_id, children, visible, collector_prereqs, child_stack, child_last) }} + {% endif %} +{% endfor %} +{% endmacro %} + @@ -14,115 +113,115 @@ --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: 960px; margin: 0 auto; } + 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 { - display: flex; - align-items: center; - gap: 12px; - flex-wrap: wrap; - margin-bottom: 20px; - } - .filter-btn { - background: var(--panel); - border: 1px solid #444; - color: var(--text); - border-radius: 6px; - padding: 6px 14px; - cursor: pointer; - font-size: 0.9rem; - text-decoration: none; - } - .filter-btn.active { - border-color: var(--kappa); - color: var(--kappa); - } - .legend { - display: flex; - gap: 16px; - font-size: 0.8rem; - color: var(--muted); - flex-wrap: wrap; - } - .legend span { display: flex; align-items: center; gap: 5px; } - /* Trader section */ + /* 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: 10px 8px; - background: var(--panel); - border: 1px solid var(--border); - border-radius: 6px; - cursor: pointer; - user-select: none; + 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.95rem; - flex: 1; - } + .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.8rem; transition: transform 0.15s; } + .chevron { color: var(--muted); font-size: 0.75rem; transition: transform 0.15s; } .trader-section.collapsed .chevron { transform: rotate(-90deg); } - .trader-body { padding: 6px 0 6px 8px; } + .trader-body { padding: 4px 0; } .trader-section.collapsed .trader-body { display: none; } - /* Tree nodes */ - .tree-root { margin: 4px 0; } - .tree-children { - margin-left: 20px; - border-left: 1px solid var(--border); - padding-left: 10px; + /* ── 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; } - .quest-node { - display: flex; - align-items: center; - gap: 8px; - padding: 5px 6px; - border-radius: 4px; - margin: 2px 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; } - .quest-node:hover { background: #1e1e1e; } - .quest-node.done .quest-label { text-decoration: line-through; color: var(--done-text); } - .quest-node.done { background: var(--done-bg); } - .quest-label { flex: 1; font-size: 0.9rem; } - .quest-label a { color: var(--accent); font-size: 0.75rem; margin-left: 6px; } - .kappa-star { color: var(--kappa); font-size: 0.75rem; flex-shrink: 0; } - .cross-trader { - font-size: 0.75rem; - color: var(--muted); - font-style: italic; - flex-shrink: 0; + .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: 4px; - padding: 2px 7px; - cursor: pointer; - font-size: 0.75rem; - flex-shrink: 0; + 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; } - .quest-node.done .toggle-btn { - border-color: #3a6a3a; - color: var(--done-text); + .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; } @@ -131,109 +230,135 @@ ← Keys  |  Collector Checklist +  |  + Loadout Planner

Quest Trees

- All quests - ★ Collector only + All quests + ★ Collector only + + | + + Flow + List + + | + + + +
- Required for Collector - Marked done - ← Trader Cross-trader dependency + Collector req + Done + cross Other trader
-{% macro render_node(qid, quest_by_id, children, visible, collector_prereqs) %} -{% 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 qid in collector_prereqs %}{% endif %} - - {{ q.name }} - {% if q.wiki_link %}wiki{% endif %} - - -
- {% if visible_kids %} -
- {% for cid in visible_kids %} - {% set child = quest_by_id[cid] %} - {% if child.trader != q.trader %} -
- {% if cid in collector_prereqs %}{% endif %} - - {{ child.name }} - {% if child.wiki_link %}wiki{% endif %} - - ← {{ child.trader }} - -
- {% else %} - {{ render_node(cid, quest_by_id, children, visible, collector_prereqs) }} - {% endif %} - {% endfor %} -
- {% endif %} -
-{% endmacro %} - + {# ── FLOW VIEW ── #} +
{% for trader in traders %} {% set roots = trader_roots[trader] %} - {% set total_trader = namespace(n=0) %} - {% set done_trader = namespace(n=0) %} - {# count visible quests for this 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_trader.n = total_trader.n + 1 %} - {% if quest_by_id[qid].done %}{% set done_trader.n = done_trader.n + 1 %}{% endif %} + {% 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 %} - -
+
{{ trader }} - {{ done_trader.n }} / {{ total_trader.n }} + {{ done_t.n }} / {{ total_t.n }}
{% for root_id in roots %} - {{ render_node(root_id, quest_by_id, children, visible, collector_prereqs) }} + {{ render_chain(root_id, quest_by_id, children, visible, collector_prereqs, 0) }} {% endfor %}
{% endfor %} +
+ + {# ── LIST VIEW ── #} +
+ {% 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 %} +
+
+ {{ trader }} + {{ done_t.n }} / {{ total_t.n }} + +
+
+
+ {% for root_id in roots %} + {{ render_list_item(root_id, quest_by_id, children, visible, collector_prereqs, [], loop.last) }} + {% endfor %} +
+
+
+ {% endfor %} +