index : btroops | |
Virtual board game-aid for BattleTroops, an infantry combat simulator wargame published by FASA in 1989. |
aboutsummaryrefslogtreecommitdiff |
diff options
Diffstat (limited to 'src/modules/gameboard.js')
-rw-r--r-- | src/modules/gameboard.js | 348 |
1 files changed, 348 insertions, 0 deletions
diff --git a/src/modules/gameboard.js b/src/modules/gameboard.js new file mode 100644 index 0000000..9f8723e --- /dev/null +++ b/src/modules/gameboard.js @@ -0,0 +1,348 @@ +import * as firingArc from './game/firing_arc.js'; +import * as sightLine from './game/sight_line.js'; +import * as soldier from './game/soldier.js'; + +function getCellContents(cell) { + return cell.querySelectorAll('*:not(use[href="#hex"])'); +} + +function getGridIndex({ parentElement: { dataset: { x }, parentElement: { dataset: { y }}}}) { + return { x: +x, y: +y }; +} + +function getHex(cell) { + return cell.querySelector('use[href="#hex"]'); +} + +function getCellOccupant(cell) { + return cell.querySelector('.counter'); +} + +function getCells(svg) { + return svg.querySelectorAll('g[data-y] > g[data-x]'); +} + +function getLockedSightLine(svg) { + return svg.querySelector('line.sight-line:not(.active)'); +} + +function getSightLine(svg) { + return svg.querySelector('line.sight-line'); +} + +function getActiveSightLine(svg) { + return svg.querySelector('line.sight-line.active'); +} + +function isGrenade(el) { + return el && el.getAttribute('href') === '#counter-grenade'; +} + +function isClone(counter) { + const isClone = counter.classList.contains('clone'), + { allegiance: clAl, number: clNum } = counter.dataset; + + return { + of: function ({ dataset: { allegiance, number }}) { + return isClone && clAl == allegiance && clNum == number; + } + }; +} + +function getCellPosition(cell) { + let pt = new DOMPoint(0, 0), + transform = getComputedStyle(cell).transform.match(/-?\d+\.?\d*/g), + mtx = new DOMMatrix(transform); + pt = pt.matrixTransform(mtx); + + transform = getComputedStyle(cell.parentElement).transform.match(/-?\d+\.?\d*/g); + mtx = new DOMMatrix(transform); + pt = pt.matrixTransform(mtx); + + return pt; +} + +function getCell(x, y) { + return svg.querySelector(`g[data-y="${y}"] > g[data-x="${x}"]`); +} + +function getCounterAtGridIndex(x, y) { + return getCell(x, y).querySelector('.counter'); +} + +function getSelected() { + return svg.querySelector(`.counter.selected[data-allegiance][data-number]`); +} + +function clearSightLine() { + sightLine.setHexes([]); + sightLine.clear(); + distanceCallback && distanceCallback(); +} + +function updateSightLine(cell) { + const { dataset: { x: sX }, parentElement: { dataset: { y: sY }}} = cell, + { dataset: { x: tX }, parentElement: { dataset: { y: tY }}} = sightLine.getLockTarget(); + + const selector = sightLine.calcIndexes(+sX, +sY, +tX, +tY) + .map(([x, y]) => `g[data-y="${y}"] g[data-x="${x}"] use[href="#hex"]`) + .join(', '); + + const hexes = svg.querySelectorAll(selector); + sightLine.setHexes(hexes); + sightLine.update(getCellPosition(cell)); + distanceCallback && distanceCallback(hexes.length - 1); +} + +function drawSightLine(sourceCell, targetCell) { + const { dataset: { x: sX }, parentElement: { dataset: { y: sY }}} = sourceCell, + { dataset: { x: tX }, parentElement: { dataset: { y: tY }}} = targetCell; + + const selector = sightLine.calcIndexes(+sX, +sY, +tX, +tY) + .map(([x, y]) => `g[data-y="${y}"] g[data-x="${x}"] use[href="#hex"]`) + .join(', '); + + const hexes = svg.querySelectorAll(selector); + sightLine.setHexes(hexes); + const line = sightLine.create(getCellPosition(sourceCell), getCellPosition(targetCell)); + svg.querySelector('.board').appendChild(line); + distanceCallback && distanceCallback(hexes.length - 1); +} + +let svg, distanceCallback, proneFlagCallback, selectCallback, + placing = []; + +export function setDistanceCallback(callback) { + distanceCallback = callback; +} + +export function setProneFlagCallback(callback) { + proneFlagCallback = callback; +} + +export function setSelectCallback(callback) { + selectCallback = callback; +} + +export function start(el) { + svg = el; + + getCells(svg).forEach(cell => { + cell.addEventListener('click', e => { + const state = { + placing: placing, + hex: getHex(cell), + occupant: getCellOccupant(cell), + contents: getCellContents(cell) + }; + + let toPlace = placing.pop(); + + if (isGrenade(toPlace)) { + state.hex.after(toPlace); + } else if (toPlace && !state.occupant) { + soldier.place(svg, toPlace, cell); + placing.push(toPlace); + const lockedSl = getLockedSightLine(svg); + + if (!lockedSl) { + clearSightLine(); + } else { + updateSightLine(cell); + } + } else if (toPlace && state.occupant) { + if (toPlace === state.occupant) { + if ('previous' in toPlace.dataset) { + const trace = soldier.getTrace(svg, toPlace); + toPlace.remove(); + toPlace = getCounterAtGridIndex(...toPlace.dataset.previous.split(',')); + toPlace.classList.remove('clone'); + toPlace.classList.add(soldier.getSelectedClass()); + if (!('previous' in toPlace.dataset)) { + trace.remove(); + } else { + const points = trace.getAttribute('points').split(' '); + points.pop(); + trace.setAttributeNS(null, 'points', points.join(' ')); + } + placing.push(toPlace); + const lockedSl = getLockedSightLine(svg); + + if (!lockedSl) { + clearSightLine(); + } else { + updateSightLine(toPlace.parentElement); + } + } else { + unSelect(); + } + } else if (!state.occupant.classList.contains('clone')) { + select(state.occupant); + } else { + if (isClone(state.occupant).of(toPlace)) { + if (!('previous' in state.occupant.dataset)) { + state.occupant.classList.remove('clone'); + state.occupant.classList.add(soldier.getSelectedClass()); + toPlace.remove(); + toPlace = state.occupant; + soldier.removeClones(svg, toPlace); + soldier.getTrace(svg, toPlace).remove(); + const lockedSl = getLockedSightLine(svg); + + if (!lockedSl) { + clearSightLine(); + } else { + updateSightLine(cell); + } + } else { + const index = getGridIndex(state.occupant), + trace = soldier.getTrace(svg, toPlace), + pos = getCellPosition(cell), + points = trace.getAttribute('points').split(' ').filter(p => p != `${pos.x},${pos.y}`).join(' ');; + + let current = toPlace; + + trace.setAttributeNS(null, 'points', points); + + while (current.dataset.previous != `${index.x},${index.y}`) { + current = getCounterAtGridIndex(...current.dataset.previous.split(',')); + } + + current.dataset.previous = state.occupant.dataset.previous; + state.occupant.remove(); + } + } + placing.push(toPlace); + } + } else if (!toPlace && state.occupant) { + select(state.occupant); + } else { + console.log('removing cell contents'); + state.contents.forEach(el => el.remove()); + } + }); + + cell.addEventListener('dblclick', e => { + const toPlace = placing.pop(), + occupant = getCellOccupant(cell); + + if (toPlace && occupant && toPlace == occupant) { + const { number, allegiance } = toPlace.dataset, + selector = `[data-allegiance="${allegiance}"][data-number="${number}"]`; + + svg.querySelectorAll(selector).forEach(el => el.remove()); + } + }); + + cell.addEventListener('contextmenu', e => { + e.preventDefault(); + + sightLine.toggleLock(cell); + cell.dispatchEvent(new MouseEvent('pointerover')); + }); + + cell.addEventListener('pointerover', e => { + let selected = getSelected(); + + if (selected) { + let sl = getSightLine(svg), + isOnBoard = selected.parentElement.hasAttribute('data-x'), + sourceCell = selected.parentElement; + + if (isOnBoard && (!sl || sl.classList.contains('active')) && sourceCell != cell) { + drawSightLine(sourceCell, cell); + } + } + + let occupant = getCellOccupant(cell); + + if (occupant) { + firingArc.toggleCounterVisibility(svg, occupant, true); + } + }); + + cell.addEventListener('pointerout', e => { + let sl = getActiveSightLine(svg); + + if (sl) { + clearSightLine(); + } + + let occupant = getCellOccupant(cell); + + if (occupant) { + firingArc.toggleCounterVisibility(svg, occupant, false); + } + }); + }); + + // debug + const c = soldier.getCounter(svg, { dataset: { allegiance: 'davion', number: '1' }}); + soldier.place(svg, c, getCell(17, 25)); + select(c); +} + +export function select(selected) { + const counter = soldier.getCounter(svg, selected); + + if (counter) { + unSelect(); + placing.push(counter); + counter.classList.add(soldier.getSelectedClass()); + firingArc.get(svg, counter).forEach(el => el.removeAttribute('clip-path')); + selectCallback && selectCallback({ prone: soldier.hasProne(counter), ...counter.dataset }); + } +} + +export function unSelect() { + const selected = getSelected(); + + if (selected) { + placing = []; + getSelected().classList.remove(soldier.getSelectedClass()); + clearSightLine(); + firingArc.clipAll(svg); + } +} + +export function endMove() { + const selected = getSelected(); + + if (selected) { + soldier.endMove(svg, selected); + unSelect(); + } +} + +export function endTurn(allegiance) { + firingArc.clear(svg, allegiance); +} + +export function toggleProne() { + const selected = getSelected(), + isOnBoard = selected && selected.parentElement.hasAttribute('data-x'); + + if (selected && isOnBoard) { + soldier.toggleProne(selected); + } +} + +export function toggleFiringArcVisibility() { + firingArc.toggleVisibility(svg, this.dataset.allegiance); +} + +export function setFiringArc() { + const counter = getSelected(), + isOnBoard = counter => counter && counter.parentElement.hasAttribute('data-x'); + + if (isOnBoard(counter)) { + firingArc.set(svg, this.dataset.size, counter, getCellPosition(counter.parentElement)); + } +} + +export function setGrenade() { + let counter = document.createElementNS(svgns, 'use'); + counter.setAttributeNS(null, 'href', '#counter-grenade'); + + placing.push(counter); +} |