v0.1.1 -
feat: collector task checklist. chore: docs updated, gitignore updated. Docs: TARKOV_DEV_API.md fully explains tarkov.dev's api for future coding agents and forgetful people llike me.
This commit is contained in:
7
.claude/settings.json
Normal file
7
.claude/settings.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(curl:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -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
|
||||||
120
README.md
120
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**
|
## What it does
|
||||||
--full list w/ locations and whats behind the lock
|
|
||||||
-- vendor price
|
- **Key tracker** — full list of keys with personal priority ratings (IGNORE / LOW / MED / HIGH / SUPER), map tagging, notes, and quest flags
|
||||||
-- My personal 0-4 priority scale.
|
- **Collector checklist** — all 255 quests required to unlock *The Collector* (Kappa), with per-quest done/not-done tracking and a progress bar
|
||||||
-- Flag useless keys.
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|||||||
458
TARKOV_DEV_API.md
Normal file
458
TARKOV_DEV_API.md
Normal file
@@ -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`.
|
||||||
47
app.py
47
app.py
@@ -194,5 +194,52 @@ def rate_all():
|
|||||||
return redirect(base_url)
|
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__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
|
|||||||
@@ -82,10 +82,10 @@ def upsert_keys(conn, keys):
|
|||||||
else:
|
else:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO keys (api_id, name, short_name, weight_kg, uses)
|
INSERT INTO keys (api_id, name, short_name, weight_kg, uses, wiki_url, grid_image_url)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
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
|
inserted += 1
|
||||||
|
|
||||||
|
|||||||
141
import_quests.py
Normal file
141
import_quests.py
Normal file
@@ -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()
|
||||||
143
templates/collector.html
Normal file
143
templates/collector.html
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OnlyScavs – Collector Checklist</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #121212;
|
||||||
|
--panel: #1a1a1a;
|
||||||
|
--text: #eee;
|
||||||
|
--muted: #bbb;
|
||||||
|
--border: #333;
|
||||||
|
--accent: #9ccfff;
|
||||||
|
--done-bg: #1a2a1a;
|
||||||
|
--done-text: #6ec96e;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.page {
|
||||||
|
max-width: 780px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 { margin-bottom: 4px; }
|
||||||
|
.subtitle {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.progress-bar-wrap {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
height: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-bar-fill {
|
||||||
|
background: var(--done-text);
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
.trader-group { margin-bottom: 8px; }
|
||||||
|
.trader-header {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.quest-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-bottom: 1px solid #222;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.quest-row.done {
|
||||||
|
background: var(--done-bg);
|
||||||
|
}
|
||||||
|
.quest-row.done .quest-name {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--done-text);
|
||||||
|
}
|
||||||
|
.quest-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.quest-name a {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.toggle-btn {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.quest-row.done .toggle-btn {
|
||||||
|
background: #1e3a1e;
|
||||||
|
border-color: #3a6a3a;
|
||||||
|
color: var(--done-text);
|
||||||
|
}
|
||||||
|
nav { margin-bottom: 20px; }
|
||||||
|
nav a { color: var(--accent); font-size: 0.9rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<nav><a href="/">← Back to Keys</a></nav>
|
||||||
|
<h1>Collector Checklist</h1>
|
||||||
|
<p class="subtitle">
|
||||||
|
{{ done }} / {{ total }} quests completed
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="progress-bar-wrap">
|
||||||
|
<div class="progress-bar-fill" style="width: {{ (done / total * 100) | round(1) if total else 0 }}%"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set ns = namespace(current_trader=None) %}
|
||||||
|
{% for quest in quests %}
|
||||||
|
{% if quest.trader != ns.current_trader %}
|
||||||
|
{% if ns.current_trader is not none %}</div>{% endif %}
|
||||||
|
<div class="trader-group">
|
||||||
|
<div class="trader-header">{{ quest.trader }}</div>
|
||||||
|
{% set ns.current_trader = quest.trader %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/collector/toggle" style="margin:0">
|
||||||
|
<input type="hidden" name="quest_id" value="{{ quest.id }}">
|
||||||
|
<input type="hidden" name="done" value="{{ '0' if quest.done else '1' }}">
|
||||||
|
<div class="quest-row {% if quest.done %}done{% endif %}">
|
||||||
|
<span class="quest-name">
|
||||||
|
{{ quest.name }}
|
||||||
|
{% if quest.wiki_link %}
|
||||||
|
<a href="{{ quest.wiki_link }}" target="_blank">wiki</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<button class="toggle-btn" type="submit">
|
||||||
|
{{ '✓ Done' if quest.done else 'Mark done' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endfor %}
|
||||||
|
{% if ns.current_trader is not none %}</div>{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -168,6 +168,7 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
<nav style="margin-bottom:12px"><a href="/collector">Collector Checklist →</a></nav>
|
||||||
<h1>OnlyScavs – Keys</h1>
|
<h1>OnlyScavs – Keys</h1>
|
||||||
|
|
||||||
<form method="get" class="filters">
|
<form method="get" class="filters">
|
||||||
|
|||||||
Reference in New Issue
Block a user