UNPKG

prendy

Version:

Make games with prerendered backdrops using babylonjs and repond

395 lines (394 loc) 18.8 kB
import { forEach } from "chootils/dist/loops"; import { subtractPointsSafer } from "chootils/dist/points3d"; import { toRadians } from "chootils/dist/speedAngleDistance"; import { getShortestAngle, getVectorAngle } from "chootils/dist/speedAngleDistance2d"; import { getPrevState, getRefs, getState, makeEffects, makeParamEffects, onNextTick, setState, startParamEffectsGroup, stopParamEffectsGroup, } from "repond"; import { addMoverEffects } from "repond-movers"; import { cloneObjectWithJson } from "repond/dist/utils"; import { setGlobalPositionWithCollisions } from "../helpers/babylonjs/setGlobalPositionWithCollisions"; import { point3dToVector3 } from "../helpers/babylonjs/vectors"; import { getDefaultInRangeFunction, getModelNameFromDoll, getQuickDistanceBetweenDolls, inRangesAreTheSame, saveModelStuffToDoll, setDollAnimWeight, setupLightMaterial, updateDollScreenPosition, } from "../helpers/prendyUtils/dolls"; import { meta } from "../meta"; // TODO add to asset options? const rangeOptions = { touch: 1, // prev 2 talk: 2, // prev 3 see: 6, // prev 20 }; export const rangeOptionsQuick = { touch: rangeOptions.touch * rangeOptions.touch, talk: rangeOptions.talk * rangeOptions.talk, see: rangeOptions.see * rangeOptions.see, }; export const dollParamEffects = makeParamEffects({ dollName: "", modelName: "" }, // defaultParams ({ itemEffect, params: { dollName, modelName } }) => ({ waitForModelToLoad: itemEffect({ run() { saveModelStuffToDoll({ dollName, modelName }); }, id: `doll_waitForModelToLoad${dollName}_${modelName}`, check: { type: "models", id: modelName, prop: "isLoaded", becomes: true }, atStepEnd: true, }), // When the place and all characters are loaded whenWholePlaceFinishesLoading: itemEffect({ run() { const { prendyOptions } = meta.assets; const modelRefs = getRefs().models[modelName]; if (modelRefs.materialRefs) { forEach(modelRefs.materialRefs, (materialRef) => setupLightMaterial(materialRef)); } setupLightMaterial(modelRefs.materialRef); const { nowPlaceName } = getState().global.main; const { modelNamesByPlace } = prendyOptions; const modelNamesForPlace = modelNamesByPlace[nowPlaceName]; const isInPlace = modelNamesForPlace.includes(modelName); if (!isInPlace) { setState({ dolls: { [dollName]: { isVisible: false } } }); } else { setState({ dolls: { [dollName]: { isVisible: true } } }); } }, id: `doll_whenWholePlaceFinishesLoading${dollName}_${modelName}`, check: { type: "global", prop: ["isLoadingBetweenPlaces"], becomes: false }, atStepEnd: true, step: "respondToNewPlace", }), })); // FIXME // maybe allow repond to run 'addedOrRemoved' rules for initialState? // NOTE rules can be manually triggered atleast, but the rule might not know an item was added export function startDynamicDollRulesForInitialState() { const { dollNames } = meta.assets; forEach(dollNames, (dollName) => { const { modelName } = getState().dolls[dollName]; if (modelName) startParamEffectsGroup("doll", { dollName, modelName }); }); } export function stopDynamicDollRulesForInitialState() { const { dollNames } = meta.assets; forEach(dollNames, (dollName) => { const { modelName } = getState().dolls[dollName]; if (modelName) stopParamEffectsGroup("doll", { dollName, modelName }); }); } export const dollEffects = makeEffects(({ itemEffect, effect }) => ({ // -------------------------------- // loading model stuff // -------------------------------- whenModelNameChanges: itemEffect({ run({ itemId: dollName, newValue: newModelName, prevValue: prevModelName }) { // stop the previous dynamic rule, and start the new one stopParamEffectsGroup("doll", { dollName, modelName: prevModelName }); startParamEffectsGroup("doll", { dollName, modelName: newModelName }); }, check: { type: "dolls", prop: "modelName" }, }), whenDollAddedOrRemoved: effect({ run(diffInfo) { //TODO maybe make it easier to add itemAdded rule and itemRemoved rule // stop the previous dynamic rule, and start the new one forEach(diffInfo.itemsRemoved.dolls, (dollName) => { const { modelName } = getPrevState().dolls[dollName]; stopParamEffectsGroup("doll", { dollName, modelName }); }); forEach(diffInfo.itemsAdded.dolls, (dollName) => { const { modelName } = getState().dolls[dollName]; startParamEffectsGroup("doll", { dollName, modelName }); }); }, check: { type: "dolls", addedOrRemoved: true }, }), // -------------------------------- // animations // -------------------------------- whenNowAnimationChanged: itemEffect({ run({ newValue: nowAnimation, itemState, itemId: dollName }) { const { modelInfoByName } = meta.assets; const { modelName } = itemState; const animationNames = modelInfoByName[modelName].animationNames; // type T_ModelName = typeof modelName; // let newWeights = {} as Record< // AnimationNameByModel[T_ModelName], // number // >; let newWeights = {}; forEach(animationNames, (aniName) => { newWeights[aniName] = nowAnimation === aniName ? 1 : 0; }); setDollAnimWeight(dollName, newWeights); }, check: { type: "dolls", prop: "nowAnimation" }, step: "dollAnimation", atStepEnd: true, }), whenAnimWeightsChanged: itemEffect({ run({ newValue: animWeights, itemState, itemRefs: dollRefs }) { const { modelInfoByName } = meta.assets; const { gameTimeSpeed } = getState().global.main; const { modelName } = itemState; const animationNames = modelInfoByName[modelName].animationNames; if (!dollRefs.aniGroupsRef) return; forEach(animationNames, (aniName) => { if (!dollRefs.aniGroupsRef) return; const aniRef = dollRefs?.aniGroupsRef?.[aniName]; // const { timerSpeed } = getRefs().global.main; // if (aniRef._speedRatio !== timerSpeed) { // aniRef._speedRatio = timerSpeed; // } if (!aniRef) { console.warn("tried to use undefined animation", aniName); return; } // console.log("gameTimeSpeed", gameTimeSpeed); if (aniRef && aniRef?.speedRatio !== gameTimeSpeed) { aniRef.speedRatio = gameTimeSpeed; } const animWeight = animWeights[aniName]; const animIsStopped = animWeight < 0.003; // stops playing if the weight is 0ish if (animIsStopped) { if (aniRef?.isPlaying) aniRef.stop(); } else { if (!aniRef?.isPlaying) aniRef.start(itemState.animationLoops); } aniRef?.setWeightForAllAnimatables(animWeights[aniName]); }); }, check: { type: "dolls", prop: "animWeights" }, atStepEnd: true, step: "dollAnimation2", }), // -------------------------------- // other drawing stuff // -------------------------------- whenRotationYChanged: itemEffect({ run({ newValue: newRotationY, itemRefs }) { if (!itemRefs.meshRef) return; itemRefs.meshRef.rotation.y = toRadians(newRotationY); }, atStepEnd: true, check: { type: "dolls", prop: "rotationY" }, }), // // ___________________________________ // position whenPositionChangesToEdit: itemEffect({ run({ newValue: newPosition, prevValue: prevPosition, itemRefs, itemId: dollName }) { if (!itemRefs.meshRef) return; if (itemRefs.canGoThroughWalls) { // console.log("not checking collisions and setting position", dollName); itemRefs.meshRef.setAbsolutePosition(point3dToVector3(newPosition)); } else { const { editedPosition, positionWasEdited, collidedPosOffset } = setGlobalPositionWithCollisions(itemRefs.meshRef, newPosition); // (itemRefs.meshRef as Mesh).position = newMeshPosition; // if a collision cauhed the mesh to not reach the position, update the position state if (positionWasEdited) { const shouldChangeAngle = Math.abs(collidedPosOffset.z) > 0.01 || Math.abs(collidedPosOffset.x) > 0.01; let newYRotation = getState().dolls[dollName].rotationYGoal; const positionOffset = subtractPointsSafer(prevPosition, editedPosition); newYRotation = getVectorAngle({ x: positionOffset.z, y: positionOffset.x }); // console.log("collidedPosOffset", collidedPosOffset, "positionOffset", positionOffset); setState(() => ({ dolls: { [dollName]: { position: editedPosition, rotationYGoal: shouldChangeAngle ? newYRotation : undefined, }, }, })); } } updateDollScreenPosition(dollName); }, check: { type: "dolls", prop: "position" }, step: "editPosition", atStepEnd: true, }), whenPositionChangesCheckInRange: effect({ run(_diffInfo) { // console.log("a doll moved", _diffInfo); const { dollNames } = meta.assets; // forEach(diffInfo.itemsChanged.dolls) const defaultInRange = getDefaultInRangeFunction(dollNames); const newQuickDistancesMap = {}; // { rabbit : { cricket: 50, rabbit: 1000 } // cricket : { cricket: 1000, rabbit: 50 }} let somethingChanged = false; const newDollsState = {}; const tempNewDollsState = {}; // forEach(diffInfo.itemsChanged.dolls as DollName[], (dollName) => { // if (!diffInfo.propsChangedBool.dolls[dollName].position) return; forEach(dollNames, (dollName) => { // if (!diffInfo.propsChangedBool.dolls[dollName].position) return; // if position changed const dollState = getState().dolls[dollName]; if (!dollState.isVisible) return; // stop checking more if isVisible is false newQuickDistancesMap[dollName] = {}; // // newDollsState[dollName] = { inRange: defaultInRange() }; tempNewDollsState[dollName] = { inRange: defaultInRange() }; // get quick distances to each other doll forEach(dollNames, (otherDollName) => { const otherDollState = getState().dolls[otherDollName]; if (!otherDollState.isVisible) return; let quickDistance = 100000000; if (dollName === otherDollName) { quickDistance = 100000000; } // if the reverse distance was already found use that if (newQuickDistancesMap[otherDollName]?.[dollName] !== undefined) { quickDistance = newQuickDistancesMap[otherDollName][dollName]; } else { quickDistance = getQuickDistanceBetweenDolls(dollName, otherDollName); } newQuickDistancesMap[dollName][otherDollName] = quickDistance; tempNewDollsState[dollName].inRange[otherDollName].touch = quickDistance < rangeOptionsQuick.touch; tempNewDollsState[dollName].inRange[otherDollName].talk = quickDistance < rangeOptionsQuick.talk; tempNewDollsState[dollName].inRange[otherDollName].see = quickDistance < rangeOptionsQuick.see; }); const currentDollState = getState().dolls[dollName]; const tempNewDollState = tempNewDollsState[dollName]; if (tempNewDollState?.inRange) { if (!inRangesAreTheSame(tempNewDollState.inRange, currentDollState.inRange // FIXME DeepReadonlyObjects )) { newDollsState[dollName] = tempNewDollState; somethingChanged = true; } } }); if (somethingChanged) { setState({ dolls: newDollsState }); } }, check: { type: "dolls", prop: ["position", "isVisible"] }, atStepEnd: true, step: "checkCollisions", }), whenHidingUpdateInRange: itemEffect({ run({ newValue: newIsVisible, itemId: dollName }) { const { dollNames } = meta.assets; // return early if it didn't just hide if (newIsVisible) return; const defaultInRange = getDefaultInRangeFunction(dollNames); const tempNewAllDollsState = {}; const newAllDollsState = {}; // set all inRange to false for the doll that went invisible tempNewAllDollsState[dollName] = { inRange: defaultInRange() }; forEach(dollNames, (otherDollName) => { if (otherDollName === dollName) return; const otherDollState = getState().dolls[otherDollName]; tempNewAllDollsState[otherDollName] = { inRange: cloneObjectWithJson(otherDollState.inRange) }; // set the doll that became invisible to not in range for each other doll tempNewAllDollsState[otherDollName].inRange[dollName].touch = false; tempNewAllDollsState[otherDollName].inRange[dollName].talk = false; tempNewAllDollsState[otherDollName].inRange[dollName].see = false; const tempNewDollState = tempNewAllDollsState[otherDollName]; if (tempNewDollState?.inRange) { if (!inRangesAreTheSame(tempNewDollState.inRange, otherDollState.inRange // FIXME DeepReadonlyObjects )) { newAllDollsState[otherDollName] = tempNewDollState; } } }); // do it on next ticket , because the step that reacts to inRange changing is already done onNextTick(() => { setState({ dolls: newAllDollsState }); }); }, check: { type: "dolls", prop: "isVisible" }, // atStepEnd: true, // step: "positionReaction", }), // when doll isVisibleChanges, check in range // should be a dynamic rule ? updateDollScreenPositionWhenSlateMoves: effect({ run() { const { dollNames } = meta.assets; const { playerCharacter } = getState().global.main; const { dollName } = getState().characters[playerCharacter]; if (!dollName) return; // NOTE TODO ideally add for each character automatically as a param effects? forEach(dollNames, (dollName) => updateDollScreenPosition(dollName)); }, // this happens before rendering because its in "derive" instead of "subscribe" check: { type: "global", prop: ["slatePos", "slateZoom"] }, step: "slatePosition", atStepEnd: true, }), whenToggledMeshesChanges: itemEffect({ run({ newValue: toggledMeshes, itemId: dollName, itemRefs }) { const { modelInfoByName } = meta.assets; const { otherMeshes } = itemRefs; if (!otherMeshes) return; const modelName = getModelNameFromDoll(dollName); const modelInfo = modelInfoByName[modelName]; const typedMeshNames = modelInfo.meshNames; forEach(typedMeshNames, (meshName) => { const newToggle = toggledMeshes[meshName]; const theMesh = otherMeshes[meshName]; if (theMesh && newToggle !== undefined) theMesh.setEnabled(newToggle); }); }, check: { type: "dolls", prop: "toggledMeshes" }, step: "default", atStepEnd: true, }), whenIsVisibleChanges: itemEffect({ run({ newValue: isVisible, itemId: dollName, itemRefs: dollRefs }) { if (!dollRefs.meshRef) return console.warn("isVisible change: no mesh ref for", dollName); if (dollName === "shoes") { console.log("shoes isVisible change", isVisible); } if (isVisible) { dollRefs.meshRef.setEnabled(true); // dollRefs.canCollide = true; } else { dollRefs.meshRef.setEnabled(false); // setEnabled also toggles mesh collisions // dollRefs.canCollide = false; } }, check: { type: "dolls", prop: "isVisible" }, step: "default", atStepEnd: true, }), // ------------------------------------ // Mover rules // rotationY whenRotationGoalChangedToFix: itemEffect({ run({ prevValue: oldYRotation, newValue: newYRotation, itemId: dollName }) { const yRotationDifference = oldYRotation - newYRotation; if (Math.abs(yRotationDifference) > 180) { const shortestAngle = getShortestAngle(oldYRotation, newYRotation); let editedYRotation = oldYRotation + shortestAngle; setState({ dolls: { [dollName]: { rotationYGoal: editedYRotation } } }); } }, check: { type: "dolls", prop: "rotationYGoal" }, step: "dollCorrectRotationAndPosition", atStepEnd: true, }), ...addMoverEffects("dolls", "position", "3d"), ...addMoverEffects("dolls", "rotationY"), ...addMoverEffects("dolls", "animWeights", "multi"), }));