Files
dwroller/public/print.html
Alex e511bff882 Add weapon images, full rule scraping, and expanded quick reference
- scripts/generate-weapon-images.js: AI image generation for weapon cards
  supporting HuggingFace FLUX.1-schnell, OpenAI DALL-E 3, and Gemini Imagen;
  images served from public/weapon-images/ via new static route in server.js
- public/cards.html: load manifest.json and show AI art on weapon cards
- scripts/scrape-rules-from-pdfs.py: PyMuPDF scraper extracting full rule
  text from all five rulebook PDFs with bold-span heading detection
- scripts/tag-and-dedup-rules.js: standalone dedup helper (JS-side grouping)
- database/routes/rulesRoutes.js: remove content truncation in search/random,
  add admin dedup endpoint (JS-side, fast), fix RulesTab to fetch full content
- src/components/RulesTab.jsx: fetch full rule content on modal open
- public/print.html: expanded quick reference with pre-gen character stat
  blocks (Sepheran + Lucian), Astartes traits sheet with correct derived SBs
  and TBs, full Critical Hit Tables for all 4 locations × 4 damage types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 22:50:50 +01:00

1037 lines
49 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 lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Deathwatch Quick Reference Print</title>
<style>
/* ── Screen styles ── */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Georgia', serif;
font-size: 10pt;
color: #111;
background: #f4f4f4;
padding: 20px;
}
h1 {
font-size: 22pt;
text-align: center;
letter-spacing: 3px;
text-transform: uppercase;
border-bottom: 3px double #000;
padding-bottom: 6px;
margin-bottom: 4px;
}
.subtitle {
text-align: center;
font-size: 9pt;
color: #555;
margin-bottom: 16px;
}
.sheet {
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
padding: 18px 20px;
margin-bottom: 24px;
page-break-inside: avoid;
}
.sheet h2 {
font-size: 13pt;
text-transform: uppercase;
letter-spacing: 2px;
border-bottom: 2px solid #000;
padding-bottom: 4px;
margin-bottom: 10px;
}
.sheet h3 {
font-size: 10pt;
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 1px solid #999;
padding-bottom: 2px;
margin: 10px 0 6px;
}
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
/* ── Tables ── */
table {
width: 100%;
border-collapse: collapse;
font-size: 8.5pt;
}
th {
background: #222;
color: #fff;
font-family: 'Arial Narrow', Arial, sans-serif;
font-size: 8pt;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 4px 6px;
text-align: left;
white-space: nowrap;
}
td {
padding: 3px 6px;
border-bottom: 1px solid #e0e0e0;
vertical-align: top;
}
tr:nth-child(even) td { background: #f7f7f7; }
tr:hover td { background: #e8f0fe; }
/* ── Reference boxes ── */
.ref-box {
border: 1px solid #bbb;
border-radius: 3px;
padding: 8px 10px;
margin-bottom: 8px;
}
.ref-box .label {
font-weight: bold;
font-size: 9pt;
text-transform: uppercase;
letter-spacing: 0.5px;
display: block;
margin-bottom: 3px;
}
.ref-box p, .ref-box li { font-size: 8.5pt; line-height: 1.4; }
.ref-box ul { padding-left: 14px; }
.highlight { background: #fffbe6; border-color: #e0c030; }
.dark-box { background: #222; color: #fff; border-color: #000; }
.dark-box .label { color: #f0c040; }
/* ── Stat grid ── */
.stat-row {
display: grid;
grid-template-columns: repeat(9, 1fr);
gap: 4px;
margin: 6px 0;
}
.stat-cell {
border: 1px solid #999;
text-align: center;
padding: 4px 2px;
}
.stat-cell .abbr { font-weight: bold; font-size: 8pt; display: block; border-bottom: 1px solid #ccc; padding-bottom: 2px; margin-bottom: 2px; }
.stat-cell .val { font-size: 9pt; }
.type-e { color: #c00; }
.type-r { color: #b04000; }
.type-i { color: #00c; }
.type-x { color: #080; }
.footer { text-align: center; font-size: 7.5pt; color: #999; margin-top: 8px; }
/* ── Print overrides ── */
@media print {
body { background: #fff; padding: 0; font-size: 9pt; }
.sheet { border: none; border-radius: 0; margin-bottom: 0; padding: 10px 14px; page-break-after: always; }
.sheet:last-child { page-break-after: avoid; }
h1 { font-size: 18pt; }
.no-print { display: none !important; }
th { background: #000 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
tr:nth-child(even) td { background: #f5f5f5 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
.btn {
display: inline-block;
padding: 10px 24px;
background: #1a3a6a;
color: #fff;
border: none;
border-radius: 4px;
font-size: 11pt;
cursor: pointer;
margin: 0 8px 20px;
font-family: Arial, sans-serif;
}
.btn:hover { background: #2a5aaa; }
.btn-outline {
background: transparent;
border: 2px solid #1a3a6a;
color: #1a3a6a;
}
.btn-outline:hover { background: #eef; }
.controls { text-align: center; margin-bottom: 16px; }
#loading { text-align:center; color:#666; padding:12px; font-style:italic; }
</style>
</head>
<body>
<div class="controls no-print">
<h1 style="border:none;margin-bottom:8px;">⚔ Deathwatch Quick Reference</h1>
<p style="color:#555;margin-bottom:16px;font-size:10pt;">Printable reference sheets for the Deathwatch RPG</p>
<button class="btn" onclick="window.print()">🖨 Print All Sheets</button>
<button class="btn btn-outline" onclick="loadWeapons()">↺ Reload Weapons</button>
</div>
<!-- ════════════════════════════════════════════════════════════
SHEET 1 CORE DICE MECHANICS
═════════════════════════════════════════════════════════════ -->
<div class="sheet" id="sheet-mechanics">
<h2>Core Mechanics Quick Reference</h2>
<div class="two-col">
<div>
<h3>Making a Test</h3>
<div class="ref-box highlight">
<span class="label">Standard Test</span>
<ul>
<li>Roll d100 (percentile dice)</li>
<li><strong>Success:</strong> result ≤ characteristic or skill value</li>
<li><strong>Failure:</strong> result > characteristic or skill value</li>
<li>Natural <strong>01</strong> = automatic success (Righteous Fury possible)</li>
<li>Natural <strong>100</strong> = automatic failure (Perils of Warp if psyker)</li>
</ul>
</div>
<div class="ref-box">
<span class="label">Degrees of Success / Failure</span>
<ul>
<li>DoS/DoF = how many full 10s you beat or miss by</li>
<li>Roll 22 vs BS 48 (Sepheran) → beat by 26 → <strong>2 DoS</strong></li>
<li>Roll 73 vs WS 46 (Lucian) → miss by 27 → <strong>2 DoF</strong></li>
<li>Each DoS on Full Auto = 1 extra hit (up to RoF)</li>
<li>Each DoS on Semi Auto = 1 extra hit (up to RoF1)</li>
</ul>
</div>
<div class="ref-box">
<span class="label">Opposed Tests</span>
<ul>
<li>Both sides roll and compare DoS</li>
<li>Most DoS wins. Ties go to the initiating party</li>
</ul>
</div>
<div class="ref-box">
<span class="label">Extended Tests</span>
<ul>
<li>Accumulate a target number of DoS over multiple rolls</li>
<li>Each failure reduces accumulated DoS by 1</li>
</ul>
</div>
</div>
<div>
<h3>Rolling Damage</h3>
<div class="ref-box dark-box">
<span class="label">Step-by-Step Damage</span>
<ol style="padding-left:16px;">
<li style="margin-bottom:4px">Roll weapon damage dice (e.g. 1d10+5)</li>
<li style="margin-bottom:4px">Add Strength Bonus for melee (SB = S ÷ 10, round down — brothers in PA: S 61 → SB 6 × 2 = <strong>SB 12</strong>)</li>
<li style="margin-bottom:4px">Subtract target's Armour Points (Armour) for the location</li>
<li style="margin-bottom:4px">Subtract target's Toughness Bonus (TB = T ÷ 10 × Unnatural — brothers: <strong>TB 8</strong>)</li>
<li style="margin-bottom:4px">Remainder = Wounds lost (minimum 1 if hit landed)</li>
</ol>
<p style="margin-top:6px;font-size:8pt;color:#ccc;">
<strong>Formula:</strong> Damage Armour TB = Wounds<br>
Brothers in PA: Damage <strong>Armour 810</strong> <strong>TB 8</strong> = Wounds
</p>
</div>
<div class="ref-box">
<span class="label">Damage Types</span>
<ul>
<li><span class="type-e"><strong>E</strong> Energy:</span> ignores armour not rated for it; burns</li>
<li><span class="type-r"><strong>R</strong> Rending:</span> extra pen on max die roll (Tearing)</li>
<li><span class="type-i"><strong>I</strong> Impact:</span> blunt force; causes stunning</li>
<li><span class="type-x"><strong>X</strong> Explosive:</span> bolt rounds; scatter damage</li>
</ul>
</div>
<div class="ref-box">
<span class="label">Penetration (Pen)</span>
<ul>
<li>Pen reduces effective Armour by that amount</li>
<li>Cannot reduce Armour below 0</li>
<li><strong>Power Field:</strong> on max die result, Armour = 0</li>
</ul>
</div>
<div class="ref-box highlight">
<span class="label">Righteous Fury (natural 1 to hit)</span>
<ul>
<li>Re-roll damage, add to original result</li>
<li>On a 10 on the re-roll, roll again on the Critical table</li>
<li>Only applies to damage dice (not fixed bonuses)</li>
</ul>
</div>
</div>
</div>
<h3>Hit Locations (d100)</h3>
<table>
<thead>
<tr>
<th>Roll</th><th>Location</th>
<th>Roll</th><th>Location</th>
<th>Roll</th><th>Location</th>
<th>Roll</th><th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>0110</td><td>Head</td>
<td>1120</td><td>Right Arm</td>
<td>2130</td><td>Left Arm</td>
<td>3170</td><td>Body</td>
</tr>
<tr>
<td>7185</td><td>Right Leg</td>
<td>8600</td><td>Left Leg</td>
<td colspan="4" style="color:#666;font-style:italic">Aim at location: add/subtract 20 to roll to aim at specific location</td>
</tr>
</tbody>
</table>
</div>
<!-- ════════════════════════════════════════════════════════════
SHEET 1b PRE-GENERATED CHARACTERS
═════════════════════════════════════════════════════════════ -->
<div class="sheet" id="sheet-chars">
<h2>Pre-Generated Characters</h2>
<div class="two-col">
<div>
<h3>Brother Sepheran — Blood Angels Devastator</h3>
<div class="stat-row">
<div class="stat-cell"><span class="abbr">WS</span><span class="val">47</span></div>
<div class="stat-cell"><span class="abbr">BS</span><span class="val">48</span></div>
<div class="stat-cell"><span class="abbr">S</span><span class="val">41</span></div>
<div class="stat-cell"><span class="abbr">T</span><span class="val">43</span></div>
<div class="stat-cell"><span class="abbr">Ag</span><span class="val">42</span></div>
<div class="stat-cell"><span class="abbr">Int</span><span class="val">50</span></div>
<div class="stat-cell"><span class="abbr">Per</span><span class="val">43</span></div>
<div class="stat-cell"><span class="abbr">WP</span><span class="val">48</span></div>
<div class="stat-cell"><span class="abbr">Fel</span><span class="val">38</span></div>
</div>
<div class="ref-box" style="margin-top:6px">
<span class="label">Derived Stats</span>
<ul>
<li>SB 4 · TB 4 · AB 4</li>
<li>Move: 4 / 8 / 12 / 24 (Half/Full/Charge/Run)</li>
<li>Wounds: 20 (Space Marine base + TB bonus)</li>
</ul>
</div>
<div class="ref-box">
<span class="label">Gear</span>
<p>Astartes Power Armour · Frag ×6 · Krak ×6 · Combat Knife</p>
</div>
</div>
<div>
<h3>Brother Lucian — Ultramarines Apothecary</h3>
<div class="stat-row">
<div class="stat-cell"><span class="abbr">WS</span><span class="val">46</span></div>
<div class="stat-cell"><span class="abbr">BS</span><span class="val">40</span></div>
<div class="stat-cell"><span class="abbr">S</span><span class="val">41</span></div>
<div class="stat-cell"><span class="abbr">T</span><span class="val">40</span></div>
<div class="stat-cell"><span class="abbr">Ag</span><span class="val">43</span></div>
<div class="stat-cell"><span class="abbr">Int</span><span class="val">43</span></div>
<div class="stat-cell"><span class="abbr">Per</span><span class="val">41</span></div>
<div class="stat-cell"><span class="abbr">WP</span><span class="val">42</span></div>
<div class="stat-cell"><span class="abbr">Fel</span><span class="val">43</span></div>
</div>
<div class="ref-box" style="margin-top:6px">
<span class="label">Derived Stats</span>
<ul>
<li>SB 4 · TB 4 · AB 4</li>
<li>Move: 4 / 8 / 12 / 24 (Half/Full/Charge/Run)</li>
<li>Wounds: 20 (Space Marine base + TB bonus)</li>
</ul>
</div>
<div class="ref-box">
<span class="label">Gear</span>
<p>Astartes Power Armour · Frag ×6 · Krak ×6 · Combat Knife</p>
</div>
</div>
</div>
<div class="ref-box highlight" style="margin-top:10px">
<span class="label">Astartes Power Armour (worn by both brothers)</span>
<table style="margin-top:4px">
<thead><tr><th>Location</th><th>Head</th><th>Arms</th><th>Body</th><th>Legs</th></tr></thead>
<tbody><tr><td><strong>Armour</strong></td><td>8</td><td>8</td><td>10</td><td>8</td></tr></tbody>
</table>
<p style="margin-top:4px;font-size:8pt">Unnatural Toughness ×2 applies (T bonus doubled for soak). Counts as Powered — no movement penalty.</p>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════
SHEET 1c ASTARTES TRAITS & COMBAT MATHS
═════════════════════════════════════════════════════════════ -->
<div class="sheet" id="sheet-astartes">
<h2>Space Marine Astartes Traits &amp; Combat Maths</h2>
<div class="two-col">
<div>
<h3>Strength in Combat (Power Armour worn)</h3>
<div class="ref-box dark-box">
<span class="label">Effective Strength Bonus (SB) Step-by-Step</span>
<ol style="padding-left:16px;line-height:1.8">
<li>Base S characteristic (e.g. S 41)</li>
<li>+ 20 from Astartes Power Armour → <strong>S 61</strong></li>
<li>SB = S 61 ÷ 10 (round down) → <strong>SB 6</strong></li>
<li>× 2 Unnatural Strength trait → <strong>SB 12</strong></li>
</ol>
<p style="margin-top:6px;font-size:8pt;color:#ccc;">Both brothers: S 41 → SB <strong>12</strong> in power armour</p>
</div>
<div class="ref-box dark-box" style="margin-top:8px">
<span class="label">Effective Toughness Bonus (TB)</span>
<ol style="padding-left:16px;line-height:1.8">
<li>Base T characteristic (Sepheran T 43 / Lucian T 40)</li>
<li>TB = T ÷ 10 (round down) → <strong>TB 4</strong></li>
<li>× 2 Unnatural Toughness trait → <strong>TB 8</strong></li>
</ol>
<p style="margin-top:6px;font-size:8pt;color:#ccc;">Both brothers: TB <strong>8</strong> effective soak per hit</p>
</div>
<div class="ref-box highlight" style="margin-top:8px">
<span class="label">Damage Taken — Formula &amp; Examples</span>
<p style="font-size:8.5pt;font-weight:bold">Damage roll Armour TB = Wounds</p>
<table style="margin-top:6px;font-size:8pt">
<thead><tr><th>Attack</th><th>Roll</th><th> Armour</th><th> TB</th><th>= Wounds</th></tr></thead>
<tbody>
<tr><td colspan="5" style="color:#888;font-style:italic;padding-top:4px">Brothers receiving a hit (Armour 810, TB 8)</td></tr>
<tr><td>Combat Knife vs body</td><td>1d10+3 avg 8</td><td> 10</td><td> 8</td><td><strong>0</strong></td></tr>
<tr><td>Bolter vs body (Pen 4)</td><td>1d10+9 avg 14</td><td> (104)=6</td><td> 8</td><td><strong>06</strong></td></tr>
<tr><td colspan="5" style="color:#888;font-style:italic;padding-top:6px">Brothers attacking (SB 12 in power armour)</td></tr>
<tr><td>Sepheran — Bolter vs unarmoured (Pen 4)</td><td>1d10+9 avg 14</td><td> 0</td><td> target TB</td><td><strong>14 TB</strong></td></tr>
<tr><td>Lucian — Combat Knife melee + SB 12</td><td>1d10+3+12 avg 21</td><td> target Armour</td><td> target TB</td><td><strong>21 Armour TB</strong></td></tr>
<tr><td>Either — unarmed strike + SB 12</td><td>1d103+12 avg 14</td><td> target Armour</td><td> target TB</td><td><strong>14 Armour TB</strong></td></tr>
</tbody>
</table>
<p style="font-size:7.5pt;color:#888;margin-top:4px">Pen reduces Armour, not TB. Minimum 1 Wound if the hit lands.</p>
</div>
<div class="ref-box" style="margin-top:8px">
<span class="label">Power Fist SB Multiplier</span>
<ul>
<li>Power Fist adds ×1 to existing multiplier</li>
<li>Marine: Unnatural ×2 + Power Fist = <strong>×3</strong> (not ×4)</li>
<li>SB 6 (S 61) × 3 = <strong>SB 18</strong> with Power Fist</li>
</ul>
</div>
</div>
<div>
<h3>Standard Astartes Traits</h3>
<table>
<thead><tr><th>Trait</th><th>Effect</th></tr></thead>
<tbody>
<tr>
<td><strong>Unnatural Strength (×2)</strong></td>
<td>Strength Bonus doubled. Also: skill tests based on S are one difficulty level easier per multiplier past ×1.</td>
</tr>
<tr>
<td><strong>Unnatural Toughness (×2)</strong></td>
<td>Toughness Bonus doubled. Felling (X) weapons reduce the effective multiplier by X.</td>
</tr>
<tr>
<td><strong>Sturdy</strong></td>
<td>+20 bonus to all tests made to resist Grappling and the Takedown talent.</td>
</tr>
<tr>
<td><strong>True Grit</strong> <em>(implant)</em></td>
<td>Whenever you suffer Critical Damage, halve the result (round up). e.g. 6 Critical → 3.</td>
</tr>
<tr>
<td><strong>Black Carapace</strong> <em>(implant)</em></td>
<td>Armour worn counts as Size (Hulking) but enemies gain <strong>no</strong> bonus to hit you. Full control of power armour.</td>
</tr>
<tr>
<td><strong>Fearless</strong></td>
<td>Immune to Fear tests and Pinning from normal weapons.</td>
</tr>
</tbody>
</table>
<h3 style="margin-top:10px">Implant Abilities</h3>
<table>
<thead><tr><th>Implant</th><th>Combat Benefit</th></tr></thead>
<tbody>
<tr><td><strong>Biscopea</strong></td><td>Grants Unnatural Strength (×2)</td></tr>
<tr><td><strong>Ossmodula</strong></td><td>Grants Unnatural Toughness (×2)</td></tr>
<tr><td><strong>Larraman's Organ</strong></td><td>No Blood Loss. Grants True Grit talent.</td></tr>
<tr><td><strong>Oolitic Kidney</strong></td><td>Re-roll failed T tests vs poisons &amp; Toxic attacks.</td></tr>
<tr><td><strong>Preomnor</strong></td><td>+20 T test vs ingested poisons.</td></tr>
<tr><td><strong>Multi-lung</strong></td><td>+30 T test vs gas; re-roll failed results; breathe water.</td></tr>
<tr><td><strong>Occulobe</strong></td><td>Heightened Senses (Sight): +10 Per-based tests using vision.</td></tr>
<tr><td><strong>Lyman's Ear</strong></td><td>Heightened Senses (Hearing): +10 Per-based tests using hearing.</td></tr>
<tr><td><strong>Betcher's Gland</strong></td><td>Spit acid: Range 3m · 1d5 E · Pen 4 · Toxic. 3+ DoS = blind 1d5 rounds.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════
SHEET 2 COMBAT ACTIONS
═════════════════════════════════════════════════════════════ -->
<div class="sheet" id="sheet-combat">
<h2>Combat Actions</h2>
<div class="two-col">
<div>
<h3>Action Types</h3>
<table>
<thead><tr><th>Type</th><th>Actions Used</th></tr></thead>
<tbody>
<tr><td><strong>Free Action</strong></td><td>Does not use either action</td></tr>
<tr><td><strong>Half Action</strong></td><td>1 action (two per turn)</td></tr>
<tr><td><strong>Full Action</strong></td><td>Both actions</td></tr>
<tr><td><strong>Reaction</strong></td><td>Out of turn, once per round</td></tr>
<tr><td><strong>Extended Action</strong></td><td>Multiple turns</td></tr>
</tbody>
</table>
<h3 style="margin-top:10px">Common Half Actions</h3>
<table>
<thead><tr><th>Action</th><th>Effect</th></tr></thead>
<tbody>
<tr><td><strong>Standard Attack</strong></td><td>Single melee or ranged attack</td></tr>
<tr><td><strong>Aim</strong></td><td>+10 to next attack (or +20 Full Aim)</td></tr>
<tr><td><strong>Feint</strong></td><td>Opposed WS; target cannot Dodge/Parry next hit</td></tr>
<tr><td><strong>Guarded Attack</strong></td><td>10 WS, +10 to Parry/Dodge until next turn</td></tr>
<tr><td><strong>Move</strong></td><td>Move up to full Move value in metres</td></tr>
<tr><td><strong>Reload</strong></td><td>Reload weapon (time varies)</td></tr>
<tr><td><strong>Stand / Mount</strong></td><td>Stand from prone, mount vehicle</td></tr>
<tr><td><strong>Use a Skill</strong></td><td>Use a Half-Action skill</td></tr>
</tbody>
</table>
</div>
<div>
<h3>Common Full Actions</h3>
<table>
<thead><tr><th>Action</th><th>Effect</th></tr></thead>
<tbody>
<tr><td><strong>All-Out Attack</strong></td><td>+20 WS; cannot Dodge or Parry until next turn</td></tr>
<tr><td><strong>Called Shot</strong></td><td>20 to attack; choose hit location</td></tr>
<tr><td><strong>Charge</strong></td><td>Move up to 3× Move; +20 WS on charge attack</td></tr>
<tr><td><strong>Full Auto Burst</strong></td><td>10 BS; 1 hit per DoS up to Auto value</td></tr>
<tr><td><strong>Semi-Auto Burst</strong></td><td>0 BS; 1 extra hit per 2 DoS up to Semi value</td></tr>
<tr><td><strong>Suppressive Fire</strong></td><td>Area of fire; targets must test Pinning</td></tr>
<tr><td><strong>Grapple</strong></td><td>Opposed S test to grab; subsequent grapple actions</td></tr>
<tr><td><strong>Run</strong></td><td>Move 2× Run; 20 to hit you with ranged until next turn</td></tr>
<tr><td><strong>Sprint</strong></td><td>Move Run+AB; Fatigue at end</td></tr>
</tbody>
</table>
<h3 style="margin-top:10px">Reactions</h3>
<table>
<thead><tr><th>Reaction</th><th>Test / Effect</th></tr></thead>
<tbody>
<tr><td><strong>Dodge</strong></td><td>Ag test; on success, avoid hit entirely</td></tr>
<tr><td><strong>Parry</strong></td><td>WS test; on success, deflect melee hit</td></tr>
</tbody>
</table>
<div class="ref-box highlight" style="margin-top:10px">
<span class="label">Firing into Melee</span>
<ul>
<li>20 BS penalty to avoid hitting allies</li>
<li>On a miss, randomly determine if ally is hit instead</li>
<li>Blast weapons can never be fired into melee</li>
</ul>
</div>
</div>
</div>
<h3>Ranged Modifiers</h3>
<table>
<thead><tr><th>Condition</th><th>Modifier</th><th>Condition</th><th>Modifier</th></tr></thead>
<tbody>
<tr><td>Point Blank (≤ 2m)</td><td>+30</td><td>Long Range (× 3)</td><td>10</td></tr>
<tr><td>Short Range (½ range)</td><td>+10</td><td>Extreme Range (× 4)</td><td>30</td></tr>
<tr><td>Standard Range</td><td>+0</td><td>Target is Prone (ranged)</td><td>10</td></tr>
<tr><td>Aim (Half)</td><td>+10</td><td>Target in Cover</td><td>10 to 30</td></tr>
<tr><td>Aim (Full)</td><td>+20</td><td>Firing into Melee</td><td>20</td></tr>
</tbody>
</table>
<h3 style="margin-top:10px">Melee Modifiers</h3>
<table>
<thead><tr><th>Condition</th><th>Modifier</th><th>Condition</th><th>Modifier</th></tr></thead>
<tbody>
<tr><td>All-Out Attack</td><td>+20 WS</td><td>Guarded Attack</td><td>10 WS</td></tr>
<tr><td>Charge</td><td>+20 WS</td><td>Outnumbered (2v1)</td><td>10 WS</td></tr>
<tr><td>Target is Prone</td><td>+20 WS</td><td>Outnumbered (3v1+)</td><td>20 WS</td></tr>
<tr><td>Defender in Cover</td><td>No penalty</td><td>Ganging Up Bonus (attacker)</td><td>+10 WS per extra</td></tr>
</tbody>
</table>
</div>
<!-- ════════════════════════════════════════════════════════════
SHEET 3 WEAPONS (loaded from API)
═════════════════════════════════════════════════════════════ -->
<div class="sheet" id="sheet-weapons">
<h2>Weapons</h2>
<div id="loading">Loading weapons from database…</div>
<div id="weapons-content" style="display:none">
<h3>Melee Weapons</h3>
<table id="melee-table">
<thead>
<tr>
<th>Name</th>
<th>Damage</th>
<th>Pen</th>
<th>Special</th>
<th>Wt (kg)</th>
<th>Req</th>
<th>Renown</th>
<th>Source</th>
</tr>
</thead>
<tbody id="melee-body"></tbody>
</table>
<h3 style="margin-top:14px">Ranged Weapons</h3>
<table id="ranged-table">
<thead>
<tr>
<th>Name</th>
<th>Class</th>
<th>Range</th>
<th>RoF</th>
<th>Damage</th>
<th>Pen</th>
<th>Clip</th>
<th>Rld</th>
<th>Special</th>
<th>Source</th>
</tr>
</thead>
<tbody id="ranged-body"></tbody>
</table>
<h3 style="margin-top:14px">Grenades &amp; Explosives</h3>
<table id="grenade-table">
<thead>
<tr><th>Name</th><th>Class</th><th>Damage / Effect</th><th>Source</th></tr>
</thead>
<tbody id="grenade-body"></tbody>
</table>
<div class="footer" id="weapon-footer"></div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════
SHEET 4 SQUAD MODE & COHESION
═════════════════════════════════════════════════════════════ -->
<div class="sheet" id="sheet-squad">
<h2>Squad Mode &amp; Cohesion</h2>
<div class="two-col">
<div>
<h3>Entering Squad Mode</h3>
<div class="ref-box">
<span class="label">Requirements</span>
<ul>
<li>All Battle-Brothers within <strong>10m</strong> of the Squad Leader</li>
<li>Squad Leader declares Squad Mode as a <strong>Free Action</strong></li>
<li>Squad maintains a Cohesion pool (max = number of members)</li>
</ul>
</div>
<div class="ref-box highlight">
<span class="label">Cohesion</span>
<ul>
<li>Starts at full (one point per Battle-Brother)</li>
<li>Spend Cohesion to activate <strong>Squad Mode abilities</strong></li>
<li>Lose 1 Cohesion when a Battle-Brother is Stunned, Unconscious, or moves &gt;10m from leader</li>
<li>Restore 1 Cohesion: spend a Full Action and pass a Command test</li>
<li>Cohesion 0 = Squad Mode ends</li>
</ul>
</div>
<h3>Common Squad Mode Abilities</h3>
<table>
<thead><tr><th>Ability</th><th>Cost</th><th>Effect</th></tr></thead>
<tbody>
<tr><td>Tactical Spacing</td><td>1</td><td>+10 BS and no 20 for firing into melee</td></tr>
<tr><td>Covering Fire</td><td>1</td><td>Allies gain +20 to Dodge</td></tr>
<tr><td>Suppression</td><td>1</td><td>Enemies in zone must pass Pinning test or cannot advance</td></tr>
<tr><td>Combat Formation</td><td>1</td><td>+10 WS; all attacks count as charging</td></tr>
<tr><td>Bolter Discipline</td><td>2</td><td>Full Auto hits an extra target per DoS</td></tr>
<tr><td>Devastator Stance</td><td>2</td><td>Heavy weapons count as Braced</td></tr>
</tbody>
</table>
</div>
<div>
<h3>Solo Mode Abilities</h3>
<div class="ref-box">
<span class="label">Using Solo Abilities</span>
<ul>
<li>Available when <strong>NOT</strong> in Squad Mode (or when solo)</li>
<li>Each Chapter has unique Solo abilities</li>
<li>Activated as Free Actions unless stated otherwise</li>
<li>May have limited uses per combat</li>
</ul>
</div>
<h3 style="margin-top:10px">Pinning</h3>
<div class="ref-box">
<span class="label">Suppressive Fire / Pinning</span>
<ul>
<li>Target within the Suppressive Fire zone: Pinning test = WP test</li>
<li><strong>Pinned:</strong> must make a WP test each turn to act normally</li>
<li>Failure: may only take a Half Action to move to cover or go Prone</li>
<li>Passing 3 WP tests consecutively removes Pinned condition</li>
<li>Space Marines are immune to Pinning from normal weapons</li>
<li>Hordes and creatures with Fearless are immune</li>
</ul>
</div>
<h3 style="margin-top:10px">Hordes</h3>
<div class="ref-box">
<span class="label">Fighting Hordes</span>
<ul>
<li>Hordes have a <strong>Magnitude</strong> (size) rather than individual wounds</li>
<li>Damage that exceeds the horde's TB reduces Magnitude</li>
<li><strong>Burst (X)</strong> and <strong>Blast</strong> weapons reduce Magnitude directly</li>
<li>Magnitude 0 = horde destroyed / breaks and flees</li>
<li>Hordes attack as one: damage = number attack + weapon damage</li>
</ul>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════
SHEET 5 CRITICAL HIT TABLES
═════════════════════════════════════════════════════════════ -->
<div class="sheet" id="sheet-crits">
<h2>Critical Hit Tables (CR p.252259)</h2>
<p style="font-size:7.5pt;color:#555;margin-bottom:8px">
Triggered when total Damage exceeds Wounds. True Grit (Space Marines): halve result, round up.
<strong>S</strong>=Stunned Rds · <strong>F</strong>=Fatigue levels · <strong>BL</strong>=Blood Loss · <strong>½Mv</strong>=half Move · <strong></strong>=instant death
</p>
<style>
.crit-table { width:100%; border-collapse:collapse; font-size:7pt; margin-bottom:14px; }
.crit-table th { background:#222; color:#fff; padding:3px 4px; text-align:center; white-space:nowrap; }
.crit-table th.loc { background:#1a3a6a; font-size:8pt; text-align:left; padding:3px 6px; }
.crit-table td { padding:2px 4px; border-bottom:1px solid #e8e8e8; vertical-align:top; line-height:1.3; }
.crit-table td:first-child { font-weight:bold; text-align:center; background:#f5f5f5; width:18px; white-space:nowrap; }
.crit-table tr:hover td { background:#f0f4ff; }
.crit-dead { color:#c00; font-weight:bold; }
.crit-bad { color:#c06000; }
.crit-ok { color:#333; }
</style>
<!-- ── HEAD ── -->
<table class="crit-table">
<thead>
<tr>
<th class="loc" colspan="5">HEAD</th>
</tr>
<tr>
<th>#</th>
<th style="width:23%">⚡ Energy (E)</th>
<th style="width:23%">💥 Explosive (X)</th>
<th style="width:23%">🔨 Impact (I)</th>
<th style="width:23%">🗡 Rending (R)</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td>10 all Tests 1 Rd</td><td>Half Action only, F1</td><td>T test or F1</td><td>F1 (no effect if helmeted)</td></tr>
<tr><td>2</td><td>Blind 1 Rd</td><td>Blind+deaf 1 Rd, F2</td><td>F1, 10 WS+BS 1 Rd</td><td>10 WS+BS 1d10 Rds, F1</td></tr>
<tr><td>3</td><td class="crit-ok">S1, F1</td><td>F2</td><td>Blind 1 Rd, F2</td><td>Helmet torn off (or ear lost, F2)</td></tr>
<tr><td>4</td><td class="crit-ok">F2, blind 1d5 Rds</td><td>Prone, S1, F2</td><td>F1d5</td><td>Eye scooped out, F1d5, S1</td></tr>
<tr><td>5</td><td class="crit-bad">Blind 1d10 Rds, F3</td><td class="crit-bad">S1d10, perma deaf, Fel1d10, F1d5</td><td>S1</td><td class="crit-bad">S1d5, F1d5, helmet off</td></tr>
<tr><td>6</td><td class="crit-bad">Blind 1d10 hrs, Fel1d10, F1d5</td><td class="crit-dead">† head explodes</td><td>S1d5, F2</td><td class="crit-bad">Lose eye/nose/ear (roll 1d10), BL, F1d5</td></tr>
<tr><td>7</td><td class="crit-bad">Perma blind, Fel=1d10, F1d10</td><td class="crit-dead">† + ammo detonates nearby</td><td class="crit-bad">S1d10, ½Mv 1d10 hrs</td><td class="crit-bad">Perma blind, Fel=1d10, BL, F1d10</td></tr>
<tr><td>8</td><td class="crit-dead"></td><td class="crit-dead">† (gory)</td><td class="crit-dead"></td><td class="crit-dead"></td></tr>
<tr><td>9</td><td class="crit-dead">† brain explodes, 4m splash</td><td class="crit-dead">† crimson mist</td><td class="crit-dead">† head bursts, 4m WS/BS 10</td><td class="crit-dead">† head flies 2d10m</td></tr>
<tr><td>10+</td><td class="crit-dead">† body on fire, 2d10m rampage</td><td class="crit-dead">† + allies WP test or flee</td><td class="crit-dead">† + attack chains to next target</td><td class="crit-dead">† + gore drenches 4m, 10 WS/BS</td></tr>
</tbody>
</table>
<!-- ── ARM ── -->
<table class="crit-table">
<thead>
<tr><th class="loc" colspan="5">ARM</th></tr>
<tr><th>#</th><th>⚡ Energy (E)</th><th>💥 Explosive (X)</th><th>🔨 Impact (I)</th><th>🗡 Rending (R)</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>30 arm Tests 1 Rd</td><td>F1</td><td>Drop held item</td><td>Drop held item</td></tr>
<tr><td>2</td><td>Arm useless 1d5 Rds, F1</td><td>Drop held, F2</td><td>F1</td><td>Drop held, F1</td></tr>
<tr><td>3</td><td>S1, F2, useless 1d5 Rds</td><td>1d5 fingers gone, F3</td><td>F1, drop held</td><td>F2, drop held</td></tr>
<tr><td>4</td><td class="crit-ok">S1, F3, useless 1d10 Rds</td><td class="crit-ok">F1d5, S1, arm useless</td><td>S1, useless 1d5 Rds, F1</td><td>Prone, F2, useless 1d10 Rds</td></tr>
<tr><td>5</td><td class="crit-ok">½WS+BS 1 Rd, F1d5</td><td class="crit-bad">T or lose hand, F1d5</td><td class="crit-ok">½WS+BS 1d10, F1, Ag or drop</td><td class="crit-bad">F1d5, BL, arm useless (needs medic)</td></tr>
<tr><td>6</td><td class="crit-bad">½WS+BS 1d10, F1d5, T or lose hand</td><td class="crit-bad">Arm broken (one arm), F1d5, BL</td><td class="crit-bad">1d5 fingers, F1, T or lose hand</td><td class="crit-bad">1d5 fingers, F3, T or lose hand</td></tr>
<tr><td>7</td><td class="crit-bad">Arm broken, S1, F1d5</td><td class="crit-bad">T or †, S1d10, F1d10, BL</td><td class="crit-bad">Arm broken, F2</td><td class="crit-bad">Arm broken, BL, F1d5</td></tr>
<tr><td>8</td><td class="crit-bad">Arm severed, T or S1, F1d10, BL</td><td class="crit-dead"></td><td class="crit-bad">Arm severed, T or †, S1d10, F1d5, BL</td><td class="crit-bad">Arm severed, T or †, S1d10, F1d10, BL</td></tr>
<tr><td>9</td><td class="crit-bad">Arm gone, T or †, F1d10</td><td class="crit-dead">† + weapons explode</td><td class="crit-dead"></td><td class="crit-dead"></td></tr>
<tr><td>10+</td><td class="crit-dead"></td><td class="crit-dead">† + ammo detonates</td><td class="crit-dead">† + shrapnel 2m</td><td class="crit-dead">† + gun spasms (5% hits random)</td></tr>
</tbody>
</table>
<!-- ── BODY ── -->
<table class="crit-table">
<thead>
<tr><th class="loc" colspan="5">BODY</th></tr>
<tr><th>#</th><th>⚡ Energy (E)</th><th>💥 Explosive (X)</th><th>🔨 Impact (I)</th><th>🗡 Rending (R)</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>Half Action only</td><td>Blown 1d5m prone, F1/m</td><td>Half Action only</td><td>No effect (armoured); F1 (bare)</td></tr>
<tr><td>2</td><td>F1</td><td>Blown 1d10m, F1/m</td><td>F1</td><td>Armour 1, F1</td></tr>
<tr><td>3</td><td>F2, S1</td><td>Armour destroyed; if bare: blown 1d10m, F2/m</td><td>Broken rib, F2, S1</td><td>S1, F2</td></tr>
<tr><td>4</td><td class="crit-ok">F1d10</td><td class="crit-ok">F1d5, S1, prone</td><td class="crit-ok">F1d5, S1</td><td class="crit-ok">F1d5 (blood pool)</td></tr>
<tr><td>5</td><td class="crit-ok">Prone, Ag or fire, F1d5</td><td class="crit-ok">S1, F1d10, prone</td><td class="crit-ok">F1d5, S2</td><td class="crit-ok">F1d5</td></tr>
<tr><td>6</td><td class="crit-bad">Prone, S1d10, Ag or fire, F1d5</td><td class="crit-bad">S1, F1d10, BL</td><td class="crit-bad">Knocked 1d5m, F1d5, S2</td><td class="crit-bad">Prone, F1d10</td></tr>
<tr><td>7</td><td class="crit-bad">S2d10, T halved</td><td class="crit-bad">S1d10, F1d10, BL, Half Actions 1d10 hrs</td><td class="crit-bad">1d5 ribs broken (20%/Rd: die), F1d5</td><td class="crit-bad">Guts open (hold with 1 arm), F1d5, BL</td></tr>
<tr><td>8</td><td class="crit-bad">S2d10, STAgFel all halved, Fel perm halved</td><td class="crit-dead"></td><td class="crit-bad">BL, F1d10</td><td class="crit-bad">T or †, perm 1d10 T, F1d10, BL</td></tr>
<tr><td>9</td><td class="crit-dead"></td><td class="crit-dead">† + ammo explodes</td><td class="crit-dead"></td><td class="crit-dead"></td></tr>
<tr><td>10+</td><td class="crit-dead">† + 50% ammo explodes</td><td class="crit-dead">† + gore drenches nearby</td><td class="crit-dead">† + thrown 1d10m</td><td class="crit-dead">† + gory pool 4m</td></tr>
</tbody>
</table>
<!-- ── LEG ── -->
<table class="crit-table">
<thead>
<tr><th class="loc" colspan="5">LEG</th></tr>
<tr><th>#</th><th>⚡ Energy (E)</th><th>💥 Explosive (X)</th><th>🔨 Impact (I)</th><th>🗡 Rending (R)</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>F1</td><td>Knocked 1m back</td><td>F1</td><td>F1</td></tr>
<tr><td>2</td><td>½Mv 1 Rd</td><td>Prone, F1</td><td>½Mv 1 Rd, F1</td><td>Ag or prone, F1</td></tr>
<tr><td>3</td><td>S1, ½Mv 1d5 Rds</td><td>S1, ½Mv 1d5 Rds, F1</td><td>S1, ½Mv 1d5 Rds, F1</td><td>F1, BL</td></tr>
<tr><td>4</td><td class="crit-ok">F1d5, ½Mv 1d5 Rds</td><td class="crit-ok">Fly 1d5m, F1/m, ½Mv 1d10</td><td class="crit-ok">F1d5, ½Mv 1d5 Rds</td><td class="crit-ok">Kneecap, ½Mv until medic, F2</td></tr>
<tr><td>5</td><td class="crit-ok">F1, ½Mv 2d10 Rds</td><td class="crit-bad">T or lose foot, F1d5</td><td class="crit-ok">Ag 20 1d10 Rds, F1d5</td><td class="crit-bad">F1d5, ½Mv 1d10 hrs</td></tr>
<tr><td>6</td><td class="crit-bad">T or lose foot, ½Mv (medic), F2</td><td class="crit-bad">Leg broken, F1d10, BL</td><td class="crit-bad">T or lose foot, ½Mv (medic), F2</td><td class="crit-bad">T or lose foot, ½Mv (medic), F1d5</td></tr>
<tr><td>7</td><td class="crit-bad">Leg broken, T or S1, F1d5</td><td class="crit-bad">T or †, S1d10, F1d10, BL</td><td class="crit-bad">Leg broken, F2</td><td class="crit-bad">Leg broken, BL, F1d10</td></tr>
<tr><td>8</td><td class="crit-bad">Leg severed, T or S1, F1d10, BL</td><td class="crit-dead"></td><td class="crit-bad">Leg off, T or †, S1d10, F1d5, BL</td><td class="crit-bad">Leg off, T or †, S1d10, F1d10, BL</td></tr>
<tr><td>9</td><td class="crit-bad">Leg gone, T or †</td><td class="crit-dead">† + shrapnel 2m</td><td class="crit-dead"></td><td class="crit-dead"></td></tr>
<tr><td>10+</td><td class="crit-dead">† (immolated)</td><td class="crit-dead">† + ammo detonates</td><td class="crit-dead">† + death screams 2d10m</td><td class="crit-dead">† + blood 6m pool</td></tr>
</tbody>
</table>
</div>
<!-- ════════════════════════════════════════════════════════════
SHEET 5b SPECIAL WEAPON QUALITIES
═════════════════════════════════════════════════════════════ -->
<div class="sheet" id="sheet-qualities">
<h2>Weapon Special Qualities</h2>
<div class="three-col">
<div>
<div class="ref-box">
<span class="label">Accurate</span>
<p>+10 to Aim actions; on half action aim, +1d10 damage on DoS 2+</p>
</div>
<div class="ref-box">
<span class="label">Balanced</span>
<p>+10 to Parry tests when wielded.</p>
</div>
<div class="ref-box">
<span class="label">Blast (X)</span>
<p>Hits all within X metres of impact point. Each target rolls separately to dodge.</p>
</div>
<div class="ref-box">
<span class="label">Concussive (X)</span>
<p>Target hit must pass a Toughness test with X × 10 penalty or be Stunned for 1 round.</p>
</div>
<div class="ref-box">
<span class="label">Corrosive</span>
<p>Armour at hit location loses 1d10 Armour permanently. After Armour=0, target takes 1d10 E per round.</p>
</div>
<div class="ref-box">
<span class="label">Crippling (X)</span>
<p>Hit location suffers Crippling damage; X to all tests using that location until healed.</p>
</div>
<div class="ref-box">
<span class="label">Defensive</span>
<p>+15 to Parry; 10 to attack with this weapon.</p>
</div>
</div>
<div>
<div class="ref-box">
<span class="label">Devastating (X)</span>
<p>If damage exceeds TB+Armour, target suffers additional 1d10 per X rating.</p>
</div>
<div class="ref-box">
<span class="label">Flame</span>
<p>Hits all in a 30°/30m cone. Targets hit must pass Ag or catch fire. No dodge vs cone (only full cover).</p>
</div>
<div class="ref-box">
<span class="label">Flexible</span>
<p>Cannot be Parried.</p>
</div>
<div class="ref-box">
<span class="label">Force</span>
<p>Psykers only: add Psy Rating to damage and penetration.</p>
</div>
<div class="ref-box">
<span class="label">Power Field</span>
<p>On a maximum damage die result, enemy weapon's Armour becomes 0. Also: enemy weapon takes a Toughness test or is destroyed.</p>
</div>
<div class="ref-box">
<span class="label">Primitive (X)</span>
<p>Max die result = X (e.g., Primitive (6) caps each die at 6). Does not apply to non-Primitive targets in armour.</p>
</div>
<div class="ref-box">
<span class="label">Razor Sharp</span>
<p>On DoS 2+, doubles Pen value.</p>
</div>
</div>
<div>
<div class="ref-box">
<span class="label">Recharge</span>
<p>Must wait 1 full round between shots (even if clip has ammo).</p>
</div>
<div class="ref-box">
<span class="label">Reliable</span>
<p>Only jams on a result of 100 (instead of 9600).</p>
</div>
<div class="ref-box">
<span class="label">Sanctified</span>
<p>Counts as Holy against Daemonic creatures; ignores Daemonic trait's damage reduction.</p>
</div>
<div class="ref-box">
<span class="label">Scatter</span>
<p>At point blank: +10 damage, full Pen. At longer ranges: 3 Pen. Each extra hit by +0d10.</p>
</div>
<div class="ref-box">
<span class="label">Snare (X)</span>
<p>Target must pass AgX×10 or become Immobilised until they pass the test on their turn.</p>
</div>
<div class="ref-box">
<span class="label">Tearing</span>
<p>Roll one extra damage die; drop the lowest result.</p>
</div>
<div class="ref-box">
<span class="label">Unbalanced</span>
<p>10 to Parry tests when used.</p>
</div>
<div class="ref-box">
<span class="label">Unwieldy</span>
<p>Cannot be used to Parry. 10 WS.</p>
</div>
</div>
</div>
</div>
<script>
function parseRanged(dmgStr) {
if (!dmgStr) return {}
const parts = dmgStr.split(';').map(s => s.trim())
const r = {}
for (const p of parts) {
if (/^Range /i.test(p)) r.range = p.replace(/^Range /i,'')
else if (/^Pen /i.test(p)) r.pen = p.replace(/^Pen /i,'')
else if (/^Clip /i.test(p)) r.clip = p.replace(/^Clip /i,'')
else if (/^Reload /i.test(p))r.reload = p.replace(/^Reload /i,'')
else if (/^[S-]\/[0-9-]/.test(p)) r.rof = p
else if (/\dd\d/.test(p)) r.damage = p
else if (p) r.special = (r.special ? r.special+', ':'')+p
}
return r
}
function esc(s) {
if (!s) return '—'
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
}
function srcShort(s) {
if (!s) return '—'
return s.replace('Deathwatch ','').replace('Core Rulebook','CR').replace('First Founding','FF').replace('Rites of Battle','RoB').replace('Mark of the Xenos','MoX').replace('The Jericho Reach','JR').replace('Game Master','GM').replace("'s Kit",'').replace('Living Errata v1.1','Errata')
}
async function loadWeapons() {
const loadingEl = document.getElementById('loading')
const contentEl = document.getElementById('weapons-content')
loadingEl.style.display = 'block'
contentEl.style.display = 'none'
try {
const res = await fetch('/api/weapons')
const weapons = await res.json()
const melee = weapons.filter(w => w.category === 'Melee Weapon')
const ranged = weapons.filter(w => w.category === 'Ranged Weapon')
const grenades= weapons.filter(w => w.category === 'Grenade' || w.category === 'Other')
// ── Melee table
const meleeBody = document.getElementById('melee-body')
meleeBody.innerHTML = melee.map(w => {
const s = w.stats || {}
// Some melee have everything in damage string (mixed format)
let dmg = s.damage || '—', pen = s.pen, special = s.special, wt = s.wt, req = s.req, renown = s.renown
// If damage contains semicolons, it's the mixed format
if (dmg.includes(';') || dmg.includes('Pen')) {
// Split out pen and special from damage string
const bits = dmg.split(';').map(x=>x.trim())
dmg = bits[0]
for (const b of bits.slice(1)) {
if (/^Pen \d/i.test(b)) pen = b.replace(/^Pen /i,'')
else if (b && b!=='-') special = (special?special+', ':'')+b
}
}
return `<tr>
<td><strong>${esc(w.name)}</strong></td>
<td>${esc(dmg)}</td>
<td>${esc(pen)||'—'}</td>
<td>${esc(special)||'—'}</td>
<td>${(wt&&wt!=='-')?esc(wt):'—'}</td>
<td>${req||'—'}</td>
<td>${esc(renown)||'—'}</td>
<td>${esc(srcShort(w.source))}</td>
</tr>`
}).join('')
// ── Ranged table
const rangedBody = document.getElementById('ranged-body')
rangedBody.innerHTML = ranged.map(w => {
const s = w.stats || {}
const p = parseRanged(s.damage)
const cls = (s.class||'').split(' - ').pop() // "Basic - Bolt" → "Bolt"
return `<tr>
<td><strong>${esc(w.name)}</strong></td>
<td>${esc(cls)||'—'}</td>
<td>${esc(p.range||s.range)||'—'}</td>
<td>${esc(p.rof||s.rof)||'—'}</td>
<td>${esc(p.damage||'—')}</td>
<td>${esc(p.pen||s.pen)||'—'}</td>
<td>${esc(p.clip||s.clip)||'—'}</td>
<td>${esc(p.reload||s.rld)||'—'}</td>
<td>${esc(p.special||s.special)||'—'}</td>
<td>${esc(srcShort(w.source))}</td>
</tr>`
}).join('')
// ── Grenades
const grenadeBody = document.getElementById('grenade-body')
grenadeBody.innerHTML = grenades.map(w => {
const s = w.stats || {}
const p = parseRanged(s.damage)
return `<tr>
<td><strong>${esc(w.name)}</strong></td>
<td>${esc((s.class||'').split(' - ').pop()||w.category)}</td>
<td>${esc(p.damage||s.damage||'—')}${p.special?' · '+esc(p.special):''}</td>
<td>${esc(srcShort(w.source))}</td>
</tr>`
}).join('')
document.getElementById('weapon-footer').textContent =
`${weapons.length} weapons loaded from database · ${melee.length} melee · ${ranged.length} ranged · ${grenades.length} grenades/other`
loadingEl.style.display = 'none'
contentEl.style.display = 'block'
} catch(e) {
loadingEl.textContent = 'Failed to load weapons from database. Is the server running?'
console.error(e)
}
}
loadWeapons()
</script>
</body>
</html>