from flask import Flask, render_template, request, redirect, url_for, jsonify import sqlite3 app = Flask(__name__) 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 landing(): return render_template("landing.html") def _keys_context(): conn = get_db() maps = conn.execute("SELECT id, name FROM maps ORDER BY name").fetchall() map_filter = request.args.get("map_id", type=int) sort = request.args.get("sort", "priority_desc") show = request.args.get("show", "all") key_map_rows = conn.execute("SELECT key_id, map_id FROM key_maps").fetchall() key_maps = {} for row in key_map_rows: key_maps.setdefault(row["key_id"], set()).add(row["map_id"]) key_query = """ SELECT k.id, k.name, k.icon_url, k.grid_image_url, k.wiki_url, r.priority, r.reason, COALESCE(r.used_in_quest, 0) AS used_in_quest FROM keys k """ params = [] if map_filter: key_query += """ JOIN key_maps kmf ON k.id = kmf.key_id AND kmf.map_id = ? """ params.append(map_filter) key_query += " LEFT JOIN key_ratings r ON k.id = r.key_id " if show == "rated": key_query += " WHERE r.priority IS NOT NULL " elif show == "unrated": key_query += " WHERE r.priority IS NULL " elif show == "quest": key_query += " WHERE COALESCE(r.used_in_quest, 0) = 1 " if sort == "name_asc": order_by = "k.name ASC" elif sort == "name_desc": order_by = "k.name DESC" elif sort == "priority_asc": order_by = "CASE WHEN r.priority IS NULL THEN 1 ELSE 0 END, r.priority ASC, k.name" else: order_by = "CASE WHEN r.priority IS NULL THEN 1 ELSE 0 END, r.priority DESC, k.name" key_query += f" ORDER BY {order_by} " keys = conn.execute(key_query, params).fetchall() conn.close() key_maps = {k: sorted(v) for k, v in key_maps.items()} return dict(keys=keys, maps=maps, key_maps=key_maps, map_filter=map_filter, sort=sort, show=show) @app.route("/keys") def keys_page(): return render_template("keys.html", **_keys_context()) @app.route("/rate", methods=["POST"]) def rate_key(): key_id = request.form["key_id"] priority = request.form.get("priority") if priority == "": priority = None reason = request.form.get("reason", "") used_in_quest = 1 if request.form.get("used_in_quest") == "on" else 0 map_filter = request.form.get("map_id") sort = request.form.get("sort") show = request.form.get("show") map_ids = [] for value in request.form.getlist("map_ids"): try: map_ids.append(int(value)) except ValueError: continue conn = get_db() conn.execute(""" INSERT INTO key_ratings (key_id, priority, reason, used_in_quest) VALUES (?, ?, ?, ?) ON CONFLICT(key_id) DO UPDATE SET priority = excluded.priority, reason = excluded.reason, used_in_quest = excluded.used_in_quest, updated_at = CURRENT_TIMESTAMP """, (key_id, priority, reason, used_in_quest)) conn.execute("DELETE FROM key_maps WHERE key_id = ?", (key_id,)) if map_ids: conn.executemany( "INSERT OR IGNORE INTO key_maps (key_id, map_id) VALUES (?, ?)", [(key_id, map_id) for map_id in map_ids], ) conn.commit() conn.close() redirect_args = {} if map_filter: redirect_args["map_id"] = map_filter if sort: redirect_args["sort"] = sort if show: redirect_args["show"] = show base_url = url_for("keys_page", **redirect_args) return redirect(f"{base_url}#key-{key_id}") def _update_key(conn, key_id, priority, reason, used_in_quest, map_ids): conn.execute(""" INSERT INTO key_ratings (key_id, priority, reason, used_in_quest) VALUES (?, ?, ?, ?) ON CONFLICT(key_id) DO UPDATE SET priority = excluded.priority, reason = excluded.reason, used_in_quest = excluded.used_in_quest, updated_at = CURRENT_TIMESTAMP """, (key_id, priority, reason, used_in_quest)) conn.execute("DELETE FROM key_maps WHERE key_id = ?", (key_id,)) if map_ids: conn.executemany( "INSERT OR IGNORE INTO key_maps (key_id, map_id) VALUES (?, ?)", [(key_id, map_id) for map_id in map_ids], ) @app.route("/rate_all", methods=["POST"]) def rate_all(): key_ids = request.form.getlist("key_ids") save_one = request.form.get("save_one") map_filter = request.form.get("map_id") sort = request.form.get("sort") show = request.form.get("show") if save_one: key_ids = [save_one] conn = get_db() for key_id in key_ids: priority = request.form.get(f"priority_{key_id}") if priority is None: continue if priority == "": priority = None reason = request.form.get(f"reason_{key_id}", "") used_in_quest = 1 if request.form.get(f"used_in_quest_{key_id}") == "on" else 0 map_ids = [] for value in request.form.getlist(f"map_ids_{key_id}"): try: map_ids.append(int(value)) except ValueError: continue _update_key(conn, key_id, priority, reason, used_in_quest, map_ids) conn.commit() conn.close() redirect_args = {} if map_filter: redirect_args["map_id"] = map_filter if sort: redirect_args["sort"] = sort if show: redirect_args["show"] = show base_url = url_for("keys_page", **redirect_args) if save_one: return redirect(f"{base_url}#key-{save_one}") return redirect(base_url) @app.route("/quests") 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(""" 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 collector_row = conn.execute("SELECT id FROM quests WHERE name = 'Collector'").fetchone() collector_prereqs = set() if collector_row: 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 quest_id FROM deps """, (collector_row["id"],)).fetchall() collector_prereqs = {r[0] for r in rows} conn.close() # 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 "") # Filter to collector-only if requested if only_collector: visible = set(collector_prereqs) else: visible = set(quest_by_id.keys()) # 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()) return render_template( "quests.html", quest_by_id=quest_by_id, children=children, trader_roots=trader_roots, traders=traders, 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() if not collector: conn.close() return "Run import_quests.py first to populate quest data.", 503 # 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 quest_id FROM deps """, (collector["id"],)).fetchall() collector_prereqs = {r[0] for r in rows} conn.close() # 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"]) def collector_toggle(): quest_id = request.form["quest_id"] done = 1 if request.form.get("done") == "1" else 0 conn = get_db() conn.execute(""" INSERT INTO quest_progress (quest_id, done) VALUES (?, ?) ON CONFLICT(quest_id) DO UPDATE SET done = excluded.done """, (quest_id, done)) conn.commit() conn.close() return jsonify({"quest_id": quest_id, "done": done}) # --- Loadout planner helpers --- # Known user-facing slot filters: (label, slot_nameid) LOADOUT_SLOT_FILTERS = [ ("Suppressor", "mod_muzzle"), ("Scope", "mod_scope"), ("Flashlight", "mod_tactical"), ("Stock", "mod_stock"), ("Foregrip", "mod_foregrip"), ] def _sort_col(sort): return { "weight_asc": "weight_kg ASC NULLS LAST", "weight_desc": "weight_kg DESC NULLS LAST", "name_asc": "name ASC", "name_desc": "name DESC", "class_desc": "armor_class DESC NULLS LAST, weight_kg ASC NULLS LAST", "class_asc": "armor_class ASC NULLS LAST, weight_kg ASC NULLS LAST", "capacity_desc": "capacity DESC NULLS LAST, weight_kg ASC NULLS LAST", "capacity_asc": "capacity ASC NULLS LAST, weight_kg ASC NULLS LAST", }.get(sort, "weight_kg ASC NULLS LAST") @app.route("/loadout") def loadout(): conn = get_db() tab = request.args.get("tab", "guns") sort = request.args.get("sort", "weight_asc") guns = armor = helmets = headwear = backpacks = rigs = plates = [] builder_guns = builder_armor = builder_helmets = builder_rigs = builder_backpacks = [] requires = request.args.getlist("requires") # list of slot_nameids that must exist min_class = request.args.get("min_class", 0, type=int) min_capacity = request.args.get("min_capacity", 0, type=int) sort_frag = _sort_col(sort) if tab == "guns": if requires: placeholders = ",".join("?" * len(requires)) # Gun must have ALL required slots; compute lightest build weight guns = conn.execute(f""" SELECT g.*, (g.weight_kg + COALESCE(( SELECT SUM(s.min_w) FROM ( SELECT gs.slot_id, MIN(m.weight_kg) AS min_w FROM gun_slots gs JOIN gun_slot_items gsi ON gsi.gun_id = gs.gun_id AND gsi.slot_id = gs.slot_id JOIN gear_items m ON m.id = gsi.item_id WHERE gs.gun_id = g.id AND gs.slot_nameid IN ({placeholders}) AND m.weight_kg IS NOT NULL GROUP BY gs.slot_id ) s ), 0.0)) AS lightest_build_weight FROM gear_items g WHERE g.category = 'gun' AND ( SELECT COUNT(DISTINCT gs2.slot_nameid) FROM gun_slots gs2 WHERE gs2.gun_id = g.id AND gs2.slot_nameid IN ({placeholders}) ) = ? ORDER BY lightest_build_weight ASC NULLS LAST """, requires + requires + [len(requires)]).fetchall() else: guns = conn.execute(f""" SELECT *, weight_kg AS lightest_build_weight FROM gear_items WHERE category = 'gun' ORDER BY {sort_frag} """).fetchall() elif tab == "armor": armor = conn.execute(f""" SELECT * FROM gear_items WHERE category = 'armor' AND (? = 0 OR armor_class >= ?) ORDER BY {sort_frag} """, (min_class, min_class)).fetchall() elif tab == "helmets": helmets = conn.execute(f""" SELECT * FROM gear_items WHERE category = 'helmet' AND (? = 0 OR armor_class >= ?) ORDER BY {sort_frag} """, (min_class, min_class)).fetchall() elif tab == "headwear": headwear = conn.execute(f""" SELECT * FROM gear_items WHERE category = 'headwear' AND (? = 0 OR armor_class >= ?) ORDER BY {sort_frag} """, (min_class, min_class)).fetchall() elif tab == "backpacks": backpacks = conn.execute(f""" SELECT * FROM gear_items WHERE category = 'backpack' AND (? = 0 OR capacity >= ?) ORDER BY {sort_frag} """, (min_capacity, min_capacity)).fetchall() elif tab == "rigs": rigs = conn.execute(f""" SELECT * FROM gear_items WHERE category = 'rig' AND (? = 0 OR capacity >= ?) AND (? = 0 OR armor_class >= ?) ORDER BY {sort_frag} """, (min_capacity, min_capacity, min_class, min_class)).fetchall() elif tab == "plates": plates = conn.execute(f""" SELECT * FROM gear_items WHERE category = 'plate' AND (? = 0 OR armor_class >= ?) ORDER BY {sort_frag} """, (min_class, min_class)).fetchall() elif tab == "builder": builder_guns = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='gun' ORDER BY name").fetchall() builder_armor = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='armor' ORDER BY name").fetchall() builder_helmets = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='helmet' ORDER BY name").fetchall() builder_rigs = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='rig' ORDER BY name").fetchall() builder_backpacks = conn.execute("SELECT id, name, weight_kg FROM gear_items WHERE category='backpack' ORDER BY name").fetchall() # IDs of carriers that have at least one open plate slot (shell weight only) open_slot_rows = conn.execute("SELECT DISTINCT carrier_id FROM armor_open_slots").fetchall() carrier_ids_with_open_slots = {row["carrier_id"] for row in open_slot_rows} conn.close() return render_template( "loadout.html", tab=tab, sort=sort, guns=guns, armor=armor, helmets=helmets, headwear=headwear, backpacks=backpacks, rigs=rigs, plates=plates, slot_filters=LOADOUT_SLOT_FILTERS, requires=requires, min_class=min_class, min_capacity=min_capacity, builder_guns=builder_guns, builder_armor=builder_armor, builder_helmets=builder_helmets, builder_rigs=builder_rigs, builder_backpacks=builder_backpacks, carrier_ids_with_open_slots=carrier_ids_with_open_slots, ) @app.route("/loadout/gun/") def gun_detail(gun_id): conn = get_db() gun = conn.execute( "SELECT * FROM gear_items WHERE id = ? AND category = 'gun'", (gun_id,) ).fetchone() if not gun: conn.close() return "Gun not found.", 404 # All slots for this gun, with every compatible mod sorted by weight slots_raw = conn.execute(""" SELECT gs.slot_id, gs.slot_name, gs.slot_nameid, gs.required, m.id AS mod_id, m.name AS mod_name, m.short_name AS mod_short, m.weight_kg, m.grid_image_url, m.wiki_url, m.mod_type FROM gun_slots gs LEFT JOIN gun_slot_items gsi ON gsi.gun_id = gs.gun_id AND gsi.slot_id = gs.slot_id LEFT JOIN gear_items m ON m.id = gsi.item_id WHERE gs.gun_id = ? ORDER BY gs.slot_name, m.weight_kg ASC NULLS LAST """, (gun_id,)).fetchall() # Group by slot slots = {} slot_order = [] for row in slots_raw: sid = row["slot_id"] if sid not in slots: slots[sid] = { "slot_id": sid, "slot_name": row["slot_name"], "slot_nameid": row["slot_nameid"], "required": row["required"], "mods": [], } slot_order.append(sid) if row["mod_id"]: slots[sid]["mods"].append(dict(row)) # Split into required vs optional slots KEY_SLOTS = {"mod_muzzle", "mod_magazine"} ordered_slots = [slots[s] for s in slot_order] # Required slots (always needed) shown at top — key slots (magazine/muzzle) highlighted key_slots = [s for s in ordered_slots if s["required"] and s["slot_nameid"] in KEY_SLOTS] req_slots = [s for s in ordered_slots if s["required"] and s["slot_nameid"] not in KEY_SLOTS] optional_slots = [s for s in ordered_slots if not s["required"]] # Lightest total (base + lightest per REQUIRED slot only) lightest_total = (gun["weight_kg"] or 0) + sum( s["mods"][0]["weight_kg"] for s in ordered_slots if s["required"] and s["mods"] and s["mods"][0]["weight_kg"] is not None ) conn.close() return render_template( "gun_detail.html", gun=gun, key_slots=key_slots, req_slots=req_slots, optional_slots=optional_slots, lightest_total=lightest_total, ) @app.route("/loadout/gun//slots.json") def gun_slots_json(gun_id): """Returns slot summary for the expandable row (lightest mod per slot only).""" conn = get_db() rows = conn.execute(""" SELECT gs.slot_name, gs.slot_nameid, gs.required, m.name AS mod_name, m.weight_kg FROM gun_slots gs LEFT JOIN ( SELECT gsi.gun_id, gsi.slot_id, m2.name, m2.weight_kg FROM gun_slot_items gsi JOIN gear_items m2 ON m2.id = gsi.item_id WHERE gsi.gun_id = ? AND m2.weight_kg = ( SELECT MIN(m3.weight_kg) FROM gun_slot_items gsi3 JOIN gear_items m3 ON m3.id = gsi3.item_id WHERE gsi3.gun_id = gsi.gun_id AND gsi3.slot_id = gsi.slot_id AND m3.weight_kg IS NOT NULL ) GROUP BY gsi.slot_id ) m ON m.gun_id = gs.gun_id AND m.slot_id = gs.slot_id WHERE gs.gun_id = ? ORDER BY gs.slot_name """, (gun_id, gun_id)).fetchall() conn.close() KEY_SLOTS = {"mod_muzzle", "mod_magazine"} result = [ { "slot_name": r["slot_name"], "slot_nameid": r["slot_nameid"], "required": r["required"], "mod_name": r["mod_name"], "weight_kg": r["weight_kg"], "key": r["slot_nameid"] in KEY_SLOTS, } for r in rows ] return jsonify(result) @app.route("/loadout/carrier//slots.json") def carrier_slots_json(carrier_id): """Returns open plate slots and allowed plates for a carrier (armor or rig).""" conn = get_db() rows = conn.execute(""" SELECT aos.slot_nameid, aos.zones, p.id AS plate_id, p.name AS plate_name, p.short_name AS plate_short, p.weight_kg, p.armor_class, p.durability, p.material FROM armor_open_slots aos LEFT JOIN armor_slot_plates asp ON asp.carrier_id = aos.carrier_id AND asp.slot_nameid = aos.slot_nameid LEFT JOIN gear_items p ON p.id = asp.plate_id WHERE aos.carrier_id = ? ORDER BY aos.slot_nameid, p.armor_class DESC, p.weight_kg ASC """, (carrier_id,)).fetchall() conn.close() # Group by slot slots = {} slot_order = [] for row in rows: sn = row["slot_nameid"] if sn not in slots: slots[sn] = {"slot_nameid": sn, "zones": row["zones"], "plates": []} slot_order.append(sn) if row["plate_id"]: slots[sn]["plates"].append({ "id": row["plate_id"], "name": row["plate_name"], "short_name": row["plate_short"], "weight_kg": row["weight_kg"], "armor_class": row["armor_class"], "durability": row["durability"], "material": row["material"], }) return jsonify([slots[s] for s in slot_order]) @app.route("/loadout/save-build", methods=["POST"]) def save_build(): data = request.get_json() or {} name = (data.get("name") or "My Build").strip() or "My Build" gun_id = data.get("gun_id") or None armor_id = data.get("armor_id") or None helmet_id = data.get("helmet_id") or None rig_id = data.get("rig_id") or None backpack_id = data.get("backpack_id") or None notes = data.get("notes", "") conn = get_db() cur = conn.execute(""" INSERT INTO saved_builds (name, gun_id, armor_id, helmet_id, rig_id, backpack_id, notes) VALUES (?, ?, ?, ?, ?, ?, ?) """, (name, gun_id, armor_id, helmet_id, rig_id, backpack_id, notes)) build_id = cur.lastrowid conn.commit() conn.close() return jsonify({"build_id": build_id, "name": name}) @app.route("/meds") def meds(): import requests as _req API_URL = "https://api.tarkov.dev/graphql" query = """ { items(types: [injectors], lang: en) { id name shortName iconLink wikiLink properties { __typename ... on ItemPropertiesStim { useTime cures stimEffects { type skill { name } value percent duration delay chance } } } } } """ try: resp = _req.post(API_URL, json={"query": query}, timeout=8) raw_items = resp.json()["data"]["items"] except Exception: raw_items = [] # ── helper: pick first matching effect ────────────────────────────── def pick(effects, type_str, positive_only=False, negative_only=False): for e in effects: if e["type"] == type_str: if positive_only and e["value"] <= 0: continue if negative_only and e["value"] >= 0: continue return e return None def pick_skill(effects, skill_name): for e in effects: if e["type"] == "Skill" and e.get("skill") and e["skill"]["name"] == skill_name: return e return None # ── collect all skill names across all injectors ────────────────── all_skills = [] for item in raw_items: p = item.get("properties") or {} for e in p.get("stimEffects", []): if e["type"] == "Skill" and e.get("skill"): sn = e["skill"]["name"] if sn not in all_skills: all_skills.append(sn) skill_rows = sorted(all_skills) # ── build injector data rows ────────────────────────────────────── injectors = [] for item in raw_items: p = item.get("properties") or {} effs = p.get("stimEffects", []) def _val(eff): return round(eff["value"], 2) if eff else None def _dur(eff): return eff["duration"] if eff else 0 def _delay(eff): return eff["delay"] if eff else 0 hp_e = pick(effs, "Health regeneration", positive_only=True) stam_e = pick(effs, "Stamina recovery", positive_only=True) stam_neg_e = pick(effs, "Stamina recovery", negative_only=True) stam_rec_e = stam_e or stam_neg_e maxstam_e = pick(effs, "Max stamina") weight_e = pick(effs, "Weight limit") energy_e = pick(effs, "Energy recovery") hydra_e = pick(effs, "Hydration recovery") bleed_e = pick(effs, "Stops and prevents bleedings") anti_e = pick(effs, "Antidote") tremor_e = pick(effs, "Hands tremor") tunnel_e = pick(effs, "Tunnel effect") pain_e = pick(effs, "Pain") temp_e = pick(effs, "Body temperature") # skills dict skills = {} for sn in skill_rows: se = pick_skill(effs, sn) if se: skills[sn] = {"value": round(se["value"], 1), "duration": se["duration"]} # tags for column filtering tags = [] if hp_e or bleed_e or anti_e: tags.append("heal") if maxstam_e or stam_rec_e: tags.append("stam") if skills: tags.append("skill") if temp_e or weight_e: tags.append("special") if not tags: tags.append("special") injectors.append({ "name": item["name"], "short": item["shortName"], "icon": item.get("iconLink"), "wiki": item.get("wikiLink"), "tags": ",".join(tags), # healing "hp_regen": _val(hp_e), "hp_regen_dur": _dur(hp_e), "stops_bleed": bool(bleed_e), "antidote": bool(anti_e), # stamina "max_stam": _val(maxstam_e), "max_stam_dur": _dur(maxstam_e), "stam_rec": _val(stam_rec_e), "stam_rec_dur": _dur(stam_rec_e), # weight "weight": _val(weight_e), "weight_dur": _dur(weight_e), # special "body_temp": _val(temp_e), "body_temp_dur": _dur(temp_e), "energy": round(energy_e["value"], 2) if energy_e else None, "energy_dur": _dur(energy_e), "hydration": round(hydra_e["value"], 2) if hydra_e else None, "hydration_dur": _dur(hydra_e), # skills "skills": skills, # side effects "tremor": bool(tremor_e), "tremor_delay": _delay(tremor_e), "tremor_dur": _dur(tremor_e), "tunnel": bool(tunnel_e), "tunnel_delay": _delay(tunnel_e), "tunnel_dur": _dur(tunnel_e), "pain": bool(pain_e), "pain_delay": _delay(pain_e), "pain_dur": _dur(pain_e), }) # ── situation guide ─────────────────────────────────────────────── situations = { "bleed": [ {"short": "Zagustin", "desc": "Stops bleeding, +Vitality 180s", "warn": "tremors delayed"}, {"short": "AHF1-M", "desc": "Stops bleeding, +Health 60s", "warn": "-hydration"}, {"short": "Perfotoran", "desc": "Stops bleed + antidote + regen", "warn": "-energy after"}, {"short": "xTG-12", "desc": "Antidote only (no bleed stop)", "warn": "-Health skill"}, ], "regen": [ {"short": "eTG-c", "desc": "+6.5 HP/s for 60s (fast burst)", "warn": "-energy after"}, {"short": "Adrenaline","desc": "+4 HP/s for 15s, stam boost", "warn": "-hydration after"}, {"short": "PNB", "desc": "+3 HP/s for 40s, +Strength", "warn": "tremors + skill debuff"}, {"short": "Propital", "desc": "+1 HP/s for 300s, skill buffs", "warn": "tremors at 270s"}, {"short": "Perfotoran","desc": "+1.5 HP/s for 60s + antidote", "warn": "-energy after"}, ], "stam": [ {"short": "Trimadol", "desc": "+3 stam rec, +10 max stam, 180s", "warn": "-energy/-hydration"}, {"short": "SJ6", "desc": "+2 stam rec, +30 max stam, 240s", "warn": "tremors + tunnel after"}, {"short": "Meldonin", "desc": "+0.5 stam rec, +Endurance 900s", "warn": "-hydration/-energy (minor)"}, {"short": "L1", "desc": "+30 max stam, +Strength, 120s", "warn": "-hydration/-energy"}, {"short": "SJ1", "desc": "+Endurance/Strength 180s", "warn": "-energy/-hydration after"}, {"short": "Adrenaline","desc": "Short burst +Endurance/Strength", "warn": "-Stress Resist"}, ], "skill": [ {"short": "Obdolbos 2", "desc": "All skills +20, weight +45%, 1800s", "warn": "-stam, -HP regen"}, {"short": "3-(b-TG)", "desc": "+Attention/Perception/Strength 240s", "warn": "tremors after"}, {"short": "SJ12", "desc": "+Perception 600s, body cool", "warn": "overheats at end"}, {"short": "2A2-(b-TG)", "desc": "+Attention/Perception, weight +15%, 900s", "warn": "-hydration"}, {"short": "Trimadol", "desc": "Broad skill buff + stam, 180s", "warn": "-energy/-hydration"}, ], "special": [ {"short": "SJ9", "desc": "Cools body -7°, 300s — hot map survival", "warn": "HP drain + tremors"}, {"short": "SJ12", "desc": "Cools body -4°, +Perception, 600s", "warn": "rebound heat after"}, {"short": "M.U.L.E.", "desc": "Weight limit +50% for 900s", "warn": "-HP regen"}, {"short": "Obdolbos","desc": "25% chance: all buffs + all debuffs", "warn": "may kill you"}, ], "risky": [ {"short": "Obdolbos", "desc": "25% chance everything fires at once", "warn": "may cause -600 HP"}, {"short": "PNB", "desc": "Fast HP/Strength burst then hard crash", "warn": "-Health/-Vitality 180s"}, {"short": "SJ9", "desc": "-HP regen whole duration, tremors", "warn": "don't use while injured"}, {"short": "Propital", "desc": "Tremors + tunnel vision at 270s delay", "warn": "plan ahead"}, ], } return render_template("meds.html", injectors=injectors, skill_rows=skill_rows, situations=situations) @app.route("/barters") def barters(): import requests as _req API_URL = "https://api.tarkov.dev/graphql" query = """ { barters(lang: en) { id trader { name } level taskUnlock { name } requiredItems { item { id name shortName iconLink wikiLink } count } rewardItems { item { id name shortName iconLink wikiLink } count } } } """ try: resp = _req.post(API_URL, json={"query": query}, timeout=15) data = resp.json() raw_barters = data.get("data", {}).get("barters", []) except Exception: raw_barters = [] barter_list = [] for b in raw_barters: reward_items = b.get("rewardItems", []) required_items = b.get("requiredItems", []) if not reward_items or not required_items: continue # Use first reward item as the "output" item reward = reward_items[0] reward_item = reward.get("item") or {} reward_count = reward.get("count", 1) required = [] for ri in required_items: item = ri.get("item") or {} required.append({ "id": item.get("id", ""), "name": item.get("name", "Unknown"), "short": item.get("shortName", ""), "icon": item.get("iconLink"), "count": ri.get("count", 1), }) task_unlock = b.get("taskUnlock") barter_list.append({ "id": b.get("id", ""), "trader": (b.get("trader") or {}).get("name", "Unknown"), "level": b.get("level", 1), "task_unlock": task_unlock.get("name") if task_unlock else None, "reward_name": reward_item.get("name", "Unknown"), "reward_short": reward_item.get("shortName", ""), "reward_icon": reward_item.get("iconLink"), "reward_wiki": reward_item.get("wikiLink"), "reward_count": reward_count, "required": required, }) barter_list.sort(key=lambda b: (b["trader"], b["level"], b["reward_name"])) return render_template("barters.html", barters=barter_list) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)