Files
onlyscavs/templates/loadout.html
2026-03-01 21:49:32 +00:00

913 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html>
<head>
<title>OnlyScavs Loadout Planner</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;
--amber: #ffd580;
}
body {
font-family: sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
padding: 16px;
}
.page { max-width: 1100px; margin: 0 auto; }
h1 { margin-bottom: 4px; }
nav { margin-bottom: 20px; }
nav a { color: var(--accent); font-size: 0.9rem; }
a { color: var(--accent); }
/* Tab bar */
.tab-bar {
display: flex;
gap: 2px;
margin-bottom: 20px;
border-bottom: 2px solid var(--border);
flex-wrap: wrap;
}
.tab-bar a {
text-decoration: none;
color: var(--muted);
padding: 8px 16px;
font-size: 0.95rem;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.tab-bar a.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.tab-bar a:hover { color: var(--text); }
/* Filter bar */
.filter-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 16px;
}
.filter-bar label { color: var(--muted); font-size: 0.9rem; }
.filter-bar select, .filter-bar input[type=number] {
background: #222;
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 8px;
font-size: 0.9rem;
}
.filter-bar button {
background: #2a2a2a;
color: var(--text);
border: 1px solid #444;
border-radius: 6px;
padding: 4px 12px;
cursor: pointer;
font-size: 0.9rem;
}
.filter-bar button:hover { border-color: var(--accent); }
/* Slot filter checkboxes */
.slot-check {
display: inline-flex;
align-items: center;
gap: 5px;
background: #222;
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px 9px;
font-size: 0.85rem;
cursor: pointer;
color: var(--muted);
}
.slot-check.active {
border-color: var(--accent);
color: var(--accent);
}
.slot-check input { cursor: pointer; }
/* Gear table */
.gear-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
.gear-table th {
text-align: left;
padding: 6px 10px;
color: var(--muted);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
.gear-table td {
padding: 7px 10px;
border-bottom: 1px solid #1e1e1e;
vertical-align: middle;
}
.gear-table tr:hover td { background: #1c1c1c; }
.gear-table img {
width: 48px;
height: 48px;
border-radius: 4px;
background: #222;
object-fit: contain;
}
.w { font-weight: bold; color: var(--amber); white-space: nowrap; }
.muted { color: var(--muted); }
.name-cell strong { display: block; }
.name-cell small { color: var(--muted); font-size: 0.8rem; }
/* Armor class badges */
.cls {
display: inline-block;
padding: 2px 7px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: bold;
background: #2a2a2a;
border: 1px solid #444;
}
.cls-1, .cls-2 { border-color: #444; color: #aaa; }
.cls-3 { border-color: #5a7a3a; color: #8fc87f; }
.cls-4 { border-color: #3a6a8a; color: #7fc4e8; }
.cls-5 { border-color: #7a4a8a; color: #c090e0; }
.cls-6 { border-color: #8a4a3a; color: #e09070; }
/* Empty state */
.empty {
color: var(--muted);
padding: 28px 14px;
font-size: 0.95rem;
}
/* Build builder */
.builder-total {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px 24px;
text-align: center;
margin-bottom: 20px;
}
.builder-total .big { font-size: 2rem; font-weight: bold; color: var(--amber); }
.builder-total .label { color: var(--muted); font-size: 0.9rem; }
.builder-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.slot-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
}
.slot-card h3 {
margin: 0 0 8px;
font-size: 0.8rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.07em;
}
.slot-card select {
width: 100%;
background: #222;
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 5px 8px;
font-size: 0.85rem;
}
.slot-weight { margin-top: 6px; font-size: 0.85rem; color: var(--amber); min-height: 1.2em; }
.save-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.save-row input[type=text] {
background: #222;
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 5px 10px;
font-size: 0.9rem;
width: 200px;
}
.save-row button {
background: #2a2a2a;
color: var(--text);
border: 1px solid #444;
border-radius: 6px;
padding: 5px 14px;
cursor: pointer;
font-size: 0.9rem;
}
.save-row button:hover { border-color: var(--accent); }
.save-status { color: var(--muted); font-size: 0.85rem; }
</style>
</head>
<body>
<div class="page">
<nav>
<a href="/">← Keys</a> &nbsp;|&nbsp;
<a href="/collector">Collector</a> &nbsp;|&nbsp;
<a href="/meds">Injectors</a>
</nav>
<h1>Loadout Planner</h1>
<p style="color:var(--muted);margin:0 0 16px;font-size:0.9rem;">
Find the lightest gear for each slot. Filter by requirements.
</p>
<div class="tab-bar">
{% for t_id, t_label in [('guns','Guns'),('armor','Armor'),('helmets','Helmets'),('headwear','Headwear'),('backpacks','Backpacks'),('rigs','Rigs'),('plates','Plates'),('builder','Build Builder')] %}
<a href="/loadout?tab={{ t_id }}" class="{% if tab == t_id %}active{% endif %}">{{ t_label }}</a>
{% endfor %}
</div>
{# =============================== GUNS TAB =============================== #}
{% if tab == "guns" %}
<form method="get" class="filter-bar">
<input type="hidden" name="tab" value="guns">
<span style="color:var(--muted);font-size:0.85rem;">Must have slot:</span>
{% for label, nameid in slot_filters %}
<label class="slot-check {% if nameid in requires %}active{% endif %}">
<input type="checkbox" name="requires" value="{{ nameid }}"
{% if nameid in requires %}checked{% endif %}>
{{ label }}
</label>
{% endfor %}
<label>Sort</label>
<select name="sort">
<option value="weight_asc" {% if sort=='weight_asc' %}selected{% endif %}>Weight ↑</option>
<option value="weight_desc" {% if sort=='weight_desc' %}selected{% endif %}>Weight ↓</option>
<option value="name_asc" {% if sort=='name_asc' %}selected{% endif %}>Name A→Z</option>
</select>
<button type="submit">Filter</button>
{% if requires %}<a href="/loadout?tab=guns" style="font-size:0.85rem;color:var(--muted)">clear</a>{% endif %}
</form>
{% if requires %}
<p style="color:var(--muted);font-size:0.85rem;margin-bottom:12px;">
"Lightest build" = gun base weight + lightest compatible mod per required slot.
Guns without all required slots are hidden.
</p>
{% endif %}
<table class="gear-table">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Caliber</th>
<th>Ergo</th>
<th title="Vertical recoil">Recoil</th>
<th>Base weight</th>
<th>{% if requires %}Lightest build{% else %}Slots{% endif %}</th>
</tr>
</thead>
<tbody>
{% for gun in guns %}
<tr class="gun-row" data-gun-id="{{ gun.id }}" onclick="toggleGunRow(this)" style="cursor:pointer">
<td>
{% if gun.grid_image_url %}
<img src="{{ gun.grid_image_url }}" loading="lazy" alt="">
{% endif %}
</td>
<td class="name-cell">
<strong>{{ gun.short_name or gun.name }}</strong>
{% if gun.short_name and gun.short_name != gun.name %}
<small>{{ gun.name }}</small>
{% endif %}
</td>
<td class="muted">{{ gun.caliber or '—' }}</td>
<td class="muted">{{ gun.ergonomics or '—' }}</td>
<td class="muted">{{ gun.recoil_vertical or '—' }}</td>
<td class="w">
{% if gun.weight_kg is not none %}{{ "%.3f"|format(gun.weight_kg) }} kg{% else %}—{% endif %}
</td>
<td>
{% if requires %}
<span class="w">{% if gun.lightest_build_weight is not none %}{{ "%.3f"|format(gun.lightest_build_weight) }} kg{% else %}—{% endif %}</span>
{% else %}
<span class="muted" style="font-size:0.8rem">▶ expand</span>
{% endif %}
</td>
</tr>
<tr class="gun-expand-row" id="expand-{{ gun.id }}" style="display:none">
<td colspan="7" style="padding:0">
<div class="gun-expand-inner" style="padding:10px 14px;background:#151515;border-bottom:1px solid var(--border)">
<div class="expand-loading" style="color:var(--muted);font-size:0.85rem">Loading slots…</div>
</div>
</td>
</tr>
{% else %}
<tr><td colspan="7" class="empty">No guns found matching those requirements.</td></tr>
{% endfor %}
</tbody>
</table>
<style>
.gun-row:hover td { background: #1c1c1c; }
.gun-row.expanded td { background: #181818; }
.slot-summary { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.slot-pill {
display: flex; flex-direction: column;
background: #1e1e1e; border: 1px solid var(--border);
border-radius: 6px; padding: 5px 10px; font-size: 0.8rem; min-width: 110px;
}
.slot-pill.key { border-color: #5a7a3a; background: #141e10; }
.slot-pill.optional { opacity: 0.55; }
.slot-pill .sp-name { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; }
.slot-pill .sp-mod { font-size: 0.82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
.slot-pill .sp-w { color: var(--amber); font-size: 0.82rem; font-weight: bold; margin-top: 2px; }
.expand-footer { display: flex; align-items: center; gap: 14px; margin-top: 8px; padding-top: 8px; border-top: 1px solid #222; }
.expand-total { color: var(--amber); font-weight: bold; font-size: 0.9rem; }
.expand-link { font-size: 0.82rem; }
</style>
<script>
const _gunSlotCache = {};
function toggleGunRow(tr) {
const gunId = tr.dataset.gunId;
const expandRow = document.getElementById('expand-' + gunId);
const isOpen = expandRow.style.display !== 'none';
if (isOpen) {
expandRow.style.display = 'none';
tr.classList.remove('expanded');
return;
}
tr.classList.add('expanded');
expandRow.style.display = '';
if (_gunSlotCache[gunId]) {
renderGunExpand(gunId, _gunSlotCache[gunId]);
return;
}
fetch('/loadout/gun/' + gunId + '/slots.json')
.then(r => r.json())
.then(data => {
_gunSlotCache[gunId] = data;
renderGunExpand(gunId, data);
})
.catch(() => {
const inner = expandRow.querySelector('.gun-expand-inner');
inner.innerHTML = '<span style="color:var(--muted)">Failed to load slots.</span>';
});
}
function renderGunExpand(gunId, slots) {
const inner = document.getElementById('expand-' + gunId).querySelector('.gun-expand-inner');
if (!slots.length) {
inner.innerHTML = '<span style="color:var(--muted);font-size:0.85rem">No slot data available for this gun.</span>';
return;
}
const KEY = new Set(['mod_muzzle', 'mod_magazine']);
let baseWeight = 0;
const baseCell = document.querySelector('[data-gun-id="' + gunId + '"] td:nth-child(6)');
if (baseCell) baseWeight = parseFloat(baseCell.textContent) || 0;
// Only sum required slots for the lightest build weight
let total = baseWeight;
let reqPills = '';
let optPills = '';
for (const s of slots) {
const isKey = KEY.has(s.slot_nameid);
const w = s.weight_kg != null ? s.weight_kg.toFixed(3) + ' kg' : '—';
if (s.required) {
if (s.weight_kg != null) total += s.weight_kg;
reqPills += `<div class="slot-pill${isKey ? ' key' : ''}">
<span class="sp-name">${s.slot_name}</span>
<span class="sp-mod">${s.mod_name || '—'}</span>
<span class="sp-w">${w}</span>
</div>`;
} else {
optPills += `<div class="slot-pill optional">
<span class="sp-name">${s.slot_name}</span>
<span class="sp-mod">${s.mod_name || '—'}</span>
<span class="sp-w">${w}</span>
</div>`;
}
}
const optSection = optPills
? `<div style="font-size:0.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin:8px 0 4px">Optional slots (not included in weight)</div>
<div class="slot-summary">${optPills}</div>`
: '';
inner.innerHTML = `
<div class="slot-summary">${reqPills || '<span style="color:var(--muted);font-size:0.82rem">No required slots</span>'}</div>
${optSection}
<div class="expand-footer">
<span class="expand-total">Lightest build (required slots): ${total.toFixed(3)} kg</span>
<a class="expand-link" href="/loadout/gun/${gunId}">Full breakdown →</a>
</div>`;
}
</script>
{% endif %}
{# =============================== ARMOR TAB =============================== #}
{% if tab == "armor" %}
<form method="get" class="filter-bar">
<input type="hidden" name="tab" value="armor">
<label>Min class</label>
<select name="min_class">
<option value="0" {% if min_class==0 %}selected{% endif %}>Any</option>
{% for c in range(1,7) %}
<option value="{{ c }}" {% if min_class==c %}selected{% endif %}>Class {{ c }}+</option>
{% endfor %}
</select>
<label>Sort</label>
<select name="sort">
<option value="weight_asc" {% if sort=='weight_asc' %}selected{% endif %}>Weight ↑</option>
<option value="weight_desc" {% if sort=='weight_desc' %}selected{% endif %}>Weight ↓</option>
<option value="class_desc" {% if sort=='class_desc' %}selected{% endif %}>Class ↓</option>
<option value="class_asc" {% if sort=='class_asc' %}selected{% endif %}>Class ↑</option>
<option value="name_asc" {% if sort=='name_asc' %}selected{% endif %}>Name A→Z</option>
</select>
<button type="submit">Filter</button>
</form>
<table class="gear-table">
<thead>
<tr>
<th></th><th>Name</th><th>Class</th>
<th>Durability</th><th>Material</th><th>Zones</th><th>Weight</th>
</tr>
</thead>
<tbody>
{% for item in armor %}
<tr>
<td>{% if item.grid_image_url %}<img src="{{ item.grid_image_url }}" loading="lazy" alt="">{% endif %}</td>
<td class="name-cell">
<strong>{{ item.short_name or item.name }}</strong>
{% if item.short_name and item.short_name != item.name %}<small>{{ item.name }}</small>{% endif %}
{% if item.wiki_url %}<a href="{{ item.wiki_url }}" target="_blank" style="font-size:0.78rem;margin-left:4px">wiki</a>{% endif %}
</td>
<td>
{% if item.armor_class %}
<span class="cls cls-{{ item.armor_class }}">{{ item.armor_class }}</span>
{% else %}—{% endif %}
</td>
<td class="muted">{{ item.durability | int if item.durability else '—' }}</td>
<td class="muted">{{ item.material or '—' }}</td>
<td class="muted" style="font-size:0.8rem">{{ item.zones or '—' }}</td>
<td class="w">
{% if item.weight_kg is not none %}{{ "%.3f"|format(item.weight_kg) }} kg{% else %}—{% endif %}
{% if item.id in carrier_ids_with_open_slots %}<br><small class="muted">no plates</small>{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="7" class="empty">No armor found.</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# =============================== HELMETS TAB =============================== #}
{% if tab == "helmets" %}
<form method="get" class="filter-bar">
<input type="hidden" name="tab" value="helmets">
<label>Min class</label>
<select name="min_class">
<option value="0" {% if min_class==0 %}selected{% endif %}>Any</option>
{% for c in range(1,7) %}
<option value="{{ c }}" {% if min_class==c %}selected{% endif %}>Class {{ c }}+</option>
{% endfor %}
</select>
<label>Sort</label>
<select name="sort">
<option value="weight_asc" {% if sort=='weight_asc' %}selected{% endif %}>Weight ↑</option>
<option value="weight_desc" {% if sort=='weight_desc' %}selected{% endif %}>Weight ↓</option>
<option value="class_desc" {% if sort=='class_desc' %}selected{% endif %}>Class ↓</option>
<option value="class_asc" {% if sort=='class_asc' %}selected{% endif %}>Class ↑</option>
<option value="name_asc" {% if sort=='name_asc' %}selected{% endif %}>Name A→Z</option>
</select>
<button type="submit">Filter</button>
</form>
<table class="gear-table">
<thead>
<tr>
<th></th><th>Name</th><th>Class</th>
<th>Durability</th><th>Head zones</th><th>Deafening</th><th>Weight</th>
</tr>
</thead>
<tbody>
{% for item in helmets %}
<tr>
<td>{% if item.grid_image_url %}<img src="{{ item.grid_image_url }}" loading="lazy" alt="">{% endif %}</td>
<td class="name-cell">
<strong>{{ item.short_name or item.name }}</strong>
{% if item.short_name and item.short_name != item.name %}<small>{{ item.name }}</small>{% endif %}
{% if item.wiki_url %}<a href="{{ item.wiki_url }}" target="_blank" style="font-size:0.78rem;margin-left:4px">wiki</a>{% endif %}
</td>
<td>
{% if item.armor_class %}
<span class="cls cls-{{ item.armor_class }}">{{ item.armor_class }}</span>
{% else %}<span class="muted"></span>{% endif %}
</td>
<td class="muted">{{ item.durability | int if item.durability else '—' }}</td>
<td class="muted" style="font-size:0.8rem">{{ item.head_zones or '—' }}</td>
<td class="muted">{{ item.deafening or '—' }}</td>
<td class="w">{% if item.weight_kg is not none %}{{ "%.3f"|format(item.weight_kg) }} kg{% else %}—{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="7" class="empty">No helmets found.</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# =============================== HEADWEAR TAB =============================== #}
{% if tab == "headwear" %}
<form method="get" class="filter-bar">
<input type="hidden" name="tab" value="headwear">
<label>Min class</label>
<select name="min_class">
<option value="0" {% if min_class==0 %}selected{% endif %}>Any</option>
{% for c in range(1,7) %}
<option value="{{ c }}" {% if min_class==c %}selected{% endif %}>Class {{ c }}+</option>
{% endfor %}
</select>
<label>Sort</label>
<select name="sort">
<option value="weight_asc" {% if sort=='weight_asc' %}selected{% endif %}>Weight ↑</option>
<option value="weight_desc" {% if sort=='weight_desc' %}selected{% endif %}>Weight ↓</option>
<option value="class_desc" {% if sort=='class_desc' %}selected{% endif %}>Class ↓</option>
<option value="class_asc" {% if sort=='class_asc' %}selected{% endif %}>Class ↑</option>
<option value="name_asc" {% if sort=='name_asc' %}selected{% endif %}>Name A→Z</option>
</select>
<button type="submit">Filter</button>
</form>
<p style="color:var(--muted);font-size:0.85rem;margin-bottom:12px;">Face masks, armored masks, and non-helmet head protection. Does not cover the top of the head.</p>
<table class="gear-table">
<thead>
<tr>
<th></th><th>Name</th><th>Class</th>
<th>Durability</th><th>Head zones</th><th>Weight</th>
</tr>
</thead>
<tbody>
{% for item in headwear %}
<tr>
<td>{% if item.grid_image_url %}<img src="{{ item.grid_image_url }}" loading="lazy" alt="">{% endif %}</td>
<td class="name-cell">
<strong>{{ item.short_name or item.name }}</strong>
{% if item.short_name and item.short_name != item.name %}<small>{{ item.name }}</small>{% endif %}
{% if item.wiki_url %}<a href="{{ item.wiki_url }}" target="_blank" style="font-size:0.78rem;margin-left:4px">wiki</a>{% endif %}
</td>
<td>
{% if item.armor_class %}
<span class="cls cls-{{ item.armor_class }}">{{ item.armor_class }}</span>
{% else %}<span class="muted"></span>{% endif %}
</td>
<td class="muted">{{ item.durability | int if item.durability else '—' }}</td>
<td class="muted" style="font-size:0.8rem">{{ item.head_zones or '—' }}</td>
<td class="w">{% if item.weight_kg is not none %}{{ "%.3f"|format(item.weight_kg) }} kg{% else %}—{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="6" class="empty">No headwear found.</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# =============================== BACKPACKS TAB =============================== #}
{% if tab == "backpacks" %}
<form method="get" class="filter-bar">
<input type="hidden" name="tab" value="backpacks">
<label>Min slots</label>
<input type="number" name="min_capacity" value="{{ min_capacity or '' }}" min="0" placeholder="0" style="width:60px">
<label>Sort</label>
<select name="sort">
<option value="weight_asc" {% if sort=='weight_asc' %}selected{% endif %}>Weight ↑</option>
<option value="weight_desc" {% if sort=='weight_desc' %}selected{% endif %}>Weight ↓</option>
<option value="capacity_desc" {% if sort=='capacity_desc' %}selected{% endif %}>Capacity ↓</option>
<option value="capacity_asc" {% if sort=='capacity_asc' %}selected{% endif %}>Capacity ↑</option>
<option value="name_asc" {% if sort=='name_asc' %}selected{% endif %}>Name A→Z</option>
</select>
<button type="submit">Filter</button>
</form>
<table class="gear-table">
<thead>
<tr>
<th></th><th>Name</th><th>Capacity (slots)</th><th>Weight</th>
</tr>
</thead>
<tbody>
{% for item in backpacks %}
<tr>
<td>{% if item.grid_image_url %}<img src="{{ item.grid_image_url }}" loading="lazy" alt="">{% endif %}</td>
<td class="name-cell">
<strong>{{ item.short_name or item.name }}</strong>
{% if item.short_name and item.short_name != item.name %}<small>{{ item.name }}</small>{% endif %}
{% if item.wiki_url %}<a href="{{ item.wiki_url }}" target="_blank" style="font-size:0.78rem;margin-left:4px">wiki</a>{% endif %}
</td>
<td class="muted">{{ item.capacity or '—' }}</td>
<td class="w">{% if item.weight_kg is not none %}{{ "%.3f"|format(item.weight_kg) }} kg{% else %}—{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="4" class="empty">No backpacks found.</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# =============================== RIGS TAB =============================== #}
{% if tab == "rigs" %}
<form method="get" class="filter-bar">
<input type="hidden" name="tab" value="rigs">
<label>Min slots</label>
<input type="number" name="min_capacity" value="{{ min_capacity or '' }}" min="0" placeholder="0" style="width:60px">
<label>Min class</label>
<select name="min_class">
<option value="0" {% if min_class==0 %}selected{% endif %}>Any</option>
{% for c in range(1,7) %}
<option value="{{ c }}" {% if min_class==c %}selected{% endif %}>Class {{ c }}+</option>
{% endfor %}
</select>
<label>Sort</label>
<select name="sort">
<option value="weight_asc" {% if sort=='weight_asc' %}selected{% endif %}>Weight ↑</option>
<option value="weight_desc" {% if sort=='weight_desc' %}selected{% endif %}>Weight ↓</option>
<option value="capacity_desc" {% if sort=='capacity_desc' %}selected{% endif %}>Capacity ↓</option>
<option value="class_desc" {% if sort=='class_desc' %}selected{% endif %}>Class ↓</option>
<option value="name_asc" {% if sort=='name_asc' %}selected{% endif %}>Name A→Z</option>
</select>
<button type="submit">Filter</button>
</form>
<table class="gear-table">
<thead>
<tr>
<th></th><th>Name</th><th>Class</th><th>Capacity (slots)</th><th>Zones</th><th>Weight</th>
</tr>
</thead>
<tbody>
{% for item in rigs %}
<tr>
<td>{% if item.grid_image_url %}<img src="{{ item.grid_image_url }}" loading="lazy" alt="">{% endif %}</td>
<td class="name-cell">
<strong>{{ item.short_name or item.name }}</strong>
{% if item.short_name and item.short_name != item.name %}<small>{{ item.name }}</small>{% endif %}
{% if item.wiki_url %}<a href="{{ item.wiki_url }}" target="_blank" style="font-size:0.78rem;margin-left:4px">wiki</a>{% endif %}
</td>
<td>
{% if item.armor_class %}
<span class="cls cls-{{ item.armor_class }}">{{ item.armor_class }}</span>
{% else %}<span class="muted"></span>{% endif %}
</td>
<td class="muted">{{ item.capacity or '—' }}</td>
<td class="muted" style="font-size:0.8rem">{{ item.zones or '—' }}</td>
<td class="w">
{% if item.weight_kg is not none %}{{ "%.3f"|format(item.weight_kg) }} kg{% else %}—{% endif %}
{% if item.id in carrier_ids_with_open_slots %}<br><small class="muted">no plates</small>{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="empty">No rigs found.</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# =============================== PLATES TAB =============================== #}
{% if tab == "plates" %}
<p style="color:var(--muted);font-size:0.85rem;margin-bottom:12px;">
Armor plates that slot into plate carriers. Carrier shell weight does <em>not</em> include plates — add them separately when building your loadout.
</p>
<form method="get" class="filter-bar">
<input type="hidden" name="tab" value="plates">
<label>Min class</label>
<select name="min_class">
<option value="0" {% if min_class==0 %}selected{% endif %}>Any</option>
{% for c in range(1,7) %}
<option value="{{ c }}" {% if min_class==c %}selected{% endif %}>Class {{ c }}+</option>
{% endfor %}
</select>
<label>Sort</label>
<select name="sort">
<option value="weight_asc" {% if sort=='weight_asc' %}selected{% endif %}>Weight ↑</option>
<option value="weight_desc" {% if sort=='weight_desc' %}selected{% endif %}>Weight ↓</option>
<option value="class_desc" {% if sort=='class_desc' %}selected{% endif %}>Class ↓</option>
<option value="class_asc" {% if sort=='class_asc' %}selected{% endif %}>Class ↑</option>
<option value="name_asc" {% if sort=='name_asc' %}selected{% endif %}>Name A→Z</option>
</select>
<button type="submit">Filter</button>
</form>
<table class="gear-table">
<thead>
<tr>
<th></th><th>Name</th><th>Class</th>
<th>Durability</th><th>Material</th><th>Zones</th><th>Weight</th>
</tr>
</thead>
<tbody>
{% for item in plates %}
<tr>
<td>{% if item.grid_image_url %}<img src="{{ item.grid_image_url }}" loading="lazy" alt="">{% endif %}</td>
<td class="name-cell">
<strong>{{ item.short_name or item.name }}</strong>
{% if item.short_name and item.short_name != item.name %}<small>{{ item.name }}</small>{% endif %}
{% if item.wiki_url %}<a href="{{ item.wiki_url }}" target="_blank" style="font-size:0.78rem;margin-left:4px">wiki</a>{% endif %}
</td>
<td>
{% if item.armor_class %}
<span class="cls cls-{{ item.armor_class }}">{{ item.armor_class }}</span>
{% else %}—{% endif %}
</td>
<td class="muted">{{ item.durability | int if item.durability else '—' }}</td>
<td class="muted">{{ item.material or '—' }}</td>
<td class="muted" style="font-size:0.8rem">{{ item.zones or '—' }}</td>
<td class="w">{% if item.weight_kg is not none %}{{ "%.3f"|format(item.weight_kg) }} kg{% else %}—{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="7" class="empty">No plates found.</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# =============================== BUILD BUILDER TAB =============================== #}
{% if tab == "builder" %}
<script>
const WEIGHTS = {
{% for item in builder_guns + builder_armor + builder_helmets + builder_rigs + builder_backpacks %}
"{{ item.id }}": {{ item.weight_kg if item.weight_kg is not none else 0 }},
{% endfor %}
};
// carriers that have open plate slots (shell weight only)
const CARRIERS_WITH_OPEN_SLOTS = new Set({{ carrier_ids_with_open_slots | list | tojson }});
// plate weight cache: id -> weight_kg
const PLATE_WEIGHTS = {};
// currently selected plate weights per open slot, keyed by "carrierSlot|slotNameId"
const _plateSlotWeights = {};
function recalcWeight() {
const slots = ['gun', 'armor', 'helmet', 'rig', 'backpack'];
let total = 0;
for (const slot of slots) {
const sel = document.getElementById('slot_' + slot);
const id = sel ? sel.value : '';
const w = id ? (WEIGHTS[id] || 0) : 0;
const disp = document.getElementById('sw_' + slot);
if (disp) {
if (id) {
const isCarrier = CARRIERS_WITH_OPEN_SLOTS.has(id);
disp.textContent = w.toFixed(3) + ' kg' + (isCarrier ? ' (shell only)' : '');
} else {
disp.textContent = '';
}
}
total += w;
}
// Add plate weights
for (const pw of Object.values(_plateSlotWeights)) {
total += pw;
}
document.getElementById('total-weight').textContent = total.toFixed(3);
}
const _carrierSlotCache = {};
function onCarrierChange(slot) {
const sel = document.getElementById('slot_' + slot);
const carrierId = sel ? sel.value : '';
const container = document.getElementById('plates_' + slot);
container.innerHTML = '';
// Clear plate slot weights for this carrier slot
for (const key of Object.keys(_plateSlotWeights)) {
if (key.startsWith(slot + '|')) delete _plateSlotWeights[key];
}
if (!carrierId || !CARRIERS_WITH_OPEN_SLOTS.has(carrierId)) {
recalcWeight();
return;
}
if (_carrierSlotCache[carrierId]) {
renderPlateSlots(slot, carrierId, _carrierSlotCache[carrierId]);
return;
}
fetch('/loadout/carrier/' + carrierId + '/slots.json')
.then(r => r.json())
.then(data => {
_carrierSlotCache[carrierId] = data;
renderPlateSlots(slot, carrierId, data);
});
}
function renderPlateSlots(carrierSlot, carrierId, slots) {
const container = document.getElementById('plates_' + carrierSlot);
container.innerHTML = '';
for (const slot of slots) {
const key = carrierSlot + '|' + slot.slot_nameid;
const label = document.createElement('label');
label.style.cssText = 'display:block;margin-top:8px;font-size:0.82rem;color:var(--muted)';
label.textContent = slot.slot_nameid.replace(/_/g, ' ') + (slot.zones ? ' (' + slot.zones + ')' : '');
const sel = document.createElement('select');
sel.style.cssText = 'width:100%;margin-top:2px';
const none = document.createElement('option');
none.value = '';
none.textContent = '— No plate —';
sel.appendChild(none);
for (const p of slot.plates) {
PLATE_WEIGHTS[p.id] = p.weight_kg || 0;
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = (p.short_name || p.name) +
' (Cls ' + (p.armor_class || '?') + ', ' +
(p.weight_kg != null ? p.weight_kg.toFixed(3) : '?') + ' kg)';
sel.appendChild(opt);
}
sel.addEventListener('change', () => {
const pid = sel.value;
_plateSlotWeights[key] = pid ? (PLATE_WEIGHTS[pid] || 0) : 0;
recalcWeight();
});
container.appendChild(label);
container.appendChild(sel);
_plateSlotWeights[key] = 0;
}
recalcWeight();
}
function saveBuild() {
const slots = ['gun', 'armor', 'helmet', 'rig', 'backpack'];
const payload = { name: document.getElementById('build-name').value.trim() || 'My Build' };
for (const s of slots) {
const sel = document.getElementById('slot_' + s);
payload[s + '_id'] = (sel && sel.value) ? sel.value : null;
}
fetch('/loadout/save-build', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(d => {
document.getElementById('save-status').textContent = 'Saved as "' + d.name + '" (build #' + d.build_id + ')';
})
.catch(() => {
document.getElementById('save-status').textContent = 'Error saving build.';
});
}
</script>
<div class="builder-total">
<div class="label">Total loadout weight</div>
<div class="big"><span id="total-weight">0.000</span> kg</div>
</div>
<div class="builder-grid">
{% set slot_defs = [
('gun', 'Primary Weapon', builder_guns, false),
('armor', 'Body Armor', builder_armor, true),
('helmet', 'Helmet', builder_helmets, false),
('rig', 'Chest Rig', builder_rigs, true),
('backpack', 'Backpack', builder_backpacks, false),
] %}
{% for slot_id, slot_label, items, has_plates in slot_defs %}
<div class="slot-card">
<h3>{{ slot_label }}</h3>
<select id="slot_{{ slot_id }}" onchange="{% if has_plates %}onCarrierChange('{{ slot_id }}'){% else %}recalcWeight(){% endif %}">
<option value="">— None —</option>
{% for item in items %}
<option value="{{ item.id }}">
{{ item.name }}{% if item.weight_kg is not none %} ({{ "%.3f"|format(item.weight_kg) }} kg{% if item.id in carrier_ids_with_open_slots %} shell{% endif %}){% endif %}
</option>
{% endfor %}
</select>
<div class="slot-weight" id="sw_{{ slot_id }}"></div>
{% if has_plates %}<div id="plates_{{ slot_id }}"></div>{% endif %}
</div>
{% endfor %}
</div>
<div class="save-row">
<input type="text" id="build-name" placeholder="Build name…">
<button onclick="saveBuild()">Save Build</button>
<span class="save-status" id="save-status"></span>
</div>
{% endif %}
</div>
</body>
</html>