index : btroops | |
Virtual board game-aid for BattleTroops, an infantry combat simulator wargame published by FASA in 1989. |
aboutsummaryrefslogtreecommitdiff |
diff options
Diffstat (limited to 'src/modules/game/firingArc.js')
-rw-r--r-- | src/modules/game/firingArc.js | 348 |
1 files changed, 0 insertions, 348 deletions
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 }; |