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 { requestResource, build } from './modules/scenario.js'; import { scenarios } from './modules/scenarios.js'; globalThis.svgns = 'http://www.w3.org/2000/svg'; if (window.IS_DEV) { const source = new EventSource('/esbuild'); source.addEventListener('change', () => location.reload()); // source.addEventListener('message', (e) => console.log(e)); } const mapPlaceholder = document.querySelector('.map-placeholder'), distanceOutput = document.getElementById('status'), contentVisToggleEl = document.querySelector('#edge-inputs input[type="checkbox"].visible'), fileName = localStorage.getItem('map') || 'scenario-side_show', map = scenarios[fileName]?.hashed || `assets/images/${fileName}.svg`, scenarioRequest = requestResource(map), fileInputEl = document.querySelector('input[type="file"]'), dice = document.querySelectorAll('.die'), mapResourceEl = document.querySelector('object'), d6 = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }, attrNames = { 'primary-weapon': 'weapon', 'troop-number': 'number', 'squad-number': 'squad' }, 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 scenarioTemplate; async function buildScenario(req) { gameboard.stop(); recordSheet.stop(); const map = document.querySelector('object').contentDocument.querySelector('svg'); const template = scenarioTemplate.querySelector('svg').cloneNode(true); map.replaceWith(template); await build(template, req); const scenario = await req; const scenarioUnits = scenario.querySelectorAll('svg g.counter'); scenarioUnits.forEach(cntr => { cntr.querySelectorAll('use').forEach(use => { const [attr] = use.classList; const val = use.getAttributeNS(null, 'href').split('#').pop().split('-').pop(); cntr.setAttributeNS(null, `data-${attrNames[attr]}`, val); }); }); mapResourceEl.style.opacity = 1; mapPlaceholder.style.opacity = 0; panzoom.start(template); gameboard.start(template); // recordSheet.start(svg.querySelector('.start-locations'), gameboard.getUnits()); recordSheet.start(null, scenarioUnits); //const [_, trooper] = gameboard.getUnits(); //Observable.notify('select', trooper); //gameboard.setCounter('prone'); //Observable.notify('select'); } 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' : 'inline'; } // 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)]; } 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}:not(.inactive)`)); el.setAttribute('disabled', ''); updateTurnCounter(); enableEndTurnButton(opponent); clearMoveEndedIndicators(opponentRecords); gameboard.clearFiringArcs(opponent); if (opponentRecords.length > 0) { Observable.notify('select', opponentRecords.at(0)); //firstOpponentRecord.scrollIntoView({ behavior: 'smooth' }); } }) ); document.querySelectorAll('.set-firing-arc').forEach(el => el.addEventListener('click', gameboard.setFiringArc) ); document.querySelectorAll('.counters-list button, button.attacker, button.defender').forEach(el => { el.addEventListener('click', e => gameboard.setCounter(el.className)); }); document.querySelector('.set-mech-template').addEventListener('click', gameboard.setMechTemplate); document.querySelectorAll('#toggle-firing-arc-vis input').forEach(el => el.addEventListener('input', gameboard.toggleFiringArcVisibility) ); 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; const reader = new FileReader(); reader.onload = function () { const parser = new DOMParser(); const doc = parser.parseFromString(reader.result, "image/svg+xml"); mapResourceEl.addEventListener( 'transitionend', () => buildScenario(doc), { once: true } ); }; mapPlaceholder.style.opacity = 1; mapResourceEl.style.opacity = 0; reader.readAsText(file); }); document.querySelector('#roll-dice').addEventListener('click', () => { dice.forEach(el => { el.classList.remove('roll-in'); el.classList.add('roll-out'); }); }); document.querySelectorAll('.select-elevation button').forEach(el => el.addEventListener('click', () => { const current = document.querySelector('.select-elevation input:checked'); const siblingMethod = `${el.classList.contains('up') ? 'previous' : 'next'}ElementSibling`; let next = current; do { next = next[siblingMethod]; } while (next !== null && !next.matches('input')); if (next) { next.checked = true; const event = new Event('change', { value: next.value }); next.dispatchEvent(event); } })); document.querySelectorAll('[name="select-elevation"]').forEach(el => { el.addEventListener('change', function (e) { document.querySelector('object').contentDocument.querySelector('.gameboard').dataset.viewElevation = this.value; }); }); document.querySelector('#toggle-grid-vis input').addEventListener('change', function () { const svg = document.querySelector('object').contentDocument.querySelector('svg'); svg.querySelector('.grid').style.display = this.checked ? 'inline' : 'none'; svg.querySelector('#dots').style.display = this.checked ? 'inline' : 'none'; }); document.querySelectorAll('.view-squad').forEach(b => b.addEventListener('click', e => { const currentSquad = b.closest('.records-header').querySelector('.squad-number text'); const currentSquadContainer = b.closest('[id$="-record"]').querySelector(`.records > .squad-${currentSquad.textContent}`); if (b.value === 'next') { //const toSquad = currentSquadContainer.nextElementSibling || b.closest('[id$="-record"]').querySelector('.records > :first-child'); const toSquad = currentSquadContainer.nextElementSibling; if (!toSquad) return; currentSquad.textContent = +toSquad.className.match(/\d+/); currentSquadContainer.addEventListener('transitionend', e => { console.log('transitionend', 'current', currentSquadContainer, 'next', toSquad); currentSquadContainer.style.display = 'none'; toSquad.style.display = 'block'; b.closest('[id$="-record"]').querySelector('.records').scrollTo(0, 0); toSquad.style.transform = 'translateX(0)'; }, { once: true }); currentSquadContainer.style.transform = 'translateX(-100%)'; } else { const toSquad = currentSquadContainer.previousElementSibling; if (!toSquad) return; currentSquad.textContent = +toSquad.className.match(/\d+/); currentSquadContainer.addEventListener('transitionend', e => { currentSquadContainer.style.display = 'none'; toSquad.style.display = 'block'; b.closest('[id$="-record"]').querySelector('.records').scrollTo(0, 0); toSquad.style.transform = 'translateX(0)'; }, { once: true }); currentSquadContainer.style.transform = 'translateX(100%)'; } })); contentVisToggleEl.addEventListener('input', toggleContentVis); contentVisToggleEl.checked = (localStorage.getItem('content-visibility') !== 'false'); toggleContentVis(); mapSelectDialog .init() .selectCurrentOptionOnPageLoad() .showOnClick() .updateValueOnSelection() .changeMapOnConfirm(data => { const scenarioRequest = requestResource(data); mapResourceEl.addEventListener( 'transitionend', () => buildScenario(scenarioRequest), { once: true } ); mapPlaceholder.style.opacity = 1; mapResourceEl.style.opacity = 0; }); mapResourceEl.addEventListener('load', load); 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);