prendy
Version:
Make games with prerendered backdrops using babylonjs and repond
296 lines (259 loc) • 10.8 kB
text/typescript
import { Point3D } from "chootils/dist/points3d";
import { meta } from "../meta";
import { StatePath, getState, onNextTick, setState, stopNewEffect } from "repond";
import { addSubEvents, makeEventTypes, II } from "repond-events";
import { getStateAtPath } from "repond";
import { get2DAngleFromCharacterToSpot, getCharDollStuff } from "../helpers/prendyUtils/characters";
import { getGlobalState, setGlobalState } from "../helpers/prendyUtils/global";
import { doWhenNowCamChanges, doWhenNowSegmentChanges, getSegmentFromSegmentRules } from "../helpers/prendyUtils/scene";
import {
CameraNameByPlace,
CharacterName,
PlaceName,
SegmentNameByPlace,
SpotNameByPlace,
WallNameByPlace,
} from "../types";
type ToPlaceOption<T_PlaceName extends PlaceName> = {
toPlace: T_PlaceName;
toSpot?: SpotNameByPlace[T_PlaceName];
toPositon?: Point3D;
// NOTE might be able to make this auto if the first spot is inside a cam collider?
toCam?: CameraNameByPlace[T_PlaceName];
toSegment?: SegmentNameByPlace[T_PlaceName]; // could use nicer type like SegmentNameFromCamAndPlace, or a new SegmentNameFromPlace?
};
export const sceneEvents = makeEventTypes(({ event }) => ({
changeSegmentAtLoop: event({
run: async ({ place, segment }, { runMode, liveId }) => {
const segmentChangeEffectId = "changeSegmentAtLoop" + "_" + liveId;
const stopSegmentChangeEffect = () => stopNewEffect(segmentChangeEffectId);
const endEvent = () => {
stopSegmentChangeEffect();
setState({ liveEvents: { [liveId]: { goalEndTime: 0 } } });
};
if (runMode === "start") {
// NOTE WARNING This will probably break if goalSegmentNameAtLoop changes from somewhere else!!!
// to fix: could listen to changes to goalSegmentNameAtLoop
// might be fixed now that doWhenNowSegmentChanges listens to any change, instead of waiting for the expected segment name
// FIXME this can not work? (the async resolve part)
onNextTick(() => {
setGlobalState((state) => {
const { goalSegmentNameAtLoop } = state;
if (goalSegmentNameAtLoop) {
// TEMP resolve straight away if there's already a goalSegmentNameAtLoop
console.error("there was already a goalSegmentNameAtLoop when running changeSegmentAtLoop");
endEvent();
return {};
}
// End this event once the segment changes
doWhenNowSegmentChanges(segment, () => endEvent(), segmentChangeEffectId);
// Set the goal segment name for when the backdrop video loops
return { goalSegmentNameAtLoop: segment };
});
});
} else {
stopSegmentChangeEffect();
}
},
params: { place: "" as PlaceName, segment: "" as SegmentNameByPlace[PlaceName] },
duration: Infinity,
}),
changeCameraAtLoop: event({
run: async ({ place, cam }, { runMode, liveId }) => {
const camChangeEffectId = "changeCameraAtLoop" + "_" + liveId;
const stopCamChangeEffect = () => stopNewEffect(camChangeEffectId);
const endEvent = () => {
stopCamChangeEffect();
setState({ liveEvents: { [liveId]: { goalEndTime: 0 } } });
};
if (runMode === "start") {
setState((state) => {
const { goalCamNameAtLoop } = state.global.main;
if (goalCamNameAtLoop) {
// TEMP resolve straight away if there's already a goalCamNameAtLoop
console.error("there was already a goalSegmentNameAtLoop when running changeSegmentAtLoopAsync");
endEvent();
return {};
}
doWhenNowCamChanges(cam, () => endEvent(), camChangeEffectId);
return {
// AnyCameraName needed if there's only 1 place
global: { main: { goalCamNameAtLoop: cam } },
};
});
} else {
stopCamChangeEffect();
}
},
params: { place: "" as PlaceName, cam: "" as CameraNameByPlace[PlaceName] },
duration: Infinity,
}),
hideWall: event({
run: async ({ place, wall, unhide }, { runMode }) => {
if (runMode !== "start") return;
let wallShouldShow = false;
if (unhide) wallShouldShow = true;
setState((state) => ({
places: {
[place]: { toggledWalls: { ...(state.places[place].toggledWalls as any), [wall]: wallShouldShow } },
},
}));
},
params: {
place: "" as PlaceName,
wall: "",
unhide: undefined as undefined | boolean,
},
}),
showStoryView: event({
run: async ({ hide = false }, { runMode, liveId, elapsedTime }) => {
if (runMode !== "start") return;
const GUESSED_FADE_TIME = 1000; // note could listen to something like isFullyFaded and return here, but maybe a set time is okay
setGlobalState({ storyOverlayToggled: hide });
// await delay(GUESSED_FADE_TIME);
setState({ liveEvents: { [liveId]: { goalEndTime: elapsedTime + GUESSED_FADE_TIME } } });
},
params: { hide: undefined as boolean | undefined },
duration: Infinity,
}),
setSegment: event({
run: async ({ place, segment }, { runMode, isFirstAdd, liveId }) => {
if (isFirstAdd) addSubEvents(liveId, [II("scene", "changeSegmentAtLoop", { place, segment })]);
},
params: { place: "" as PlaceName, segment: "" as SegmentNameByPlace[PlaceName] },
}),
setCamera: event({
run: async ({ place, cam, whenToRun = "now" }, { runMode, isFirstAdd, liveId }) => {
if (whenToRun === "at loop") {
if (isFirstAdd) addSubEvents(liveId, [II("scene", "changeCameraAtLoop", { place, cam })]);
} else {
if (runMode === "start") {
const endEvent = () => setState({ liveEvents: { [liveId]: { goalEndTime: 0 } } });
const { nowPlaceName } = getState().global.main;
const { nowCamName } = getState().global.main;
// already on that camera
if (nowCamName === cam) {
endEvent();
return;
}
// AnyCameraName needed if there's only 1 place
setState({ global: { main: { goalCamName: cam } } }, () => endEvent());
}
}
},
params: {
place: "" as PlaceName,
cam: "" as CameraNameByPlace[PlaceName],
whenToRun: undefined as "at loop" | "now" | undefined,
},
}),
canTriggerCamera: event({
run: async ({ place, cam, whenToRun = "now" }, { runMode, isFirstAdd, liveId }) => {
if (whenToRun === "at loop") {
if (isFirstAdd) addSubEvents(liveId, [II("scene", "changeCameraAtLoop", { place, cam })]);
} else {
if (runMode === "start") {
const endEvent = () => setState({ liveEvents: { [liveId]: { goalEndTime: 0 } } });
const { nowPlaceName } = getState().global.main;
const { nowCamName } = getState().global.main;
// already on that camera
if (nowCamName === cam) {
endEvent();
return;
}
// AnyCameraName needed if there's only 1 place
setState({ global: { main: { goalCamName: cam } } }, () => endEvent());
}
}
},
params: {
place: "" as PlaceName,
cam: "" as CameraNameByPlace[PlaceName],
whenToRun: undefined as "at loop" | "now" | undefined,
},
}),
goToNewPlace: event({
run: async ({ toOption, who }, { runMode, liveId }) => {
if (runMode !== "start") return;
const playerCharacter = getGlobalState().playerCharacter as CharacterName;
const character = who || playerCharacter;
const { placeInfoByName } = meta.assets!;
// NOTE could include waitForPlaceFullyLoaded here so it can be awaited
let { toSpot, toPlace, toPositon, toCam, toSegment } = toOption;
const { dollName } = getCharDollStuff(character) ?? {};
if (!dollName) return;
onNextTick(() => {
setState((state) => {
const newPlaceDefaultCamName = placeInfoByName[toPlace].cameraNames[0];
const nowSegmentName = state.global.main.nowSegmentName;
const placeInfo = placeInfoByName[toPlace];
// toSpot = toSpot ?? (placeInfo.spotNames[0] as SpotNameByPlace[T_PlaceName]);
toSpot = toSpot; // ?? (placeInfo.spotNames[0] as SpotNameByPlace[T_PlaceName]);
toPositon = toPositon;
toCam = toCam ?? (placeInfo.cameraNames[0] as NonNullable<typeof toCam>); // types as a cam for the chosen place
toSegment = toSegment ?? (placeInfo.segmentNames[0] as SegmentNameByPlace[PlaceName]);
const foundRuleSegmentName = getSegmentFromSegmentRules(toPlace, toCam);
if (foundRuleSegmentName) toSegment = foundRuleSegmentName;
return {
global: {
main: {
goalPlaceName: toPlace,
goalSegmentWhenGoalPlaceLoads: toSegment || nowSegmentName,
goalCamWhenNextPlaceLoads: toCam || newPlaceDefaultCamName,
},
},
dolls: { [dollName]: { goalPositionAtNewPlace: toPositon, goalSpotNameAtNewPlace: toSpot } },
};
});
});
},
params: {
toOption: { toPlace: "" as PlaceName } as ToPlaceOption<PlaceName>,
who: undefined as undefined | CharacterName,
},
}),
}));
// TODO Add CustomEventParams type for
// - setSegment
// - setCamera
// - goToNewPlace;
// ideas
// disable/enable camCubes ?
// start/stop characterFollowinglayer ?
// Special types
type SceneHideWallParams<T_Place extends PlaceName> = {
place: T_Place;
wall: WallNameByPlace[T_Place];
unhide?: boolean;
};
type SceneSetSegmentParams<T_Place extends PlaceName> = {
place: T_Place;
segment: SegmentNameByPlace[T_Place];
};
type SceneSetCameraParams<T_Place extends PlaceName> = {
place: T_Place;
cam: CameraNameByPlace[T_Place];
whenToRun?: "at loop" | "now" | undefined;
};
type SceneGoToNewPlaceParams<T_Place extends PlaceName> = {
toOption: ToPlaceOption<T_Place>;
who?: undefined | CharacterName;
};
type CanTriggerCameraParams<T_Place extends PlaceName> = {
place: T_Place;
cam: CameraNameByPlace[T_Place];
can?: boolean | undefined;
};
export type SceneEventParameters<T_Group, T_Event, T_GenericParamA> = T_Group extends "scene"
? T_Event extends "hideWall"
? SceneHideWallParams<T_GenericParamA & PlaceName>
: T_Event extends "setSegment"
? SceneSetSegmentParams<T_GenericParamA & PlaceName>
: T_Event extends "setCamera"
? SceneSetCameraParams<T_GenericParamA & PlaceName>
: T_Event extends "goToNewPlace"
? SceneGoToNewPlaceParams<T_GenericParamA & PlaceName>
: T_Event extends "canTriggerCamera"
? CanTriggerCameraParams<T_GenericParamA & PlaceName>
: never
: never;