import React, { useState } from "react"; import type { Asset, Entity, Project } from "../engine/schema.ts"; export function Field2D({ label, value, onChange, min, step = 0.1, }: { label: string; value: number; onChange: (v: number) => void; min?: number; step?: number; }) { return ( ); } export function FrameThumb({ asset, index, size = 40, }: { asset?: Asset; index: number; size?: number; }) { const f = asset?.image?.frames[index], im = asset?.image; if (!im || !f) return {index}; const scale = size / Math.max(f.width, f.height); return ( ); } export function Inspector2D({ type, c, node, project, change, editImage, preview, brush, }: { type: string; c: any; node: Entity; project: Project; change: (v: any) => void; editImage: (id: string) => void; preview: (name: string) => void; brush: (frame: number | null, fill?: boolean) => void; }) { const [paintMode, setPaintMode] = useState("brush"); const set = (key: string, value: any) => change({ ...c, [key]: value }); const num = ( key: string, label: string, fallback = 0, min?: number, step = 0.1, ) => ( set(key, v)} min={min} step={step} /> ); const bool = (key: string, label: string, fallback = false) => ( ); const select = ( key: string, label: string, options: [string, string][], fallback = "", ) => ( ); const pair = ( key: string, label: string, fallback = [0, 0], min?: number, ) => (
{[0, 1].map((i) => ( { const a = [...(c[key] || fallback)]; a[i] = v; set(key, a); }} /> ))}
); const asset = project.assets.find((a) => a.id === c.assetId), frames = asset?.image?.frames || []; const imageSelect = ( <> {asset && ( )} ); const sorting = ( <> {num("layer", "Слой", 0, undefined, 1)} {num("order", "Порядок в слое", 0, undefined, 1)} {bool("sortY", "Сортировать по Y")} {num("alpha", "Непрозрачность", 1, 0)} {bool("lit", "Освещение 3D")} ); if (type === "sprite") return ( <> {imageSelect} {pair( "size", "Размер", frames[c.frame || 0] ? [ frames[c.frame || 0].width / asset!.image!.pixelsPerUnit, frames[c.frame || 0].height / asset!.image!.pixelsPerUnit, ] : [1, 1], 0.001, )} {c.size && ( )} {bool("flipX", "Отразить X")} {bool("flipY", "Отразить Y")} {sorting} ); if (type === "tilemap") return ( <> {imageSelect} {pair("tileSize", "Тайл", [1, 1], 0.001)} {num("width", "Столбцы", 32, 1, 1)} {num("height", "Строки", 18, 1, 1)} {bool("collisions", "Коллизии тайлов")} {sorting}

Рисование в виде 2D. Один мазок — один шаг Undo. Начало карты: нижний левый угол.

{[ ["brush", "Кисть"], ["fill", "Заливка"], ["erase", "Ластик"], ].map(([v, l]) => ( ))}
{(frames.length ? frames : [{ name: "Квадрат" }]).map((f, i) => ( ))}

{c.cells.length} тайлов. Выберите инструмент Q/W/E/R, чтобы закончить рисование.

); if (type === "spriteAnimator") { const clips = c.clips || []; return ( <> {select("autoplay", "Автозапуск", [ ["", "Нет"], ...clips.map((cl: any) => [cl.name, cl.name] as [string, string]), ])} {bool("autoStates", "Авто: idle / run / jump")}

Автоматические состояния используют скорость и grounded из Character2D.

{clips.map((cl: any, i: number) => { const update = (v: any) => set( "clips", clips.map((old: any, j: number) => j === i ? { ...old, ...v } : old, ), ); return (
update({ fps })} />
); })} ); } if (type === "rigidbody2d") return ( <> {select( "type", "Тип тела", [ ["fixed", "Неподвижное"], ["dynamic", "Динамическое"], ["kinematic", "Кинематическое"], ], "dynamic", )} {num("mass", "Масса", 1, 0)} {num("friction", "Трение", 0.5, 0)} {num("restitution", "Упругость", 0, 0)} {num("gravityScale", "Множитель гравитации", 1)} {num("linearDamping", "Сопротивление", 0, 0)} {num("angularDamping", "Угловое сопротивление", 0, 0)} {bool("lockRotation", "Фиксировать вращение", true)} {bool("ccd", "Непрерывные коллизии", true)} ); if (type === "collider2d") return ( <> {c.shape === "box" && pair("size", "Размер", [1, 1], 0.001)} {["circle", "capsule"].includes(c.shape) && num("radius", "Радиус", 0.5, 0.001)} {c.shape === "capsule" && num("height", "Высота", 1.8, 0.001)} {c.shape === "polygon" && ( <>

Вершины выпуклого контура по порядку.

{c.points.map((p: number[], i: number) => (
{[0, 1].map((j) => ( set( "points", c.points.map((old: number[], k: number) => k === i ? old.map((x, l) => (l === j ? v : x)) : old, ), ) } /> ))} {c.points.length > 3 && ( )}
))} )} {pair("offset", "Смещение")} {bool("sensor", "Триггер")} {bool("oneWay", "Односторонняя платформа")} {num("membership", "Группа (битовая маска)", 1, 0, 1)} {num("mask", "Взаимодействует с", 65535, 0, 1)} ); if (type === "character2d") return ( <> {select( "mode", "Игра", [ ["platformer", "Платформер"], ["topDown", "Вид сверху"], ], "platformer", )} {bool("controls", "WASD / стрелки / касание", true)} {num("speed", "Скорость", 5, 0)} {c.mode !== "topDown" && ( <> {num("jumpSpeed", "Скорость прыжка", 8, 0)} {num("gravity", "Гравитация персонажа", 20, 0)} {num("autostep", "Высота ступени", 0.2, 0)} )}

Прыжок: пробел. При собственном скрипте отключите встроенное управление.

); if (type === "joint2d") return ( <> {select( "type", "Соединение", [ ["revolute", "Шарнир"], ["fixed", "Жёсткое"], ["rope", "Верёвка"], ["spring", "Пружина"], ], "revolute", )} {select( "targetId", "Второе тело", project.scenes .find((s) => s.id === project.activeSceneId)! .entities.filter( (n) => n.id !== node.id && n.components.rigidbody2d, ) .map((n) => [n.id, n.name]), )} {pair("anchor", "Крепление")} {pair("targetAnchor", "Крепление цели")} {["rope", "spring"].includes(c.type) && num("length", "Длина", 1, 0.001)} {c.type === "spring" && ( <> {num("stiffness", "Жёсткость", 50, 0)} {num("damping", "Затухание", 5, 0)} )} ); if (type === "camera2d") return ( <> {num("orthoWidth", "Ширина кадра", 20, 0.1)} {bool("pixelPerfect", "Pixel Perfect")} {num("pixelsPerUnit", "Пикселей на единицу", 100, 0.1)} {pair("offset", "Смещение", [0, 0])} ); return null; }