function getCsrfToken() { const meta = document.querySelector('meta[name="csrf-token"]'); return meta ? meta.getAttribute('content') : ''; } function getCurrentMode() { const checked = document.querySelector('input[name="zoneMode"]:checked'); return checked ? checked.value : 'terrain'; } function getSelectedPaint() { const active = document.querySelector('.bf-paint.active'); if (active) { return active.getAttribute('data-paint'); } return 'forest'; } function loadScenario(id) { if (window.bfStorage && typeof window.bfStorage.load === 'function') { return window.bfStorage.load(id); } return null; } function saveScenario(id, scenario) { if (window.bfStorage && typeof window.bfStorage.save === 'function') { window.bfStorage.save(id, scenario); } } function applyTile(button, paint, zone, isObjective) { button.setAttribute('data-paint', paint); if (zone) { button.setAttribute('data-zone', zone); } else { button.removeAttribute('data-zone'); } if (isObjective) { button.setAttribute('data-objective', '1'); } else { button.removeAttribute('data-objective'); } } function loadGrid(tileMap, deploymentZones, objective) { document.querySelectorAll('.bf-tile').forEach((button) => { const x = button.getAttribute('data-x'); const y = button.getAttribute('data-y'); const key = `${x}:${y}`; if (tileMap[key]) { button.setAttribute('data-paint', tileMap[key]); } button.removeAttribute('data-zone'); button.removeAttribute('data-objective'); }); Object.entries(deploymentZones).forEach(([team, positions]) => { positions.forEach((pos) => { const selector = `.bf-tile[data-x="${pos.x}"][data-y="${pos.y}"]`; const button = document.querySelector(selector); if (button) { button.setAttribute('data-zone', team); } }); }); if (objective) { const selector = `.bf-tile[data-x="${objective.x}"][data-y="${objective.y}"]`; const button = document.querySelector(selector); if (button) { button.setAttribute('data-objective', '1'); } } } function readGrid() { const tileMap = {}; const deploymentZones = { alpha: [], bravo: [] }; let objective = null; document.querySelectorAll('.bf-tile').forEach((button) => { const x = Number(button.getAttribute('data-x')); const y = Number(button.getAttribute('data-y')); const paint = button.getAttribute('data-paint'); if (paint && paint !== 'open') { tileMap[`${x}:${y}`] = paint; } const zone = button.getAttribute('data-zone'); if (zone === 'alpha' || zone === 'bravo') { deploymentZones[zone].push({ x, y }); } if (button.getAttribute('data-objective') === '1') { objective = { x, y }; } }); return { tileMap, deploymentZones, objective }; } function buildScenarioJson(scenarioId, tileMap, deploymentZones, objective) { const widthInput = document.querySelector('input[name="battlefieldWidth"]'); const heightInput = document.querySelector('input[name="battlefieldHeight"]'); const result = { id: scenarioId, name: scenarioId, battlefieldWidth: widthInput ? Number(widthInput.value) : 8, battlefieldHeight: heightInput ? Number(heightInput.value) : 8, battlefieldTerrain: tileMap, teamA: { units: [], }, teamB: { units: [], }, deploymentZones, objectives: {}, victoryCondition: 'eliminate_all', holdRoundsRequired: 1, }; if (objective) { result.victoryCondition = 'hold_objective'; result.holdRoundsRequired = 3; result.objectiveId = 'objective-1'; result.objectiveX = objective.x; result.objectiveY = objective.y; } return result; } function paintTile(button, ev) { const mode = getCurrentMode(); if (ev.shiftKey) { applyTile(button, 'open', null, false); return; } if (mode === 'objective') { document.querySelectorAll('.bf-tile[data-objective]').forEach((other) => { other.removeAttribute('data-objective'); }); applyTile(button, 'open', null, true); return; } if (mode === 'alpha' || mode === 'bravo') { const current = button.getAttribute('data-zone'); if (current === mode) { button.removeAttribute('data-zone'); } else { button.setAttribute('data-zone', mode); } return; } applyTile(button, getSelectedPaint(), null, false); } function selectPalette(event) { document.querySelectorAll('.bf-paint').forEach((b) => b.classList.remove('active')); event.currentTarget.classList.add('active'); } function showToast(message) { const toast = document.createElement('div'); toast.className = 'bf-toast'; toast.appendChild(document.createTextNode(message)); document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } function showErrors(errors) { const existing = document.querySelector('.bf-errors'); if (existing) { existing.remove(); } const div = document.createElement('div'); div.className = 'bf-errors'; const ul = document.createElement('ul'); errors.forEach((msg) => { const li = document.createElement('li'); li.appendChild(document.createTextNode(msg)); ul.appendChild(li); }); div.appendChild(ul); const form = document.getElementById('battlefield-form'); if (form) { form.parentNode.insertBefore(div, form); } } async function save(event) { event.preventDefault(); const form = document.getElementById('battlefield-form'); if (!form) { return; } const id = form.getAttribute('data-scenario-id'); if (!id) { return; } const { tileMap, deploymentZones, objective } = readGrid(); const scenario = buildScenarioJson(id, tileMap, deploymentZones, objective); const csrf = getCsrfToken(); let res; try { res = await fetch(`/scenarios/${encodeURIComponent(id)}/edit/battlefield`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, }, body: JSON.stringify(scenario), }); } catch (e) { showErrors([`Network error: ${e.message}`]); return; } if (res.ok) { const data = await res.json(); if (data.scenario) { saveScenario(id, data.scenario); showToast('Saved.'); } else { showErrors([(data.errors || ['Unknown error']).join(', ')]); } } else if (res.status === 400) { let data = {}; try { data = await res.json(); } catch (e) { // body wasn't JSON } showErrors(data.errors || ['Validation failed (HTTP 400).']); } else { showErrors([`Save failed: HTTP ${res.status}`]); } } document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('battlefield-form'); if (!form) { return; } const id = form.getAttribute('data-scenario-id'); if (id) { const scenario = loadScenario(id); if (scenario) { const battlefield = scenario.battlefield || {}; const terrain = battlefield.terrain || {}; const rawZones = scenario.deploymentZones || {}; const deploymentZones = { alpha: [], bravo: [] }; ['alpha', 'bravo'].forEach((team) => { if (rawZones[team] && Array.isArray(rawZones[team].positions)) { deploymentZones[team] = rawZones[team].positions; } }); const objectiveMarker = scenario.objectives ? scenario.objectives['objective-1'] : null; const objective = objectiveMarker ? objectiveMarker.position : null; loadGrid(terrain, deploymentZones, objective); } } document.querySelectorAll('.bf-tile').forEach((button) => { button.addEventListener('click', (ev) => paintTile(button, ev)); }); document.querySelectorAll('.bf-paint').forEach((button) => { button.addEventListener('click', selectPalette); }); if (document.querySelector('.bf-paint')) { document.querySelector('.bf-paint').classList.add('active'); } form.addEventListener('submit', save); });