Compare commits

..

2 Commits

Author SHA1 Message Date
serversdwn
90f2601c1d feat: add barter calculator page 2026-03-26 05:04:13 +00:00
serversdwn
7650633af4 chore: update gear db via tarkov.dev api pull. 2026-03-26 03:37:08 +00:00
10 changed files with 484 additions and 1 deletions

71
app.py
View File

@@ -945,5 +945,76 @@ def meds():
situations=situations) 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__": 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)

BIN
tarkov.db

Binary file not shown.

398
templates/barters.html Normal file
View File

@@ -0,0 +1,398 @@
<!doctype html>
<html>
<head>
<title>OnlyScavs Barter Calculator</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--bg: #121212;
--panel: #1a1a1a;
--panel2: #1e1e1e;
--text: #eee;
--muted: #888;
--border: #2a2a2a;
--accent: #9ccfff;
--accent2: #ffd580;
--good: #6ec96e;
--bad: #e06060;
--warn: #e0a040;
}
* { box-sizing: border-box; }
body {
font-family: sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
padding: 16px;
}
.page { max-width: 1100px; margin: 0 auto; }
nav {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 24px;
font-size: 0.88rem;
}
nav a {
color: var(--muted);
text-decoration: none;
padding: 4px 10px;
border: 1px solid var(--border);
border-radius: 4px;
}
nav a:hover { color: var(--accent); border-color: var(--accent); }
nav a.active { color: var(--accent); border-color: var(--accent); background: #1a2533; }
h1 { font-size: 1.4rem; margin: 0 0 4px; }
.subtitle { color: var(--muted); font-size: 0.88rem; margin: 0 0 20px; }
/* ── Filters ── */
.filters {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 20px;
}
.filters input[type=text] {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
padding: 6px 10px;
font-size: 0.88rem;
width: 220px;
}
.filters input[type=text]:focus { outline: none; border-color: var(--accent); }
.filters select {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
padding: 6px 10px;
font-size: 0.88rem;
}
.filters label { color: var(--muted); font-size: 0.82rem; }
/* ── Barter Cards ── */
.barter-list { display: flex; flex-direction: column; gap: 12px; }
.barter-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px 18px;
}
.barter-card.task-locked {
border-color: #3a3020;
}
.barter-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.reward-icon {
width: 40px;
height: 40px;
object-fit: contain;
background: #111;
border-radius: 4px;
border: 1px solid var(--border);
flex-shrink: 0;
}
.reward-info { flex: 1; min-width: 0; }
.reward-name {
font-size: 1rem;
font-weight: 700;
color: var(--accent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.reward-name a { color: inherit; text-decoration: none; }
.reward-name a:hover { text-decoration: underline; }
.reward-meta {
font-size: 0.78rem;
color: var(--muted);
margin-top: 2px;
}
.trader-badge {
font-size: 0.75rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 99px;
background: #1e2a1e;
color: var(--good);
border: 1px solid #2a3e2a;
white-space: nowrap;
}
.task-badge {
font-size: 0.72rem;
padding: 2px 8px;
border-radius: 99px;
background: #2a2010;
color: var(--warn);
border: 1px solid #3a3020;
white-space: nowrap;
}
/* ── Required items ── */
.required-label {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 8px;
}
.required-items {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.req-item {
display: flex;
align-items: center;
gap: 6px;
background: var(--panel2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
min-width: 160px;
}
.req-icon {
width: 30px;
height: 30px;
object-fit: contain;
background: #111;
border-radius: 3px;
flex-shrink: 0;
}
.req-icon-placeholder {
width: 30px;
height: 30px;
background: #222;
border-radius: 3px;
flex-shrink: 0;
}
.req-text { flex: 1; min-width: 0; }
.req-name {
font-size: 0.82rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.req-count { font-size: 0.75rem; color: var(--muted); }
.req-price-wrap {
display: flex;
align-items: center;
gap: 4px;
}
.req-price-input {
width: 90px;
background: #111;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.82rem;
padding: 3px 6px;
text-align: right;
}
.req-price-input:focus { outline: none; border-color: var(--accent); }
.req-price-unit { font-size: 0.72rem; color: var(--muted); }
/* ── Total cost row ── */
.total-row {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.total-label { font-size: 0.82rem; color: var(--muted); }
.total-cost {
font-size: 1.1rem;
font-weight: 700;
color: var(--accent2);
font-variant-numeric: tabular-nums;
}
.total-hint { font-size: 0.75rem; color: var(--muted); }
/* ── Empty state ── */
.empty { color: var(--muted); text-align: center; padding: 48px 0; font-size: 0.9rem; }
</style>
</head>
<body>
<div class="page">
<nav>
<a href="/">Home</a>
<a href="/keys">Keys</a>
<a href="/collector">Collector</a>
<a href="/quests">Quests</a>
<a href="/loadout">Loadout</a>
<a href="/meds">Injectors</a>
<a href="/barters" class="active">Barters</a>
</nav>
<h1>Barter Calculator</h1>
<p class="subtitle">Enter flea market prices for required items to see the total rouble cost of any barter.</p>
<div class="filters">
<div>
<label>Search</label><br>
<input type="text" id="search" placeholder="item name, trader…" oninput="applyFilters()">
</div>
<div>
<label>Trader</label><br>
<select id="traderFilter" onchange="applyFilters()">
<option value="">All traders</option>
{% set traders = barters | map(attribute='trader') | unique | sort %}
{% for t in traders %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
</select>
</div>
<div>
<label>LL (min)</label><br>
<select id="llFilter" onchange="applyFilters()">
<option value="0">Any</option>
<option value="1">LL1+</option>
<option value="2">LL2+</option>
<option value="3">LL3+</option>
<option value="4">LL4</option>
</select>
</div>
<div>
<label>Task locked</label><br>
<select id="taskFilter" onchange="applyFilters()">
<option value="">All</option>
<option value="no">No task required</option>
<option value="yes">Task required</option>
</select>
</div>
</div>
{% if barters %}
<div class="barter-list" id="barterList">
{% for b in barters %}
<div class="barter-card{% if b.task_unlock %} task-locked{% endif %}"
data-trader="{{ b.trader }}"
data-level="{{ b.level }}"
data-task="{{ 'yes' if b.task_unlock else 'no' }}"
data-search="{{ (b.reward_name ~ ' ' ~ b.trader ~ ' ' ~ (b.required | map(attribute='name') | join(' '))) | lower }}">
<div class="barter-header">
{% if b.reward_icon %}
<img class="reward-icon" src="{{ b.reward_icon }}" alt="{{ b.reward_short }}" loading="lazy">
{% endif %}
<div class="reward-info">
<div class="reward-name">
{% if b.reward_wiki %}
<a href="{{ b.reward_wiki }}" target="_blank" rel="noopener">{{ b.reward_name }}{% if b.reward_count > 1 %} ×{{ b.reward_count }}{% endif %}</a>
{% else %}
{{ b.reward_name }}{% if b.reward_count > 1 %} ×{{ b.reward_count }}{% endif %}
{% endif %}
</div>
<div class="reward-meta">{{ b.trader }} · LL{{ b.level }}</div>
</div>
<span class="trader-badge">{{ b.trader }} LL{{ b.level }}</span>
{% if b.task_unlock %}
<span class="task-badge" title="Requires task: {{ b.task_unlock }}">🔒 {{ b.task_unlock }}</span>
{% endif %}
</div>
<div class="required-label">Required items</div>
<div class="required-items">
{% for ri in b.required %}
<div class="req-item">
{% if ri.icon %}
<img class="req-icon" src="{{ ri.icon }}" alt="{{ ri.short }}" loading="lazy">
{% else %}
<div class="req-icon-placeholder"></div>
{% endif %}
<div class="req-text">
<div class="req-name" title="{{ ri.name }}">{{ ri.name }}</div>
<div class="req-count">× {{ ri.count }}</div>
</div>
<div class="req-price-wrap">
<input class="req-price-input"
type="number"
min="0"
step="1"
placeholder="price"
title="Price per unit in roubles"
data-count="{{ ri.count }}"
oninput="recalc(this)">
<span class="req-price-unit"></span>
</div>
</div>
{% endfor %}
</div>
<div class="total-row">
<span class="total-label">Total cost:</span>
<span class="total-cost" data-total="0"></span>
<span class="total-hint">Enter prices above to calculate</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty">No barter data available. Check your connection to tarkov.dev.</div>
{% endif %}
</div>
<script>
// Recalculate total cost for a barter card when any price input changes
function recalc(input) {
const card = input.closest('.barter-card');
const inputs = card.querySelectorAll('.req-price-input');
let total = 0;
let allFilled = true;
inputs.forEach(inp => {
const price = parseFloat(inp.value) || 0;
const count = parseInt(inp.dataset.count, 10) || 1;
if (inp.value.trim() === '') allFilled = false;
total += price * count;
});
const totalEl = card.querySelector('.total-cost');
const hintEl = card.querySelector('.total-hint');
if (total > 0) {
totalEl.textContent = total.toLocaleString() + ' ₽';
hintEl.textContent = allFilled ? 'all items priced' : 'some items unpriced';
} else {
totalEl.textContent = '—';
hintEl.textContent = 'Enter prices above to calculate';
}
}
// Filter cards by search text, trader, level, and task lock
function applyFilters() {
const search = document.getElementById('search').value.toLowerCase();
const trader = document.getElementById('traderFilter').value;
const ll = parseInt(document.getElementById('llFilter').value, 10) || 0;
const task = document.getElementById('taskFilter').value;
document.querySelectorAll('.barter-card').forEach(card => {
const matchSearch = !search || card.dataset.search.includes(search);
const matchTrader = !trader || card.dataset.trader === trader;
const matchLL = !ll || parseInt(card.dataset.level, 10) >= ll;
const matchTask = !task || card.dataset.task === task;
card.style.display = (matchSearch && matchTrader && matchLL && matchTask) ? '' : 'none';
});
}
</script>
</body>
</html>

View File

@@ -299,6 +299,8 @@
<a href="/loadout">Loadout Planner</a> <a href="/loadout">Loadout Planner</a>
&nbsp;|&nbsp; &nbsp;|&nbsp;
<a href="/meds">Injectors</a> <a href="/meds">Injectors</a>
&nbsp;|&nbsp;
<a href="/barters">Barters</a>
</nav> </nav>
<h1>Collector Checklist</h1> <h1>Collector Checklist</h1>
<p class="subtitle"> <p class="subtitle">

View File

@@ -176,6 +176,8 @@
<a href="/loadout">Loadout Planner</a> <a href="/loadout">Loadout Planner</a>
&nbsp;|&nbsp; &nbsp;|&nbsp;
<a href="/meds">Injectors</a> <a href="/meds">Injectors</a>
&nbsp;|&nbsp;
<a href="/barters">Barters</a>
</nav> </nav>
<h1>OnlyScavs Keys</h1> <h1>OnlyScavs Keys</h1>

View File

@@ -235,6 +235,7 @@
<a href="/quests">Quests</a> <a href="/quests">Quests</a>
<a href="/loadout">Loadout</a> <a href="/loadout">Loadout</a>
<a href="/meds">Injectors</a> <a href="/meds">Injectors</a>
<a href="/barters">Barters</a>
</nav> </nav>
<h1>Key Ratings</h1> <h1>Key Ratings</h1>

View File

@@ -114,6 +114,11 @@
<div class="card-title">Injectors</div> <div class="card-title">Injectors</div>
<div class="card-desc">Compare stim effects, skills, and side effects.</div> <div class="card-desc">Compare stim effects, skills, and side effects.</div>
</a> </a>
<a class="card" href="/barters">
<div class="card-icon">🔄</div>
<div class="card-title">Barter Calculator</div>
<div class="card-desc">Calculate the true rouble cost of any barter by entering flea market prices.</div>
</a>
</div> </div>
</body> </body>

View File

@@ -226,7 +226,8 @@
<a href="/keys">Keys</a> &nbsp;|&nbsp; <a href="/keys">Keys</a> &nbsp;|&nbsp;
<a href="/collector">Collector</a> &nbsp;|&nbsp; <a href="/collector">Collector</a> &nbsp;|&nbsp;
<a href="/quests">Quests</a> &nbsp;|&nbsp; <a href="/quests">Quests</a> &nbsp;|&nbsp;
<a href="/meds">Injectors</a> <a href="/meds">Injectors</a> &nbsp;|&nbsp;
<a href="/barters">Barters</a>
</nav> </nav>
<h1>Loadout Planner</h1> <h1>Loadout Planner</h1>
<p style="color:var(--muted);margin:0 0 16px;font-size:0.9rem;"> <p style="color:var(--muted);margin:0 0 16px;font-size:0.9rem;">

View File

@@ -253,6 +253,7 @@
<a href="/collector">Collector</a> <a href="/collector">Collector</a>
<a href="/loadout">Loadout</a> <a href="/loadout">Loadout</a>
<a href="/meds" class="active">Injectors</a> <a href="/meds" class="active">Injectors</a>
<a href="/barters">Barters</a>
</nav> </nav>
<h1>Injector Quick Reference</h1> <h1>Injector Quick Reference</h1>

View File

@@ -236,6 +236,8 @@
<a href="/loadout">Loadout Planner</a> <a href="/loadout">Loadout Planner</a>
&nbsp;|&nbsp; &nbsp;|&nbsp;
<a href="/meds">Injectors</a> <a href="/meds">Injectors</a>
&nbsp;|&nbsp;
<a href="/barters">Barters</a>
</nav> </nav>
<h1>Quest Trees</h1> <h1>Quest Trees</h1>