Skip to content

Commit 7d079cc

Browse files
authored
Create rolls.js
1 parent f67a97f commit 7d079cc

1 file changed

Lines changed: 306 additions & 0 deletions

File tree

modules/rolls.js

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/* modules/rolls.js
2+
* Extra rollers for GATOR: Hit Location, Critical Check, Missile Cluster
3+
* Canon: Classic BattleTech (front/rear tables, crit counts, cluster tables)
4+
* UI Mount: window.mountGatorRollsUnderTN() — injects a small control block under TN/Dice
5+
*/
6+
(() => {
7+
'use strict';
8+
9+
// ================================
10+
// RNG helpers
11+
// ================================
12+
const RNG = {
13+
d6() { return (Math.random() * 6 | 0) + 1; },
14+
roll2d6() { return this.d6() + this.d6(); },
15+
};
16+
17+
// ================================
18+
// Canon data tables
19+
// ================================
20+
// ’Mech hit location (FRONT) — 2d6
21+
// 2 CT • 3 RT • 4 RA • 5 RL • 6 RT • 7 CT • 8 LT • 9 LL • 10 LA • 11 LA • 12 Head
22+
const HIT_FRONT = Object.freeze({
23+
2: 'CT',
24+
3: 'RT',
25+
4: 'RA',
26+
5: 'RL',
27+
6: 'RT',
28+
7: 'CT',
29+
8: 'LT',
30+
9: 'LL',
31+
10: 'LA',
32+
11: 'LA',
33+
12: 'HEAD',
34+
});
35+
36+
// ’Mech hit location (REAR) — same dice mapping, rear notation
37+
const HIT_REAR = Object.freeze({
38+
2: 'CT (Rear)',
39+
3: 'RT (Rear)',
40+
4: 'RA (Rear)',
41+
5: 'RL (Rear)',
42+
6: 'RT (Rear)',
43+
7: 'CT (Rear)',
44+
8: 'LT (Rear)',
45+
9: 'LL (Rear)',
46+
10: 'LA (Rear)',
47+
11: 'LA (Rear)',
48+
12: 'HEAD', // head has no rear
49+
});
50+
51+
// Critical check: 2–7:0 • 8–9:1 • 10–11:2 • 12:3
52+
const CRIT_BY_ROLL = Object.freeze({
53+
2:0,3:0,4:0,5:0,6:0,7:0,
54+
8:1,9:1,
55+
10:2,11:2,
56+
12:3,
57+
});
58+
59+
// Missile Cluster Hits Tables (2..12 → hits), per launcher size
60+
// Index 0 unused; array[i] corresponds to roll i.
61+
const CLUSTER = Object.freeze({
62+
2: [ ,1,1,1,1,1,1,1,2,2,2,2,2 ],
63+
4: [ ,1,2,2,2,2,2,3,3,3,3,4,4 ],
64+
5: [ ,1,2,2,3,3,3,3,3,4,4,5,5 ],
65+
6: [ ,2,2,3,3,4,4,4,4,5,5,6,6 ],
66+
10: [ ,3,3,4,6,6,6,6,6,8,8,10,10 ],
67+
15: [ ,5,5,6,9,9,9,9,9,12,12,15,15 ],
68+
20: [ ,6,6,9,12,12,12,12,12,16,16,20,20 ],
69+
});
70+
71+
// ================================
72+
// Core API
73+
// ================================
74+
function roll2d6(){ return RNG.roll2d6(); }
75+
76+
function rollLocation(facing = 'front'){
77+
const r = RNG.roll2d6();
78+
const table = (facing === 'rear') ? HIT_REAR : HIT_FRONT;
79+
return { roll: r, location: table[r] ?? '—' };
80+
}
81+
82+
function rollCrit(){
83+
const r = RNG.roll2d6();
84+
return { roll: r, crits: CRIT_BY_ROLL[r] ?? 0 };
85+
}
86+
87+
/**
88+
* rollCluster({ size, mods=0, streak=false })
89+
* size: 2,4,5,6,10,15,20
90+
* mods: cluster modifiers (e.g., +2 Artemis, +2 NARC, -1 Indirect)
91+
* streak: Streak SRMs — on hit, all missiles; on miss, zero (skip table)
92+
*/
93+
function rollCluster(opts = {}){
94+
const { size = 10, mods = 0, streak = false } = opts;
95+
if (streak) {
96+
return { roll: null, adj: null, hits: size, size, note: 'STREAK: full hits on success (skip cluster table)' };
97+
}
98+
const base = RNG.roll2d6();
99+
const adj = clamp(base + (mods|0), 2, 12);
100+
const row = CLUSTER[size];
101+
const hits = row ? (row[adj] || 0) : 0;
102+
return { roll: base, adj, hits, size };
103+
}
104+
105+
// ================================
106+
// Utils
107+
// ================================
108+
function clamp(n, lo, hi){ return n < lo ? lo : (n > hi ? hi : n); }
109+
function esc(s){ return String(s??'').replace(/[&<>"']/g,c=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c])); }
110+
111+
// ================================
112+
// Expose API (no UI)
113+
// ================================
114+
window.GATOR_ROLLS = Object.freeze({
115+
version: '1.1.0',
116+
roll2d6,
117+
rollLocation,
118+
rollCrit,
119+
rollCluster,
120+
__tables: { HIT_FRONT, HIT_REAR, CRIT_BY_ROLL, CLUSTER },
121+
});
122+
123+
// ================================
124+
// Lightweight CSS (fits TRS:80 theme)
125+
// ================================
126+
// ---- GATOR Rolls — style injection to match app theme (stacked under TN) ----
127+
(function injectGatorRollsCSS(){
128+
if (document.getElementById('gtrx-css')) return;
129+
const css = `
130+
/* Extra Rolls block */
131+
#gtr-extra-rolls{
132+
margin-top: 12px;
133+
padding-top: 10px;
134+
border-top: 1px solid var(--border);
135+
font-size: 13px;
136+
}
137+
138+
/* Controls row */
139+
#gtr-extra-rolls .gtrx-row{
140+
display:flex; flex-wrap:wrap; gap:8px 10px; align-items:center;
141+
}
142+
#gtr-extra-rolls .gtrx-sep{ width:1px; height:16px; background:var(--border); margin:0 2px; }
143+
#gtr-extra-rolls .gtrx-label{ color:var(--muted); font-size:12px; }
144+
145+
/* Readout sits BELOW controls, full width */
146+
#gtr-extra-rolls .gtrx-out{
147+
margin-top:8px;
148+
padding:6px 8px;
149+
border:1px solid var(--border);
150+
border-radius:6px;
151+
background:linear-gradient(180deg, rgba(255,255,255,.02), rgba(0,0,0,.02));
152+
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
153+
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
154+
min-height:32px;
155+
}
156+
#gtr-extra-rolls .gtrx-badge{
157+
border-radius:999px; padding:4px 8px;
158+
background:rgba(255,255,255,.04);
159+
font-weight:800; font-size:12.5px;
160+
}
161+
#gtr-extra-rolls .gtrx-note{ color:var(--muted); font-size:12px; }
162+
163+
/* Theme-consistent controls */
164+
#gtr-extra-rolls .btn.sm{ line-height:1; }
165+
#gtr-extra-rolls .gtr-in.tiny{
166+
width:64px; text-align:center; padding:6px 8px;
167+
border-radius:6px; border:1px solid var(--border); background:#0e1522; color:var(--ink);
168+
}
169+
#gtr-extra-rolls select.gtr-sel{
170+
padding:6px 8px; border-radius:6px; border:1px solid var(--border); background:#0e1522; color:var(--ink);
171+
}
172+
`;
173+
const st = document.createElement('style');
174+
st.id = 'gtrx-css';
175+
st.textContent = css;
176+
document.head.appendChild(st);
177+
})();
178+
179+
180+
// ================================
181+
// Mount helper — attach UI under current TN/Dice section
182+
// ================================
183+
// ---- Mount helper to place the UI directly BELOW the Target Number ----
184+
window.mountGatorRollsUnderTN = function mountGatorRollsUnderTN(){
185+
if (!window.GATOR_ROLLS) { console.warn('[gator] rolls API not ready'); return; }
186+
187+
// Find the Target Number block and mount AFTER its footer container
188+
const tnEl = document.getElementById('gtr-total');
189+
const footer = tnEl ? tnEl.closest('.gtr-footer') : null;
190+
191+
// Fallbacks just in case the footer isn't present for some reason
192+
const anchor =
193+
footer ||
194+
document.getElementById('roll-att-detail') ||
195+
document.getElementById('btn-roll-both') ||
196+
document.getElementById('btn-roll-att');
197+
198+
if (!anchor) { console.warn('[gator] TN anchor not found'); return; }
199+
if (document.getElementById('gtr-extra-rolls')) return; // already mounted
200+
201+
const wrap = document.createElement('div');
202+
wrap.id = 'gtr-extra-rolls';
203+
wrap.innerHTML = `
204+
<div class="gtrx-row">
205+
<button id="gtr-loc-front" type="button" class="btn sm">Location (Front)</button>
206+
<button id="gtr-loc-rear" type="button" class="btn sm">Location (Rear)</button>
207+
<button id="gtr-crit" type="button" class="btn sm">Critical Check</button>
208+
209+
<span class="gtrx-sep" aria-hidden="true"></span>
210+
211+
<span class="gtrx-label">Missiles:</span>
212+
<input id="gtr-miss-size" class="gtr-in tiny" type="number" min="2" max="20" value="10" />
213+
<select id="gtr-miss-type" class="gtr-sel">
214+
<option value="LRM" selected>LRM (1 dmg/shot)</option>
215+
<option value="SRM">SRM (2 dmg/shot)</option>
216+
</select>
217+
218+
<span class="gtrx-sep" aria-hidden="true"></span>
219+
220+
<label class="gtrx-label"><input id="gtr-mod-artemis" type="checkbox" /> Artemis +2</label>
221+
<label class="gtrx-label"><input id="gtr-mod-narc" type="checkbox" /> NARC +2</label>
222+
<label class="gtrx-label"><input id="gtr-mod-indir" type="checkbox" /> Indirect −1</label>
223+
<span class="gtrx-label">Other</span>
224+
<input id="gtr-miss-mods" class="gtr-in tiny" type="number" min="-4" max="4" value="0" />
225+
<label class="gtrx-label"><input id="gtr-miss-streak" type="checkbox" /> Streak</label>
226+
227+
<button id="gtr-miss" type="button" class="btn sm">Roll Cluster</button>
228+
</div>
229+
RESULTS
230+
<div id="gtr-extra-out" class="gtrx-out">
231+
<span class="gtrx-note">Results will appear here.</span>
232+
</div>
233+
`;
234+
235+
// Insert directly AFTER the TN footer so the whole block sits below the TN
236+
anchor.insertAdjacentElement('afterend', wrap);
237+
238+
// ------- Wiring -------
239+
const outEl = document.getElementById('gtr-extra-out');
240+
const out = (...parts) => { outEl.innerHTML = parts.join(' '); };
241+
const esc = (s)=>String(s??'').replace(/[&<>"']/g,c=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
242+
243+
// Hit Location
244+
document.getElementById('gtr-loc-front')?.addEventListener('click', () => {
245+
const r = GATOR_ROLLS.rollLocation('front');
246+
out(`<span class="gtrx-badge">Hit Loc</span>`, `Front → <b>${esc(r.location)}</b>`, `<span class="gtrx-note">(2d6=${r.roll})</span>`);
247+
});
248+
document.getElementById('gtr-loc-rear')?.addEventListener('click', () => {
249+
const r = GATOR_ROLLS.rollLocation('rear');
250+
out(`<span class="gtrx-badge">Hit Loc</span>`, `Rear → <b>${esc(r.location)}</b>`, `<span class="gtrx-note">(2d6=${r.roll})</span>`);
251+
});
252+
253+
// Critical check
254+
document.getElementById('gtr-crit')?.addEventListener('click', () => {
255+
const r = GATOR_ROLLS.rollCrit();
256+
const label = r.crits ? `<b>${r.crits} critical${r.crits>1?'s':''}</b>` : 'No criticals';
257+
out(`<span class="gtrx-badge">Critical</span>`, `${label}`, `<span class="gtrx-note">(2d6=${r.roll})</span>`);
258+
});
259+
260+
// Missile roll (clarity-first)
261+
document.getElementById('gtr-miss')?.addEventListener('click', () => {
262+
const size = Math.max(2, Math.min(20, +document.getElementById('gtr-miss-size').value || 10));
263+
const type = (document.getElementById('gtr-miss-type').value || 'LRM').toUpperCase();
264+
const isSRM = type === 'SRM';
265+
const dpm = isSRM ? 2 : 1; // damage per missile
266+
const streak = !!document.getElementById('gtr-miss-streak').checked;
267+
268+
// Auto modifiers
269+
const modsList = [];
270+
let autoMods = 0;
271+
if (document.getElementById('gtr-mod-artemis').checked) { autoMods += 2; modsList.push('+2 Artemis'); }
272+
if (document.getElementById('gtr-mod-narc').checked) { autoMods += 2; modsList.push('+2 NARC'); }
273+
if (document.getElementById('gtr-mod-indir').checked) { autoMods -= 1; modsList.push('−1 Indirect'); }
274+
275+
const manual = (+document.getElementById('gtr-miss-mods').value) || 0;
276+
if (manual) modsList.push((manual>0?'+':'') + manual + ' manual');
277+
278+
if (streak) {
279+
const hits = size, dmg = hits * dpm;
280+
out(
281+
`<span class="gtrx-badge">Missiles</span>`,
282+
`<b>STREAK:</b> on a successful to-hit, <b>${hits}/${size}</b> hit (${type}${dpm} dmg/shot → <b>${dmg} dmg</b>).`,
283+
`<span class="gtrx-note">Cluster table skipped.</span>`
284+
);
285+
return;
286+
}
287+
288+
const res = GATOR_ROLLS.rollCluster({ size, mods: autoMods + manual, streak:false });
289+
const hits = res.hits;
290+
const dmg = hits * dpm;
291+
292+
const modsPart = modsList.length
293+
? `<span class="gtrx-note">mods: ${modsList.join(', ')}</span>`
294+
: `<span class="gtrx-note">mods: none</span>`;
295+
296+
out(
297+
`<span class="gtrx-badge">Missiles</span>`,
298+
`${type} size <b>${size}</b> → <b>${hits}/${size}</b> hit`,
299+
`<span class="gtrx-note">(roll ${res.roll}${(autoMods||manual)?`, adj ${res.adj}`:''})</span>`,
300+
`<span class="gtrx-badge">${dmg} dmg</span>`,
301+
modsPart
302+
);
303+
});
304+
};
305+
306+
})();

0 commit comments

Comments
 (0)