Skip to content

Commit

Permalink
feat(Spell): use hexes when using hex grids
Browse files Browse the repository at this point in the history
  • Loading branch information
Kruptein authored Aug 11, 2024
1 parent f7ee86d commit 9a66397
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 44 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 41 additions & 35 deletions client/src/game/rendering/grid.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -92,35 +109,35 @@ 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);
}
}
}

// 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
Expand All @@ -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;
}
25 changes: 25 additions & 0 deletions client/src/game/shapes/variants/hex.ts
Original file line number Diff line number Diff line change
@@ -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);
}
51 changes: 45 additions & 6 deletions client/src/game/tools/variants/spell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -13,13 +14,15 @@ 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";
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";
Expand All @@ -28,6 +31,7 @@ export enum SpellShape {
Square = "square",
Circle = "circle",
Cone = "cone",
Hex = "hex",
}

class SpellTool extends Tool implements ITool {
Expand All @@ -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,
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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 });
}
Expand All @@ -99,19 +112,36 @@ 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), {
viewingAngle: toRadians(60),
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);
Expand Down Expand Up @@ -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();
}

Expand Down
30 changes: 27 additions & 3 deletions client/src/game/ui/tools/SpellTool.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,38 @@ 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);
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<void> {
spellTool.state.selectedSpellShape = shape;
await spellTool.drawShape();
Expand All @@ -28,7 +43,7 @@ async function selectShape(shape: SpellShape): Promise<void> {

<template>
<div v-if="selected" class="tool-detail">
<div class="selectgroup">
<div v-if="!isHexGrid" class="selectgroup">
<div
v-for="shape in shapes"
:key="shape"
Expand All @@ -52,8 +67,17 @@ async function selectShape(shape: SpellShape): Promise<void> {
type="number"
style="flex: 1; align-self: center"
min="0"
step="5"
:step="stepSize"
/>
<template v-if="isHexGrid">
<label for="oddHexOrientation" style="flex: 5">Odd Hex Orientation</label>
<input
id="oddHexOrientation"
v-model.number="spellTool.state.oddHexOrientation"
type="checkbox"
style="flex: 1; align-self: center"
/>
</template>
<label for="colour" style="flex: 5">{{ t("common.fill_color") }}</label>
<ColourPicker
v-model:colour="spellTool.state.colour"
Expand Down

0 comments on commit 9a66397

Please sign in to comment.