Web Dev Solutions

Catalin Mititiuc

import * as panzoom from './modules/pan-zoom.js'; import * as gameboard from './modules/gameboard.js'; import * as recordSheet from './modules/record_sheet.js'; import * as mapSelectDialog from './modules/map_select_dialog.js'; import { Observable } from './modules/observable.js'; import { scenarios } from './modules/scenarios.js'; globalThis.svgns = 'http://www.w3.org/2000/svg'; const mapPlaceholder = document.querySelector('.map-placeholder'), distanceOutput = document.getElementById('status'), proneToggle = document.getElementById('toggle-prone-counter'), contentVisToggleEl = document.querySelector('#content input[type="checkbox"].visible'), fileName = localStorage.getItem('map') || 'scenario-side_show', map = scenarios[fileName]?.hashed || `assets/images/${fileName}.svg`, fileInputEl = document.querySelector('input[type="file"]'), dice = document.querySelectorAll('.die'), d6 = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }, toggleContentVis = (function () { document.querySelector('#content').style.minWidth = this.checked ? '' : 0; document.querySelectorAll('#content > div').forEach(div => { if (this.checked) { div.style.display = div.id == 'record-sheet' ? 'flex' : 'block'; } else { div.style.display = 'none'; } }); localStorage.setItem('content-visibility', this.checked); }).bind(contentVisToggleEl); let mapResourceEl = document.querySelector('object'); async function requestResource(url) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); request.open('GET', url, true); request.responseType = 'document'; request.onload = function() { if (request.status === 200) { resolve(request.response); } else { reject(Error('Image didn\'t load successfully; error code:' + request.statusText)); } }; request.onerror = function() { reject(Error('There was a network error.')); }; request.send(); }); } const scenarioRequest = requestResource(map); async function buildScenario(req) { const svg = scenarioTemplate.querySelector('svg').cloneNode(true); document.querySelector('object').contentDocument.querySelector('svg').remove(); document.querySelector('object').contentDocument.append(svg); gameboard.stop(); recordSheet.stop(); const scenario = await req; const startLocs = scenario.querySelector('.start-locations'); const gb = svg.querySelector('.gameboard'); const grid = svg.querySelector('.grid'); const externalResourceEls = Array.from(scenario.querySelectorAll('use[href*=".svg"')); const refs = externalResourceEls.reduce((acc, el) => { const href = el.getAttributeNS(null, 'href'); const [filename] = href.match(/.+\.svg/); const fragmentIdentifier = href.split('.svg').pop(); (acc[filename] ??= new Set()).add(fragmentIdentifier); el.setAttributeNS(null, 'href', fragmentIdentifier); return acc; }, {}); await Promise.all( Object.keys(refs).map(filename => requestResource(`assets/images/${filename}`)) ).then(result => { const defs = svg.querySelector('defs'); Object.keys(refs).forEach((filename, index) => { const external = result[index]; refs[filename].forEach(fragmentIdentifier => { external .querySelectorAll(`${fragmentIdentifier} use`) .forEach(el => refs[filename].add(el.getAttributeNS(null, 'href'))); }); const refsQuery = [...refs[filename]].join(', '); external.querySelectorAll(refsQuery).forEach(node => defs.append(svg.ownerDocument.importNode(node, true)) ); }); }); scenario.querySelectorAll('use.mapsheet').forEach(el => gb.querySelector('#background').after(svg.ownerDocument.importNode(el, true)) ); if (startLocs) grid.before(svg.ownerDocument.importNode(startLocs, true)); const scenarioGrid = scenario.querySelector('.grid'); if (scenarioGrid) { grid.replaceWith(svg.ownerDocument.importNode(scenarioGrid, true)); } async function loadScript() { return new Promise((resolve, reject) => { const scriptEl = document.createElementNS("http://www.w3.org/2000/svg", 'script'); scriptEl.onload = () => { console.log('map.js loaded'); resolve(); }; scriptEl.onerror = () => { reject(Error('Script failed to load.')); }; const oldScript = scenario.querySelector('script'); if ('cols' in oldScript.dataset && 'rows' in oldScript.dataset) { scriptEl.dataset.rows = oldScript.dataset.rows; scriptEl.dataset.cols = oldScript.dataset.cols; } scriptEl.setAttributeNS(null, 'href', '../../map.js'); svg.append(scriptEl); }); } await loadScript(); mapResourceEl.style.opacity = 1; mapPlaceholder.style.opacity = 0; panzoom.start(svg); gameboard.start(svg); recordSheet.start(startLocs, gameboard.getUnits()); } function updateTurnCounter() { const turnCounter = document.getElementById('turn-count'); if (turnCounter.dataset.update === '1') { turnCounter.children.namedItem('count').textContent++; turnCounter.dataset.update = '0'; } else { turnCounter.dataset.update = '1'; } } function enableEndTurnButton(allegiance) { document .querySelector(`button.end-turn:not([data-allegiance="${allegiance}"])`) .removeAttribute('disabled'); } function clearMoveEndedIndicators(records) { records.forEach(el => el.classList.remove('movement-ended')); } function distance(count = '-') { distanceOutput.querySelector('#hex-count').textContent = count; distanceOutput.style.display = count === '-' ? 'none' : 'block'; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values_inclusive function getRandomIntInclusive(min, max) { const minCeiled = Math.ceil(min); const maxFloored = Math.floor(max); return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled); // The maximum is inclusive and the minimum is inclusive } // ⚀ ⚁ ⚂ ⚃ ⚄ ⚅ function roll(die) { const numsAsWords = Object.values(die); return numsAsWords[getRandomIntInclusive(0, numsAsWords.length - 1)]; } let scenarioTemplate; async function load() { const svg = this.contentDocument.querySelector('svg'), startLocs = svg.querySelector('.start-locations') // , scriptEl = this.contentDocument.querySelector('script') ; scenarioTemplate = this.contentDocument.cloneNode(svg); await buildScenario(scenarioRequest); this.style.opacity = 1; mapPlaceholder.style.opacity = 0; } document.querySelectorAll('.end-turn').forEach(el => el.addEventListener('click', ({ target: { dataset: { allegiance: opponent }}}) => { const dataSelector = `[data-allegiance="${opponent}"]`, opponentRecords = Array.from(document.querySelectorAll(`.soldier-record${dataSelector}`)), firstOpponentRecord = opponentRecords.sort((el1, el2) => el1.dataset.number > el2.dataset.number).at(0); el.setAttribute('disabled', ''); updateTurnCounter(); enableEndTurnButton(opponent); clearMoveEndedIndicators(opponentRecords); gameboard.clearFiringArcs(opponent); Observable.notify('select', firstOpponentRecord); }) ); document.querySelectorAll('.set-firing-arc').forEach(el => el.addEventListener('click', gameboard.setFiringArc) ); document.querySelector('.set-grenade').addEventListener('click', gameboard.setGrenade); document.querySelectorAll('#toggle-firing-arc-vis input').forEach(el => el.addEventListener('input', gameboard.toggleFiringArcVisibility) ); document.getElementById('toggle-prone-counter').addEventListener('input', function () { const selected = recordSheet.getSelected(); selected && gameboard.toggleProne(); }); document.querySelectorAll('.end-move').forEach(el => el.addEventListener('click', () => Observable.notify('endmove')) ); document.querySelector('#fullscreen').addEventListener('click', () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); } else if (document.exitFullscreen) { document.exitFullscreen(); } }); document.querySelector('#download-save').addEventListener('click', e => { const data = document.querySelector('object').contentDocument.documentElement.outerHTML; const element = document.createElement('a'); element.setAttribute('download', 'save.svg'); element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(data)); // element.style.display = 'none'; // document.body.appendChild(element); element.click(); // document.body.removeChild(element); }); document.querySelector('#upload-save').addEventListener('click', () => { fileInputEl.click(); }); document.querySelector('input[type="file"]').addEventListener('change', e => { const [file] = fileInputEl.files; let reader = new FileReader(); reader.onload = function () { const parser = new DOMParser(); const doc = parser.parseFromString(reader.result, "image/svg+xml"); buildScenario(doc); }; reader.readAsText(file); }); document.querySelector('#roll-dice').addEventListener('click', () => { dice.forEach(el => { el.classList.remove('roll-in'); el.classList.add('roll-out'); }); }); contentVisToggleEl.addEventListener('input', toggleContentVis); contentVisToggleEl.checked = (localStorage.getItem('content-visibility') !== 'false'); toggleContentVis(); mapSelectDialog .init() .selectCurrentOptionOnPageLoad() .showOnClick() .updateValueOnSelection() .changeMapOnConfirm(data => { mapPlaceholder.style.opacity = 1; mapResourceEl.style.opacity = 0; buildScenario(requestResource(data)); }); mapResourceEl.addEventListener('load', load); // mapResourceEl.data = map; // mapResourceEl = null; dice.forEach(el => { el.classList.add(roll(d6)); el.addEventListener('animationend', e => { if (e.animationName === 'roll-out') { el.classList.remove('roll-out'); el.classList.replace(el.classList.item(1), roll(d6)); el.classList.add('roll-in'); } }); }); Observable.subscribe('distance', distance); Observable.subscribe('proneflag', checked => proneToggle.checked = checked);