Add shared-scene 2D authoring, sprite animation and Rapier2D physics
CI / validate (push) Canceled after 0s
CI / validate (push) Canceled after 0s
This commit is contained in:
+99
-7
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user