Web Dev Solutions

Catalin Mititiuc

From ce98e325656b8419b80d8c248e0469f3a9708322 Mon Sep 17 00:00:00 2001 From: Catalin Mititiuc Date: Sat, 27 Apr 2024 15:58:11 -0700 Subject: Rename some files --- src/index.js | 34 ++-- src/modules/game.js | 348 ----------------------------------------- src/modules/game/counter.js | 133 ---------------- src/modules/game/firingArc.js | 348 ----------------------------------------- src/modules/game/firing_arc.js | 348 +++++++++++++++++++++++++++++++++++++++++ src/modules/game/sightLine.js | 145 ----------------- src/modules/game/sight_line.js | 144 +++++++++++++++++ src/modules/game/soldier.js | 133 ++++++++++++++++ src/modules/gameboard.js | 348 +++++++++++++++++++++++++++++++++++++++++ src/modules/pan-zoom.js | 33 ++++ src/modules/panzoom.js | 33 ---- src/modules/recordSheet.js | 32 ---- src/modules/record_sheet.js | 32 ++++ 13 files changed, 1055 insertions(+), 1056 deletions(-) delete mode 100644 src/modules/game.js delete mode 100644 src/modules/game/counter.js delete mode 100644 src/modules/game/firingArc.js create mode 100644 src/modules/game/firing_arc.js delete mode 100644 src/modules/game/sightLine.js create mode 100644 src/modules/game/sight_line.js create mode 100644 src/modules/game/soldier.js create mode 100644 src/modules/gameboard.js create mode 100644 src/modules/pan-zoom.js delete mode 100644 src/modules/panzoom.js delete mode 100644 src/modules/recordSheet.js create mode 100644 src/modules/record_sheet.js diff --git a/src/index.js b/src/index.js index fa99fec..40ee397 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ -import * as panzoom from './modules/panzoom.js'; -import * as game from './modules/game.js'; -import * as recordSheet from './modules/recordSheet.js'; +import * as panzoom from './modules/pan-zoom.js'; +import * as gameboard from './modules/gameboard.js'; +import * as recordSheet from './modules/record_sheet.js'; globalThis.svgns = "http://www.w3.org/2000/svg"; @@ -16,7 +16,7 @@ document.querySelector('object').addEventListener('load', function () { document .querySelector('#content input[type="checkbox"].visible') .addEventListener('input', function () { - let divs = document.querySelectorAll('#content div'); + const divs = document.querySelectorAll('#content div'); divs.forEach(d => { if (this.checked) { @@ -30,32 +30,32 @@ document window.addEventListener('load', () => { const svg = document.querySelector('object').contentDocument.querySelector('svg'); - game.start(svg); + gameboard.start(svg); panzoom.start(svg); - game.setDistanceCallback((count = '-') => { + gameboard.setDistanceCallback((count = '-') => { distanceOutput.querySelector('#hex-count').textContent = count; distanceOutput.style.display = count === '-' ? 'none' : 'block'; }); - game.setProneFlagCallback(checked => proneToggle.checked = checked); - game.setSelectCallback(data => recordSheet.select(data)); + gameboard.setProneFlagCallback(checked => proneToggle.checked = checked); + gameboard.setSelectCallback(data => recordSheet.select(data)); document.querySelectorAll('.soldier-record').forEach(el => el.addEventListener('click', () => { if (el.classList.contains('selected')) { el.classList.remove('selected'); - game.unSelect(); + gameboard.unSelect(); recordSheet.unSelect(); } else { - game.select(el); + gameboard.select(el); } }) ); document.querySelectorAll('.end-move').forEach(el => el.addEventListener('click', () => { recordSheet.endMove(); - game.endMove(); + gameboard.endMove(); })); document.querySelectorAll('.end-turn').forEach(el => @@ -82,23 +82,23 @@ window.addEventListener('load', () => { .sort((el1, el2) => el1.dataset.number > el2.dataset.number) .forEach(el => el.classList.remove('movement-ended')); - game.endTurn(allegiance); - game.select(records.at(0)); + gameboard.endTurn(allegiance); + gameboard.select(records.at(0)); }) ); document.querySelectorAll('.set-firing-arc').forEach(el => - el.addEventListener('click', game.setFiringArc) + el.addEventListener('click', gameboard.setFiringArc) ); - document.querySelector('.set-grenade').addEventListener('click', game.setGrenade); + document.querySelector('.set-grenade').addEventListener('click', gameboard.setGrenade); document.querySelectorAll('#toggle-firing-arc-vis input').forEach(el => - el.addEventListener('input', game.toggleFiringArcVisibility) + el.addEventListener('input', gameboard.toggleFiringArcVisibility) ); document.getElementById('toggle-prone-counter').addEventListener('input', function () { const selected = recordSheet.getSelected(); - selected && game.toggleProne(); + selected && gameboard.toggleProne(); }); }); diff --git a/src/modules/game.js b/src/modules/game.js deleted file mode 100644 index e66f126..0000000 --- a/src/modules/game.js +++ /dev/null @@ -1,348 +0,0 @@ -import * as firingArc from './game/firingArc.js'; -import * as sightLine from './game/sightLine.js'; -import * as counterMod from './game/counter.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) { - counterMod.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 = counterMod.getTrace(svg, toPlace); - toPlace.remove(); - toPlace = getCounterAtGridIndex(...toPlace.dataset.previous.split(',')); - toPlace.classList.remove('clone'); - toPlace.classList.add(counterMod.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(counterMod.getSelectedClass()); - toPlace.remove(); - toPlace = state.occupant; - counterMod.removeClones(svg, toPlace); - counterMod.getTrace(svg, toPlace).remove(); - const lockedSl = getLockedSightLine(svg); - - if (!lockedSl) { - clearSightLine(); - } else { - updateSightLine(cell); - } - } else { - const index = getGridIndex(state.occupant), - trace = counterMod.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 = counterMod.getCounter(svg, { dataset: { allegiance: 'davion', number: '1' }}); - counterMod.place(svg, c, getCell(17, 25)); - select(c); -} - -export function select(selected) { - const counter = counterMod.getCounter(svg, selected); - - if (counter) { - unSelect(); - placing.push(counter); - counter.classList.add(counterMod.getSelectedClass()); - firingArc.get(svg, counter).forEach(el => el.removeAttribute('clip-path')); - selectCallback && selectCallback({ prone: counterMod.hasProne(counter), ...counter.dataset }); - } -} - -export function unSelect() { - const selected = getSelected(); - - if (selected) { - placing = []; - getSelected().classList.remove(counterMod.getSelectedClass()); - clearSightLine(); - firingArc.clipAll(svg); - } -} - -export function endMove() { - const selected = getSelected(); - - if (selected) { - counterMod.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) { - counterMod.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); -} diff --git a/src/modules/game/counter.js b/src/modules/game/counter.js deleted file mode 100644 index 0d75fc6..0000000 --- a/src/modules/game/counter.js +++ /dev/null @@ -1,133 +0,0 @@ -const selectedClass = 'selected'; - -function dataSelector({ dataset: { allegiance, number }}) { - return `[data-number="${number}"][data-allegiance="${allegiance}"]`; -} - -function traceSelector(counter) { - return `polyline.move-trace${dataSelector(counter)}`; -} - -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 getClones(svg, counter) { - return svg.querySelectorAll(`.counter.clone${dataSelector(counter)}`); -} - -function getCounterAndClones(svg, counter) { - return svg.querySelectorAll(`.counter${dataSelector(counter)}`); -} - -export function getCounter(svg, selected) { - return svg.querySelector(`.counter${dataSelector(selected)}:not(.clone)`); -} - -export function getTrace(svg, counter) { - return svg.querySelector(traceSelector(counter)); -} - -export function place(svg, selected, cell) { - let points, - counterNodeList = getCounterAndClones(svg, selected); - - if (counterNodeList.length > 0 && selected.parentElement.hasAttribute('data-x')) { - let trace = svg.querySelector(traceSelector(selected)); - - let prevCoords = [ - selected.parentElement.dataset.x, - selected.parentElement.parentElement.dataset.y - ] - - let clone = selected.cloneNode(true); - clone.classList.remove(selectedClass); - clone.classList.add('clone'); - - selected.dataset.previous = prevCoords; - selected.parentElement.appendChild(clone); - cell.appendChild(selected); - - selected.childNodes.forEach(n => { - if (n.classList.contains('removed')) { - n.remove(); - } else if ('preexisting' in n.dataset) { - delete n.dataset.preexisting; - } - }); - - let previous = getCellPosition(clone.parentElement), - current = getCellPosition(selected.parentElement); - - if (!trace) { - trace = document.createElementNS(svgns, 'polyline'); - - points = `${previous.x},${previous.y} ${current.x},${current.y}`; - - trace.dataset.number = selected.dataset.number; - trace.dataset.allegiance = selected.dataset.allegiance; - trace.classList.add('move-trace'); - - svg.querySelector('.board').prepend(trace); - } else { - points = `${trace.getAttribute('points')} ${current.x},${current.y}`; - } - - trace.setAttributeNS(null, 'points', points); - } else { - selected.removeAttribute('data-x'); - cell.appendChild(selected); - } -} - -export function removeClones(svg, counter) { - getClones(svg, counter).forEach(c => c.remove()); -} - -export function endMove(svg, counter) { - const trace = svg.querySelector(traceSelector(counter)), - proneCounter = counter.querySelector('[href="#counter-prone"]'); - - if (trace) { - trace.remove(); - } - - delete counter.dataset.previous; - - if (proneCounter) { - proneCounter.dataset.preexisting = ''; - } - - removeClones(counter); -} - -export function hasProne(counter) { - return !!counter.querySelector('[href="#counter-prone"]'); -} - -export function toggleProne(counter) { - let proneCounter = counter.querySelector('[href="#counter-prone"]'); - - if (!proneCounter) { - proneCounter = document.createElementNS(svgns, 'use'); - proneCounter.setAttributeNS(null, 'href', '#counter-prone'); - counter.appendChild(proneCounter); - } else if ('preexisting' in proneCounter.dataset) { - proneCounter.classList.toggle('removed'); - } else { - proneCounter.remove(); - } -} - -export function getSelectedClass() { - return selectedClass; -} diff --git a/src/modules/game/firingArc.js b/src/modules/game/firingArc.js deleted file mode 100644 index 817bc44..0000000 --- a/src/modules/game/firingArc.js +++ /dev/null @@ -1,348 +0,0 @@ -// source: https://www.redblobgames.com/grids/hexagons/ -// Horizontal distance between hex centers is sqrt(3) * size. The vertical -// distance is 3 / 2 * size. When we calculate horzDist / vertDist, the size -// cancels out, leaving us with a unitless ratio of sqrt(3) / (3 / 2), or -// 2 * sqrt(3) / 3. - -const horzToVertDistRatio = 2 * Math.sqrt(3) / 3, - - arcSize = { - 'small': Math.atan(horzToVertDistRatio / 6), - 'medium': Math.atan(horzToVertDistRatio / 2), - 'large': Math.atan(7 * horzToVertDistRatio / 2) - }, - - firingArcVisibility = { - davion: false, - liao: false - }, - - clippedFiringArcRadius = 25; - -class Point { - constructor(x = 0, y = 0) { - this.x = +x; - this.y = +y; - } - - toString() { - return `${this.x},${this.y}`; - } -} - -function calculateAngle(xDiff, yDiff) { - yDiff = -yDiff; - let angle = Math.abs(Math.atan(yDiff / xDiff)); - - if (xDiff < 0 && yDiff > 0) { - angle = Math.PI - angle; - } else if (xDiff < 0 && yDiff < 0) { - angle = Math.PI + angle; - } else if (xDiff > 0 && yDiff < 0) { - angle = 2 * Math.PI - angle; - } - - return angle; -} - -function calcEdgePt({ x: x1, y: y1 }, { x: x2, y: y2 }, { x: [minX, maxX], y: [minY, maxY] }) { - const xDiff = x2 - x1, - yDiff = y2 - y1, - xIntercept = y => (y - y1) * xDiff / yDiff + x1, - yIntercept = x => (x - x1) * yDiff / xDiff + y1; - - let pointCoords; - - if (xDiff > 0 && yDiff > 0) { - let x = xIntercept(maxY); - - pointCoords = x <= maxX ? [x, maxY] : [maxX, yIntercept(maxX)]; - } else if (xDiff > 0 && yDiff < 0) { - let y = yIntercept(maxX); - - pointCoords = y >= minY ? [maxX, y] : [xIntercept(minY), minY]; - } else if (xDiff < 0 && yDiff < 0) { - let x = xIntercept(minY); - - pointCoords = x >= minX ? [x, minY] : [minX, yIntercept(minX)]; - } else { - let y = yIntercept(minX); - - pointCoords = y <= maxY ? [minX, y] : [xIntercept(maxY), maxY]; - } - - return new Point(...pointCoords); -} - -function touchSameEdge({ x: x1, y: y1 }, { x: x2, y: y2 }) { - return x1 === x2 || y1 === y2; -} - -function shareValue({ x: x1, y: y1 }, { x: x2, y: y2 }) { - return x1 === x2 || y1 === y2; -} - -function touchOrthogonalEdges({ x: x1, y: y1 }, { x: x2, y: y2 }, bounds) { - return (bounds.x.includes(x1) && bounds.y.includes(y2)) || (bounds.y.includes(y1) && bounds.x.includes(x2)); -} - -function getCornerPts({ x: [xMin, xMax], y: [yMin, yMax] }) { - const corners = [[xMin, yMin], [xMax, yMin], [xMax, yMax], [xMin, yMax]]; - return corners.map(([x, y]) => new Point(x, y)); -} - -function getBounds({ x, y, width, height }) { - return { - x: [x, x + width], - y: [y, y + height] - }; -} - -// which arcpt does aimpt share a value with? -// if they share an x value, we will look for corner y value -// if they share a y value, we will look for corner x value -// is aim pt non-shared value greater or less than arcpt non-shared value? -function findWhichTwoCorners(pt, bounds, ...pts) { - const ptVals = Object.values(pt), - sharedValPt = pts.find(({ x, y }) => ptVals.includes(x) || ptVals.includes(y)); - - if (!sharedValPt) { - return; - } - - const nonSharedValKey = pt.x === sharedValPt.x ? 'y' : 'x'; - let cornerVal; - - if (pt[nonSharedValKey] < sharedValPt[nonSharedValKey]) { - cornerVal = Math.min(...bounds[nonSharedValKey]); - } else { - cornerVal = Math.max(...bounds[nonSharedValKey]); - } - - return getCornerPts(bounds).filter(cp => cp[nonSharedValKey] === cornerVal); -} - -function selectCornerPoints(aimPt, arcPt1, arcPt2, bounds) { - const cornerPts = getCornerPts(bounds); - - let points; - - if (touchSameEdge(arcPt1, arcPt2)) { - // 0-corner case - points = []; - } else if (touchOrthogonalEdges(arcPt1, arcPt2, bounds)) { - if (touchSameEdge(aimPt, arcPt1) || touchSameEdge(aimPt, arcPt2)) { - // 1-corner case - let cp = cornerPts.find(cp => shareValue(cp, arcPt1) && shareValue(cp, arcPt2)); - points = [cp]; - } else { - // 3-corner case - points = cornerPts.filter(cp => !shareValue(cp, arcPt1) || !shareValue(cp, arcPt2)); - } - } else { - if (touchSameEdge(aimPt, arcPt1) || touchSameEdge(aimPt, arcPt2)) { - // 2-corner case, aim and an arc point touch the same edge - points = findWhichTwoCorners(aimPt, bounds, arcPt1, arcPt2); - } else { - // 2-corner case, aim and both arc points all touch different edges - points = cornerPts.filter(cp => shareValue(cp, aimPt) || shareValue(cp, aimPt)); - } - } - - return points; -} - -function orderPoints(arcPoints, cornerPts) { - if (cornerPts.length === 0) { - return arcPoints; - } - - const index = cornerPts.findIndex(cp => shareValue(cp, arcPoints.at(0))); - return cornerPts.slice(0, index + 1).concat(arcPoints).concat(cornerPts.slice(index + 1)); -} - -function calcArcLinePtDeltas(aimPt, pivotPt, size) { - const angle = calculateAngle(aimPt.x - pivotPt.x, aimPt.y - pivotPt.y), - arcAngle = arcSize[size], - distance = Math.sqrt((aimPt.x - pivotPt.x) ** 2 + (aimPt.y - pivotPt.y) ** 2), - yDelta = distance * Math.cos(angle) * Math.tan(arcAngle), - xDelta = distance * Math.sin(angle) * Math.tan(arcAngle); - - return { xDelta, yDelta }; -} - -function calcPoints(e, aimLine, grid, size) { - const pointer = new DOMPoint(e.clientX, e.clientY), - pointerPt = pointer.matrixTransform(grid.getScreenCTM().inverse()), - pivotPt = new Point(aimLine.getAttribute('x1'), aimLine.getAttribute('y1')), - - bounds = getBounds(grid.getBBox()), - aimPt = calcEdgePt(pivotPt, pointerPt, bounds), - - { xDelta, yDelta } = calcArcLinePtDeltas(aimPt, pivotPt, size), - arcPt1 = calcEdgePt(pivotPt, new Point(aimPt.x - xDelta, aimPt.y - yDelta), bounds), - arcPt2 = calcEdgePt(pivotPt, new Point(aimPt.x + xDelta, aimPt.y + yDelta), bounds), - - outlinePoints = [arcPt2, pivotPt, arcPt1], - cornerPoints = selectCornerPoints(aimPt, arcPt1, arcPt2, bounds), - arcPoints = orderPoints(outlinePoints, cornerPoints); - - return { aimPt, outlinePoints, arcPoints }; -} - -function setDataAttrs({ dataset: { allegiance, number }}, el) { - el.dataset.allegiance = allegiance; - el.dataset.number = number; -} - -function getClipPathId({ dataset: { allegiance, number }}) { - return `clip-path-${allegiance}-${number}`; -} - -function getUnclipped(svg) { - return svg.querySelectorAll('#firing-arcs #shapes polygon:not([clip-path]), #firing-arcs #lines polyline:not([clip-path])'); -}; - -function createAimLine(x, y, container) { - const aimLine = document.createElementNS(svgns, 'line'); - aimLine.setAttributeNS(null, 'x1', x); - aimLine.setAttributeNS(null, 'y1', y); - aimLine.setAttributeNS(null, 'x2', x); - aimLine.setAttributeNS(null, 'y2', y); - container.appendChild(aimLine); - - return aimLine; -} - -function createClipPath(x, y, id, container) { - const clipShape = document.createElementNS(svgns, 'circle'), - clipPath = document.createElementNS(svgns, 'clipPath'); - - clipShape.setAttributeNS(null, 'cx', x); - clipShape.setAttributeNS(null, 'cy', y); - clipShape.setAttributeNS(null, 'r', clippedFiringArcRadius); - - clipPath.setAttributeNS(null, 'id', id); - clipPath.appendChild(clipShape); - container.appendChild(clipPath); - - return clipPath; -} - -function createFiringArc(x, y, size, container) { - const firingArc = document.createElementNS(svgns, 'polygon'); - firingArc.setAttributeNS(null, 'points', `${x},${y}`); - firingArc.dataset.size = size; - firingArc.classList.add('firing-arc', 'active'); - container.appendChild(firingArc); - - return firingArc; -} - -function createFiringArcOutline(x, y, container) { - const firingArcOutline = document.createElementNS(svgns, 'polyline'); - firingArcOutline.setAttributeNS(null, 'points', `${x},${y}`); - container.appendChild(firingArcOutline); - - return firingArcOutline; -} - -function queryContainers(svg) { - const grid = svg.querySelector('.grid'), - arcContainer = svg.querySelector('#firing-arcs'), - arcLayer = arcContainer.querySelector('#shapes'), - outlineLayer = arcContainer.querySelector('#lines'); - - return { grid, containers: { arcContainer, arcLayer, outlineLayer }}; -} - -function create(x, y, size, counter, { arcContainer, arcLayer, outlineLayer }) { - const aimLine = createAimLine(x, y, outlineLayer), - firingArc = createFiringArc(x, y, size, arcLayer), - firingArcOutline = createFiringArcOutline(x, y, outlineLayer), - clipPath = createClipPath(x, y, getClipPathId(counter), arcContainer); - - setDataAttrs(counter, firingArc); - setDataAttrs(counter, firingArcOutline); - setDataAttrs(counter, clipPath); - - return { aimLine, firingArc, firingArcOutline }; -} - -function set(svg, size, counter, { x, y }) { - get(svg, counter).forEach(el => el.remove()); - - const { grid, containers } = queryContainers(svg), - { aimLine, firingArc, firingArcOutline } = create(x, y, size, counter, containers); - - function positionListener(e) { - const { aimPt, outlinePoints, arcPoints } = calcPoints(e, aimLine, grid, size); - - aimLine.setAttributeNS(null, 'x2', aimPt.x); - aimLine.setAttributeNS(null, 'y2', aimPt.y); - firingArcOutline.setAttributeNS(null, 'points', outlinePoints.join(' ')); - firingArc.setAttributeNS(null, 'points', arcPoints.join(' ')); - } - - function placementListener() { - aimLine.remove(); - firingArc.classList.remove('active'); - grid.removeAttribute('style'); - svg.removeEventListener('mousemove', positionListener); - } - - function cancelPlacementListener(e) { - e.preventDefault(); - - get(counter).forEach(el => el.remove()); - grid.removeAttribute('style'); - svg.removeEventListener('mousemove', positionListener); - } - - grid.style.pointerEvents = 'none'; - firingArc.addEventListener('click', placementListener, { once: true }); - firingArc.addEventListener('contextmenu', cancelPlacementListener, { once: true }); - svg.addEventListener('mousemove', positionListener); -} - -function clear(svg, allegiance) { - const selector = `#firing-arcs [data-allegiance="${allegiance}"]`; - svg.querySelectorAll(selector).forEach(el => el.remove()); -} - -function get(svg, { dataset: { allegiance, number }}) { - return svg.querySelectorAll(`#firing-arcs [data-number="${number}"][data-allegiance="${allegiance}"], #firing-arcs line`); -} - -function toggleVisibility(svg, allegiance) { - const vis = firingArcVisibility[allegiance], - clipPaths = svg.querySelectorAll(`clipPath[data-allegiance="${allegiance}"]`); - - clipPaths.forEach(cp => cp.style.display = !vis ? 'none' : ''); - firingArcVisibility[allegiance] = !vis; -} - -function toggleCounterVisibility(svg, { dataset: { number, allegiance }}, vis) { - const cp = svg.querySelector(`#clip-path-${allegiance}-${number}`), - display = vis ? 'none' : ''; - - if (cp) { - cp.style.display = firingArcVisibility[allegiance] ? 'none' : display; - } -} - -function clipAll(svg) { - getUnclipped(svg).forEach(el => { - const { number, allegiance } = el.dataset, - clipPathId = `clip-path-${allegiance}-${number}`, - isVisible = firingArcVisibility[allegiance]; - - if (isVisible) { - svg.querySelector(`#${clipPathId}`).style.display = 'none'; - } - - el.setAttributeNS(null, 'clip-path', `url(#${clipPathId})`); - }); -} - -export { set, clear, get, toggleVisibility, toggleCounterVisibility, clipAll }; diff --git a/src/modules/game/firing_arc.js b/src/modules/game/firing_arc.js new file mode 100644 index 0000000..817bc44 --- /dev/null +++ b/src/modules/game/firing_arc.js @@ -0,0 +1,348 @@ +// source: https://www.redblobgames.com/grids/hexagons/ +// Horizontal distance between hex centers is sqrt(3) * size. The vertical +// distance is 3 / 2 * size. When we calculate horzDist / vertDist, the size +// cancels out, leaving us with a unitless ratio of sqrt(3) / (3 / 2), or +// 2 * sqrt(3) / 3. + +const horzToVertDistRatio = 2 * Math.sqrt(3) / 3, + + arcSize = { + 'small': Math.atan(horzToVertDistRatio / 6), + 'medium': Math.atan(horzToVertDistRatio / 2), + 'large': Math.atan(7 * horzToVertDistRatio / 2) + }, + + firingArcVisibility = { + davion: false, + liao: false + }, + + clippedFiringArcRadius = 25; + +class Point { + constructor(x = 0, y = 0) { + this.x = +x; + this.y = +y; + } + + toString() { + return `${this.x},${this.y}`; + } +} + +function calculateAngle(xDiff, yDiff) { + yDiff = -yDiff; + let angle = Math.abs(Math.atan(yDiff / xDiff)); + + if (xDiff < 0 && yDiff > 0) { + angle = Math.PI - angle; + } else if (xDiff < 0 && yDiff < 0) { + angle = Math.PI + angle; + } else if (xDiff > 0 && yDiff < 0) { + angle = 2 * Math.PI - angle; + } + + return angle; +} + +function calcEdgePt({ x: x1, y: y1 }, { x: x2, y: y2 }, { x: [minX, maxX], y: [minY, maxY] }) { + const xDiff = x2 - x1, + yDiff = y2 - y1, + xIntercept = y => (y - y1) * xDiff / yDiff + x1, + yIntercept = x => (x - x1) * yDiff / xDiff + y1; + + let pointCoords; + + if (xDiff > 0 && yDiff > 0) { + let x = xIntercept(maxY); + + pointCoords = x <= maxX ? [x, maxY] : [maxX, yIntercept(maxX)]; + } else if (xDiff > 0 && yDiff < 0) { + let y = yIntercept(maxX); + + pointCoords = y >= minY ? [maxX, y] : [xIntercept(minY), minY]; + } else if (xDiff < 0 && yDiff < 0) { + let x = xIntercept(minY); + + pointCoords = x >= minX ? [x, minY] : [minX, yIntercept(minX)]; + } else { + let y = yIntercept(minX); + + pointCoords = y <= maxY ? [minX, y] : [xIntercept(maxY), maxY]; + } + + return new Point(...pointCoords); +} + +function touchSameEdge({ x: x1, y: y1 }, { x: x2, y: y2 }) { + return x1 === x2 || y1 === y2; +} + +function shareValue({ x: x1, y: y1 }, { x: x2, y: y2 }) { + return x1 === x2 || y1 === y2; +} + +function touchOrthogonalEdges({ x: x1, y: y1 }, { x: x2, y: y2 }, bounds) { + return (bounds.x.includes(x1) && bounds.y.includes(y2)) || (bounds.y.includes(y1) && bounds.x.includes(x2)); +} + +function getCornerPts({ x: [xMin, xMax], y: [yMin, yMax] }) { + const corners = [[xMin, yMin], [xMax, yMin], [xMax, yMax], [xMin, yMax]]; + return corners.map(([x, y]) => new Point(x, y)); +} + +function getBounds({ x, y, width, height }) { + return { + x: [x, x + width], + y: [y, y + height] + }; +} + +// which arcpt does aimpt share a value with? +// if they share an x value, we will look for corner y value +// if they share a y value, we will look for corner x value +// is aim pt non-shared value greater or less than arcpt non-shared value? +function findWhichTwoCorners(pt, bounds, ...pts) { + const ptVals = Object.values(pt), + sharedValPt = pts.find(({ x, y }) => ptVals.includes(x) || ptVals.includes(y)); + + if (!sharedValPt) { + return; + } + + const nonSharedValKey = pt.x === sharedValPt.x ? 'y' : 'x'; + let cornerVal; + + if (pt[nonSharedValKey] < sharedValPt[nonSharedValKey]) { + cornerVal = Math.min(...bounds[nonSharedValKey]); + } else { + cornerVal = Math.max(...bounds[nonSharedValKey]); + } + + return getCornerPts(bounds).filter(cp => cp[nonSharedValKey] === cornerVal); +} + +function selectCornerPoints(aimPt, arcPt1, arcPt2, bounds) { + const cornerPts = getCornerPts(bounds); + + let points; + + if (touchSameEdge(arcPt1, arcPt2)) { + // 0-corner case + points = []; + } else if (touchOrthogonalEdges(arcPt1, arcPt2, bounds)) { + if (touchSameEdge(aimPt, arcPt1) || touchSameEdge(aimPt, arcPt2)) { + // 1-corner case + let cp = cornerPts.find(cp => shareValue(cp, arcPt1) && shareValue(cp, arcPt2)); + points = [cp]; + } else { + // 3-corner case + points = cornerPts.filter(cp => !shareValue(cp, arcPt1) || !shareValue(cp, arcPt2)); + } + } else { + if (touchSameEdge(aimPt, arcPt1) || touchSameEdge(aimPt, arcPt2)) { + // 2-corner case, aim and an arc point touch the same edge + points = findWhichTwoCorners(aimPt, bounds, arcPt1, arcPt2); + } else { + // 2-corner case, aim and both arc points all touch different edges + points = cornerPts.filter(cp => shareValue(cp, aimPt) || shareValue(cp, aimPt)); + } + } + + return points; +} + +function orderPoints(arcPoints, cornerPts) { + if (cornerPts.length === 0) { + return arcPoints; + } + + const index = cornerPts.findIndex(cp => shareValue(cp, arcPoints.at(0))); + return cornerPts.slice(0, index + 1).concat(arcPoints).concat(cornerPts.slice(index + 1)); +} + +function calcArcLinePtDeltas(aimPt, pivotPt, size) { + const angle = calculateAngle(aimPt.x - pivotPt.x, aimPt.y - pivotPt.y), + arcAngle = arcSize[size], + distance = Math.sqrt((aimPt.x - pivotPt.x) ** 2 + (aimPt.y - pivotPt.y) ** 2), + yDelta = distance * Math.cos(angle) * Math.tan(arcAngle), + xDelta = distance * Math.sin(angle) * Math.tan(arcAngle); + + return { xDelta, yDelta }; +} + +function calcPoints(e, aimLine, grid, size) { + const pointer = new DOMPoint(e.clientX, e.clientY), + pointerPt = pointer.matrixTransform(grid.getScreenCTM().inverse()), + pivotPt = new Point(aimLine.getAttribute('x1'), aimLine.getAttribute('y1')), + + bounds = getBounds(grid.getBBox()), + aimPt = calcEdgePt(pivotPt, pointerPt, bounds), + + { xDelta, yDelta } = calcArcLinePtDeltas(aimPt, pivotPt, size), + arcPt1 = calcEdgePt(pivotPt, new Point(aimPt.x - xDelta, aimPt.y - yDelta), bounds), + arcPt2 = calcEdgePt(pivotPt, new Point(aimPt.x + xDelta, aimPt.y + yDelta), bounds), + + outlinePoints = [arcPt2, pivotPt, arcPt1], + cornerPoints = selectCornerPoints(aimPt, arcPt1, arcPt2, bounds), + arcPoints = orderPoints(outlinePoints, cornerPoints); + + return { aimPt, outlinePoints, arcPoints }; +} + +function setDataAttrs({ dataset: { allegiance, number }}, el) { + el.dataset.allegiance = allegiance; + el.dataset.number = number; +} + +function getClipPathId({ dataset: { allegiance, number }}) { + return `clip-path-${allegiance}-${number}`; +} + +function getUnclipped(svg) { + return svg.querySelectorAll('#firing-arcs #shapes polygon:not([clip-path]), #firing-arcs #lines polyline:not([clip-path])'); +}; + +function createAimLine(x, y, container) { + const aimLine = document.createElementNS(svgns, 'line'); + aimLine.setAttributeNS(null, 'x1', x); + aimLine.setAttributeNS(null, 'y1', y); + aimLine.setAttributeNS(null, 'x2', x); + aimLine.setAttributeNS(null, 'y2', y); + container.appendChild(aimLine); + + return aimLine; +} + +function createClipPath(x, y, id, container) { + const clipShape = document.createElementNS(svgns, 'circle'), + clipPath = document.createElementNS(svgns, 'clipPath'); + + clipShape.setAttributeNS(null, 'cx', x); + clipShape.setAttributeNS(null, 'cy', y); + clipShape.setAttributeNS(null, 'r', clippedFiringArcRadius); + + clipPath.setAttributeNS(null, 'id', id); + clipPath.appendChild(clipShape); + container.appendChild(clipPath); + + return clipPath; +} + +function createFiringArc(x, y, size, container) { + const firingArc = document.createElementNS(svgns, 'polygon'); + firingArc.setAttributeNS(null, 'points', `${x},${y}`); + firingArc.dataset.size = size; + firingArc.classList.add('firing-arc', 'active'); + container.appendChild(firingArc); + + return firingArc; +} + +function createFiringArcOutline(x, y, container) { + const firingArcOutline = document.createElementNS(svgns, 'polyline'); + firingArcOutline.setAttributeNS(null, 'points', `${x},${y}`); + container.appendChild(firingArcOutline); + + return firingArcOutline; +} + +function queryContainers(svg) { + const grid = svg.querySelector('.grid'), + arcContainer = svg.querySelector('#firing-arcs'), + arcLayer = arcContainer.querySelector('#shapes'), + outlineLayer = arcContainer.querySelector('#lines'); + + return { grid, containers: { arcContainer, arcLayer, outlineLayer }}; +} + +function create(x, y, size, counter, { arcContainer, arcLayer, outlineLayer }) { + const aimLine = createAimLine(x, y, outlineLayer), + firingArc = createFiringArc(x, y, size, arcLayer), + firingArcOutline = createFiringArcOutline(x, y, outlineLayer), + clipPath = createClipPath(x, y, getClipPathId(counter), arcContainer); + + setDataAttrs(counter, firingArc); + setDataAttrs(counter, firingArcOutline); + setDataAttrs(counter, clipPath); + + return { aimLine, firingArc, firingArcOutline }; +} + +function set(svg, size, counter, { x, y }) { + get(svg, counter).forEach(el => el.remove()); + + const { grid, containers } = queryContainers(svg), + { aimLine, firingArc, firingArcOutline } = create(x, y, size, counter, containers); + + function positionListener(e) { + const { aimPt, outlinePoints, arcPoints } = calcPoints(e, aimLine, grid, size); + + aimLine.setAttributeNS(null, 'x2', aimPt.x); + aimLine.setAttributeNS(null, 'y2', aimPt.y); + firingArcOutline.setAttributeNS(null, 'points', outlinePoints.join(' ')); + firingArc.setAttributeNS(null, 'points', arcPoints.join(' ')); + } + + function placementListener() { + aimLine.remove(); + firingArc.classList.remove('active'); + grid.removeAttribute('style'); + svg.removeEventListener('mousemove', positionListener); + } + + function cancelPlacementListener(e) { + e.preventDefault(); + + get(counter).forEach(el => el.remove()); + grid.removeAttribute('style'); + svg.removeEventListener('mousemove', positionListener); + } + + grid.style.pointerEvents = 'none'; + firingArc.addEventListener('click', placementListener, { once: true }); + firingArc.addEventListener('contextmenu', cancelPlacementListener, { once: true }); + svg.addEventListener('mousemove', positionListener); +} + +function clear(svg, allegiance) { + const selector = `#firing-arcs [data-allegiance="${allegiance}"]`; + svg.querySelectorAll(selector).forEach(el => el.remove()); +} + +function get(svg, { dataset: { allegiance, number }}) { + return svg.querySelectorAll(`#firing-arcs [data-number="${number}"][data-allegiance="${allegiance}"], #firing-arcs line`); +} + +function toggleVisibility(svg, allegiance) { + const vis = firingArcVisibility[allegiance], + clipPaths = svg.querySelectorAll(`clipPath[data-allegiance="${allegiance}"]`); + + clipPaths.forEach(cp => cp.style.display = !vis ? 'none' : ''); + firingArcVisibility[allegiance] = !vis; +} + +function toggleCounterVisibility(svg, { dataset: { number, allegiance }}, vis) { + const cp = svg.querySelector(`#clip-path-${allegiance}-${number}`), + display = vis ? 'none' : ''; + + if (cp) { + cp.style.display = firingArcVisibility[allegiance] ? 'none' : display; + } +} + +function clipAll(svg) { + getUnclipped(svg).forEach(el => { + const { number, allegiance } = el.dataset, + clipPathId = `clip-path-${allegiance}-${number}`, + isVisible = firingArcVisibility[allegiance]; + + if (isVisible) { + svg.querySelector(`#${clipPathId}`).style.display = 'none'; + } + + el.setAttributeNS(null, 'clip-path', `url(#${clipPathId})`); + }); +} + +export { set, clear, get, toggleVisibility, toggleCounterVisibility, clipAll }; diff --git a/src/modules/game/sightLine.js b/src/modules/game/sightLine.js deleted file mode 100644 index 6b94514..0000000 --- a/src/modules/game/sightLine.js +++ /dev/null @@ -1,145 +0,0 @@ -const targetClassName = 'sight-line-target', - activeClassName = 'active'; - -function evenr_to_axial(x, y) { - return { q: x - (y + (y & 1)) / 2, r: y }; -} - -function axial_to_evenr(q, r) { - return { x: q + (r + (r & 1)) / 2, y: r }; -} - -function axial_distance(q1, r1, q2, r2) { - return (Math.abs(q1 - q2) + Math.abs(q1 + r1 - q2 - r2) + Math.abs(r1 - r2)) / 2; -} - -function offset_distance(x1, y1, x2, y2) { - const { q: q1, r: r1 } = evenr_to_axial(x1, y1), - { q: q2, r: r2 } = evenr_to_axial(x2, y2); - - return axial_distance(q1, r1, q2, r2); -} - -function cube_to_axial(q, r, _) { - return { q, r }; -} - -function axial_to_cube(q, r) { - return { q, r, s: -q - r }; -} - -function cube_round(q, r, s) { - let rQ = Math.round(q), - rR = Math.round(r), - rS = Math.round(s); - - const q_diff = Math.abs(rQ - q), - r_diff = Math.abs(rR - r), - s_diff = Math.abs(rS - s); - - if (q_diff > r_diff && q_diff > s_diff) { - rQ = -rR - rS; - } else if (r_diff > s_diff) { - rR = -rQ - rS; - } else { - rS = -rQ - rR; - } - - return { q: rQ, r: rR, s: rS }; -} - -function axial_round(q, r) { - const cube = axial_to_cube(q, r), - round = cube_round(cube.q, cube.r, cube.s), - axial = cube_to_axial(round.q, round.r, round.s); - - return { q: axial.q, r: axial.r }; -} - -function lerp(a, b, t) { - return a + (b - a) * t; -} - -function axial_lerp(q1, r1, q2, r2, t) { - return { q: lerp(q1, q2, t), r: lerp(r1, r2, t) }; -} - -function lock(sightLine, cell) { - sightLine.classList.remove(activeClassName); - cell.classList.add(targetClassName); - - return cell; -} - -function unlock(sightLine, lockTarget) { - sightLine.classList.add(activeClassName); - lockTarget.classList.remove(targetClassName); - - return null; -} - -let sightLine, lockTarget, - activeHexes = []; - -export function create({ x: x1, y: y1 }, { x: x2, y: y2 }) { - const line = document.createElementNS(svgns, 'line'); - - line.classList.add('sight-line'); - line.classList.add(activeClassName); - line.setAttributeNS(null, 'x1', x1); - line.setAttributeNS(null, 'y1', y1); - line.setAttributeNS(null, 'x2', x2); - line.setAttributeNS(null, 'y2', y2); - - sightLine = line; - - return line; -} - -export function calcIndexes(x1, y1, x2, y2) { - const axial1 = evenr_to_axial(x1, y1), - axial2 = evenr_to_axial(x2, y2), - n = offset_distance(x1, y1, x2, y2), - results = []; - - for (let i = 0; i <= n; i++) { - const lerp = axial_lerp(axial1.q, axial1.r, axial2.q, axial2.r, 1.0 / n * i), - round = axial_round(lerp.q, lerp.r), - { x, y } = axial_to_evenr(round.q, round.r); - - results.push([x, y]); - } - - return results; -} - -export function clear() { - sightLine && sightLine.remove(); - sightLine = null; - - lockTarget && lockTarget.classList.remove(targetClassName); - lockTarget = null; -} - -export function update({ x, y }) { - sightLine.setAttributeNS(null, 'x1', x); - sightLine.setAttributeNS(null, 'y1', y); -} - -export function toggleLock(cell) { - lockTarget = lockTarget ? unlock(sightLine, lockTarget) : lock(sightLine, cell); -} - -export function getSightLine() { - return sightLine; -} - -export function getLockTarget() { - return lockTarget; -} - -export function setHexes(hexes) { - activeHexes.forEach(h => h.classList.remove(activeClassName)); - hexes.forEach(h => h.classList.add(activeClassName)); - activeHexes = hexes; -} diff --git a/src/modules/game/sight_line.js b/src/modules/game/sight_line.js new file mode 100644 index 0000000..411faa8 --- /dev/null +++ b/src/modules/game/sight_line.js @@ -0,0 +1,144 @@ +const targetClassName = 'sight-line-target', + activeClassName = 'active'; + +function evenr_to_axial(x, y) { + return { q: x - (y + (y & 1)) / 2, r: y }; +} + +function axial_to_evenr(q, r) { + return { x: q + (r + (r & 1)) / 2, y: r }; +} + +function axial_distance(q1, r1, q2, r2) { + return (Math.abs(q1 - q2) + Math.abs(q1 + r1 - q2 - r2) + Math.abs(r1 - r2)) / 2; +} + +function offset_distance(x1, y1, x2, y2) { + const { q: q1, r: r1 } = evenr_to_axial(x1, y1), + { q: q2, r: r2 } = evenr_to_axial(x2, y2); + + return axial_distance(q1, r1, q2, r2); +} + +function cube_to_axial(q, r, _) { + return { q, r }; +} + +function axial_to_cube(q, r) { + return { q, r, s: -q - r }; +} + +function cube_round(q, r, s) { + let rQ = Math.round(q), + rR = Math.round(r), + rS = Math.round(s); + + const q_diff = Math.abs(rQ - q), + r_diff = Math.abs(rR - r), + s_diff = Math.abs(rS - s); + + if (q_diff > r_diff && q_diff > s_diff) { + rQ = -rR - rS; + } else if (r_diff > s_diff) { + rR = -rQ - rS; + } else { + rS = -rQ - rR; + } + + return { q: rQ, r: rR, s: rS }; +} + +function axial_round(q, r) { + const cube = axial_to_cube(q, r), + round = cube_round(cube.q, cube.r, cube.s), + axial = cube_to_axial(round.q, round.r, round.s); + + return { q: axial.q, r: axial.r }; +} + +function lerp(a, b, t) { + return a + (b - a) * t; +} + +function axial_lerp(q1, r1, q2, r2, t) { + return { q: lerp(q1, q2, t), r: lerp(r1, r2, t) }; +} + +function lock(sightLine, cell) { + sightLine.classList.remove(activeClassName); + cell.classList.add(targetClassName); + + return cell; +} + +function unlock(sightLine, lockTarget) { + sightLine.classList.add(activeClassName); + lockTarget.classList.remove(targetClassName); + + return null; +} + +let sightLine, lockTarget, + activeHexes = []; + +export function create({ x: x1, y: y1 }, { x: x2, y: y2 }) { + const line = document.createElementNS(svgns, 'line'); + sightLine = line; + + line.classList.add('sight-line'); + line.classList.add(activeClassName); + line.setAttributeNS(null, 'x1', x1); + line.setAttributeNS(null, 'y1', y1); + line.setAttributeNS(null, 'x2', x2); + line.setAttributeNS(null, 'y2', y2); + + return line; +} + +export function calcIndexes(x1, y1, x2, y2) { + const axial1 = evenr_to_axial(x1, y1), + axial2 = evenr_to_axial(x2, y2), + n = offset_distance(x1, y1, x2, y2), + results = []; + + for (let i = 0; i <= n; i++) { + const lerp = axial_lerp(axial1.q, axial1.r, axial2.q, axial2.r, 1.0 / n * i), + round = axial_round(lerp.q, lerp.r), + { x, y } = axial_to_evenr(round.q, round.r); + + results.push([x, y]); + } + + return results; +} + +export function clear() { + sightLine && sightLine.remove(); + sightLine = null; + + lockTarget && lockTarget.classList.remove(targetClassName); + lockTarget = null; +} + +export function update({ x, y }) { + sightLine.setAttributeNS(null, 'x1', x); + sightLine.setAttributeNS(null, 'y1', y); +} + +export function toggleLock(cell) { + lockTarget = lockTarget ? unlock(sightLine, lockTarget) : lock(sightLine, cell); +} + +export function getSightLine() { + return sightLine; +} + +export function getLockTarget() { + return lockTarget; +} + +export function setHexes(hexes) { + activeHexes.forEach(h => h.classList.remove(activeClassName)); + hexes.forEach(h => h.classList.add(activeClassName)); + activeHexes = hexes; +} diff --git a/src/modules/game/soldier.js b/src/modules/game/soldier.js new file mode 100644 index 0000000..0d75fc6 --- /dev/null +++ b/src/modules/game/soldier.js @@ -0,0 +1,133 @@ +const selectedClass = 'selected'; + +function dataSelector({ dataset: { allegiance, number }}) { + return `[data-number="${number}"][data-allegiance="${allegiance}"]`; +} + +function traceSelector(counter) { + return `polyline.move-trace${dataSelector(counter)}`; +} + +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 getClones(svg, counter) { + return svg.querySelectorAll(`.counter.clone${dataSelector(counter)}`); +} + +function getCounterAndClones(svg, counter) { + return svg.querySelectorAll(`.counter${dataSelector(counter)}`); +} + +export function getCounter(svg, selected) { + return svg.querySelector(`.counter${dataSelector(selected)}:not(.clone)`); +} + +export function getTrace(svg, counter) { + return svg.querySelector(traceSelector(counter)); +} + +export function place(svg, selected, cell) { + let points, + counterNodeList = getCounterAndClones(svg, selected); + + if (counterNodeList.length > 0 && selected.parentElement.hasAttribute('data-x')) { + let trace = svg.querySelector(traceSelector(selected)); + + let prevCoords = [ + selected.parentElement.dataset.x, + selected.parentElement.parentElement.dataset.y + ] + + let clone = selected.cloneNode(true); + clone.classList.remove(selectedClass); + clone.classList.add('clone'); + + selected.dataset.previous = prevCoords; + selected.parentElement.appendChild(clone); + cell.appendChild(selected); + + selected.childNodes.forEach(n => { + if (n.classList.contains('removed')) { + n.remove(); + } else if ('preexisting' in n.dataset) { + delete n.dataset.preexisting; + } + }); + + let previous = getCellPosition(clone.parentElement), + current = getCellPosition(selected.parentElement); + + if (!trace) { + trace = document.createElementNS(svgns, 'polyline'); + + points = `${previous.x},${previous.y} ${current.x},${current.y}`; + + trace.dataset.number = selected.dataset.number; + trace.dataset.allegiance = selected.dataset.allegiance; + trace.classList.add('move-trace'); + + svg.querySelector('.board').prepend(trace); + } else { + points = `${trace.getAttribute('points')} ${current.x},${current.y}`; + } + + trace.setAttributeNS(null, 'points', points); + } else { + selected.removeAttribute('data-x'); + cell.appendChild(selected); + } +} + +export function removeClones(svg, counter) { + getClones(svg, counter).forEach(c => c.remove()); +} + +export function endMove(svg, counter) { + const trace = svg.querySelector(traceSelector(counter)), + proneCounter = counter.querySelector('[href="#counter-prone"]'); + + if (trace) { + trace.remove(); + } + + delete counter.dataset.previous; + + if (proneCounter) { + proneCounter.dataset.preexisting = ''; + } + + removeClones(counter); +} + +export function hasProne(counter) { + return !!counter.querySelector('[href="#counter-prone"]'); +} + +export function toggleProne(counter) { + let proneCounter = counter.querySelector('[href="#counter-prone"]'); + + if (!proneCounter) { + proneCounter = document.createElementNS(svgns, 'use'); + proneCounter.setAttributeNS(null, 'href', '#counter-prone'); + counter.appendChild(proneCounter); + } else if ('preexisting' in proneCounter.dataset) { + proneCounter.classList.toggle('removed'); + } else { + proneCounter.remove(); + } +} + +export function getSelectedClass() { + return selectedClass; +} 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); +} diff --git a/src/modules/pan-zoom.js b/src/modules/pan-zoom.js new file mode 100644 index 0000000..86f6727 --- /dev/null +++ b/src/modules/pan-zoom.js @@ -0,0 +1,33 @@ +import { pan, zoom } from 'pan-zoom'; + +const storageKey = 'pan-zoom', + zoomFactor = 0.25; + +function restorePanZoomVal(svg) { + const storedPanZoomVal = localStorage.getItem(storageKey); + + if (storedPanZoomVal) { + svg.style.transform = storedPanZoomVal; + } +} + +function addEventListeners(svg) { + svg.addEventListener('wheel', e => zoom(svg, e, zoomFactor), { passive: false }); + svg.addEventListener('pointerdown', e => pan(svg, e), { passive: false }); +} + +function storePanZoomVal(transformMatrix) { + localStorage.setItem(storageKey, transformMatrix); +} + +function observePanZoomChanges(svg) { + const observer = new MutationObserver(() => storePanZoomVal(svg.style.transform)); + + observer.observe(svg, { attributeFilter: ['style'] }); +} + +export function start(svg) { + restorePanZoomVal(svg); + addEventListeners(svg); + observePanZoomChanges(svg); +} diff --git a/src/modules/panzoom.js b/src/modules/panzoom.js deleted file mode 100644 index 86f6727..0000000 --- a/src/modules/panzoom.js +++ /dev/null @@ -1,33 +0,0 @@ -import { pan, zoom } from 'pan-zoom'; - -const storageKey = 'pan-zoom', - zoomFactor = 0.25; - -function restorePanZoomVal(svg) { - const storedPanZoomVal = localStorage.getItem(storageKey); - - if (storedPanZoomVal) { - svg.style.transform = storedPanZoomVal; - } -} - -function addEventListeners(svg) { - svg.addEventListener('wheel', e => zoom(svg, e, zoomFactor), { passive: false }); - svg.addEventListener('pointerdown', e => pan(svg, e), { passive: false }); -} - -function storePanZoomVal(transformMatrix) { - localStorage.setItem(storageKey, transformMatrix); -} - -function observePanZoomChanges(svg) { - const observer = new MutationObserver(() => storePanZoomVal(svg.style.transform)); - - observer.observe(svg, { attributeFilter: ['style'] }); -} - -export function start(svg) { - restorePanZoomVal(svg); - addEventListeners(svg); - observePanZoomChanges(svg); -} diff --git a/src/modules/recordSheet.js b/src/modules/recordSheet.js deleted file mode 100644 index 99af18f..0000000 --- a/src/modules/recordSheet.js +++ /dev/null @@ -1,32 +0,0 @@ -export function unSelect() { - const selected = getSelected(); - - if (selected) { - selected.classList.remove('selected'); - } - - document.getElementById('toggle-prone-counter').checked = false; -} - -export function getSelected() { - return document.querySelector('.soldier-record.selected'); -} - -export function select(data) { - const selector = - `#record-sheet .soldier-record[data-number="${data.number}"][data-allegiance="${data.allegiance}"]` - - unSelect(); - document.querySelector(selector).classList.add('selected'); - document.getElementById('toggle-prone-counter').checked = data.prone; -} - -export function endMove() { - const selected = getSelected(); - - if (selected) { - selected.classList.toggle('movement-ended'); - } - - unSelect(); -} diff --git a/src/modules/record_sheet.js b/src/modules/record_sheet.js new file mode 100644 index 0000000..99af18f --- /dev/null +++ b/src/modules/record_sheet.js @@ -0,0 +1,32 @@ +export function unSelect() { + const selected = getSelected(); + + if (selected) { + selected.classList.remove('selected'); + } + + document.getElementById('toggle-prone-counter').checked = false; +} + +export function getSelected() { + return document.querySelector('.soldier-record.selected'); +} + +export function select(data) { + const selector = + `#record-sheet .soldier-record[data-number="${data.number}"][data-allegiance="${data.allegiance}"]` + + unSelect(); + document.querySelector(selector).classList.add('selected'); + document.getElementById('toggle-prone-counter').checked = data.prone; +} + +export function endMove() { + const selected = getSelected(); + + if (selected) { + selected.classList.toggle('movement-ended'); + } + + unSelect(); +} -- cgit v1.2.3