Files
webeditor3d/src/interactions/interaction-links.ts

176 lines
5.4 KiB
TypeScript

import { createOpaqueId } from "../core/ids";
export const INTERACTION_TRIGGER_KINDS = ["enter", "exit", "click"] as const;
export type InteractionTriggerKind = (typeof INTERACTION_TRIGGER_KINDS)[number];
export interface TeleportPlayerAction {
type: "teleportPlayer";
targetEntityId: string;
}
export interface ToggleVisibilityAction {
type: "toggleVisibility";
targetBrushId: string;
visible?: boolean;
}
export interface PlayAnimationAction {
type: "playAnimation";
targetModelInstanceId: string;
clipName: string;
}
export interface StopAnimationAction {
type: "stopAnimation";
targetModelInstanceId: string;
}
export type InteractionAction = TeleportPlayerAction | ToggleVisibilityAction | PlayAnimationAction | StopAnimationAction;
export interface InteractionLink {
id: string;
sourceEntityId: string;
trigger: InteractionTriggerKind;
action: InteractionAction;
}
export interface CreateTeleportPlayerInteractionLinkOptions {
id?: string;
sourceEntityId: string;
trigger?: InteractionTriggerKind;
targetEntityId: string;
}
export interface CreateToggleVisibilityInteractionLinkOptions {
id?: string;
sourceEntityId: string;
trigger?: InteractionTriggerKind;
targetBrushId: string;
visible?: boolean;
}
function assertNonEmptyString(value: string, label: string) {
if (value.trim().length === 0) {
throw new Error(`${label} must be non-empty.`);
}
}
function cloneAction(action: InteractionAction): InteractionAction {
switch (action.type) {
case "teleportPlayer":
return {
type: "teleportPlayer",
targetEntityId: action.targetEntityId
};
case "toggleVisibility":
return {
type: "toggleVisibility",
targetBrushId: action.targetBrushId,
visible: action.visible
};
case "playAnimation":
return {
type: "playAnimation",
targetModelInstanceId: action.targetModelInstanceId,
clipName: action.clipName
};
case "stopAnimation":
return {
type: "stopAnimation",
targetModelInstanceId: action.targetModelInstanceId
};
}
}
export function isInteractionTriggerKind(value: unknown): value is InteractionTriggerKind {
return value === "enter" || value === "exit" || value === "click";
}
export function createTeleportPlayerInteractionLink(options: CreateTeleportPlayerInteractionLinkOptions): InteractionLink {
assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
assertNonEmptyString(options.targetEntityId, "Teleport target entity id");
return {
id: options.id ?? createOpaqueId("interaction-link"),
sourceEntityId: options.sourceEntityId,
trigger: options.trigger ?? "enter",
action: {
type: "teleportPlayer",
targetEntityId: options.targetEntityId
}
};
}
export function createToggleVisibilityInteractionLink(options: CreateToggleVisibilityInteractionLinkOptions): InteractionLink {
assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
assertNonEmptyString(options.targetBrushId, "Visibility target brush id");
if (options.visible !== undefined && typeof options.visible !== "boolean") {
throw new Error("Visibility action visible must be a boolean when authored.");
}
return {
id: options.id ?? createOpaqueId("interaction-link"),
sourceEntityId: options.sourceEntityId,
trigger: options.trigger ?? "enter",
action: {
type: "toggleVisibility",
targetBrushId: options.targetBrushId,
visible: options.visible
}
};
}
export function cloneInteractionLink(link: InteractionLink): InteractionLink {
return {
id: link.id,
sourceEntityId: link.sourceEntityId,
trigger: link.trigger,
action: cloneAction(link.action)
};
}
export function areInteractionLinksEqual(left: InteractionLink, right: InteractionLink): boolean {
if (left.id !== right.id || left.sourceEntityId !== right.sourceEntityId || left.trigger !== right.trigger) {
return false;
}
if (left.action.type !== right.action.type) {
return false;
}
switch (left.action.type) {
case "teleportPlayer":
return left.action.targetEntityId === (right.action as TeleportPlayerAction).targetEntityId;
case "toggleVisibility":
return (
left.action.targetBrushId === (right.action as ToggleVisibilityAction).targetBrushId &&
left.action.visible === (right.action as ToggleVisibilityAction).visible
);
}
}
export function cloneInteractionLinkRegistry(links: Record<string, InteractionLink>): Record<string, InteractionLink> {
return Object.fromEntries(Object.entries(links).map(([linkId, link]) => [linkId, cloneInteractionLink(link)]));
}
export function compareInteractionLinks(left: InteractionLink, right: InteractionLink): number {
if (left.sourceEntityId !== right.sourceEntityId) {
return left.sourceEntityId.localeCompare(right.sourceEntityId);
}
if (left.trigger !== right.trigger) {
return left.trigger.localeCompare(right.trigger);
}
return left.id.localeCompare(right.id);
}
export function getInteractionLinks(links: Record<string, InteractionLink>): InteractionLink[] {
return Object.values(links).sort(compareInteractionLinks);
}
export function getInteractionLinksForSource(links: Record<string, InteractionLink>, sourceEntityId: string): InteractionLink[] {
return getInteractionLinks(links).filter((link) => link.sourceEntityId === sourceEntityId);
}