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/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 ++++++++++++++++ 6 files changed, 625 insertions(+), 626 deletions(-) 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 (limited to 'src/modules/game') 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; +} -- cgit v1.2.3