Files
onlyscavs/templates/loadout.html
serversdwn 84768ae587 Add Loadout Planner and Quest Trees templates
- Created loadout.html for a comprehensive loadout planner, allowing users to filter and view gear options across various categories including guns, armor, helmets, headwear, backpacks, and rigs.
- Implemented a build builder feature to calculate total loadout weight and save builds.
- Added quests.html to display quest trees with trader dependencies, filtering options, and quest completion tracking.
2026-02-22 08:51:28 +00:00

749 lines
28 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>
</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'),('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 .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;
let total = baseWeight;
let pills = '';
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.weight_kg != null) total += s.weight_kg;
pills += `<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>`;
}
inner.innerHTML = `
<div class="slot-summary">${pills}</div>
<div class="expand-footer">
<span class="expand-total">Lightest build: ${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 %}</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 %}</td>
</tr>
{% else %}
<tr><td colspan="6" class="empty">No rigs 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 %}
};
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) disp.textContent = id ? w.toFixed(3) + ' kg' : '';
total += w;
}
document.getElementById('total-weight').textContent = total.toFixed(3);
}
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),
('armor', 'Body Armor', builder_armor),
('helmet', 'Helmet', builder_helmets),
('rig', 'Chest Rig', builder_rigs),
('backpack', 'Backpack', builder_backpacks),
] %}
{% for slot_id, slot_label, items in slot_defs %}
<div class="slot-card">
<h3>{{ slot_label }}</h3>
<select id="slot_{{ slot_id }}" onchange="recalcWeight()">
<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){% endif %}
</option>
{% endfor %}
</select>
<div class="slot-weight" id="sw_{{ slot_id }}"></div>
</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>