355 lines
12 KiB
JavaScript
355 lines
12 KiB
JavaScript
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();
|
|
Observable.notify('viewElevation', 0);
|
|
|
|
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 mapRect = mapResourceEl.getBoundingClientRect();
|
|
|
|
console.log('map viewport center', mapRect.width / 2, mapRect.height / 2, mapRect);
|
|
|
|
const [horz1, horz2] = document.querySelectorAll('.horz');
|
|
const [vert1, vert2] = document.querySelectorAll('.vert');
|
|
|
|
const hw = mapRect.width / 2 - 0.5;
|
|
const hh = mapRect.height - 1;
|
|
horz1.setAttributeNS(null, 'style', `top: ${0}px; left: ${mapRect.left}px; width: ${hw}px; height: ${hh}px;`);
|
|
horz2.setAttributeNS(null, 'style', `bottom: ${1}px; right: ${window.innerWidth - mapRect.right}px; width: ${hw}px; height: ${hh}px`);
|
|
|
|
const vw = mapRect.width;
|
|
const vh = mapRect.height / 2 - 1;
|
|
vert1.setAttributeNS(null, 'style', `top: ${0}px; left: ${0}px; width: ${vw}px; height: ${vh}px`);
|
|
vert2.setAttributeNS(null, 'style', `bottom: ${1}px; right: ${window.innerWidth - mapRect.right}px; width: ${vw}px; height: ${vh}px`);
|
|
|
|
//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)];
|
|
}
|
|
|
|
function viewElevation(elevationLevel) {
|
|
const id = elevationLevel >= 0 ? elevationLevel : 'basement';
|
|
document.querySelector(`#select-elevation-${id}`).checked = true;
|
|
document.querySelector('object').contentDocument.querySelector('.gameboard').dataset.viewElevation = elevationLevel;
|
|
}
|
|
|
|
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), {
|
|
revealCounter: true,
|
|
revealRecord: true
|
|
});
|
|
}
|
|
})
|
|
);
|
|
|
|
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) Observable.notify('viewElevation', next.value);
|
|
}));
|
|
|
|
document.querySelectorAll('[name="select-elevation"]').forEach(el => {
|
|
el.addEventListener('change', e => Observable.notify('viewElevation', el.value));
|
|
});
|
|
|
|
document.querySelector('#toggle-grid-vis').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 (currentSquadContainer) {
|
|
if (b.value === 'next') {
|
|
const toSquad = currentSquadContainer.nextElementSibling;
|
|
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%)';
|
|
} 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);
|
|
Observable.subscribe('viewElevation', viewElevation);
|