Files
BT411/docs/CONTROLS.html
T
arcattackandClaude Opus 5 e46fc706a6 CONTROLS.html: load your bindings.txt and the board becomes YOUR board
The interactive controls page was a static picture of the stock layout -- a
player's carried migration rows, rebinds and HOTAS setup were invisible to it,
and the mismatch grew with every customization.  Now a "Load your bindings.txt"
box (file picker + drag-drop) parses the real bindings grammar client-side and
rewrites the interactive keyboard in place:

  * every key shows the loaded file's truth; keys that differ from the stock
    board get a hazard outline (the legend says so); hover readouts follow
    because they were already attribute-driven;
  * the game's bindings-row-wins rule is modelled: the PgUp/PgDn volume and
    backtick view BUILT-INS keep their face only while their key is unbound;
  * rows that have no keyboard geometry (pad / padaxis / wizard joyaxis-
    joybutton-joyhat / MOUSE keys) land in an "also in your file" list;
  * one click restores the stock view.  Nothing is uploaded -- FileReader on a
    user-chosen local file, works from the extracted zip on file://.

Key-name registry walks the three key blocks in DOM order (Shift/Ctrl/Alt
resolve left-then-right; the numpad's U+2212 minus and its ASCII twin both map
to NUMPADMINUS), address meanings are harvested from the stock board itself
plus a small table for the slots the default board leaves off (unwired columns,
the cabinet-only throttle bank, PANIC).

Two self-inflicted bugs found by the harness and fixed before landing: the
address harvest invented a "BTN" label for the many stock keys that have none,
which made EVERY default row read as a customization; and generated axis labels
("AIM +") differed from the page's hand-authored vocabulary ("AIM UP"), custom-
marking the whole stock numpad.  Both now speak the board's own labels.

VERIFIED headlessly (no browser extension needed): scratchpad/
test_controls_page.py injects a self-test harness into a copy of the page,
feeds it scratchpad/fixture_migrated_bindings.txt -- a REAL board-2 migrated
file (full defaults + two authored rows + a wizard section) -- and runs it
under chrome --headless=new --dump-dom.  11/11: the two authored rows carried
and marked, zero stray custom marks across the rest of the board, both
built-ins survive, wizard rows listed, status line honest, and reset restores
the stock board exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:47:12 -05:00

1401 lines
60 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.
<title>BattleTech 4.11 — Controls</title>
<style>
:root {
--void: #05070a;
--void-2: #080d10;
--glass: #0a1410;
--glass-edge: #16261d;
--phosphor: #40ff40;
--phosphor-2: #8dffa0;
--dim: #189418;
--hazard: #b0302a;
--hazard-lit: #ff5a48;
--amber: #c8a32e;
--amber-lit: #ffd25e;
--ink: #b9cfc0;
--ink-quiet: #6d8478;
--mono: "Cascadia Mono", Consolas, "SF Mono", ui-monospace, Menlo, monospace;
--sans: system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", Roboto, sans-serif;
--u: 42px;
--gap: 5px;
}
* { box-sizing: border-box; }
body {
margin: 0;
background:
radial-gradient(120% 90% at 50% -10%, #0d1a14 0%, transparent 60%),
var(--void);
color: var(--ink);
font-family: var(--sans);
line-height: 1.65;
-webkit-font-smoothing: antialiased;
}
body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background: radial-gradient(130% 100% at 50% 45%, transparent 55%, rgba(0,0,0,.55) 100%);
z-index: 5;
}
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: clamp(28px, 5vw, 64px) clamp(16px, 4vw, 40px) 80px;
position: relative;
z-index: 1;
}
.masthead { border-bottom: 1px solid var(--glass-edge); padding-bottom: 28px; }
.eyebrow {
font-family: var(--mono);
font-size: .72rem;
letter-spacing: .28em;
text-transform: uppercase;
color: var(--dim);
margin: 0 0 14px;
}
h1 {
font-family: var(--mono);
font-size: clamp(1.9rem, 5.2vw, 3.1rem);
line-height: 1.05;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--phosphor);
text-shadow: 0 0 22px rgba(64,255,64,.28);
margin: 0 0 18px;
text-wrap: balance;
}
.lede { max-width: 64ch; margin: 0; color: var(--ink); }
.lede code { font-family: var(--mono); color: var(--phosphor-2); font-size: .92em; }
.provenance {
margin: 18px 0 0;
font-family: var(--mono);
font-size: .74rem;
letter-spacing: .08em;
color: var(--ink-quiet);
}
.glance {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 1px;
background: var(--glass-edge);
border: 1px solid var(--glass-edge);
margin: 34px 0 0;
}
.glance div { background: var(--void-2); padding: 16px 18px; }
.glance dt {
font-family: var(--mono);
font-size: .68rem;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--ink-quiet);
margin: 0 0 6px;
}
.glance dd { margin: 0; font-family: var(--mono); font-size: 1.02rem; color: var(--phosphor); }
.glance dd small { display: block; font-family: var(--sans); font-size: .78rem; color: var(--ink-quiet); letter-spacing: 0; margin-top: 3px; }
section { margin-top: 64px; }
h2 {
font-family: var(--mono);
font-size: .82rem;
letter-spacing: .3em;
text-transform: uppercase;
color: var(--phosphor);
margin: 0 0 6px;
display: flex;
align-items: center;
gap: 14px;
}
h2::after { content: ""; flex: 1; height: 1px; background: linear-gradient(90deg, var(--glass-edge), transparent); }
h3 {
font-family: var(--mono);
font-size: .78rem;
font-weight: 400;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--phosphor);
margin: 46px 0 8px;
}
.sub { margin: 0 0 26px; max-width: 64ch; color: var(--ink-quiet); font-size: .93rem; }
.panel {
background: linear-gradient(180deg, rgba(64,255,64,.03), transparent 40%), var(--glass);
border: 1px solid var(--glass-edge);
padding: clamp(16px, 3vw, 30px);
overflow-x: auto;
}
.readout {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 8px 22px;
border: 1px solid var(--glass-edge);
border-top: none;
background: var(--void-2);
padding: 13px clamp(16px, 3vw, 30px);
font-family: var(--mono);
min-height: 54px;
}
.readout .rd-name { color: var(--phosphor); font-size: 1rem; letter-spacing: .04em; }
.readout .rd-addr { color: var(--amber-lit); font-size: .82rem; border: 1px solid rgba(200,163,46,.35); padding: 1px 8px; }
.readout .rd-desc { color: var(--ink-quiet); font-size: .85rem; font-family: var(--sans); }
.readout[data-idle="true"] .rd-name { color: var(--ink-quiet); }
.kb { display: flex; flex-direction: column; gap: 14px; min-width: 940px; position: relative; padding-top: 22px; }
.kb-groups { position: absolute; inset: 0; pointer-events: none; }
.kb-groups svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.kb-groups path { fill: none; stroke-width: 1; opacity: .85; }
.kb-groups path.g-mfd { stroke: #ff9a8c; }
.kb-groups path.g-col { stroke: var(--amber-lit); }
.kb-groups .g-label {
position: absolute;
transform: translate(0, -100%);
font-family: var(--mono);
font-size: .56rem;
letter-spacing: .16em;
text-transform: uppercase;
white-space: nowrap;
padding-bottom: 3px;
}
.kb-groups .g-label.g-mfd { color: #ff9a8c; }
.kb-groups .g-label.g-col { color: var(--amber-lit); }
.kb-bank { display: flex; gap: 22px; }
.kb-main { display: flex; flex-direction: column; gap: var(--gap); }
.kb-row { display: flex; gap: var(--gap); }
.kb-row.zone { margin-top: 18px; }
.kb-side { display: flex; flex-direction: column; gap: var(--gap); justify-content: flex-end; }
.kb-nav { display: flex; flex-direction: column; gap: var(--gap); align-items: center; padding-top: calc(var(--u) + var(--gap)); height: 100%; }
.kb-nav .grow { flex: 1 1 auto; min-height: var(--gap); }
.kb-pad { display: grid; grid-template-columns: repeat(4, var(--u)); gap: var(--gap); }
.key {
width: var(--u);
height: var(--u);
flex: none;
border: 1px solid #1d2b24;
border-bottom-color: #0c1512;
background: linear-gradient(180deg, #101a16, #0b120f);
color: #4e6a5c;
font-family: var(--mono);
font-size: .72rem;
line-height: 1.1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 2px;
cursor: default;
transition: transform .08s ease, box-shadow .12s ease, border-color .12s ease;
}
.key span.k-sub { font-size: .53rem; letter-spacing: .04em; text-transform: uppercase; opacity: .85; }
.key[data-role] { cursor: pointer; }
.key:focus-visible { outline: 2px solid var(--phosphor); outline-offset: 2px; }
.key[data-role="flight"] { color: var(--phosphor); border-color: #2c6b38; background: linear-gradient(180deg, #10281a, #0a1a11); }
.key[data-role="mfd"] { color: #ff9a8c; border-color: #6b2723; background: linear-gradient(180deg, #2a100e, #1a0a09); }
.key[data-role="column"] { color: var(--amber-lit); border-color: #6d5a1c; background: linear-gradient(180deg, #251d08, #171205); }
.key[data-role]:hover, .key.is-active {
transform: translateY(-1px);
border-color: currentColor;
box-shadow: 0 0 0 1px currentColor, 0 0 18px -4px currentColor;
}
.key.w125 { width: calc(var(--u) * 1.25 + var(--gap) * .25); }
.key.w15 { width: calc(var(--u) * 1.5 + var(--gap) * .5); }
.key.w175 { width: calc(var(--u) * 1.75 + var(--gap) * .75); }
.key.w2 { width: calc(var(--u) * 2 + var(--gap)); }
.key.w225 { width: calc(var(--u) * 2.25 + var(--gap) * 1.25); }
.key.w275 { width: calc(var(--u) * 2.75 + var(--gap) * 1.75); }
.key.w6 { width: calc(var(--u) * 6.25 + var(--gap) * 5.25); }
.key.tall { grid-row: span 2; height: calc(var(--u) * 2 + var(--gap)); }
.key.pad0 { grid-column: span 2; width: calc(var(--u) * 2 + var(--gap)); }
.spacer { width: calc(var(--u) * .5); flex: none; }
.legend { display: flex; flex-wrap: wrap; gap: 10px 26px; margin-top: 20px; font-family: var(--mono); font-size: .74rem; letter-spacing: .06em; }
.legend span { display: flex; align-items: center; gap: 8px; color: var(--ink-quiet); }
.legend i { width: 13px; height: 13px; border: 1px solid; display: block; }
.lg-flight { color: var(--phosphor); border-color: currentColor; background: #10281a; }
.lg-mfd { color: #ff9a8c; border-color: currentColor; background: #2a100e; }
.lg-col { color: var(--amber-lit); border-color: currentColor; background: #251d08; }
.lg-off { color: #4e6a5c; border-color: #1d2b24; background: #101a16; }
.pad-shell { display: flex; justify-content: center; }
svg.pad { width: 100%; max-width: 720px; height: auto; }
svg.pad .body { fill: #0c1512; stroke: #1f3329; stroke-width: 2; }
svg.pad .ctl { fill: #12211a; stroke: #2c6b38; stroke-width: 1.6; cursor: pointer; transition: fill .12s ease, stroke .12s ease; }
svg.pad .ctl:hover, svg.pad .ctl.is-active { fill: #1a3b26; stroke: var(--phosphor); }
svg.pad .lbl { font-family: var(--mono); font-size: 11px; fill: var(--phosphor); letter-spacing: .06em; pointer-events: none; }
svg.pad .lbl-q { font-family: var(--mono); font-size: 10px; fill: var(--ink-quiet); letter-spacing: .08em; pointer-events: none; text-transform: uppercase; }
svg.pad .lead { stroke: #244a33; stroke-width: 1; fill: none; }
svg.pad .cap { font-family: var(--mono); font-size: 11px; fill: #cfe8d8; pointer-events: none; }
.cockpit { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; min-width: 720px; align-items: start; }
.mfd { border: 1px solid var(--glass-edge); background: #060c09; display: flex; flex-direction: column; }
.mfd-strip { display: flex; gap: 2px; padding: 3px; background: #150a09; }
.mfd-strip b {
flex: 1;
background: linear-gradient(180deg, #b0302a, #7d211d);
color: #ffd9d3;
font-family: var(--mono);
font-size: .62rem;
font-weight: 400;
text-align: center;
padding: 4px 0;
letter-spacing: .04em;
}
.mfd-face { padding: 14px 12px; text-align: center; font-family: var(--mono); }
.mfd-face em { display: block; font-style: normal; color: var(--phosphor); font-size: .8rem; letter-spacing: .12em; text-transform: uppercase; }
.mfd-face small { display: block; color: var(--ink-quiet); font-size: .68rem; margin-top: 5px; letter-spacing: .06em; }
.cockpit-note { grid-column: span 3; margin: 2px 0 0; font-size: .82rem; color: var(--ink-quiet); }
.mfd-map { min-height: 300px; }
.map-body { display: flex; align-items: stretch; flex: 1; }
.map-col { display: flex; flex-direction: column; gap: 2px; padding: 3px; background: #150a09; }
.map-col b {
flex: 1;
min-height: 26px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #c8a32e, #8d711b);
color: #2a2205;
font-family: var(--mono);
font-size: .62rem;
font-weight: 400;
letter-spacing: .04em;
padding: 0 7px;
}
.map-face {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 14px 8px;
font-family: var(--mono);
text-align: center;
background: repeating-linear-gradient(0deg, rgba(64,255,64,.05) 0 1px, transparent 1px 22px), #060c09;
}
.map-face em { font-style: normal; color: var(--phosphor); font-size: .8rem; letter-spacing: .12em; text-transform: uppercase; }
.map-face small { color: var(--ink-quiet); font-size: .66rem; letter-spacing: .06em; line-height: 1.5; }
.tbl-scroll { overflow-x: auto; border: 1px solid var(--glass-edge); }
table { width: 100%; border-collapse: collapse; font-size: .87rem; min-width: 480px; }
caption { text-align: left; font-family: var(--mono); font-size: .7rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ink-quiet); padding: 12px 14px; background: var(--void-2); }
th, td { text-align: left; padding: 9px 14px; border-top: 1px solid var(--glass-edge); }
th { font-family: var(--mono); font-size: .68rem; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-quiet); font-weight: 400; }
td.mono { font-family: var(--mono); color: var(--phosphor); white-space: nowrap; }
td.addr { font-family: var(--mono); color: var(--amber-lit); font-variant-numeric: tabular-nums; white-space: nowrap; }
tbody tr:hover td { background: rgba(64,255,64,.035); }
pre {
margin: 0;
padding: 18px clamp(14px, 3vw, 24px);
background: var(--void-2);
border: 1px solid var(--glass-edge);
overflow-x: auto;
font-family: var(--mono);
font-size: .82rem;
line-height: 1.7;
color: var(--ink);
}
pre b { color: var(--phosphor); font-weight: 400; }
pre i { color: var(--ink-quiet); font-style: normal; }
.two-col { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 22px; }
.callout {
border-left: 2px solid var(--hazard);
background: linear-gradient(90deg, rgba(176,48,42,.12), transparent 70%);
padding: 14px 18px;
margin-top: 26px;
}
.callout p { margin: 0; font-size: .9rem; }
.callout strong { font-family: var(--mono); color: var(--hazard-lit); letter-spacing: .04em; }
.reach { display: flex; justify-content: center; }
.reach-fig { margin: 0; width: min(430px, 100%); }
.reach-glass { position: relative; aspect-ratio: 4 / 3; background: var(--glass); border: 1px solid var(--glass-edge); overflow: hidden; }
.reach-band {
position: absolute;
left: 0; right: 0;
height: 50%;
background: repeating-linear-gradient(-45deg, rgba(176,48,42,.16) 0 5px, transparent 5px 11px);
display: flex; align-items: center; justify-content: center;
font-family: var(--mono); font-size: .66rem; letter-spacing: .14em; text-transform: uppercase; color: #e08a7e;
}
.reach-band.is-top { top: 0; border-bottom: 1px dashed rgba(176,48,42,.6); }
.reach-band.is-bot { bottom: 0; }
.reach-strip { position: absolute; left: 0; right: 0; height: 8px; display: flex; gap: 2px; z-index: 2; }
.reach-strip.is-top { top: 0; }
.reach-strip.is-bot { bottom: 0; }
.reach-strip i { flex: 1; background: var(--hazard); display: block; }
.reach-strip.is-lit i:nth-child(2) { background: var(--hazard-lit); }
.reach-fig figcaption { margin-top: 12px; font-size: .84rem; color: var(--ink-quiet); }
.reach-fig figcaption b { font-family: var(--mono); font-weight: 400; color: var(--phosphor-2); }
.pos-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); gap: 18px; }
.pos { margin: 0; }
.pos-box { position: relative; aspect-ratio: 16 / 9; background: var(--void-2); border: 1px solid var(--glass-edge); }
.pos-box i, .pos-box u { position: absolute; display: block; }
.pos-box i { width: 16.7%; height: 22.2%; background: #2a100e; border: 1px solid #7d3029; }
.pos-box u { width: 16.9%; height: 40%; background: #251d08; border: 1px solid var(--amber); }
.pos-box i.ul { left: 0; top: 0; }
.pos-box i.uc { left: 41.65%; top: 0; }
.pos-box i.ur { right: 0; top: 0; }
.pos-box i.ll { left: 0; bottom: 0; }
.pos-box i.lr { right: 0; bottom: 0; }
.pos-box u { left: 41.55%; bottom: 0; }
.p-left u { left: 0; bottom: 0; }
.p-left i.ll { left: 16.9%; }
.p-right u { left: auto; right: 0; bottom: 0; }
.p-right i.lr { right: 16.9%; }
.p-midleft u { left: 0; bottom: auto; top: 30%; }
.p-midright u { left: auto; right: 0; bottom: auto; top: 30%; }
.pos figcaption { margin-top: 9px; font-family: var(--mono); font-size: .72rem; letter-spacing: .12em; color: var(--phosphor); }
.pos figcaption small { display: block; font-family: var(--sans); font-size: .76rem; letter-spacing: 0; color: var(--ink-quiet); margin-top: 2px; }
footer {
margin-top: 72px;
padding-top: 22px;
border-top: 1px solid var(--glass-edge);
font-family: var(--mono);
font-size: .74rem;
letter-spacing: .06em;
color: var(--ink-quiet);
display: flex;
flex-wrap: wrap;
gap: 8px 24px;
}
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
/* ---- "load your bindings.txt" (the board becomes YOUR board) ---- */
.loader {
margin: 14px 0 4px;
padding: 12px 14px;
border: 1px dashed var(--glass-edge);
border-radius: 6px;
background: var(--void-2);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 14px;
}
.loader.dragover { border-color: var(--phosphor); background: var(--glass); }
.loader label.btn {
cursor: pointer;
padding: 6px 14px;
border: 1px solid var(--dim);
border-radius: 4px;
color: var(--phosphor-2);
font-weight: 600;
}
.loader label.btn:hover { border-color: var(--phosphor); }
.loader input[type="file"] { display: none; }
.loader .status { color: var(--dim); font-size: 0.86em; }
.loader .status b { color: var(--phosphor-2); }
.loader .reset { color: var(--dim); font-size: 0.86em; cursor: pointer; text-decoration: underline; display: none; }
.key.custom {
outline: 1px solid var(--hazard-lit);
outline-offset: -1px;
}
.key.custom .k-sub { color: var(--hazard-lit); }
.loader-extras { display: none; margin: 6px 0 0; font-size: 0.86em; color: var(--dim); }
.loader-extras b { color: var(--phosphor-2); }
.loader-legend { display: none; font-size: 0.82em; color: var(--dim); }
.loader-legend i { color: var(--hazard-lit); font-style: normal; }
</style>
<div class="wrap">
<header class="masthead">
<p class="eyebrow">Virtual World Entertainment · Tesla II pod · BT 4.11</p>
<h1>BattleTech<br>Controls Map</h1>
<p class="lede">
Every input the pod answers to. <strong>The keyboard is the button
board</strong>: the letter and number rows are the MFD banks laid out
where they sit on the panel, and flight moved to the number pad so the
board stays free. Every one of the 72 buttons is also a mouse target on
the cockpit itself.
</p>
<p class="provenance">Generated from the default <code>bindings.txt</code> · all of it rebindable</p>
</header>
<dl class="glance">
<div>
<dt>Fly</dt>
<dd>Num 8 2 4 6<small>stick · 7/9 pedals · 0 fires · 5 all-stop</small></dd>
</div>
<div>
<dt>Throttle</dt>
<dd>Shift / Ctrl<small>walks the lever; it sticks where you leave it</small></dd>
</div>
<div>
<dt>Reverse</dt>
<dd>Alt<small>the throttle handle's own button</small></dd>
</div>
<div>
<dt>Control mode</dt>
<dd>F7<small>BAS → MID → ADV. You spawn in BASIC</small></dd>
</div>
</dl>
<div class="callout">
<p>
<strong>YOU SPAWN IN BASIC.</strong> In BASIC the stick steers and the
pedals do nothing. Press <b>F7</b> once for MID, where the pedals steer
and the stick twists the torso — the full pod experience. If "turning
doesn't work", this is why, not a bug. (Control mode is not the menu's
experience setting; that one changes the simulation, not your controls.)
</p>
</div>
<section>
<h2>Keyboard</h2>
<p class="sub">
Colour is the bank each key drives: green flies the mech, red is an MFD
button bank, amber is a column beside the map. Dark keys are unbound and
free to take. Hover a key to read what it does.
</p>
<div class="panel">
<div class="kb" id="kb"></div>
<div class="legend">
<span><i class="lg-flight"></i> flight &amp; fire</span>
<span><i class="lg-mfd"></i> MFD button banks</span>
<span><i class="lg-col"></i> map columns</span>
<span><i class="lg-off"></i> unbound</span>
</div>
</div>
<div class="readout" id="rd-kb" data-idle="true">
<span class="rd-name">Select a key</span>
<span class="rd-desc">Hover a key to read what it drives.</span>
</div>
<div class="callout">
<p>
<strong>COOLANT VALVES BITE.</strong> The valve detents on
<b>1 2 3 Q W E</b> run 1 5 50 (max) <b>0 (CLOSED)</b> back to
1. One press past max shuts that loop off and everything on it
overheats. Boosting one loop starves the others — the supply is
shared. <b>4</b> is the flush (hold it), <b>R</b> re-balances every
valve.
</p>
</div>
</section>
<section>
<h2>Where the keys live on the panel</h2>
<p class="sub">
Each MFD carries eight buttons: a four-wide top row and a four-wide row
beneath it. The keyboard rows run left to right across the upper
displays, then the lower pair — the same order your hand finds them in
the pod.
</p>
<div class="panel">
<div class="cockpit">
<div class="mfd">
<div class="mfd-strip"><b>1</b><b>2</b><b>3</b><b>4</b></div>
<div class="mfd-face"><em>Heat / coolant</em><small>0x2F 0x2C<br>0x2B 0x28</small></div>
<div class="mfd-strip"><b>Q</b><b>W</b><b>E</b><b>R</b></div>
</div>
<div class="mfd">
<div class="mfd-strip"><b>5</b><b>6</b><b>7</b><b>8</b></div>
<div class="mfd-face"><em>Engineering</em><small>0x27 0x24<br>0x23 0x20</small></div>
<div class="mfd-strip"><b>T</b><b>Y</b><b>U</b><b>I</b></div>
</div>
<div class="mfd">
<div class="mfd-strip"><b>9</b><b>0</b><b>-</b><b>=</b></div>
<div class="mfd-face"><em>Comm / hotbox</em><small>0x37 0x34<br>0x33 0x30</small></div>
<div class="mfd-strip"><b>O</b><b>P</b><b>[</b><b>]</b></div>
</div>
<div class="mfd">
<div class="mfd-strip"><b>A</b><b>S</b><b>D</b><b>F</b></div>
<div class="mfd-face"><em>Left Weapons</em><small>0x0F 0x0C<br>0x0B 0x08</small></div>
<div class="mfd-strip"><b>Z</b><b>X</b><b>C</b><b>V</b></div>
</div>
<div class="mfd mfd-map">
<div class="map-body">
<div class="map-col"><b>F1</b><b>F2</b><b>F3</b><b>F4</b><b>F5</b><b>F6</b></div>
<div class="map-face">
<em>Map · radar</em>
<small>
zoom in / out<br>
thermal · CROUCH<br>
searchlight · display<br>
0x10 0x15
</small>
</div>
<div class="map-col"><b>F7</b><b>F8</b><b>F9</b><b>F10</b><b>F11</b><b>F12</b></div>
</div>
</div>
<div class="mfd">
<div class="mfd-strip"><b>H</b><b>J</b><b>K</b><b>L</b></div>
<div class="mfd-face"><em>Right Weapons</em><small>0x07 0x04<br>0x03 0x00</small></div>
<div class="mfd-strip"><b>N</b><b>M</b><b>,</b><b>.</b></div>
</div>
<p class="cockpit-note">
G and B stay unbound on purpose: they are the physical gap between
the lower clusters. The right-hand map column is control mode (F7)
and Generators AD (F9F12); F8 is a column slot the pod never
wired.
</p>
</div>
</div>
<div class="tbl-scroll" style="margin-top:26px">
<table>
<caption>The weapon and engineering displays are page-gated</caption>
<tbody>
<tr><th>Quad page</th><td>The outer buttons jump straight to a populated Eng page.</td></tr>
<tr><th>Eng page</th><td>Generator select AD, bus mode, coolant toggle, trigger configure, and <b>eject</b>.</td></tr>
<tr><th>Jams &amp; fires</th><td>A weapon gone quiet with ammo left has <b>jammed</b> — heat jams ballistics, authentically. Its Eng-page buttons <b>flash</b>. <b>Tap eject</b> to clear the round; <b>hold ~3 s</b> to jettison a burning bay before it detonates. An ammo bay fire gives you about ten seconds and it will kill you.</td></tr>
</tbody>
</table>
</div>
</section>
<section>
<h2>Gamepad</h2>
<p class="sub">An Xbox-compatible pad drives all five axes and the grip buttons. Hover a control for its RIO address.</p>
<div class="panel pad-shell">
<svg class="pad" viewBox="0 0 700 400" role="img" aria-label="Xbox controller with BattleTech bindings">
<rect class="ctl" data-name="LB — Look left" data-addr="0x44" data-desc="Hat left" x="112" y="60" width="86" height="26" rx="13"></rect>
<rect class="ctl" data-name="RB — Look right" data-addr="0x43" data-desc="Hat right" x="502" y="60" width="86" height="26" rx="13"></rect>
<rect class="ctl" data-name="LT — Left pedal" data-addr="axis" data-desc="Turn left (MID/ADV control mode)" x="132" y="26" width="66" height="24" rx="12"></rect>
<rect class="ctl" data-name="RT — Right pedal" data-addr="axis" data-desc="Turn right (MID/ADV control mode)" x="502" y="26" width="66" height="24" rx="12"></rect>
<text class="lbl-q" x="132" y="20">LT · left pedal</text>
<text class="lbl-q" x="502" y="20">RT · right pedal</text>
<text class="lbl-q" x="112" y="102">LB · look left</text>
<text class="lbl-q" x="486" y="102">RB · look right</text>
<path class="body" d="M196 112 h308 c46 0 78 26 92 74 l32 108 c14 48 -10 82 -52 82 c-36 0 -58 -22 -74 -54 l-22 -44 h-180 l-22 44 c-16 32 -38 54 -74 54 c-42 0 -66 -34 -52 -82 l32 -108 c14 -48 46 -74 92 -74 z"></path>
<circle class="ctl" data-name="Left stick — torso twist / aim" data-addr="axes" data-desc="Pod-faithful: X twists the torso, Y aims it (deadzone 0.24)" cx="248" cy="176" r="34"></circle>
<text class="cap" x="222" y="181">STICK</text>
<path class="lead" d="M214 176 H150"></path>
<text class="lbl-q" x="60" y="180">torso twist / aim</text>
<g class="ctl" data-name="D-pad — look &amp; torso centre" data-addr="0x410x44" data-desc="Up = torso centre, down = look behind, left/right = look">
<rect x="286" y="266" width="24" height="66" rx="4"></rect>
<rect x="265" y="287" width="66" height="24" rx="4"></rect>
</g>
<path class="lead" d="M265 299 H180"></path>
<text class="lbl-q" x="86" y="303">look · torso centre</text>
<circle class="ctl" data-name="Right stick Y — Throttle" data-addr="axis" data-desc="Walks the throttle lever; the position sticks" cx="404" cy="266" r="32"></circle>
<text class="cap" x="381" y="271">THRTL</text>
<path class="lead" d="M436 266 H520"></path>
<text class="lbl-q" x="524" y="270">throttle</text>
<circle class="ctl" data-name="Y — Torso centre" data-addr="0x42" data-desc="Re-centre the torso" cx="470" cy="146" r="20"></circle>
<circle class="ctl" data-name="X — Upper thumb" data-addr="0x47" data-desc="Fire group" cx="428" cy="180" r="20"></circle>
<circle class="ctl" data-name="B — Reverse thrust" data-addr="0x3F" data-desc="The throttle handle's button" cx="512" cy="180" r="20"></circle>
<circle class="ctl" data-name="A — Main trigger" data-addr="0x40" data-desc="Fires the selected weapon group" cx="470" cy="214" r="20"></circle>
<text class="lbl" x="465" y="151">Y</text>
<text class="lbl" x="423" y="185">X</text>
<text class="lbl" x="507" y="185">B</text>
<text class="lbl" x="465" y="219">A</text>
<path class="lead" d="M490 214 L556 236"></path>
<text class="lbl-q" x="560" y="240">A · main trigger</text>
<text class="lbl-q" x="560" y="196">X upper · B reverse · Y torso centre</text>
<path class="lead" d="M532 180 H556"></path>
</svg>
</div>
<div class="readout" id="rd-pad" data-idle="true">
<span class="rd-name">Select a control</span>
<span class="rd-desc">Hover the diagram to read its binding.</span>
</div>
<p class="sub" style="margin-top:22px">
<b>Flight sticks, HOTAS and pedals:</b> run <code>joyconfig.bat</code>
once. A wizard asks you to move each control in turn, writes the
bindings, and drops you into the solo menu. It only rewrites its own
section of <code>bindings.txt</code>, so hand edits elsewhere survive.
</p>
</section>
<section>
<h2>The mouse</h2>
<p class="sub">
Every button on every display is clickable and lights when the game
commands its lamp — and each reaches in <em style="font-style:normal;color:var(--phosphor-2)">under the glass</em>
beside it, so the picture is the press target. The lit strip at the edge
is only the lamp. Right-click latches a button held.
</p>
<div class="panel">
<div class="reach">
<figure class="reach-fig">
<div class="reach-glass">
<span class="reach-strip is-top is-lit" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<span class="reach-band is-top">press — top row</span>
<span class="reach-band is-bot">press — bottom row</span>
<span class="reach-strip is-bot" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
</div>
<figcaption>
One MFD. Four buttons above the glass and four below, each reaching
<b>half the pane</b> deep — so the two banks meet in the middle and
the whole face answers the mouse. Only a thin strip clears the edge
to show its lamp.
</figcaption>
</figure>
</div>
</div>
<h3>Arranging the glass</h3>
<p class="sub">
The pod bolted its displays down at one size. On a desktop panel there is
room to trade viewscreen for instrument, so all six are yours to size —
and the map to place. Set these in <code>content\environ.ini</code>,
which the game writes on first run with every option documented.
</p>
<div class="pos-grid">
<figure class="pos">
<div class="pos-box p-center"><i class="ul"></i><i class="uc"></i><i class="ur"></i><i class="ll"></i><i class="lr"></i><u></u></div>
<figcaption>CENTER<small>as the pod had it</small></figcaption>
</figure>
<figure class="pos">
<div class="pos-box p-left"><i class="ul"></i><i class="uc"></i><i class="ur"></i><i class="ll"></i><i class="lr"></i><u></u></div>
<figcaption>LEFT<small>bottom corner; the MFD slides inboard</small></figcaption>
</figure>
<figure class="pos">
<div class="pos-box p-right"><i class="ul"></i><i class="uc"></i><i class="ur"></i><i class="ll"></i><i class="lr"></i><u></u></div>
<figcaption>RIGHT<small>the same, other side</small></figcaption>
</figure>
<figure class="pos">
<div class="pos-box p-midleft"><i class="ul"></i><i class="uc"></i><i class="ur"></i><i class="ll"></i><i class="lr"></i><u></u></div>
<figcaption>MIDLEFT<small>halfway up; leaves the bottom row alone</small></figcaption>
</figure>
<figure class="pos">
<div class="pos-box p-midright"><i class="ul"></i><i class="uc"></i><i class="ur"></i><i class="ll"></i><i class="lr"></i><u></u></div>
<figcaption>MIDRIGHT<small>halfway up, right</small></figcaption>
</figure>
</div>
<div class="tbl-scroll" style="margin-top:26px">
<table>
<caption>environ.ini — display layout</caption>
<tbody>
<tr><th>BT_MFD_SCALE</th><td>All five MFDs, as a percentage of pod size. <span style="color:var(--ink-quiet)">25200, default 100</span></td></tr>
<tr><th>BT_MFD_SCALE_<span style="color:var(--phosphor)">xx</span></th><td>Any one on its own — <span class="addr" style="font-family:var(--mono)">UL UC UR LL LR</span>. Unset inherits the line above.</td></tr>
<tr><th>BT_RADAR_SCALE</th><td>The map alone.</td></tr>
<tr><th>BT_RADAR_POS</th><td class="mono">CENTER · LEFT · RIGHT · MIDLEFT · MIDRIGHT</td></tr>
<tr><th>BT_GLASS_PANELS</th><td>1 = break every display into its own desktop window instead of the composited cockpit.</td></tr>
</tbody>
</table>
</div>
<div class="callout">
<p>
<strong>Ask for too much and the layout wins.</strong> The surround
bands grow to fit whatever you ask for, and a map in a bottom corner
pushes the lower MFD beside it inboard — nothing overlaps and nothing
comes out stretched.
</p>
</div>
<h3>Filling the screen</h3>
<p class="sub" style="margin-bottom:0">
The cockpit fits itself to whatever window it gets at one uniform scale,
so drag it to any shape and nothing distorts — a window that isn't the
cockpit's shape just gets black bars. Launch with <code>-fit</code> for
borderless over the whole monitor.
</p>
</section>
<section>
<h2>Rebinding</h2>
<p class="sub">
<code>content\bindings.txt</code> is written with the full default layout
on first run. Change a line, restart. Delete the file to restore these
defaults. Upgrading from an older build migrates your file automatically:
rows you wrote and the joystick wizard's section are kept (yours beat the
defaults), the old stock layout is replaced, and the previous file is
saved as <code>bindings.old.txt</code>.
</p>
<div class="loader" id="loader">
<label class="btn" for="bindfile">Load your bindings.txt</label>
<input type="file" id="bindfile" accept=".txt">
<span class="status" id="load-status">…and the keyboard above becomes <b>your</b> keyboard
(drop the file anywhere on this box). Nothing is uploaded — this page reads it locally.</span>
<span class="reset" id="load-reset">show the stock layout again</span>
<span class="loader-legend" id="load-legend"><i>outlined keys</i> differ from the stock board</span>
</div>
<p class="loader-extras" id="load-extras"></p>
<div class="two-col">
<pre><i># grammar</i>
<b>key</b> &lt;KEY&gt; <b>button</b> &lt;addr&gt;
<b>key</b> &lt;KEY&gt; <b>axis</b> &lt;chan&gt; deflect &lt;+|-&gt; &lt;rate&gt;
<b>key</b> &lt;KEY&gt; <b>axis</b> &lt;chan&gt; slew &lt;+|-&gt; &lt;rate&gt;
<b>key</b> &lt;KEY&gt; <b>axis</b> &lt;chan&gt; set &lt;value&gt;
<b>pad</b> &lt;BTN&gt; <b>button</b> &lt;addr&gt;
<b>padaxis</b> &lt;SRC&gt; <b>axis</b> &lt;chan&gt; [invert] [slew &lt;r&gt;]
<i># deflect springs back, slew walks and sticks
# (that is the throttle lever), set jumps</i></pre>
<pre><i># examples</i>
<b>key</b> G <b>button</b> 0x0E <i># regroup weapon 1 on G</i>
<b>key</b> B <b>button</b> 0x41 <i># look behind on B</i>
<i># put driving back on WASD if you prefer</i>
<b>key</b> W <b>axis</b> Throttle slew + 0.7
<b>key</b> S <b>axis</b> Throttle slew - 0.7
<b>key</b> A <b>axis</b> LeftPedal deflect + 2.5
<b>key</b> D <b>axis</b> RightPedal deflect + 2.5</pre>
</div>
<div class="tbl-scroll" style="margin-top:22px">
<table>
<caption>Reference — channels, addresses, key names</caption>
<tbody>
<tr><th>Channels</th><td class="mono">Throttle · LeftPedal · RightPedal · JoystickX · JoystickY · Turn</td></tr>
<tr><th>Buttons</th><td class="addr">0x00 0x47</td></tr>
<tr><th>No function</th><td class="addr">0x16 0x17 0x19 0x1E 0x1F 0x380x3E <span style="color:var(--ink-quiet);font-family:var(--sans)">— the pod never wired these</span></td></tr>
<tr><th>Key names</th><td class="mono">AZ · 09 · F1F12 · NUMPAD09 · NUMPADDOT · UP DOWN LEFT RIGHT · SPACE · LSHIFT LCTRL LALT · MINUS EQUALS COMMA PERIOD LBRACKET RBRACKET BACKTICK</td></tr>
<tr><th>Mouse</th><td class="mono">MOUSE4 · MOUSE5 · MOUSEMIDDLE <span style="color:var(--ink-quiet);font-family:var(--sans)">— buttons only; left/right also press cockpit buttons</span></td></tr>
<tr><th>Pad</th><td class="mono">A B X Y · DPAD_UP/DOWN/LEFT/RIGHT · LB RB · BACK START · LS RS</td></tr>
<tr><th>Pad axes</th><td class="mono">LX LY RX RY LT RT</td></tr>
</tbody>
</table>
</div>
<div class="callout">
<p>
<strong>A BOUND KEY LOSES ITS 1995 MEANING.</strong> Binding a key
removes it from the authentic typed-hotkey channel so it cannot
double-dispatch — and this board binds nearly everything, so the old
typed hotkeys (5 = Quad page, z = Eng1, t/y/u/i/o = pilot select, +/
= target zoom) are given up by design. Unbind a key to get its 1995
meaning back. Same rule for the built-ins: <b>`</b> is the view
toggle, and J/K/L cycle MFD preset pages only while unbound.
</p>
</div>
</section>
<footer>
<span>BattleTech 4.11</span>
<span>gitea.mysticmachines.com/VWE/BT411</span>
<span>docs/CONTROLS.md · CONTROLS.txt ships with the game</span>
<span>RGB keyboards mirror the pod lamps · BT_KEYLIGHT=0 to disable</span>
</footer>
</div>
<script>
(function () {
"use strict";
var F = "flight", M = "mfd", C = "column", X = null, SP = "spacer";
var rows = [
[
["Esc", "", X], [SP],
["F1", "MAP+", C, "0x10", "Map zoom in"],
["F2", "MAP-", C, "0x11", "Map zoom out"],
["F3", "IR", C, "0x12", "Thermal sight"],
["F4", "CROUCH", C, "0x13", "Duck — present a smaller target"], [SP],
["F5", "LAMP", C, "0x14", "Searchlight"],
["F6", "DISP", C, "0x15", "Cycle the secondary display"],
["F7", "MODE", C, "0x18", "Cycle control mode: BAS / MID / ADV"],
["F8", "", C, "0x19", "Column slot the pod never wired"], [SP],
["F9", "GEN A", C, "0x1A", "Generator A on/off"],
["F10", "GEN B", C, "0x1B", "Generator B on/off"],
["F11", "GEN C", C, "0x1C", "Generator C on/off"],
["F12", "GEN D", C, "0x1D", "Generator D on/off"]
],
[
["`", "VIEW", F, "", "Cockpit / chase camera toggle"],
["1", "VALVE", M, "0x2F", "Condenser 1 valve — detents 1-5-50-CLOSED"],
["2", "VALVE", M, "0x2E", "Condenser 2 valve"],
["3", "VALVE", M, "0x2D", "Condenser 3 valve"],
["4", "FLUSH", M, "0x2C", "Coolant flush — HOLD to dump the reservoir"],
["5", "", M, "0x27", "Engineering · top 1"], ["6", "", M, "0x26", "Engineering · top 2"],
["7", "", M, "0x25", "Engineering · top 3"], ["8", "", M, "0x24", "Engineering · top 4"],
["9", "", M, "0x37", "Comm / hotbox · top 1"], ["0", "", M, "0x36", "Comm / hotbox · top 2"],
["-", "", M, "0x35", "Comm / hotbox · top 3"], ["=", "", M, "0x34", "Comm / hotbox · top 4"],
["Bksp", "", X, "", "", "w2"]
],
[
["Tab", "", X, "", "", "w15"],
["Q", "VALVE", M, "0x2B", "Condenser 4 valve"], ["W", "VALVE", M, "0x2A", "Condenser 5 valve"],
["E", "VALVE", M, "0x29", "Condenser 6 valve"], ["R", "BALANCE", M, "0x28", "Balance every coolant valve"],
["T", "", M, "0x23", "Engineering · lower 1"], ["Y", "", M, "0x22", "Engineering · lower 2"],
["U", "", M, "0x21", "Engineering · lower 3"], ["I", "", M, "0x20", "Engineering · lower 4"],
["O", "", M, "0x33", "Comm / hotbox · lower 1"], ["P", "", M, "0x32", "Comm / hotbox · lower 2"],
["[", "", M, "0x31", "Comm / hotbox · lower 3"], ["]", "", M, "0x30", "Comm / hotbox · lower 4"],
["\\", "", X, "", "", "w15"]
],
[
["Caps", "", X, "", "", "w175"],
["A", "", M, "0x0F", "Left Weapons · top 1"], ["S", "", M, "0x0E", "Left Weapons · top 2"],
["D", "", M, "0x0D", "Left Weapons · top 3"], ["F", "", M, "0x0C", "Left Weapons · top 4"],
["G", "GAP", X, "", "Panel gap between the lower clusters — free"],
["H", "", M, "0x07", "Right Weapons · top 1"], ["J", "", M, "0x06", "Right Weapons · top 2"],
["K", "", M, "0x05", "Right Weapons · top 3"], ["L", "", M, "0x04", "Right Weapons · top 4"],
[";", "", X], ["'", "", X], ["Enter", "", X, "", "", "w225"]
],
[
["Shift", "THR +", F, "axis", "Throttle up — walks the lever, position holds", "w225"],
["Z", "", M, "0x0B", "Left Weapons · lower 1"], ["X", "", M, "0x0A", "Left Weapons · lower 2"],
["C", "", M, "0x09", "Left Weapons · lower 3"], ["V", "", M, "0x08", "Left Weapons · lower 4"],
["B", "GAP", X, "", "Panel gap between the lower clusters — free"],
["N", "", M, "0x03", "Right Weapons · lower 1"], ["M", "", M, "0x02", "Right Weapons · lower 2"],
[",", "", M, "0x01", "Right Weapons · lower 3"], [".", "", M, "0x00", "Right Weapons · lower 4"],
["/", "", X], ["Shift", "THR +", F, "axis", "Throttle up", "w275"]
],
[
["Ctrl", "THR ", F, "axis", "Throttle down — walks the lever, position holds", "w125"],
["Win", "", X, "", "", "w125"],
["Alt", "REV", F, "0x3F", "Reverse thrust — the throttle handle's button", "w125"],
["Space", "TRIGGER", F, "0x40", "Main trigger", "w6"],
["Alt", "REV", F, "0x3F", "Reverse thrust", "w125"],
["Fn", "", X, "", "", "w125"],
["Menu", "", X, "", "", "w125"],
["Ctrl", "THR ", F, "axis", "Throttle down", "w125"]
]
];
var navRows = [
[["Ins", "", X], ["Home", "", X], ["PgUp", "VOL +", F, "built-in", "Game volume up (saved between sessions)"]],
[["Del", "", X], ["End", "", X], ["PgDn", "VOL ", F, "built-in", "Game volume down (saved between sessions)"]],
"grow",
[["↑", "CENTRE", F, "0x42", "Torso centre"]],
[["←", "LOOK", F, "0x44", "Look left"], ["↓", "BEHIND", F, "0x41", "Look behind"], ["→", "LOOK", F, "0x43", "Look right"]]
];
var padKeys = [
["Num", "", X], ["/", "", X], ["*", "", X], ["", "", X],
["7", "L PED", F, "axis", "Left pedal — turns in MID/ADV"],
["8", "AIM UP", F, "axis", "Torso aim up"],
["9", "R PED", F, "axis", "Right pedal — turns in MID/ADV"],
["+", "", X, "", "", "tall"],
["4", "TWIST", F, "axis", "Torso twist left"],
["5", "ALL STOP", F, "axis", "Throttle to zero"],
["6", "TWIST", F, "axis", "Torso twist right"],
["1", "PINKY", F, "0x45", "Fire — pinky"],
["2", "AIM DN", F, "axis", "Torso aim down"],
["3", "MIDDLE", F, "0x46", "Fire — middle thumb"],
["Ent", "", X, "", "", "tall"],
["0", "TRIGGER", F, "0x40", "Main trigger", "pad0"],
[".", "UPPER", F, "0x47", "Fire — upper thumb"]
];
var kbEl = document.getElementById("kb");
var rdKb = document.getElementById("rd-kb");
var rdPad = document.getElementById("rd-pad");
function makeKey(def) {
if (def[0] === SP) {
var gap = document.createElement("div");
gap.className = "spacer";
gap.setAttribute("aria-hidden", "true");
return gap;
}
var cap = def[0], sub = def[1], role = def[2], addr = def[3] || "", desc = def[4] || "", w = def[5];
var el = document.createElement(role ? "button" : "div");
el.className = "key" + (w ? " " + w : "");
if (role) {
el.type = "button";
el.setAttribute("data-role", role);
el.setAttribute("data-name", cap);
el.setAttribute("data-addr", addr);
el.setAttribute("data-desc", desc);
} else if (desc) {
el.setAttribute("data-name", cap);
el.setAttribute("data-desc", desc);
el.title = desc;
}
var main = document.createElement("span");
main.textContent = cap;
el.appendChild(main);
if (sub) {
var s = document.createElement("span");
s.className = "k-sub";
s.textContent = sub;
el.appendChild(s);
}
return el;
}
var bank = document.createElement("div");
bank.className = "kb-bank";
var main = document.createElement("div");
main.className = "kb-main";
rows.forEach(function (row, index) {
var r = document.createElement("div");
r.className = "kb-row" + (index === 1 || index === 3 ? " zone" : "");
row.forEach(function (def) { r.appendChild(makeKey(def)); });
main.appendChild(r);
});
bank.appendChild(main);
var nav = document.createElement("div");
nav.className = "kb-side";
var navWrap = document.createElement("div");
navWrap.className = "kb-nav";
navRows.forEach(function (row) {
if (row === "grow") {
var g = document.createElement("div");
g.className = "grow";
navWrap.appendChild(g);
return;
}
var r = document.createElement("div");
r.className = "kb-row";
row.forEach(function (def) { r.appendChild(makeKey(def)); });
navWrap.appendChild(r);
});
nav.appendChild(navWrap);
bank.appendChild(nav);
var side = document.createElement("div");
side.className = "kb-side";
var pad = document.createElement("div");
pad.className = "kb-pad";
padKeys.forEach(function (def) { pad.appendChild(makeKey(def)); });
side.appendChild(pad);
bank.appendChild(side);
kbEl.appendChild(bank);
function show(readout, name, addr, desc) {
readout.setAttribute("data-idle", "false");
readout.innerHTML = "";
var n = document.createElement("span");
n.className = "rd-name";
n.textContent = name;
readout.appendChild(n);
if (addr) {
var a = document.createElement("span");
a.className = "rd-addr";
a.textContent = addr;
readout.appendChild(a);
}
if (desc) {
var d = document.createElement("span");
d.className = "rd-desc";
d.textContent = desc;
readout.appendChild(d);
}
}
function bind(root, readout, selector) {
if (!root) return;
var active = null;
function handle(e) {
var t = e.target.closest(selector);
if (!t || !t.getAttribute("data-name")) return;
if (active) active.classList.remove("is-active");
active = t;
t.classList.add("is-active");
show(readout, t.getAttribute("data-name"), t.getAttribute("data-addr"), t.getAttribute("data-desc"));
}
root.addEventListener("pointerover", handle);
root.addEventListener("focusin", handle);
root.addEventListener("click", handle);
}
var byCap = {};
Array.prototype.forEach.call(main.querySelectorAll(".key"), function (el) {
var cap = el.firstChild ? el.firstChild.textContent : "";
if (cap && !byCap[cap]) byCap[cap] = el;
});
var groups = [
{ label: "Map left column", kind: "g-col", top: ["F1", "F6"] },
{ label: "Map right column", kind: "g-col", top: ["F7", "F12"] },
{ label: "Heat / coolant", kind: "g-mfd", top: ["1", "4"], bottom: ["Q", "R"] },
{ label: "Engineering", kind: "g-mfd", top: ["5", "8"], bottom: ["T", "I"] },
{ label: "Comm / hotbox", kind: "g-mfd", top: ["9", "="], bottom: ["O", "]"] },
{ label: "Left Weapons", kind: "g-mfd", top: ["A", "F"], bottom: ["Z", "V"] },
{ label: "Right Weapons", kind: "g-mfd", top: ["H", "L"], bottom: ["N", "."] }
];
function drawGroups() {
var old = kbEl.querySelector(".kb-groups");
if (old) old.remove();
var host = document.createElement("div");
host.className = "kb-groups";
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
host.appendChild(svg);
var base = kbEl.getBoundingClientRect();
var padPx = 3;
function box(a, b) {
if (!a || !b) return null;
var ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
return {
l: ra.left - base.left - padPx,
r: rb.right - base.left + padPx,
t: ra.top - base.top - padPx,
b: ra.bottom - base.top + padPx
};
}
groups.forEach(function (g) {
var top = box(byCap[g.top[0]], byCap[g.top[1]]);
if (!top) return;
var bot = g.bottom ? box(byCap[g.bottom[0]], byCap[g.bottom[1]]) : null;
var d;
if (bot) {
d = "M" + top.l + " " + top.t + "H" + top.r + "V" + bot.t +
"H" + bot.r + "V" + bot.b + "H" + bot.l + "V" + top.b + "H" + top.l + "Z";
} else {
d = "M" + top.l + " " + top.t + "H" + top.r + "V" + top.b + "H" + top.l + "Z";
}
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", d);
path.setAttribute("class", g.kind);
svg.appendChild(path);
var label = document.createElement("span");
label.className = "g-label " + g.kind;
label.textContent = g.label;
label.style.left = top.l + "px";
label.style.top = top.t + "px";
host.appendChild(label);
});
kbEl.appendChild(host);
}
drawGroups();
window.addEventListener("resize", drawGroups);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(drawGroups);
bind(kbEl, rdKb, ".key");
bind(document.querySelector("svg.pad"), rdPad, ".ctl");
//--------------------------------------------------------------------
// LOAD YOUR bindings.txt (2026-07-26): the board above stops being a
// picture of the stock layout and becomes the reader's actual keyboard.
// Entirely local -- FileReader on a user-chosen file; nothing uploaded.
//--------------------------------------------------------------------
// Registry: bindings.txt KEYNAME -> the rendered key element. Caps on the
// board are display glyphs ("`", "", "PgUp"), and Shift/Ctrl/Alt appear
// twice (left first, right second), so walk each block in DOM order.
var keyEls = {};
function reg(name, el) { if (name && el && !keyEls[name]) keyEls[name] = el; }
(function () {
var mainCap = {
"`": "BACKTICK", "-": "MINUS", "=": "EQUALS", "[": "LBRACKET",
"]": "RBRACKET", "\\": "BACKSLASH", ";": "SEMICOLON", "'": "QUOTE",
",": "COMMA", ".": "PERIOD", "/": "SLASH", "Space": "SPACE",
"Tab": "TAB", "Bksp": "BACKSPACE", "Enter": "ENTER"
};
var dupes = { "Shift": ["LSHIFT", "RSHIFT"], "Ctrl": ["LCTRL", "RCTRL"],
"Alt": ["LALT", "RALT"] };
var seen = {};
Array.prototype.forEach.call(main.querySelectorAll(".key"), function (el) {
var cap = el.firstChild ? el.firstChild.textContent : "";
if (!cap) return;
if (dupes[cap]) {
seen[cap] = (seen[cap] || 0);
reg(dupes[cap][Math.min(seen[cap], 1)], el);
seen[cap] += 1;
return;
}
if (mainCap[cap]) { reg(mainCap[cap], el); return; }
if (/^[A-Z0-9]$/.test(cap) || /^F1?[0-9]$/.test(cap)) reg(cap, el);
});
var navCap = { "Ins": "INSERT", "Home": "HOME", "PgUp": "PAGEUP",
"Del": "DELETE", "End": "END", "PgDn": "PAGEDOWN",
"↑": "UP", "←": "LEFT", "↓": "DOWN",
"→": "RIGHT" };
Array.prototype.forEach.call(navWrap.querySelectorAll(".key"), function (el) {
var cap = el.firstChild ? el.firstChild.textContent : "";
if (navCap[cap]) reg(navCap[cap], el);
});
var padCap = { "/": "NUMPADSLASH", "*": "NUMPADSTAR",
"": "NUMPADMINUS", "+": "NUMPADPLUS",
".": "NUMPADDOT" };
Array.prototype.forEach.call(pad.querySelectorAll(".key"), function (el) {
var cap = el.firstChild ? el.firstChild.textContent : "";
if (/^[0-9]$/.test(cap)) reg("NUMPAD" + cap, el);
else if (padCap[cap]) reg(padCap[cap], el);
// the pad's minus renders as U+2212; accept the ASCII twin too
else if (cap === "-") reg("NUMPADMINUS", el);
});
})();
// Remember every key's stock face, for reset and for the differs-mark.
var stock = {};
Object.keys(keyEls).forEach(function (name) {
var el = keyEls[name], subEl = el.querySelector(".k-sub");
stock[name] = {
sub: subEl ? subEl.textContent : "",
addr: el.getAttribute("data-addr") || "",
desc: el.getAttribute("data-desc") || "",
name: el.getAttribute("data-name") || ""
};
});
// What an address MEANS: harvested from the stock board itself, plus the
// slots the default board leaves off (unwired columns, the cabinet-only
// throttle bank) so a custom row still reads as something.
var addrInfo = {};
Object.keys(stock).forEach(function (name) {
var s = stock[name];
// Keep the sub EXACTLY as the stock board shows it -- most MFD bank keys
// have no short label at all, and inventing one ("BTN") made every
// default row read as a customization when a real file was loaded.
if (/^0x/i.test(s.addr) && !addrInfo[s.addr.toLowerCase()])
addrInfo[s.addr.toLowerCase()] = { sub: s.sub, desc: s.desc };
});
[["0x16", "—", "Column slot the pod never wired"],
["0x17", "—", "Column slot the pod never wired"],
["0x1e", "—", "Column slot the pod never wired"],
["0x1f", "—", "Column slot the pod never wired"],
["0x38", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
["0x39", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
["0x3a", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
["0x3b", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
["0x3c", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"],
["0x3d", "PANIC", "The pod's panic button"],
["0x3e", "AUX", "Throttle bank -- cabinet-only (inert on desktop)"]
].forEach(function (a) {
if (!addrInfo[a[0]]) addrInfo[a[0]] = { sub: a[1], desc: a[2] };
});
var chanShort = { "THROTTLE": "THR", "JOYSTICKX": "TWIST",
"JOYSTICKY": "AIM", "LEFTPEDAL": "L PED",
"RIGHTPEDAL": "R PED", "TURN": "TURN" };
// One parsed row -> a display face {sub, addr, desc}.
function faceOf(tok) {
var verb = (tok[2] || "").toUpperCase();
if (verb === "BUTTON") {
var addr = (tok[3] || "").toLowerCase();
var info = addrInfo[addr] || { sub: "BTN", desc: "RIO button" };
return { sub: info.sub, addr: tok[3] || "", desc: info.desc };
}
if (verb === "KEYPAD")
return { sub: "KP " + tok[3] + "/" + tok[4], addr: "keypad",
desc: "MFD keypad unit " + tok[3] + ", key " + tok[4] };
if (verb === "AXIS") {
var ch = (tok[3] || "").toUpperCase();
var short_ = chanShort[ch] || tok[3] || "?";
var how = (tok[4] || "").toUpperCase();
if (how === "SET")
return { sub: (ch === "THROTTLE" && tok[5] === "0")
? "ALL STOP" : short_ + "=" + tok[5],
addr: "axis", desc: tok[3] + " set to " + tok[5] };
// Speak the board's own label vocabulary, or every stock axis row in
// a loaded file reads as a "customization" purely by spelling
// ("AIM +" vs the hand-authored "AIM UP").
var axisLabel = {
"JOYSTICKY+": "AIM UP", "JOYSTICKY-": "AIM DN",
"JOYSTICKX+": "TWIST", "JOYSTICKX-": "TWIST",
"LEFTPEDAL+": "L PED", "LEFTPEDAL-": "L PED",
"RIGHTPEDAL+": "R PED", "RIGHTPEDAL-": "R PED",
"THROTTLE+": "THR +", "THROTTLE-": "THR ",
"TURN+": "TURN", "TURN-": "TURN"
};
var sign = tok[5] === "-" ? "-" : "+";
var lbl = axisLabel[ch + sign]
|| (short_ + (sign === "-" ? " " : " +"));
return { sub: lbl, addr: "axis",
desc: tok[3] + " " + (how === "SLEW" ? "slew" : "deflect")
+ " " + (tok[5] || "") + " " + (tok[6] || "") };
}
return null;
}
function setFace(el, face, isCustom) {
var subEl = el.querySelector(".k-sub");
if (face && face.sub) {
if (!subEl) {
subEl = document.createElement("span");
subEl.className = "k-sub";
el.appendChild(subEl);
}
subEl.textContent = face.sub;
} else if (subEl) {
subEl.remove();
}
el.setAttribute("data-name", el.firstChild.textContent);
el.setAttribute("data-addr", face ? face.addr : "");
el.setAttribute("data-desc", face ? face.desc : "");
el.classList.toggle("custom", !!isCustom);
}
var statusEl = document.getElementById("load-status");
var resetEl = document.getElementById("load-reset");
var legendEl = document.getElementById("load-legend");
var extrasEl = document.getElementById("load-extras");
function resetBoard() {
Object.keys(keyEls).forEach(function (name) {
var el = keyEls[name], s = stock[name];
var subEl = el.querySelector(".k-sub");
if (s.sub) {
if (!subEl) {
subEl = document.createElement("span");
subEl.className = "k-sub";
el.appendChild(subEl);
}
subEl.textContent = s.sub;
} else if (subEl) {
subEl.remove();
}
if (s.name) el.setAttribute("data-name", s.name);
else el.removeAttribute("data-name");
el.setAttribute("data-addr", s.addr);
el.setAttribute("data-desc", s.desc);
el.classList.remove("custom");
});
statusEl.innerHTML = "Showing the <b>stock</b> layout.";
resetEl.style.display = "none";
legendEl.style.display = "none";
extrasEl.style.display = "none";
}
function applyFile(text, fname) {
// Parse every row; group by keyboard / elsewhere.
var perKey = {}, extras = [], rows = 0;
text.split(/\r?\n/).forEach(function (raw) {
var line = raw.replace(/#.*$/, "").trim();
if (!line) return;
var tok = line.split(/\s+/);
var kind = tok[0].toUpperCase();
if (kind === "KEY" && tok.length >= 3) {
rows += 1;
var name = tok[1].toUpperCase();
if (/^MOUSE/.test(name)) { extras.push(line); return; }
(perKey[name] = perKey[name] || []).push(tok);
} else if (kind === "PAD" || kind === "PADAXIS"
|| kind === "JOYAXIS" || kind === "JOYBUTTON"
|| kind === "JOYHAT") {
rows += 1;
extras.push(line);
}
});
if (rows === 0) {
statusEl.innerHTML = "That file has no binding rows — is it really "
+ "a bindings.txt?";
return;
}
// The board becomes the file: every registered key resets, then rows
// land. Built-ins (backtick view, PgUp/PgDn volume) keep their face
// unless the file binds that key -- the game's bindings-row-wins rule.
var builtIns = { "BACKTICK": 1, "PAGEUP": 1, "PAGEDOWN": 1 };
var customCount = 0;
Object.keys(keyEls).forEach(function (name) {
var el = keyEls[name];
var rowsHere = perKey[name];
if (!rowsHere || rowsHere.length === 0) {
if (builtIns[name]) { // built-in survives an unbound key
setFace(el, { sub: stock[name].sub, addr: stock[name].addr,
desc: stock[name].desc }, false);
return;
}
var wasBound = !!stock[name].sub;
setFace(el, null, false);
if (wasBound) el.setAttribute("data-desc", "Unbound in your file "
+ "(stock: " + stock[name].sub + ")");
return;
}
var face = faceOf(rowsHere[0]);
if (!face) { setFace(el, null, false); return; }
if (rowsHere.length > 1) {
face.desc += " · also: " + rowsHere.slice(1).map(function (t) {
return t.slice(2).join(" ");
}).join(" / ");
}
var isCustom = (face.sub !== stock[name].sub)
|| (face.addr !== stock[name].addr);
if (isCustom) customCount += 1;
setFace(el, face, isCustom);
});
statusEl.innerHTML = "Showing <b>" + (fname || "your bindings")
+ "</b> — " + rows + " row(s), <b>" + customCount
+ "</b> key(s) differ from stock.";
resetEl.style.display = "inline";
legendEl.style.display = customCount ? "inline" : "none";
if (extras.length) {
extrasEl.innerHTML = "<b>Also in your file</b> (not on the keyboard): "
+ extras.map(function (l) {
return "<code>" + l.replace(/&/g, "&amp;").replace(/</g, "&lt;")
+ "</code>";
}).join(" · ");
extrasEl.style.display = "block";
} else {
extrasEl.style.display = "none";
}
}
var loaderEl = document.getElementById("loader");
var fileEl = document.getElementById("bindfile");
fileEl.addEventListener("change", function () {
var f = fileEl.files && fileEl.files[0];
if (!f) return;
var r = new FileReader();
r.onload = function () { applyFile(String(r.result), f.name); };
r.readAsText(f);
});
resetEl.addEventListener("click", resetBoard);
["dragover", "dragenter"].forEach(function (ev) {
loaderEl.addEventListener(ev, function (e) {
e.preventDefault();
loaderEl.classList.add("dragover");
});
});
["dragleave", "drop"].forEach(function (ev) {
loaderEl.addEventListener(ev, function (e) {
e.preventDefault();
loaderEl.classList.remove("dragover");
});
});
loaderEl.addEventListener("drop", function (e) {
var f = e.dataTransfer && e.dataTransfer.files
&& e.dataTransfer.files[0];
if (!f) return;
var r = new FileReader();
r.onload = function () { applyFile(String(r.result), f.name); };
r.readAsText(f);
});
})();
</script>