Add interaction-links.ts and update scene-document.ts
This commit is contained in:
@@ -20,7 +20,8 @@ import {
|
||||
} from "./project-time-settings";
|
||||
import { type ScenePath } from "./paths";
|
||||
|
||||
export const SCENE_DOCUMENT_VERSION = 44 as const;
|
||||
export const SCENE_DOCUMENT_VERSION = 45 as const;
|
||||
export const CONTROL_SURFACE_FOUNDATION_SCENE_DOCUMENT_VERSION = 45 as const;
|
||||
export const NPC_PRESENCE_SCENE_DOCUMENT_VERSION = 44 as const;
|
||||
export const PATH_FOUNDATION_SCENE_DOCUMENT_VERSION = 43 as const;
|
||||
export const NPC_COLLIDER_SCENE_DOCUMENT_VERSION = 42 as const;
|
||||
|
||||
478
src/interactions/interaction-links.ts
Normal file
478
src/interactions/interaction-links.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import {
|
||||
areControlEffectsEqual,
|
||||
cloneControlEffect,
|
||||
createModelInstanceControlTargetRef,
|
||||
createPlayModelAnimationControlEffect,
|
||||
createPlaySoundControlEffect,
|
||||
createSoundEmitterControlTargetRef,
|
||||
createStopModelAnimationControlEffect,
|
||||
createStopSoundControlEffect,
|
||||
getControlEffectLabel,
|
||||
type ControlEffect
|
||||
} from "../controls/control-surface";
|
||||
|
||||
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;
|
||||
loop?: boolean;
|
||||
}
|
||||
|
||||
export interface StopAnimationAction {
|
||||
type: "stopAnimation";
|
||||
targetModelInstanceId: string;
|
||||
}
|
||||
|
||||
export interface PlaySoundAction {
|
||||
type: "playSound";
|
||||
targetSoundEmitterId: string;
|
||||
}
|
||||
|
||||
export interface StopSoundAction {
|
||||
type: "stopSound";
|
||||
targetSoundEmitterId: string;
|
||||
}
|
||||
|
||||
export interface ControlInteractionAction {
|
||||
type: "control";
|
||||
effect: ControlEffect;
|
||||
}
|
||||
|
||||
export type InteractionAction =
|
||||
| TeleportPlayerAction
|
||||
| ToggleVisibilityAction
|
||||
| PlayAnimationAction
|
||||
| StopAnimationAction
|
||||
| PlaySoundAction
|
||||
| StopSoundAction
|
||||
| ControlInteractionAction;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface CreatePlayAnimationInteractionLinkOptions {
|
||||
id?: string;
|
||||
sourceEntityId: string;
|
||||
trigger?: InteractionTriggerKind;
|
||||
targetModelInstanceId: string;
|
||||
clipName: string;
|
||||
loop?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateStopAnimationInteractionLinkOptions {
|
||||
id?: string;
|
||||
sourceEntityId: string;
|
||||
trigger?: InteractionTriggerKind;
|
||||
targetModelInstanceId: string;
|
||||
}
|
||||
|
||||
export interface CreatePlaySoundInteractionLinkOptions {
|
||||
id?: string;
|
||||
sourceEntityId: string;
|
||||
trigger?: InteractionTriggerKind;
|
||||
targetSoundEmitterId: string;
|
||||
}
|
||||
|
||||
export interface CreateStopSoundInteractionLinkOptions {
|
||||
id?: string;
|
||||
sourceEntityId: string;
|
||||
trigger?: InteractionTriggerKind;
|
||||
targetSoundEmitterId: string;
|
||||
}
|
||||
|
||||
export interface CreateControlInteractionLinkOptions {
|
||||
id?: string;
|
||||
sourceEntityId: string;
|
||||
trigger?: InteractionTriggerKind;
|
||||
effect: ControlEffect;
|
||||
}
|
||||
|
||||
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,
|
||||
loop: action.loop
|
||||
};
|
||||
case "stopAnimation":
|
||||
return {
|
||||
type: "stopAnimation",
|
||||
targetModelInstanceId: action.targetModelInstanceId
|
||||
};
|
||||
case "playSound":
|
||||
return {
|
||||
type: "playSound",
|
||||
targetSoundEmitterId: action.targetSoundEmitterId
|
||||
};
|
||||
case "stopSound":
|
||||
return {
|
||||
type: "stopSound",
|
||||
targetSoundEmitterId: action.targetSoundEmitterId
|
||||
};
|
||||
case "control":
|
||||
return {
|
||||
type: "control",
|
||||
effect: cloneControlEffect(action.effect)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function isInteractionTriggerKind(
|
||||
value: unknown
|
||||
): value is InteractionTriggerKind {
|
||||
return value === "enter" || value === "exit" || value === "click";
|
||||
}
|
||||
|
||||
export function isControlInteractionAction(
|
||||
action: InteractionAction
|
||||
): action is ControlInteractionAction {
|
||||
return action.type === "control";
|
||||
}
|
||||
|
||||
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 createPlayAnimationInteractionLink(
|
||||
options: CreatePlayAnimationInteractionLinkOptions
|
||||
): InteractionLink {
|
||||
assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
|
||||
assertNonEmptyString(
|
||||
options.targetModelInstanceId,
|
||||
"Play animation target model instance id"
|
||||
);
|
||||
assertNonEmptyString(options.clipName, "Play animation clip name");
|
||||
|
||||
return {
|
||||
id: options.id ?? createOpaqueId("interaction-link"),
|
||||
sourceEntityId: options.sourceEntityId,
|
||||
trigger: options.trigger ?? "enter",
|
||||
action: {
|
||||
type: "playAnimation",
|
||||
targetModelInstanceId: options.targetModelInstanceId,
|
||||
clipName: options.clipName,
|
||||
loop: options.loop
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createStopAnimationInteractionLink(
|
||||
options: CreateStopAnimationInteractionLinkOptions
|
||||
): InteractionLink {
|
||||
assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
|
||||
assertNonEmptyString(
|
||||
options.targetModelInstanceId,
|
||||
"Stop animation target model instance id"
|
||||
);
|
||||
|
||||
return {
|
||||
id: options.id ?? createOpaqueId("interaction-link"),
|
||||
sourceEntityId: options.sourceEntityId,
|
||||
trigger: options.trigger ?? "enter",
|
||||
action: {
|
||||
type: "stopAnimation",
|
||||
targetModelInstanceId: options.targetModelInstanceId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createPlaySoundInteractionLink(
|
||||
options: CreatePlaySoundInteractionLinkOptions
|
||||
): InteractionLink {
|
||||
assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
|
||||
assertNonEmptyString(
|
||||
options.targetSoundEmitterId,
|
||||
"Play sound target sound emitter id"
|
||||
);
|
||||
|
||||
return {
|
||||
id: options.id ?? createOpaqueId("interaction-link"),
|
||||
sourceEntityId: options.sourceEntityId,
|
||||
trigger: options.trigger ?? "enter",
|
||||
action: {
|
||||
type: "playSound",
|
||||
targetSoundEmitterId: options.targetSoundEmitterId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createStopSoundInteractionLink(
|
||||
options: CreateStopSoundInteractionLinkOptions
|
||||
): InteractionLink {
|
||||
assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
|
||||
assertNonEmptyString(
|
||||
options.targetSoundEmitterId,
|
||||
"Stop sound target sound emitter id"
|
||||
);
|
||||
|
||||
return {
|
||||
id: options.id ?? createOpaqueId("interaction-link"),
|
||||
sourceEntityId: options.sourceEntityId,
|
||||
trigger: options.trigger ?? "enter",
|
||||
action: {
|
||||
type: "stopSound",
|
||||
targetSoundEmitterId: options.targetSoundEmitterId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createControlInteractionLink(
|
||||
options: CreateControlInteractionLinkOptions
|
||||
): InteractionLink {
|
||||
assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
|
||||
|
||||
return {
|
||||
id: options.id ?? createOpaqueId("interaction-link"),
|
||||
sourceEntityId: options.sourceEntityId,
|
||||
trigger: options.trigger ?? "enter",
|
||||
action: {
|
||||
type: "control",
|
||||
effect: cloneControlEffect(options.effect)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getInteractionActionControlEffect(
|
||||
action: InteractionAction
|
||||
): ControlEffect | null {
|
||||
switch (action.type) {
|
||||
case "playAnimation":
|
||||
return createPlayModelAnimationControlEffect({
|
||||
target: createModelInstanceControlTargetRef(action.targetModelInstanceId),
|
||||
clipName: action.clipName,
|
||||
loop: action.loop
|
||||
});
|
||||
case "stopAnimation":
|
||||
return createStopModelAnimationControlEffect({
|
||||
target: createModelInstanceControlTargetRef(action.targetModelInstanceId)
|
||||
});
|
||||
case "playSound":
|
||||
return createPlaySoundControlEffect({
|
||||
target: createSoundEmitterControlTargetRef(action.targetSoundEmitterId)
|
||||
});
|
||||
case "stopSound":
|
||||
return createStopSoundControlEffect({
|
||||
target: createSoundEmitterControlTargetRef(action.targetSoundEmitterId)
|
||||
});
|
||||
case "control":
|
||||
return cloneControlEffect(action.effect);
|
||||
case "teleportPlayer":
|
||||
case "toggleVisibility":
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getInteractionActionLabel(action: InteractionAction): string {
|
||||
switch (action.type) {
|
||||
case "teleportPlayer":
|
||||
return "Teleport Player";
|
||||
case "toggleVisibility":
|
||||
return "Toggle Visibility";
|
||||
case "playAnimation":
|
||||
return "Play Animation";
|
||||
case "stopAnimation":
|
||||
return "Stop Animation";
|
||||
case "playSound":
|
||||
return "Play Sound";
|
||||
case "stopSound":
|
||||
return "Stop Sound";
|
||||
case "control":
|
||||
return getControlEffectLabel(action.effect);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
case "playAnimation":
|
||||
return (
|
||||
left.action.targetModelInstanceId ===
|
||||
(right.action as PlayAnimationAction).targetModelInstanceId &&
|
||||
left.action.clipName === (right.action as PlayAnimationAction).clipName &&
|
||||
left.action.loop === (right.action as PlayAnimationAction).loop
|
||||
);
|
||||
case "stopAnimation":
|
||||
return (
|
||||
left.action.targetModelInstanceId ===
|
||||
(right.action as StopAnimationAction).targetModelInstanceId
|
||||
);
|
||||
case "playSound":
|
||||
return (
|
||||
left.action.targetSoundEmitterId ===
|
||||
(right.action as PlaySoundAction).targetSoundEmitterId
|
||||
);
|
||||
case "stopSound":
|
||||
return (
|
||||
left.action.targetSoundEmitterId ===
|
||||
(right.action as StopSoundAction).targetSoundEmitterId
|
||||
);
|
||||
case "control":
|
||||
return areControlEffectsEqual(
|
||||
left.action.effect,
|
||||
(right.action as ControlInteractionAction).effect
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user