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:
Ivan Fontosh
2026-07-27 06:47:59 +08:00
parent 4aa0f257d5
commit bdeb64e356
17 changed files with 413 additions and 15 deletions
+1
View File
@@ -23,6 +23,7 @@ function scene(id: string): Scene {
previewRotationDeg: 0,
darkenScene: false,
traps: [],
grid: { enabled: false, type: 'square', sizeN: 0.06, color: '#ffffff' },
media: { videos: [], audios: [] },
settings: { autoplayVideo: false, autoplayAudio: true, loopVideo: true, loopAudio: true },
connections: [],
@@ -28,6 +28,7 @@ function scene(id: string, title: string): Scene {
previewRotationDeg: 0,
darkenScene: false,
traps: [],
grid: { enabled: false, type: 'square', sizeN: 0.06, color: '#ffffff' },
media: { videos: [], audios: [] },
settings: { autoplayVideo: false, autoplayAudio: false, loopVideo: false, loopAudio: false },
connections: [],
+2
View File
@@ -20,6 +20,7 @@ import type {
Scene,
SceneDarknessEvent,
SceneDarknessState,
SceneGrid,
SceneId,
SceneTrap,
SceneTrapsEvent,
@@ -703,6 +704,7 @@ export type ScenePatch = {
previewRotationDeg?: 0 | 90 | 180 | 270;
darkenScene?: boolean;
traps?: SceneTrap[];
grid?: SceneGrid;
settings?: Partial<Scene['settings']>;
media?: Partial<Scene['media']>;
layout?: Partial<Scene['layout']>;
+3
View File
@@ -9,6 +9,7 @@ import type {
SceneId,
} from './ids';
import type { MaterialLegend } from './materialLegend';
import type { SceneGrid } from './sceneGrid';
import type { SceneTrap } from './sceneTraps';
export const PROJECT_SCHEMA_VERSION = 9 as const;
@@ -162,6 +163,8 @@ export type Scene = {
darkenScene: boolean;
/** Ловушки на карте (только для image-превью); расстановка в проекте. */
traps: SceneTrap[];
/** Боевая сетка поверх превью (под ловушками/эффектами). */
grid: SceneGrid;
media: SceneMediaRefs;
settings: SceneSettings;
connections: SceneId[];
+1
View File
@@ -5,6 +5,7 @@ export * from './materialLegend';
export * from './materials';
export * from './npcs';
export * from './sceneDarkness';
export * from './sceneGrid';
export * from './sceneTraps';
export * from './sceneView';
export * from './videoPlayback';
+61
View File
@@ -0,0 +1,61 @@
/** Настройки боевой сетки на превью сцены (сохраняется в проекте). */
export const SCENE_GRID_TYPES = ['square', 'hex'] as const;
export type SceneGridType = (typeof SCENE_GRID_TYPES)[number];
export type SceneGrid = {
/** Наложить сетку на изображение сцены. */
enabled: boolean;
type: SceneGridType;
/** Размер ячейки относительно min(w, h) картинки. */
sizeN: number;
/** Hex `#rrggbb`. */
color: string;
};
export const DEFAULT_SCENE_GRID_SIZE_N = 0.06;
export const SCENE_GRID_SIZE_MIN = 0.02;
export const SCENE_GRID_SIZE_MAX = 0.25;
export const DEFAULT_SCENE_GRID_COLOR = '#ffffff';
export const DEFAULT_SCENE_GRID: SceneGrid = {
enabled: false,
type: 'square',
sizeN: DEFAULT_SCENE_GRID_SIZE_N,
color: DEFAULT_SCENE_GRID_COLOR,
};
export function clampSceneGridSizeN(sizeN: number): number {
if (!Number.isFinite(sizeN)) return DEFAULT_SCENE_GRID_SIZE_N;
return Math.max(SCENE_GRID_SIZE_MIN, Math.min(SCENE_GRID_SIZE_MAX, sizeN));
}
export function normalizeSceneGridColor(raw: unknown): string {
if (typeof raw !== 'string') return DEFAULT_SCENE_GRID_COLOR;
const s = raw.trim();
if (/^#[0-9a-fA-F]{6}$/u.test(s)) return s.toLowerCase();
if (/^#[0-9a-fA-F]{3}$/u.test(s)) {
const r = s[1]!;
const g = s[2]!;
const b = s[3]!;
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
}
return DEFAULT_SCENE_GRID_COLOR;
}
export function normalizeSceneGrid(raw: unknown): SceneGrid {
if (!raw || typeof raw !== 'object') return { ...DEFAULT_SCENE_GRID };
const obj = raw as Partial<SceneGrid>;
const type: SceneGridType = obj.type === 'hex' ? 'hex' : 'square';
return {
enabled: Boolean(obj.enabled),
type,
sizeN: clampSceneGridSizeN(typeof obj.sizeN === 'number' ? obj.sizeN : DEFAULT_SCENE_GRID_SIZE_N),
color: normalizeSceneGridColor(obj.color),
};
}
export function sceneGridTypeLabelRu(type: SceneGridType): string {
return type === 'hex' ? 'Гексогональная' : 'Квадратная';
}