From 9a66397bc9daa11ee2deb577c83d6ecfc30a6a73 Mon Sep 17 00:00:00 2001 From: Darragh Van Tichelen Date: Sun, 11 Aug 2024 10:43:39 +0200 Subject: [PATCH] feat(Spell): use hexes when using hex grids --- CHANGELOG.md | 7 +++ client/src/game/rendering/grid.ts | 76 +++++++++++++------------ client/src/game/shapes/variants/hex.ts | 25 ++++++++ client/src/game/tools/variants/spell.ts | 51 +++++++++++++++-- client/src/game/ui/tools/SpellTool.vue | 30 +++++++++- 5 files changed, 145 insertions(+), 44 deletions(-) create mode 100644 client/src/game/shapes/variants/hex.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b8db283..bcc54f7d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,13 @@ tech changes will usually be stripped from release notes for the public - Can be used to enable/disable certain features campaign wide - Currently limited to chat & dice +### Changed + +- Spell tool: + - now renders hexes instead of squares in Hex grid mode + - step size changed to 1 in Hex grid mode + - shape bar is no longer visible, only hex is available in hex grid mode for now + ## [2024.2.0] - 2024-05-18 ### Added diff --git a/client/src/game/rendering/grid.ts b/client/src/game/rendering/grid.ts index b467a99a4..92c9bb249 100644 --- a/client/src/game/rendering/grid.ts +++ b/client/src/game/rendering/grid.ts @@ -1,5 +1,5 @@ import { g2lz, g2l } from "../../core/conversions"; -import { type GlobalPoint, toArrayP, subtractP, addP } from "../../core/geometry"; +import { type GlobalPoint, subtractP, addP, toGP } from "../../core/geometry"; import { DEFAULT_GRID_SIZE, DEFAULT_HEX_RADIUS, GridType, getCellCenter } from "../../core/grid"; import { getHexNeighbour, getHexVertexVector } from "../../core/grid/hex"; import type { AxialCoords } from "../../core/grid/types"; @@ -58,12 +58,29 @@ function drawHexPolygon( ctx.strokeStyle = style?.stroke ?? "rgba(225, 0, 0, 0.8)"; ctx.lineWidth = style?.strokeWidth ?? 5; + const vertices = createHex(center, size, grid).map((p) => g2l(p)); + + ctx.beginPath(); + for (const [i, vertex] of vertices.entries()) { + if (i === 0) ctx.moveTo(vertex.x, vertex.y); + else ctx.lineTo(vertex.x, vertex.y); + } + ctx.closePath(); + + ctx.fill(); + ctx.stroke(); +} + +export function createHex( + center: GlobalPoint, + size: number, + grid: { type: GridType; oddHexOrientation: boolean; radius?: number }, +): GlobalPoint[] { const radius = grid.radius ?? DEFAULT_HEX_RADIUS; - const localRadius = g2lz(radius); let currentCell: AxialCoords = { q: 0, r: 0 }; if (size === 1) { - drawSingleHexPolygon(currentCell, toArrayP(g2l(center)), localRadius, ctx); + return createSingleHex(currentCell, center, radius); } else { // This function can be used to draw non-grid aligned hexagons // We first need to figure out what the vector is between the grid's {q:0,r:0} and our custom hexagon's {q:0,r:0} @@ -92,13 +109,13 @@ function drawHexPolygon( // eslint-disable-next-line no-inner-declarations function even(v1: number, v2: number, n: number): void { for (let i = 0; i <= evenSteps; i++) { - let v = addP(currentCellCenter, getHexVertexVector(m6(v1), localRadius, isFlat)); - ctx.lineTo(v.x, v.y); - v = addP(currentCellCenter, getHexVertexVector(m6(v2), localRadius, isFlat)); - ctx.lineTo(v.x, v.y); + let v = addP(currentCellCenter, getHexVertexVector(m6(v1), radius, isFlat)); + vertices.push(v); + v = addP(currentCellCenter, getHexVertexVector(m6(v2), radius, isFlat)); + vertices.push(v); if (i < evenSteps) { currentCell = getHexNeighbour(currentCell, m6(n)); - currentCellCenter = g2l(addP(getCellCenter(currentCell, grid.type, radius), offsetVector)); + currentCellCenter = addP(getCellCenter(currentCell, grid.type, radius), offsetVector); } } } @@ -106,21 +123,21 @@ function drawHexPolygon( // eslint-disable-next-line no-inner-declarations function odd(v1: number, v2: number, n: number): void { for (let i = 0; i < oddSteps; i++) { - let v = addP(currentCellCenter, getHexVertexVector(m6(v1), localRadius, isFlat)); - ctx.lineTo(v.x, v.y); + let v = addP(currentCellCenter, getHexVertexVector(m6(v1), radius, isFlat)); + vertices.push(v); currentCell = getHexNeighbour(currentCell, m6(n)); - currentCellCenter = g2l(addP(getCellCenter(currentCell, grid.type, radius), offsetVector)); - v = addP(currentCellCenter, getHexVertexVector(m6(v2), localRadius, isFlat)); - ctx.lineTo(v.x, v.y); + currentCellCenter = addP(getCellCenter(currentCell, grid.type, radius), offsetVector); + v = addP(currentCellCenter, getHexVertexVector(m6(v2), radius, isFlat)); + vertices.push(v); } } // First step to a corner of the hexagon for (let i = 0; i < (isFlat ? evenSteps : oddSteps); i++) currentCell = getHexNeighbour(currentCell, m6(1)); - let currentCellCenter = g2l(addP(getCellCenter(currentCell, grid.type, radius), offsetVector)); - const start = addP(currentCellCenter, getHexVertexVector(m6(2), localRadius, isFlat)); - ctx.moveTo(start.x, start.y); - ctx.beginPath(); + let currentCellCenter = addP(getCellCenter(currentCell, grid.type, radius), offsetVector); + const start = addP(currentCellCenter, getHexVertexVector(m6(2), radius, isFlat)); + + const vertices: GlobalPoint[] = [start]; // Now we move along the exterior of the hexagon in a structured pattern // Even and Odd are used to handle the different number of steps that can occur when dealing with evenly sized hexagons @@ -135,31 +152,20 @@ function drawHexPolygon( even(3, 2, 1); odd(1, 2, 0); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); + return vertices; } } -function drawSingleHexPolygon( - cell: AxialCoords, - center: [number, number], - radius: number, - ctx: CanvasRenderingContext2D, -): void { - const x0 = center[0] + radius * Math.sqrt(3) * (cell.q + cell.r / 2); - const y0 = center[1] + ((radius * 3) / 2) * cell.r; +function createSingleHex(cell: AxialCoords, center: GlobalPoint, radius: number): GlobalPoint[] { + const x0 = center.x + radius * Math.sqrt(3) * (cell.q + cell.r / 2); + const y0 = center.y + ((radius * 3) / 2) * cell.r; - ctx.beginPath(); + const vertices: GlobalPoint[] = []; const angle = (Math.PI * 2) / 6; for (let i = 0; i < 6; i++) { const x = x0 + radius * Math.cos(i * angle - Math.PI / 6); const y = y0 + radius * Math.sin(i * angle - Math.PI / 6); - if (i === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); + vertices.push(toGP(x, y)); } - ctx.closePath(); - - ctx.stroke(); - ctx.fill(); + return vertices; } diff --git a/client/src/game/shapes/variants/hex.ts b/client/src/game/shapes/variants/hex.ts new file mode 100644 index 000000000..862e6ca30 --- /dev/null +++ b/client/src/game/shapes/variants/hex.ts @@ -0,0 +1,25 @@ +import { type GlobalPoint } from "../../../core/geometry"; +import type { GridType } from "../../../core/grid"; +import type { GlobalId, LocalId } from "../../id"; +import { createHex } from "../../rendering/grid"; + +import { Polygon } from "./polygon"; + +export function createHexPolygon( + center: GlobalPoint, + size: number, + grid: { type: GridType; oddHexOrientation: boolean; radius?: number }, + options?: { + lineWidth?: number[]; + openPolygon?: boolean; + id?: LocalId; + uuid?: GlobalId; + isSnappable?: boolean; + }, +): Polygon { + const vertices = createHex(center, size, grid); + if (vertices.length < 2) { + throw new Error("Hexagon has less than 2 vertices"); + } + return new Polygon(vertices[0]!, vertices.slice(1), options); +} diff --git a/client/src/game/tools/variants/spell.ts b/client/src/game/tools/variants/spell.ts index 8f7c0e67e..f7297d129 100644 --- a/client/src/game/tools/variants/spell.ts +++ b/client/src/game/tools/variants/spell.ts @@ -4,6 +4,7 @@ import { reactive, watch } from "vue"; import { g2l, getUnitDistance, l2g, toRadians } from "../../../core/conversions"; import { toGP, toLP } from "../../../core/geometry"; import type { LocalPoint } from "../../../core/geometry"; +import { DEFAULT_HEX_RADIUS, GridType } from "../../../core/grid"; import { InvalidationMode, NO_SYNC, SyncMode, UI_SYNC } from "../../../core/models/types"; import { i18n } from "../../../i18n"; import { sendShapePositionUpdate } from "../../api/emits/shape/core"; @@ -13,6 +14,7 @@ import type { ICircle } from "../../interfaces/shapes/circle"; import { ToolName } from "../../models/tools"; import type { ITool, ToolPermission } from "../../models/tools"; import { Circle } from "../../shapes/variants/circle"; +import { createHexPolygon } from "../../shapes/variants/hex"; import { Rect } from "../../shapes/variants/rect"; import { accessSystem } from "../../systems/access"; import { floorState } from "../../systems/floors/state"; @@ -20,6 +22,7 @@ import { playerSystem } from "../../systems/players"; import { propertiesSystem } from "../../systems/properties"; import { selectedSystem } from "../../systems/selected"; import { selectedState } from "../../systems/selected/state"; +import { locationSettingsState } from "../../systems/settings/location/state"; import { SelectFeatures } from "../models/select"; import { Tool } from "../tool"; import { activateTool, toolMap } from "../tools"; @@ -28,6 +31,7 @@ export enum SpellShape { Square = "square", Circle = "circle", Cone = "cone", + Hex = "hex", } class SpellTool extends Tool implements ITool { @@ -39,6 +43,7 @@ class SpellTool extends Tool implements ITool { state = reactive({ selectedSpellShape: SpellShape.Square, showPublic: true, + oddHexOrientation: false, colour: "rgb(63, 127, 191)", size: 5, @@ -56,6 +61,12 @@ class SpellTool extends Tool implements ITool { if (this.shape !== undefined) await this.drawShape(); }, ); + watch( + () => this.state.oddHexOrientation, + async () => { + if (this.shape !== undefined) await this.drawShape(); + }, + ); watch( () => this.state.selectedSpellShape, async () => { @@ -87,9 +98,11 @@ class SpellTool extends Tool implements ITool { const ogPoint = toGP(0, 0); let startPosition = ogPoint; + let shapeCenter = ogPoint; if (this.shape !== undefined) { startPosition = this.shape.refPoint; + shapeCenter = this.shape.center; const syncMode = this.state.showPublic !== syncChanged ? SyncMode.TEMP_SYNC : SyncMode.NO_SYNC; layer.removeShape(this.shape, { sync: syncMode, recalculate: false, dropShapeId: true }); } @@ -99,12 +112,14 @@ class SpellTool extends Tool implements ITool { this.shape = new Circle(startPosition, getUnitDistance(this.state.size), { isSnappable: false }); break; case SpellShape.Square: - this.shape = new Rect( - startPosition, - getUnitDistance(this.state.size), - getUnitDistance(this.state.size), - { isSnappable: false }, - ); + { + this.shape = new Rect( + startPosition, + getUnitDistance(this.state.size), + getUnitDistance(this.state.size), + { isSnappable: false }, + ); + } break; case SpellShape.Cone: this.shape = new Circle(startPosition, getUnitDistance(this.state.size), { @@ -112,6 +127,21 @@ class SpellTool extends Tool implements ITool { isSnappable: false, }); break; + case SpellShape.Hex: + { + const gridType = locationSettingsState.raw.gridType.value; + this.shape = createHexPolygon( + shapeCenter, + this.state.size, + { + type: gridType, + oddHexOrientation: this.state.oddHexOrientation, + radius: DEFAULT_HEX_RADIUS, + }, + { isSnappable: false }, + ); + } + break; } const c = tinycolor(this.state.colour); @@ -165,6 +195,15 @@ class SpellTool extends Tool implements ITool { if (!selectedSystem.hasSelection && this.state.selectedSpellShape === SpellShape.Cone) { this.state.selectedSpellShape = SpellShape.Circle; } + if (locationSettingsState.raw.gridType.value === GridType.Square) { + if (this.state.selectedSpellShape === SpellShape.Hex) { + this.state.selectedSpellShape = SpellShape.Square; + } + } else { + if (this.state.selectedSpellShape !== SpellShape.Hex) { + this.state.selectedSpellShape = SpellShape.Hex; + } + } await this.drawShape(); } diff --git a/client/src/game/ui/tools/SpellTool.vue b/client/src/game/ui/tools/SpellTool.vue index 9f254818f..149c2e1c9 100644 --- a/client/src/game/ui/tools/SpellTool.vue +++ b/client/src/game/ui/tools/SpellTool.vue @@ -3,14 +3,21 @@ import { computed } from "vue"; import { useI18n } from "vue-i18n"; import ColourPicker from "../../../core/components/ColourPicker.vue"; +import { GridType } from "../../../core/grid"; import { baseAdjust } from "../../../core/http"; import { selectedState } from "../../systems/selected/state"; +import { locationSettingsState } from "../../systems/settings/location/state"; import { SpellShape, spellTool } from "../../tools/variants/spell"; const { t } = useI18n(); const selected = spellTool.isActiveTool; -const shapes = Object.values(SpellShape); + +const isHexGrid = computed(() => locationSettingsState.reactive.gridType.value !== GridType.Square); + +const shapes = computed(() => + isHexGrid.value ? [SpellShape.Hex] : [SpellShape.Square, SpellShape.Circle, SpellShape.Cone], +); const canConeBeCast = computed(() => selectedState.reactive.selected.size > 0); @@ -18,8 +25,16 @@ const translationMapping = { [SpellShape.Square]: t("game.ui.tools.DrawTool.square"), [SpellShape.Circle]: t("game.ui.tools.DrawTool.circle"), [SpellShape.Cone]: t("game.ui.tools.DrawTool.cone"), + [SpellShape.Hex]: t("game.ui.tools.DrawTool.square"), }; +const stepSize = computed(() => { + if (isHexGrid.value && spellTool.state.selectedSpellShape === SpellShape.Square) { + return 1; + } + return 5; +}); + async function selectShape(shape: SpellShape): Promise { spellTool.state.selectedSpellShape = shape; await spellTool.drawShape(); @@ -28,7 +43,7 @@ async function selectShape(shape: SpellShape): Promise {