Add shared-scene 2D authoring, sprite animation and Rapier2D physics
CI / validate (push) Canceled after 0s

This commit is contained in:
emil28092005
2026-09-12 04:58:59 +03:00
parent 6f497d1977
commit 3afac743a4
32 changed files with 4999 additions and 270 deletions
+99 -7
View File
@@ -1,3 +1,4 @@
import { validate2D, validateImage, type ImageSettings } from "./two-d.ts";
export type Vec3 = [number, number, number];
export type Component = Record<string, any>;
export interface Transform {
@@ -22,7 +23,8 @@ export interface Geometry {
export interface Asset {
id: string;
name: string;
kind: "model" | "geometry" | "prefab";
kind: "model" | "geometry" | "prefab" | "image";
image?: ImageSettings;
uri?: string;
geometry?: Geometry;
entities?: Entity[];
@@ -50,15 +52,26 @@ export interface Project {
name: string;
revision: number;
activeSceneId: string;
scenes: { id: string; name: string; entities: Entity[] }[];
scenes: {
id: string;
name: string;
mode?: "2d" | "3d";
entities: Entity[];
}[];
assets: Asset[];
scripts: ScriptAsset[];
settings: {
physics2d?: { gravity: [number, number] };
background: string;
ambient: number;
shadows: boolean;
renderScale: number;
rendering?: { toneMapping?: boolean; exposure?: number; contrast?: number; defaultLights?: boolean };
rendering?: {
toneMapping?: boolean;
exposure?: number;
contrast?: number;
defaultLights?: boolean;
};
controls?: Partial<
Record<"attack" | "jump" | "dash" | "sprint" | "reset", string[]>
>;
@@ -254,12 +267,16 @@ export function validateProject(p: Project) {
for (const c of Object.values(n.components))
if (!c || typeof c !== "object" || Array.isArray(c))
throw Error("Компонент должен быть объектом");
validate2D(n, p, list);
const c = n.components,
m = c.mesh;
if (m?.type === "custom") validateGeometry(m.geometry);
if (
m?.assetId &&
!p.assets.some((a) => a.id === m.assetId && a.kind !== "prefab")
!p.assets.some(
(a) =>
a.id === m.assetId && a.kind !== "prefab" && a.kind !== "image",
)
)
throw Error("Не найден ресурс " + m.assetId);
if (
@@ -304,13 +321,25 @@ export function validateProject(p: Project) {
}
}
};
for (const s of p.scenes) nodes(s.entities);
for (const s of p.scenes) {
if (s.mode && !["2d", "3d"].includes(s.mode))
throw Error("Неизвестный режим сцены");
nodes(s.entities);
}
if (
p.settings.physics2d &&
(!Array.isArray(p.settings.physics2d.gravity) ||
p.settings.physics2d.gravity.length !== 2 ||
!p.settings.physics2d.gravity.every(Number.isFinite))
)
throw Error("Некорректная гравитация 2D");
for (const a of p.assets) {
if (
typeof a.name !== "string" ||
!["model", "geometry", "prefab"].includes(a.kind)
!["model", "geometry", "prefab", "image"].includes(a.kind)
)
throw Error("Некорректный ресурс");
if (a.kind === "image") validateImage(a);
if (a.kind === "geometry") validateGeometry(a.geometry!);
if (a.kind === "prefab") {
nodes(a.entities!);
@@ -318,7 +347,11 @@ export function validateProject(p: Project) {
throw Error("Префабу нужен один корневой объект");
}
if (a.uri && !a.uri.startsWith("data:")) {
if (!/^(\/|\.\/*)?assets\/[a-zA-Z0-9_.-]+\.(glb|gltf)$/i.test(a.uri))
if (
!/^(\/|\.\/*)?assets\/[a-zA-Z0-9_.-]+\.(glb|gltf|png|jpg|jpeg|webp)$/i.test(
a.uri,
)
)
throw Error("Импортируйте ресурс в assets/, внешние пути запрещены");
}
}
@@ -347,6 +380,8 @@ export function remapEntityReferences(
) {
if (ids.has(n.components.camera?.targetId))
n.components.camera.targetId = ids.get(n.components.camera.targetId);
if (ids.has(n.components.joint2d?.targetId))
n.components.joint2d.targetId = ids.get(n.components.joint2d.targetId);
const binding = n.components.script,
script = scripts.find((s) => s.id === binding?.scriptId);
if (binding && script)
@@ -361,6 +396,63 @@ export function remapEntityReferences(
return n;
}
export const componentDefaults: Record<string, Component> = {
sprite: {
assetId: "",
frame: 0,
color: "#ffffff",
alpha: 1,
layer: 0,
order: 0,
flipX: false,
flipY: false,
lit: false,
},
tilemap: {
assetId: "",
tileSize: [1, 1],
width: 32,
height: 18,
cells: [],
layer: 0,
order: 0,
collisions: false,
},
collider2d: {
shape: "box",
size: [1, 1],
offset: [0, 0],
sensor: false,
oneWay: false,
membership: 1,
mask: 65535,
},
rigidbody2d: {
type: "dynamic",
mass: 1,
friction: 0.5,
restitution: 0,
gravityScale: 1,
lockRotation: true,
ccd: true,
},
character2d: {
mode: "platformer",
controls: true,
speed: 5,
jumpSpeed: 8,
gravity: 20,
autostep: 0.2,
},
spriteAnimator: { autoplay: "", clips: [] },
joint2d: {
type: "revolute",
targetId: "",
anchor: [0, 0],
targetAnchor: [0, 0],
length: 1,
stiffness: 50,
damping: 5,
},
mesh: { type: "box", size: [1, 1, 1] },
material: {
color: "#91a697",