diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fe5b64e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to OnlyScavs will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.4.0] - 2026-04-06 + +### Added +- Ammo Chart page (`/ammo`) — scatter plot of damage vs. armor penetration, colored by caliber, with clickable legend, live search/filter, and sortable stats table +- `import_ammo.py` — imports all ammo rounds from tarkov.dev GraphQL API into local `ammo` table + +## [0.3.0] - 2026-03-29 + +### Added +- Barter Calculator page (`/barters`) — shows all trader barters with true rouble cost breakdown +- Carry Efficiency metric for rigs and backpacks (capacity per kg) +- Landing page hero image and updated card grid + +### Changed +- Keys page redesigned with grouped display by map and inline editing for ratings/notes + +### Fixed +- Gear database refreshed from tarkov.dev API pull + +## [0.2.0] - 2026-03-01 + +### Added +- Loadout Planner (`/loadout`) — browse guns, armor, helmets, rigs, and backpacks; save builds +- Quest Trees (`/quests`) — visualize quest chains and trader dependencies +- Armor plate support with open slot system (`armor_open_slots`, `armor_slot_plates` tables) +- Injectors page (`/meds`) — compare stim/stimulator effects, skills, and side effects +- `import_gear.py` — imports weapons, armor, helmets, rigs, and mod attachments +- `migrations_v2.sql` — unified `gear_items` schema with weapon slots and armor plate slots + +### Fixed +- Quest tree display cleaned up +- Key ratings not saving correctly + +## [0.1.1] - 2026-02-21 + +### Added +- Collector checklist (`/collector`) — track Kappa container quest progress with recursive prerequisite tree via SQL CTE +- `import_quests.py` — imports all tasks and quest dependencies from tarkov.dev +- `TARKOV_DEV_API.md` — full tarkov.dev GraphQL API reference for all queries used in the project +- `.gitignore` and project docs + +## [0.1.0] - 2026-01-25 + +### Added +- Initial release — key ratings tool (`/keys`) +- `import_keys.py` — fetches all key items from tarkov.dev GraphQL API +- SQLite database with `keys`, `key_ratings`, and `maps` tables +- Sorting and filtering on key ratings view +- `migrations_v1.sql` — maps tagging and `used_in_quest` flag + +[Unreleased]: https://github.com/serversdwn/onlyscavs/compare/v0.3.0...HEAD +[0.3.0]: https://github.com/serversdwn/onlyscavs/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/serversdwn/onlyscavs/compare/v0.1.1...v0.2.0 +[0.1.1]: https://github.com/serversdwn/onlyscavs/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/serversdwn/onlyscavs/releases/tag/v0.1.0 diff --git a/TARKOV_DEV_API.md b/TARKOV_DEV_API.md index ed502de..bbab520 100644 --- a/TARKOV_DEV_API.md +++ b/TARKOV_DEV_API.md @@ -454,5 +454,6 @@ craftUnlock[] – craft objects |---|---|---| | [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 | +| [import_ammo.py](import_ammo.py) | `ammo { item { id name } caliber damage penetrationPower ... }` | Fetches all ammo rounds into `ammo` table | 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 a376737..1d45b50 100644 --- a/app.py +++ b/app.py @@ -1102,5 +1102,40 @@ def barters(): return render_template("barters.html", barters=barter_list) +@app.route("/ammo") +def ammo(): + caliber_filter = request.args.get("caliber", "").strip() + search = request.args.get("q", "").strip().lower() + + conn = get_db() + try: + exists = conn.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='ammo'" + ).fetchone() + if not exists: + return render_template("ammo.html", rounds=[], calibers=[], caliber=caliber_filter, q=search) + + calibers = [r["caliber"] for r in conn.execute( + "SELECT DISTINCT caliber FROM ammo WHERE caliber IS NOT NULL ORDER BY caliber" + ).fetchall()] + + query = "SELECT * FROM ammo WHERE 1=1" + params = [] + if caliber_filter: + query += " AND caliber = ?" + params.append(caliber_filter) + if search: + query += " AND (LOWER(name) LIKE ? OR LOWER(short_name) LIKE ?)" + params.extend([f"%{search}%", f"%{search}%"]) + query += " ORDER BY caliber, penetration_power DESC" + + rows = conn.execute(query, params).fetchall() + rounds = [dict(r) for r in rows] + finally: + conn.close() + + return render_template("ammo.html", rounds=rounds, calibers=calibers, caliber=caliber_filter, q=search) + + if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/import_ammo.py b/import_ammo.py new file mode 100644 index 0000000..20abc60 --- /dev/null +++ b/import_ammo.py @@ -0,0 +1,183 @@ +import requests +import sqlite3 +import sys + +DB_PATH = "tarkov.db" +API_URL = "https://api.tarkov.dev/graphql" + +GRAPHQL_QUERY = """ +{ + ammo { + item { + id + name + shortName + gridImageLink + wikiLink + } + caliber + damage + armorDamage + penetrationPower + penetrationChance + fragmentationChance + ricochetChance + initialSpeed + lightBleedModifier + heavyBleedModifier + projectileCount + tracer + tracerColor + ammoType + accuracyModifier + recoilModifier + staminaBurnPerDamage + } +} +""" + + +def fetch_ammo(): + response = requests.post( + API_URL, + json={"query": GRAPHQL_QUERY}, + timeout=30 + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + raise RuntimeError(data["errors"]) + + return data["data"]["ammo"] + + +def ensure_schema(conn): + conn.executescript(""" + CREATE TABLE IF NOT EXISTS ammo ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + short_name TEXT, + caliber TEXT, + damage INTEGER, + armor_damage INTEGER, + penetration_power INTEGER, + penetration_chance REAL, + fragmentation_chance REAL, + ricochet_chance REAL, + initial_speed REAL, + light_bleed_modifier REAL, + heavy_bleed_modifier REAL, + projectile_count INTEGER DEFAULT 1, + tracer INTEGER DEFAULT 0, + tracer_color TEXT, + ammo_type TEXT, + accuracy_modifier REAL, + recoil_modifier REAL, + stamina_burn_per_damage REAL, + grid_image_url TEXT, + wiki_url TEXT, + imported_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_ammo_caliber ON ammo(caliber); + CREATE INDEX IF NOT EXISTS idx_ammo_damage ON ammo(damage); + CREATE INDEX IF NOT EXISTS idx_ammo_pen ON ammo(penetration_power); + """) + conn.commit() + + +def upsert_ammo(conn, rounds): + inserted = 0 + updated = 0 + skipped = 0 + + cursor = conn.cursor() + + for r in rounds: + item = r.get("item") or {} + api_id = item.get("id") + name = item.get("name") + + if not api_id or not name: + skipped += 1 + continue + + row_data = ( + name, + item.get("shortName"), + r.get("caliber"), + r.get("damage"), + r.get("armorDamage"), + r.get("penetrationPower"), + r.get("penetrationChance"), + r.get("fragmentationChance"), + r.get("ricochetChance"), + r.get("initialSpeed"), + r.get("lightBleedModifier"), + r.get("heavyBleedModifier"), + r.get("projectileCount") or 1, + 1 if r.get("tracer") else 0, + r.get("tracerColor"), + r.get("ammoType"), + r.get("accuracyModifier"), + r.get("recoilModifier"), + r.get("staminaBurnPerDamage"), + item.get("gridImageLink"), + item.get("wikiLink"), + ) + + cursor.execute("SELECT id FROM ammo WHERE id = ?", (api_id,)) + exists = cursor.fetchone() + + if exists: + cursor.execute(""" + UPDATE ammo SET + name = ?, short_name = ?, caliber = ?, + damage = ?, armor_damage = ?, penetration_power = ?, + penetration_chance = ?, fragmentation_chance = ?, ricochet_chance = ?, + initial_speed = ?, light_bleed_modifier = ?, heavy_bleed_modifier = ?, + projectile_count = ?, tracer = ?, tracer_color = ?, ammo_type = ?, + accuracy_modifier = ?, recoil_modifier = ?, stamina_burn_per_damage = ?, + grid_image_url = ?, wiki_url = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """, (*row_data, api_id)) + updated += 1 + else: + cursor.execute(""" + INSERT INTO ammo ( + id, name, short_name, caliber, + damage, armor_damage, penetration_power, + penetration_chance, fragmentation_chance, ricochet_chance, + initial_speed, light_bleed_modifier, heavy_bleed_modifier, + projectile_count, tracer, tracer_color, ammo_type, + accuracy_modifier, recoil_modifier, stamina_burn_per_damage, + grid_image_url, wiki_url + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (api_id, *row_data)) + inserted += 1 + + conn.commit() + return inserted, updated, skipped + + +def main(): + print("Fetching ammo data from tarkov.dev...") + rounds = fetch_ammo() + print(f" Got {len(rounds)} rounds") + + conn = sqlite3.connect(DB_PATH) + try: + ensure_schema(conn) + inserted, updated, skipped = upsert_ammo(conn, rounds) + print(f" Inserted: {inserted} Updated: {updated} Skipped: {skipped}") + finally: + conn.close() + + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/tarkov.db b/tarkov.db index a4b6ac7..9001bba 100644 Binary files a/tarkov.db and b/tarkov.db differ diff --git a/templates/ammo.html b/templates/ammo.html new file mode 100644 index 0000000..d25f5f0 --- /dev/null +++ b/templates/ammo.html @@ -0,0 +1,587 @@ + + + + + Ammo Chart — OnlyScavs + + + + + + + + +
+ + + {% if not rounds %} +
+ No ammo data yet. + Run python import_ammo.py to import from tarkov.dev. +
+ {% else %} + +
+ + + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + +
NameCaliberDamagePenArmor DmgFrag %VelocityPelletsTracer
+
+ + {% endif %} +
+ +
+ + + + + diff --git a/templates/landing.html b/templates/landing.html index efe82b2..9792b4f 100644 --- a/templates/landing.html +++ b/templates/landing.html @@ -233,6 +233,7 @@ Loadout Injectors Barters + Ammo @@ -284,6 +285,12 @@
Calculate the true rouble cost of any barter deal.
Open →
+ +
+
Ammo Chart
+
Scatter plot of damage vs. armor penetration for every caliber.
+
Open →
+