feat: add match.js renderer and battle UI styles
This commit is contained in:
@@ -19,3 +19,22 @@ table.bf-grid button[data-zone="alpha"] { outline: 3px solid #00c; }
|
|||||||
table.bf-grid button[data-zone="bravo"] { outline: 3px solid #0c0; }
|
table.bf-grid button[data-zone="bravo"] { outline: 3px solid #0c0; }
|
||||||
.bf-errors { color: #c00; }
|
.bf-errors { color: #c00; }
|
||||||
.bf-toast { background: #dfd; padding: 0.5rem 1rem; margin: 0.5rem 0; }
|
.bf-toast { background: #dfd; padding: 0.5rem 1rem; margin: 0.5rem 0; }
|
||||||
|
.bf-match { display: flex; gap: 1rem; }
|
||||||
|
.bf-grid { display: grid; gap: 1px; background: #ddd; padding: 1px; width: max-content; }
|
||||||
|
.bf-tile { width: 2.5rem; height: 2.5rem; border: 0; background: #fff; padding: 0; position: relative; }
|
||||||
|
.bf-tile[data-bf-terrain="forest"] { background: #cfc; }
|
||||||
|
.bf-tile[data-bf-terrain="rough"] { background: #fec; }
|
||||||
|
.bf-tile[data-bf-terrain="water"] { background: #cce; }
|
||||||
|
.bf-tile[data-bf-terrain="blocking"] { background: #444; }
|
||||||
|
.bf-unit { width: 100%; height: 100%; font-size: 0.7rem; border: 0; cursor: pointer; }
|
||||||
|
.bf-unit--alpha { background: #cce5ff; }
|
||||||
|
.bf-unit--bravo { background: #ffcccc; }
|
||||||
|
.bf-unit--inactive { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
.bf-unit--winner { outline: 3px solid #c00; }
|
||||||
|
.bf-action--active { font-weight: bold; }
|
||||||
|
.bf-toast { background: #ffd; padding: 0.5rem 1rem; margin: 0.25rem 0; border: 1px solid #cc9; }
|
||||||
|
.bf-log { max-height: 12rem; overflow: auto; background: #f4f4f4; padding: 0.5rem; margin-top: 1rem; }
|
||||||
|
.bf-log-entry { margin: 0; font-family: monospace; font-size: 0.85rem; }
|
||||||
|
.bf-empty { color: #777; font-style: italic; }
|
||||||
|
.bf-result { margin-top: 1rem; padding: 1rem; background: #efe; border: 2px solid #393; }
|
||||||
|
.bf-result[hidden] { display: none; }
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
const CSRF_META = 'meta[name="csrf-token"]';
|
||||||
|
const ACTIVE_TEAM_LABEL = { alpha: 'Alpha', bravo: 'Bravo' };
|
||||||
|
|
||||||
|
function getCsrfToken() {
|
||||||
|
const meta = document.querySelector(CSRF_META);
|
||||||
|
return meta ? meta.getAttribute('content') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnvelope() {
|
||||||
|
const raw = localStorage.getItem('match:current');
|
||||||
|
if (raw === null) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!parsed || typeof parsed !== 'object' || !parsed.match || !parsed.matchId) return null;
|
||||||
|
return parsed;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnvelope(envelope) {
|
||||||
|
localStorage.setItem('match:current', JSON.stringify(envelope));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEnvelope() {
|
||||||
|
localStorage.removeItem('match:current');
|
||||||
|
}
|
||||||
|
|
||||||
|
function el(tag, attrs = {}, text = null) {
|
||||||
|
const node = document.createElement(tag);
|
||||||
|
Object.entries(attrs).forEach(([k, v]) => {
|
||||||
|
if (v !== null && v !== undefined) {
|
||||||
|
node.setAttribute(k, String(v));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (text !== null) {
|
||||||
|
node.appendChild(document.createTextNode(String(text)));
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNoMatch(root) {
|
||||||
|
while (root.firstChild) root.removeChild(root.firstChild);
|
||||||
|
const empty = el('p', { class: 'bf-empty' }, 'No match in progress.');
|
||||||
|
const link = el('a', { href: '/' }, 'Return to home');
|
||||||
|
root.appendChild(empty);
|
||||||
|
root.appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHeader(match) {
|
||||||
|
const header = document.getElementById('bf-match-header');
|
||||||
|
const team = document.getElementById('bf-active-team');
|
||||||
|
const round = document.getElementById('bf-round');
|
||||||
|
header.setAttribute('data-bf-active-team', match.activeTeamId);
|
||||||
|
header.setAttribute('data-bf-round', String(match.round));
|
||||||
|
while (team.firstChild) team.removeChild(team.firstChild);
|
||||||
|
team.appendChild(
|
||||||
|
document.createTextNode(ACTIVE_TEAM_LABEL[match.activeTeamId] || match.activeTeamId),
|
||||||
|
);
|
||||||
|
while (round.firstChild) round.removeChild(round.firstChild);
|
||||||
|
round.appendChild(document.createTextNode(String(match.round)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGrid(root, match) {
|
||||||
|
while (root.firstChild) root.removeChild(root.firstChild);
|
||||||
|
const { width, height, terrain } = match.battlefield;
|
||||||
|
root.setAttribute('data-bf-width', String(width));
|
||||||
|
root.setAttribute('data-bf-height', String(height));
|
||||||
|
|
||||||
|
const unitsByKey = {};
|
||||||
|
match.units.forEach((unit) => {
|
||||||
|
if (unit.health > 0) {
|
||||||
|
unitsByKey[`${unit.position.x}:${unit.position.y}`] = unit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y += 1) {
|
||||||
|
for (let x = 0; x < width; x += 1) {
|
||||||
|
const key = `${x}:${y}`;
|
||||||
|
const terrainType = (terrain && terrain[key]) || 'open';
|
||||||
|
const tile = el('button', {
|
||||||
|
type: 'button',
|
||||||
|
class: 'bf-tile',
|
||||||
|
'data-bf-x': x,
|
||||||
|
'data-bf-y': y,
|
||||||
|
'data-bf-terrain': terrainType,
|
||||||
|
});
|
||||||
|
const unit = unitsByKey[key];
|
||||||
|
if (unit) {
|
||||||
|
const isActive = unit.teamId === match.activeTeamId;
|
||||||
|
const isWinner = match.winnerTeamId && unit.teamId === match.winnerTeamId;
|
||||||
|
const cls = ['bf-unit', `bf-unit--${unit.teamId}`];
|
||||||
|
if (isActive) cls.push('bf-unit--active');
|
||||||
|
else cls.push('bf-unit--inactive');
|
||||||
|
if (isWinner) cls.push('bf-unit--winner');
|
||||||
|
const unitBtn = el(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: cls.join(' '),
|
||||||
|
'data-bf-unit-id': unit.id,
|
||||||
|
'data-bf-team': unit.teamId,
|
||||||
|
},
|
||||||
|
`${unit.id.split('-')[1] || unit.id} (${unit.health})`,
|
||||||
|
);
|
||||||
|
tile.appendChild(unitBtn);
|
||||||
|
}
|
||||||
|
root.appendChild(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLog(root, match) {
|
||||||
|
while (root.firstChild) root.removeChild(root.firstChild);
|
||||||
|
const log = Array.isArray(match.actionLog) ? match.actionLog.slice(-10) : [];
|
||||||
|
log.forEach((entry) => {
|
||||||
|
root.appendChild(el('p', { class: 'bf-log-entry' }, entry));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResult(match) {
|
||||||
|
const result = document.getElementById('bf-result');
|
||||||
|
const winner = document.getElementById('bf-winner');
|
||||||
|
while (winner.firstChild) winner.removeChild(winner.firstChild);
|
||||||
|
winner.appendChild(
|
||||||
|
document.createTextNode(ACTIVE_TEAM_LABEL[match.winnerTeamId] || match.winnerTeamId),
|
||||||
|
);
|
||||||
|
result.removeAttribute('hidden');
|
||||||
|
const endBtn = document.getElementById('bf-end-turn');
|
||||||
|
if (endBtn) endBtn.setAttribute('disabled', 'disabled');
|
||||||
|
['bf-action-move', 'bf-action-attack', 'bf-action-ability'].forEach((id) => {
|
||||||
|
const btn = document.getElementById(id);
|
||||||
|
if (btn) btn.setAttribute('disabled', 'disabled');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderState() {
|
||||||
|
const envelope = getEnvelope();
|
||||||
|
const grid = document.getElementById('bf-grid');
|
||||||
|
if (!envelope) {
|
||||||
|
renderNoMatch(grid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { match } = envelope;
|
||||||
|
renderHeader(match);
|
||||||
|
renderGrid(grid, match);
|
||||||
|
renderLog(document.getElementById('bf-log'), match);
|
||||||
|
if (match.winnerTeamId) {
|
||||||
|
renderResult(match);
|
||||||
|
} else {
|
||||||
|
document.getElementById('bf-result').setAttribute('hidden', 'hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toast(message) {
|
||||||
|
const host = document.getElementById('bf-toast-host');
|
||||||
|
if (!host) return;
|
||||||
|
const node = el('div', { class: 'bf-toast' }, message);
|
||||||
|
host.appendChild(node);
|
||||||
|
setTimeout(() => node.remove(), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dispatch(verb, params) {
|
||||||
|
const envelope = getEnvelope();
|
||||||
|
if (!envelope) {
|
||||||
|
toast('No match in progress.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = { matchId: envelope.matchId, match: envelope.match, ...params };
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/matches/current/${verb}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
'X-Turn-Token': envelope.turnToken || '',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
if (response.status === 200) {
|
||||||
|
setEnvelope({
|
||||||
|
matchId: envelope.matchId,
|
||||||
|
match: data.match,
|
||||||
|
turnToken: data.turnToken,
|
||||||
|
});
|
||||||
|
renderState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.status === 409) {
|
||||||
|
if (data.error === 'turn') {
|
||||||
|
const fresh = getEnvelope();
|
||||||
|
if (fresh) {
|
||||||
|
setEnvelope(fresh);
|
||||||
|
renderState();
|
||||||
|
toast('Turn has changed; resynced.');
|
||||||
|
} else {
|
||||||
|
renderNoMatch(document.getElementById('bf-grid'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.match) {
|
||||||
|
setEnvelope({
|
||||||
|
matchId: envelope.matchId,
|
||||||
|
match: data.match,
|
||||||
|
turnToken: data.turnToken || envelope.turnToken,
|
||||||
|
});
|
||||||
|
renderState();
|
||||||
|
}
|
||||||
|
if (data.error) toast(data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.error) {
|
||||||
|
toast(data.error);
|
||||||
|
} else {
|
||||||
|
toast(`Action failed: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast(`Network error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeMode = 'move';
|
||||||
|
let selectedUnitId = null;
|
||||||
|
|
||||||
|
function updateSelectedUnitIndicator() {
|
||||||
|
const node = document.getElementById('bf-selected-unit');
|
||||||
|
if (!node) return;
|
||||||
|
while (node.firstChild) node.removeChild(node.firstChild);
|
||||||
|
node.appendChild(document.createTextNode(selectedUnitId || 'none'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAbilityOptions() {
|
||||||
|
const host = document.getElementById('bf-ability-options');
|
||||||
|
if (!host) return;
|
||||||
|
while (host.firstChild) host.removeChild(host.firstChild);
|
||||||
|
if (activeMode !== 'ability' || !selectedUnitId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const envelope = getEnvelope();
|
||||||
|
if (!envelope) return;
|
||||||
|
const unit = envelope.match.units.find((u) => u.id === selectedUnitId);
|
||||||
|
if (!unit || !Array.isArray(unit.abilities) || unit.abilities.length === 0) {
|
||||||
|
host.appendChild(el('p', { class: 'bf-empty' }, 'No abilities.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unit.abilities.forEach((abilityId) => {
|
||||||
|
const radio = el('input', {
|
||||||
|
type: 'radio',
|
||||||
|
name: 'bf-ability',
|
||||||
|
value: abilityId,
|
||||||
|
id: `bf-ability-${abilityId}`,
|
||||||
|
});
|
||||||
|
const label = el('label', { for: `bf-ability-${abilityId}` }, abilityId);
|
||||||
|
const wrap = el('span', {});
|
||||||
|
wrap.appendChild(radio);
|
||||||
|
wrap.appendChild(label);
|
||||||
|
host.appendChild(wrap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMode(mode) {
|
||||||
|
activeMode = mode;
|
||||||
|
['move', 'attack', 'ability'].forEach((m) => {
|
||||||
|
const btn = document.getElementById(`bf-action-${m}`);
|
||||||
|
if (btn) {
|
||||||
|
if (m === mode) btn.classList.add('bf-action--active');
|
||||||
|
else btn.classList.remove('bf-action--active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
renderAbilityOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTileClick(event) {
|
||||||
|
const tile = event.target.closest('.bf-tile');
|
||||||
|
if (!tile) return;
|
||||||
|
const x = Number(tile.getAttribute('data-bf-x'));
|
||||||
|
const y = Number(tile.getAttribute('data-bf-y'));
|
||||||
|
const unitBtn = tile.querySelector('.bf-unit');
|
||||||
|
if (unitBtn) {
|
||||||
|
const unitId = unitBtn.getAttribute('data-bf-unit-id');
|
||||||
|
const team = unitBtn.getAttribute('data-bf-team');
|
||||||
|
const envelope = getEnvelope();
|
||||||
|
if (!envelope) return;
|
||||||
|
if (team !== envelope.match.activeTeamId) {
|
||||||
|
toast('That unit is not on the active team.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeMode === 'attack') {
|
||||||
|
toast('Pick a friendly unit to attack with, then click an enemy.');
|
||||||
|
selectedUnitId = unitId;
|
||||||
|
updateSelectedUnitIndicator();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedUnitId = unitId;
|
||||||
|
updateSelectedUnitIndicator();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeMode === 'move' && selectedUnitId) {
|
||||||
|
dispatch('move', { unitId: selectedUnitId, x, y });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeMode === 'attack' && selectedUnitId) {
|
||||||
|
const envelope = getEnvelope();
|
||||||
|
if (!envelope) return;
|
||||||
|
const attacker = envelope.match.units.find((u) => u.id === selectedUnitId);
|
||||||
|
if (!attacker) return;
|
||||||
|
const target = envelope.match.units.find(
|
||||||
|
(u) => u.position.x === x && u.position.y === y && u.teamId !== attacker.teamId,
|
||||||
|
);
|
||||||
|
if (!target) {
|
||||||
|
toast('No enemy on that tile.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch('attack', { attackerId: selectedUnitId, targetId: target.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeMode === 'ability' && selectedUnitId) {
|
||||||
|
const radio = document.querySelector('input[name="bf-ability"]:checked');
|
||||||
|
if (!radio) {
|
||||||
|
toast('Pick an ability first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch('ability', {
|
||||||
|
unitId: selectedUnitId,
|
||||||
|
abilityId: radio.value,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUnitClick(event) {
|
||||||
|
const unitBtn = event.target.closest('.bf-unit');
|
||||||
|
if (!unitBtn) return;
|
||||||
|
const unitId = unitBtn.getAttribute('data-bf-unit-id');
|
||||||
|
const team = unitBtn.getAttribute('data-bf-team');
|
||||||
|
const envelope = getEnvelope();
|
||||||
|
if (!envelope) return;
|
||||||
|
if (activeMode === 'attack' && team !== envelope.match.activeTeamId) {
|
||||||
|
if (!selectedUnitId) {
|
||||||
|
toast('Pick a friendly attacker first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch('attack', { attackerId: selectedUnitId, targetId: unitId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (team === envelope.match.activeTeamId) {
|
||||||
|
selectedUnitId = unitId;
|
||||||
|
updateSelectedUnitIndicator();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast('That unit is not on the active team.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireEvents() {
|
||||||
|
document.getElementById('bf-grid').addEventListener('click', (event) => {
|
||||||
|
const unitBtn = event.target.closest('.bf-unit');
|
||||||
|
if (unitBtn) {
|
||||||
|
onUnitClick(event);
|
||||||
|
} else {
|
||||||
|
onTileClick(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
['move', 'attack', 'ability'].forEach((mode) => {
|
||||||
|
const btn = document.getElementById(`bf-action-${mode}`);
|
||||||
|
if (btn) btn.addEventListener('click', () => setMode(mode));
|
||||||
|
});
|
||||||
|
const endBtn = document.getElementById('bf-end-turn');
|
||||||
|
if (endBtn) {
|
||||||
|
endBtn.addEventListener('click', () => dispatch('end-turn', {}));
|
||||||
|
}
|
||||||
|
const returnHome = document.getElementById('bf-return-home');
|
||||||
|
if (returnHome) {
|
||||||
|
returnHome.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
removeEnvelope();
|
||||||
|
window.location.href = '/';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
wireEvents();
|
||||||
|
setMode('move');
|
||||||
|
renderState();
|
||||||
|
});
|
||||||
+60
-2
@@ -188,14 +188,63 @@ async function startMatch() {
|
|||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.match) {
|
if (data.match) {
|
||||||
setCurrentMatch(data.match);
|
setCurrentMatch({ matchId: data.matchId, match: data.match, turnToken: data.turnToken });
|
||||||
window.location.href = '/';
|
window.location.href = '/matches/current';
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line no-alert
|
// eslint-disable-next-line no-alert
|
||||||
alert(`Start match failed: ${data.errors ? data.errors.join(', ') : 'unknown'}`);
|
alert(`Start match failed: ${data.errors ? data.errors.join(', ') : 'unknown'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startBundledMatch(id) {
|
||||||
|
if (typeof id !== 'string' || !/^[a-z0-9-]+$/.test(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const csrf = getCsrfToken();
|
||||||
|
let manifest;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/scenarios/bundled/${encodeURIComponent(id)}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(`Could not load scenario: HTTP ${res.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
manifest = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(`Network error: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(`/scenarios/${encodeURIComponent(id)}/start`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrf,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(manifest),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(`Network error: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(`Start match failed: HTTP ${response.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.match && data.matchId && data.turnToken) {
|
||||||
|
setCurrentMatch({ matchId: data.matchId, match: data.match, turnToken: data.turnToken });
|
||||||
|
window.location.href = '/matches/current';
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(`Start match failed: ${(data.errors || ['unknown']).join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.bfStorage = {
|
window.bfStorage = {
|
||||||
load,
|
load,
|
||||||
save,
|
save,
|
||||||
@@ -204,6 +253,7 @@ window.bfStorage = {
|
|||||||
currentMatch,
|
currentMatch,
|
||||||
setCurrentMatch,
|
setCurrentMatch,
|
||||||
removeCurrentMatch,
|
removeCurrentMatch,
|
||||||
|
startBundledMatch,
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -216,4 +266,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
startMatch();
|
startMatch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
document.querySelectorAll('[data-bf-bundle]').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const id = button.getAttribute('data-bf-bundle');
|
||||||
|
if (id) {
|
||||||
|
startBundledMatch(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user