// crafts.js (() => { const API = 'https://api.tarkov.dev/graphql'; // official GraphQL endpoint (playground at /) // ---------- GraphQL ---------- // 1) All crafts: structure only (station, required items, rewards). // NOTE: No variables here—keeps it compatible and avoids "unused variable" errors. const CRAFTS_Q = ` query AllCrafts { crafts { id duration station { name } # station.level is not in the public schema we saw—just show the name. requiredItems { count item { id name } } rewardItems { count item { id name } } } } `; // 2) Items by IDs with prices for a specific mode (regular/pve) // Official examples show avg24hPrice and sellFor { price, source } on items. const ITEMS_BY_IDS_Q = ` query ItemsByIds($ids: [ID!], $mode: GameMode, $lang: LanguageCode) { items(ids: $ids, gameMode: $mode, lang: $lang) { id name avg24hPrice sellFor { price source } } } `; // ---------- DOM ---------- const modeRadios = Array.from(document.querySelectorAll('input[name="hc-mode"]')); const metricSel = document.getElementById('hc-metric'); const topNInput = document.getElementById('hc-topn'); const refreshBtn = document.getElementById('hc-refresh'); const updatedEl = document.getElementById('hc-updated'); const statusEl = document.getElementById('hc-status'); const tbody = document.getElementById('hc-tbody'); const chartCanvas = document.getElementById('hc-chart'); const applyFeeChk = document.getElementById('hc-applyfee'); const feePctInput = document.getElementById('hc-fee'); let chart; let craftsCache = []; let pricesCache = new Map(); // id -> item pricing record // ---------- Helpers ---------- const fmt = n => '₽' + new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(Math.round(n || 0)); const nowStr = () => new Date().toLocaleTimeString(); const getMode = () => (modeRadios.find(r => r.checked)?.value) || 'regular'; function fleaPriceFromSellFor(sellFor) { if (!Array.isArray(sellFor)) return 0; const flea = sellFor.find(x => x?.source === 'Flea Market'); return Number(flea?.price || 0); } function getUnitPrice(itemRec, metric) { if (!itemRec) return 0; if (metric === 'avg24hPrice') return Number(itemRec.avg24hPrice || 0); // default flea: try sellFor(Flea Market), fallback to avg24h return fleaPriceFromSellFor(itemRec.sellFor) || Number(itemRec.avg24hPrice || 0); } function netAfterFee(gross, applyFee, feePct) { if (!applyFee) return gross; const pct = Math.max(0, Math.min(100, Number(feePct || 0))); return gross * (1 - pct / 100); } function collectIds(crafts) { const ids = new Set(); for (const c of crafts) { for (const ri of (c.requiredItems || [])) ids.add(ri.item?.id); for (const ro of (c.rewardItems || [])) ids.add(ro.item?.id); } ids.delete(undefined); return Array.from(ids); } async function gql(query, variables) { const resp = await fetch(API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ query, variables }) }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const json = await resp.json(); if (json.errors) throw new Error(json.errors[0]?.message || 'GraphQL error'); return json.data; } async function fetchCrafts() { // Fix: no variables passed—prevents "Variable $lang is never used" error. const data = await gql(CRAFTS_Q); return data?.crafts ?? []; } async function fetchItemsByIds(ids, mode) { // batch to avoid giant payloads const out = new Map(); const BATCH = 80; for (let i = 0; i < ids.length; i += BATCH) { const slice = ids.slice(i, i + BATCH); const data = await gql(ITEMS_BY_IDS_Q, { ids: slice, mode, lang: 'en' }); for (const it of data?.items || []) out.set(it.id, it); } return out; } function summarize(crafts, metric, applyFee, feePct) { const rows = []; for (const c of crafts) { // Inputs (cost) const inputs = (c.requiredItems || []).map(ri => { const rec = pricesCache.get(ri.item?.id); const unit = getUnitPrice(rec, metric); return { name: ri.item?.name || 'Unknown', count: ri.count || 0, unit, total: unit * (ri.count || 0) }; }); // Outputs (sell) const outputs = (c.rewardItems || []).map(ro => { const rec = pricesCache.get(ro.item?.id); const unit = getUnitPrice(rec, metric); return { name: ro.item?.name || 'Unknown', count: ro.count || 0, unit, total: unit * (ro.count || 0) }; }); const cost = inputs.reduce((s, x) => s + x.total, 0); const sellGross = outputs.reduce((s, x) => s + x.total, 0); const sellNet = netAfterFee(sellGross, applyFee, feePct); const profit = sellNet - cost; // Only station name (no level), because station.level isn't public in the docs/examples we referenced. const stationLabel = c.station?.name || 'Station'; let craftName = 'Unknown craft'; if (outputs.length === 1) craftName = `${outputs[0].name} ×${outputs[0].count}`; else if (outputs.length > 1) craftName = `${outputs[0].name} ×${outputs[0].count} (+${outputs.length - 1} other)`; rows.push({ id: c.id, craft: craftName, station: stationLabel, cost, sellGross, sellNet, profit, inputs }); } rows.sort((a, b) => b.profit - a.profit); return rows; } function renderTable(rows) { tbody.innerHTML = ''; if (!rows.length) { tbody.innerHTML = `