diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..00d0363 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(curl:*)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3eb3374 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Database — source of truth lives locally, not in git +*.db +*.db-shm +*.db-wal + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.egg-info/ +dist/ +build/ +.eggs/ + +# Virtual environments +venv/ +.venv/ +env/ + +# Editor / IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Secrets / env +.env +*.env diff --git a/README.md b/README.md index fa1b6ec..ea92e9e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,112 @@ -##Place hoder, for this here personalized tarkov DB im building. +# OnlyScavs v0.1.1 +A personal Escape from Tarkov database and toolkit. The goal is to maintain a **local SQLite database that I fully control** — tarkov.dev is used only as a one-time (or on-demand) data source to seed it. Once imported, the local DB is the source of truth and can be edited, annotated, and extended freely without relying on any external API being up or accurate. -##Items to be tracked = ## -**Weight Management Mostly** ---Weapons and weapons parts ---helmets + armor and rigs ---backpacks +--- -**Keys** ---full list w/ locations and whats behind the lock --- vendor price --- My personal 0-4 priority scale. --- Flag useless keys. \ No newline at end of file +## What it does + +- **Key tracker** — full list of keys with personal priority ratings (IGNORE / LOW / MED / HIGH / SUPER), map tagging, notes, and quest flags +- **Collector checklist** — all 255 quests required to unlock *The Collector* (Kappa), with per-quest done/not-done tracking and a progress bar + +--- + +## Setup + +### 1. Install dependencies +```bash +pip install flask requests +``` + +### 2. Initialize the database +Run the imports in order. Each script creates its own tables if they don't exist. + +```bash +# Import all keys from tarkov.dev into local DB +python3 import_keys.py + +# Import all quests/tasks and their dependency graph +python3 import_quests.py +``` + +Then apply the maps migration (adds maps table + key–map relationships): +```bash +python3 -c " +import sqlite3 +conn = sqlite3.connect('tarkov.db') +conn.executescript(open('migrations_v1.sql').read()) +conn.commit() +conn.close() +" +``` + +> After this, the DB is yours. You don't need tarkov.dev running to use the app. + +### 3. Run the app +```bash +python3 app.py +``` + +Open **http://127.0.0.1:5000** + +--- + +## Re-importing data + +The import scripts can be re-run any time to pull fresh data from tarkov.dev (e.g. after a big patch). They use `INSERT OR REPLACE` / `INSERT OR IGNORE` so they won't duplicate records, but **any manual edits to imported fields (name, wiki_link, etc.) will be overwritten**. Personal data (ratings, notes, map tags, quest progress) is stored in separate tables and is safe. + +```bash +python3 import_keys.py # refresh key list +python3 import_quests.py # refresh quest list + dependency graph +``` + +--- + +## Project structure + +``` +onlyscavs/ +├── app.py # Flask web app +├── import_keys.py # Seeds keys table from tarkov.dev +├── import_quests.py # Seeds quests + quest_deps tables from tarkov.dev +├── migrations_v1.sql # Maps table + key_maps + used_in_quest flag +├── tarkov.db # Local SQLite DB (gitignored — stays on your machine) +├── templates/ +│ ├── index.html # Key ratings UI +│ └── collector.html # Collector checklist UI +└── TARKOV_DEV_API.md # tarkov.dev GraphQL API reference +``` + +--- + +## Database schema + +| Table | Purpose | +|---|---| +| `keys` | All key items (seeded from tarkov.dev, then local) | +| `key_ratings` | Personal ratings, notes, quest flags per key | +| `key_maps` | Which maps each key is used on | +| `maps` | Map list | +| `quests` | All tasks/quests (seeded from tarkov.dev) | +| `quest_deps` | Quest prerequisite graph (which quest unlocks which) | +| `quest_progress` | Personal done/not-done state per quest | + +--- + +## Routes + +| Route | Description | +|---|---| +| `GET /` | Key list with filters, sorting, and inline rating forms | +| `POST /rate` | Save rating for a single key | +| `POST /rate_all` | Save ratings for all visible keys | +| `GET /collector` | Collector checklist with progress bar | +| `POST /collector/toggle` | Mark a quest done or not done | + +--- + +## Planned + +- Weight/loadout tracker (weapons, armor, rigs, backpacks) +- Vendor price comparison +- Key location notes (what's behind the door) diff --git a/TARKOV_DEV_API.md b/TARKOV_DEV_API.md new file mode 100644 index 0000000..ed502de --- /dev/null +++ b/TARKOV_DEV_API.md @@ -0,0 +1,458 @@ +# tarkov.dev GraphQL API Reference + +> Community-made, real-time EFT data. No API key required. + +- **Endpoint:** `https://api.tarkov.dev/graphql` +- **Playground:** `https://api.tarkov.dev/` (interactive explorer with autocomplete) +- **Protocol:** GraphQL over HTTP POST +- **Auth:** None — completely open +- **Source:** https://github.com/the-hideout/tarkov-api + +--- + +## How to query (Python) + +```python +import requests + +API_URL = "https://api.tarkov.dev/graphql" + +def gql(query): + r = requests.post(API_URL, json={"query": query}) + r.raise_for_status() + data = r.json() + if "errors" in data: + raise RuntimeError(data["errors"]) + return data["data"] +``` + +--- + +## Common arguments (most queries accept these) + +| Arg | Type | Notes | +|------------|----------------|--------------------------------------------| +| `lang` | `LanguageCode` | e.g. `en`, `ru`, `de`, `fr`, `es`, `zh` | +| `gameMode` | `GameMode` | `regular` or `pve` | +| `limit` | `Int` | Max results to return | +| `offset` | `Int` | Pagination offset | + +--- + +## All available root queries + +``` +achievements(lang, limit, offset) +ammo(lang, gameMode, limit, offset) +archivedItemPrices(id, limit, offset) +barters(lang, gameMode, limit, offset) +bosses(lang, gameMode, name, limit, offset) +crafts(lang, gameMode, limit, offset) +fleaMarket(lang, gameMode) +goonReports(lang, gameMode, limit, offset) +handbookCategories(lang, limit, offset) +hideoutStations(lang, gameMode, limit, offset) +historicalItemPrices(id, days, lang, gameMode, limit, offset) +item(id, normalizedName, lang, gameMode) +items(ids, name, names, type, types, categoryNames, handbookCategoryNames, bsgCategoryId, bsgCategoryIds, bsgCategory, lang, gameMode, limit, offset) +itemCategories(lang, limit, offset) +itemPrices(id, gameMode, limit, offset) +lootContainers(lang, limit, offset) +maps(lang, gameMode, name, enemies, limit, offset) +mastering(lang) +playerLevels() +prestige(lang, gameMode) +questItems(lang) +skills(lang) +stationaryWeapons(lang, limit, offset) +status() +task(id, lang, gameMode) +tasks(faction, lang, gameMode, limit, offset) +traders(lang, gameMode, limit, offset) +``` + +--- + +## Query examples + +### Server status +```graphql +{ + status { + currentStatuses { + name + message + status + } + messages { + time + type + content + solveTime + } + } +} +``` + +### Single item by name +```graphql +{ + items(name: "colt m4a1") { + id + name + shortName + basePrice + avg24hPrice + changeLast48hPercent + width + height + weight + wikiLink + iconLink + gridImageLink + sellFor { + price + currency + source + } + buyFor { + price + currency + source + } + } +} +``` + +### Items by type (e.g. all keys) +```graphql +{ + items(types: [keys]) { + id + name + shortName + wikiLink + gridImageLink + properties { + ... on ItemPropertiesKey { + uses + } + } + } +} +``` + +Valid `ItemType` values include: `keys`, `ammo`, `armor`, `backpack`, `gun`, `headwear`, `rig`, `medical`, `food`, `barter`, `container`, `grenade`, `headphones`, `knife`, `stimulator`, `suppressor`, `weapon` + +### All tasks / quests +```graphql +{ + tasks { + id + name + wikiLink + minPlayerLevel + kappaRequired + trader { + name + } + taskRequirements { + task { + id + name + } + status + } + objectives { + id + type + description + maps { + name + normalizedName + } + ... on TaskObjectiveItem { + item { name shortName } + count + foundInRaid + } + ... on TaskObjectiveShoot { + targetNames + count + } + ... on TaskObjectiveLocation { + locationNames + } + ... on TaskObjectiveQuestItem { + questItem { name } + count + } + ... on TaskObjectiveSkill { + skillLevel { name level } + } + ... on TaskObjectiveTraderLevel { + trader { name } + level + } + } + startRewards { + items { item { name } count } + traderStanding { trader { name } standing } + } + finishRewards { + items { item { name } count } + experience + traderStanding { trader { name } standing } + offerUnlock { trader { name } item { name } } + } + } +} +``` + +> **Note:** `objectives` returns a `TaskObjective` **interface** — use inline fragments (`... on TypeName`) to access type-specific fields. + +### Single task by ID +```graphql +{ + task(id: "5936d90786f7742b1420ba5b") { + name + trader { name } + taskRequirements { task { id name } } + } +} +``` + +### Ammo stats +```graphql +{ + ammo { + item { name shortName } + caliber + damage + armorDamage + penetrationPower + penetrationChance + fragmentationChance + initialSpeed + lightBleedModifier + heavyBleedModifier + } +} +``` + +### Traders +```graphql +{ + traders { + id + name + normalizedName + currency { name } + levels { + level + requiredPlayerLevel + requiredReputation + requiredCommerce + cashOffers { + item { name } + price + currency + minTraderLevel + taskUnlock { name } + } + } + barters { + requiredItems { item { name } count } + rewardItems { item { name } count } + minTraderLevel + taskUnlock { name } + } + } +} +``` + +### Hideout stations +```graphql +{ + hideoutStations { + id + name + levels { + level + constructionTime + itemRequirements { item { name } count } + stationLevelRequirements { station { name } level } + skillRequirements { skill { name } level } + traderRequirements { trader { name } requirementType value } + bonuses { type value } + } + } +} +``` + +### Maps +```graphql +{ + maps { + id + name + normalizedName + raidDuration + players + bosses { + boss { name } + spawnChance + spawnLocations { name chance } + escorts { boss { name } amount { count chance } } + } + extracts { + name + faction + switches { name } + } + } +} +``` + +### Crafts +```graphql +{ + crafts { + station { name } + level + duration + requiredItems { item { name } count } + rewardItems { item { name } count } + taskUnlock { name } + } +} +``` + +### Barters +```graphql +{ + barters { + trader { name } + level + taskUnlock { name } + requiredItems { item { name } count } + rewardItems { item { name } count } + } +} +``` + +### Flea market info +```graphql +{ + fleaMarket { + enabled + minPlayerLevel + sellOfferFeeRate + sellRequirementFeeRate + } +} +``` + +### Achievements +```graphql +{ + achievements { + id + name + description + hidden + rarity + playersCompletedPercent + side + } +} +``` + +### Goon reports (roaming boss locations) +```graphql +{ + goonReports { + map { name } + timestamp + } +} +``` + +--- + +## Key types reference + +### Item fields +``` +id, name, shortName, normalizedName +basePrice, avg24hPrice, low24hPrice, high24hPrice +changeLast48h, changeLast48hPercent +lastLowPrice, lastOfferCount +width, height, weight, velocity +wikiLink, iconLink, gridImageLink, inspectImageLink, image8xLink +types[] – ItemType enum values this item belongs to +sellFor[] – { price, currency, source, vendor { name } } +buyFor[] – { price, currency, source, vendor { name } } +bartersFor[] – Barter objects +bartersUsing[] – Barter objects +usedInTasks[] – Task objects +receivedFromTasks[] – Task objects +properties – ItemProperties union (see below) +category – { id, name } +``` + +### ItemProperties union types +Use `... on ItemPropertiesX { }` inline fragments: +- `ItemPropertiesKey` → `uses` +- `ItemPropertiesAmmo` → `caliber, damage, penetrationPower, ...` +- `ItemPropertiesArmor` → `class, durability, material, zones[]` +- `ItemPropertiesArmorAttachment` → `class, durability, material, zones[], headCoverage` +- `ItemPropertiesBackpack` → `capacity, grids[]` +- `ItemPropertiesChestRig` → `capacity, class, durability, zones[]` +- `ItemPropertiesWeapon` → `caliber, fireRate, ergonomics, recoil...` +- `ItemPropertiesMagazine` → `capacity, caliber, ergonomics` +- `ItemPropertiesScope` → `ergonomics, recoil, zoomLevels[]` +- `ItemPropertiesMedKit` → `uses, useTime, hpCostLightBleeding, hpCostHeavyBleeding` +- `ItemPropertiesFood` → `energy, hydration, stimEffects[]` +- `ItemPropertiesStimulator` → `stimEffects[]` +- `ItemPropertiesHelmet` → `class, durability, material, headZones[], deafening` +- `ItemPropertiesGlasses` → `class, durability, blindnessProtection` +- `ItemPropertiesNightVision` → `intensity, noiseIntensity, noiseScale` +- `ItemPropertiesContainer` → `capacity` +- `ItemPropertiesGrenade` → `type, fuse, fragments, minExplosionDistance, maxExplosionDistance` + +### Task / quest fields +``` +id, name, normalizedName +wikiLink +minPlayerLevel +kappaRequired – bool, whether needed for Kappa (The Collector) +restartable +trader { id, name } +map { id, name } – nullable, map-specific tasks only +experience +taskRequirements[] – { task { id, name }, status[] } +objectives[] – TaskObjective interface (use inline fragments) +startRewards – TaskRewards +finishRewards – TaskRewards +``` + +### TaskRewards fields +``` +items[] – { item { name }, count } +traderStanding[] – { trader { name }, standing } +offerUnlock[] – { trader { name }, level, item { name } } +skillLevelReward[] – { name, level } +craftUnlock[] – craft objects +``` + +--- + +## How we use it in this project + +| Script | Query | Purpose | +|---|---|---| +| [import_keys.py](import_keys.py) | `items(types: [keys])` | Fetches all key items into `keys` table | +| [import_quests.py](import_quests.py) | `tasks { id name wikiLink trader taskRequirements }` | Fetches all tasks and their dependencies into `quests` + `quest_deps` tables | + +The Collector prerequisite tree is computed from `quest_deps` using a recursive SQL CTE in [app.py](app.py) at `/collector`. diff --git a/app.py b/app.py index 261fba9..e6eb38a 100644 --- a/app.py +++ b/app.py @@ -194,5 +194,52 @@ def rate_all(): return redirect(base_url) +@app.route("/collector") +def collector(): + conn = get_db() + 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 + + # Recursive CTE to get all transitive prerequisites + prereqs = 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 + ORDER BY q.trader, q.name + """, (collector["id"],)).fetchall() + + 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) + + +@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 redirect(url_for("collector")) + + if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/import_keys.py b/import_keys.py index 8f00f70..3d48aaf 100644 --- a/import_keys.py +++ b/import_keys.py @@ -82,10 +82,10 @@ def upsert_keys(conn, keys): else: cursor.execute( """ - INSERT INTO keys (api_id, name, short_name, weight_kg, uses) - VALUES (?, ?, ?, ?, ?) + INSERT INTO keys (api_id, name, short_name, weight_kg, uses, wiki_url, grid_image_url) + VALUES (?, ?, ?, ?, ?, ?, ?) """, - (api_id, name, short_name, weight, uses, icon_url, wiki_url) + (api_id, name, short_name, weight, uses, wiki_url, grid_image_url) ) inserted += 1 diff --git a/import_quests.py b/import_quests.py new file mode 100644 index 0000000..16558d5 --- /dev/null +++ b/import_quests.py @@ -0,0 +1,141 @@ +import requests +import sqlite3 + +DB_PATH = "tarkov.db" +API_URL = "https://api.tarkov.dev/graphql" + +QUERY = """ +{ + tasks { + id + name + wikiLink + trader { + name + } + taskRequirements { + task { + id + } + } + } +} +""" + +def fetch_quests(): + print("Fetching quests from Tarkov.dev...") + response = requests.post(API_URL, json={"query": QUERY}) + response.raise_for_status() + + result = response.json() + + if "errors" in result: + print("GraphQL Errors:") + print(result["errors"]) + raise Exception("GraphQL query failed.") + + return result["data"]["tasks"] + +def setup_db(conn): + cur = conn.cursor() + + cur.execute(""" + CREATE TABLE IF NOT EXISTS quests ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + trader TEXT NOT NULL, + wiki_link TEXT + ) + """) + + cur.execute(""" + CREATE TABLE IF NOT EXISTS quest_deps ( + quest_id TEXT NOT NULL, + depends_on TEXT NOT NULL, + PRIMARY KEY (quest_id, depends_on) + ) + """) + + conn.commit() + +def store_quests(conn, quests): + cur = conn.cursor() + + for q in quests: + cur.execute( + "INSERT OR REPLACE INTO quests VALUES (?, ?, ?, ?)", + (q["id"], q["name"], q["trader"]["name"], q.get("wikiLink")) + ) + + for req in q.get("taskRequirements") or []: + cur.execute( + "INSERT OR IGNORE INTO quest_deps VALUES (?, ?)", + (q["id"], req["task"]["id"]) + ) + + conn.commit() + +def get_collector_id(conn): + cur = conn.cursor() + cur.execute("SELECT id FROM quests WHERE name = 'Collector'") + row = cur.fetchone() + if not row: + raise Exception("Collector quest not found in DB.") + return row[0] + +def get_all_prereqs(conn, quest_id, seen=None): + if seen is None: + seen = set() + + cur = conn.cursor() + cur.execute( + "SELECT depends_on FROM quest_deps WHERE quest_id = ?", + (quest_id,) + ) + + deps = [r[0] for r in cur.fetchall()] + + for dep in deps: + if dep not in seen: + seen.add(dep) + get_all_prereqs(conn, dep, seen) + + return seen + +def main(): + quests = fetch_quests() + + conn = sqlite3.connect(DB_PATH) + setup_db(conn) + store_quests(conn, quests) + + collector_id = get_collector_id(conn) + prereqs = get_all_prereqs(conn, collector_id) + + print("\n=== Quests Required for Collector ===\n") + + if prereqs: + cur = conn.cursor() + placeholders = ",".join("?" * len(prereqs)) + cur.execute( + f""" + SELECT name, trader, wiki_link + FROM quests + WHERE id IN ({placeholders}) + ORDER BY trader, name + """, + list(prereqs) + ) + + rows = cur.fetchall() + for name, trader, _ in rows: + print(f"[ ] {trader}: {name}") + + print(f"\nTotal required quests: {len(rows)}") + else: + print("Collector has no prerequisites (unexpected).") + + conn.close() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tarkov.db b/tarkov.db index 9f5c8ce..1834c93 100644 Binary files a/tarkov.db and b/tarkov.db differ diff --git a/templates/collector.html b/templates/collector.html new file mode 100644 index 0000000..0d646f7 --- /dev/null +++ b/templates/collector.html @@ -0,0 +1,143 @@ + + +
++ {{ done }} / {{ total }} quests completed +
+ + + + {% set ns = namespace(current_trader=None) %} + {% for quest in quests %} + {% if quest.trader != ns.current_trader %} + {% if ns.current_trader is not none %}