feat(scene): battle grid overlay with color and trap VFX stacking
Add configurable square/hex grid in the scene editor (persisted and shown on control/presentation) and keep trap activation effects above trap icons. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import type { SceneGrid } from '../../../shared/types';
|
||||
|
||||
import styles from './SceneGridOverlay.module.css';
|
||||
|
||||
type Viewport = { x: number; y: number; w: number; h: number };
|
||||
|
||||
type Props = {
|
||||
grid: SceneGrid | null | undefined;
|
||||
viewport: Viewport | null;
|
||||
};
|
||||
|
||||
function drawSquareGrid(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
w: number,
|
||||
h: number,
|
||||
cell: number,
|
||||
): void {
|
||||
ctx.beginPath();
|
||||
for (let x = 0; x <= w + 0.5; x += cell) {
|
||||
ctx.moveTo(x + 0.5, 0);
|
||||
ctx.lineTo(x + 0.5, h);
|
||||
}
|
||||
for (let y = 0; y <= h + 0.5; y += cell) {
|
||||
ctx.moveTo(0, y + 0.5);
|
||||
ctx.lineTo(w, y + 0.5);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/** Flat-top hex grid; `cell` ≈ ширина гекса (расстояние между параллельными сторонами по X ≈ 0.75·cell между центрами). */
|
||||
function drawHexGrid(ctx: CanvasRenderingContext2D, w: number, h: number, cell: number): void {
|
||||
const hexW = cell;
|
||||
const vert = (Math.sqrt(3) / 2) * hexW;
|
||||
const horiz = hexW * 0.75;
|
||||
const r = hexW / 2;
|
||||
|
||||
ctx.beginPath();
|
||||
const cols = Math.ceil(w / horiz) + 2;
|
||||
const rows = Math.ceil(h / vert) + 2;
|
||||
for (let row = -1; row < rows; row++) {
|
||||
for (let col = -1; col < cols; col++) {
|
||||
const cx = col * horiz;
|
||||
const cy = row * vert + (col % 2 === 0 ? 0 : vert / 2);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const a = (Math.PI / 3) * i;
|
||||
const x = cx + r * Math.cos(a);
|
||||
const y = cy + r * Math.sin(a);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.closePath();
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
export function SceneGridOverlay({ grid, viewport }: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas || !viewport || !grid?.enabled) return;
|
||||
const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
|
||||
const w = Math.max(1, Math.round(viewport.w));
|
||||
const h = Math.max(1, Math.round(viewport.h));
|
||||
canvas.width = Math.round(w * dpr);
|
||||
canvas.height = Math.round(h * dpr);
|
||||
canvas.style.width = `${w}px`;
|
||||
canvas.style.height = `${h}px`;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const minDim = Math.min(w, h);
|
||||
const cell = Math.max(4, grid.sizeN * minDim);
|
||||
ctx.strokeStyle = grid.color || '#ffffff';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = 0.72;
|
||||
|
||||
if (grid.type === 'hex') {
|
||||
drawHexGrid(ctx, w, h, cell);
|
||||
} else {
|
||||
drawSquareGrid(ctx, w, h, cell);
|
||||
}
|
||||
}, [
|
||||
grid?.enabled,
|
||||
grid?.type,
|
||||
grid?.sizeN,
|
||||
grid?.color,
|
||||
viewport?.x,
|
||||
viewport?.y,
|
||||
viewport?.w,
|
||||
viewport?.h,
|
||||
]);
|
||||
|
||||
if (!viewport || !grid?.enabled) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.layer}
|
||||
style={{ left: viewport.x, top: viewport.y, width: viewport.w, height: viewport.h }}
|
||||
aria-hidden
|
||||
>
|
||||
<canvas ref={canvasRef} className={styles.canvas} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user