prendy
Version:
Make games with prerendered backdrops using babylonjs and repond
606 lines (507 loc) • 21.2 kB
text/typescript
import { Ray, RayHelper, TargetCamera, Vector3 } from "@babylonjs/core";
import { defaultPosition, pointIsZero } from "chootils/dist/points2d";
import { getShortestAngle, getSpeedAndAngleFromVector, getVectorAngle } from "chootils/dist/speedAngleDistance2d";
import { getRefs, getState, makeEffects, setState } from "repond";
import { getScene } from "../helpers/babylonjs/getSceneOrEngineUtils";
import { getCharDollStuff } from "../helpers/prendyUtils/characters";
import { clearTimeoutSafe } from "../helpers/utils";
import { meta } from "../meta";
import { CharacterName } from "../types";
const LEAVE_GROUND_CANT_JUMP_DELAY = 100; // ms
const downRay = new Ray(Vector3.Zero(), Vector3.Zero());
const downRayHelper = new RayHelper(downRay);
const downRayDirection = new Vector3(0, -1, 0);
const downRayRelativeOrigin = new Vector3(0, 3, 0);
// Forward ray is actually a down ray but slightly infront of the player
const RAY_FRONT_DIST = 0.25;
const frontRay = new Ray(Vector3.Zero(), Vector3.Zero());
const frontRayHelper = new RayHelper(frontRay);
const frontRayDirection = downRayDirection;
const frontRayRelativeOrigin = new Vector3(
// dollPosRefs.velocity.x * 0.1,
0,
3,
RAY_FRONT_DIST
// dollPosRefs.velocity.z * 0.1
);
export const playerEffects = makeEffects(({ itemEffect, effect }) => ({
whenDirectionKeysPressed: effect({
run() {
const { ArrowDown, ArrowLeft, ArrowUp, ArrowRight, KeyW, KeyA, KeyS, KeyD } = getState().keyboards.main;
const rightPressed = ArrowRight || KeyD;
const upPressed = ArrowUp || KeyW;
const leftPressed = ArrowLeft || KeyA;
const downPressed = ArrowDown || KeyS;
const newInputVelocity = { x: 0, y: 0 };
if (downPressed) newInputVelocity.y = 1;
if (upPressed) newInputVelocity.y = -1;
if (leftPressed) newInputVelocity.x = -1;
if (rightPressed) newInputVelocity.x = 1;
// if moving diagonally, reduce the velocity , so its like limited to a joystick
if (newInputVelocity.x != 0 && newInputVelocity.y !== 0) {
newInputVelocity.x *= 0.75;
newInputVelocity.y *= 0.75;
}
// Temporarily disabled because of issue with key held down at next camera
setState({ players: { main: { inputVelocity: newInputVelocity } } });
},
step: "input",
check: {
type: "keyboards",
id: "main",
prop: ["ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp", "KeyW", "KeyA", "KeyS", "KeyD"],
},
}),
whenInteractKeyPressed: itemEffect({
run() {
setState({
players: { main: { interactButtonPressTime: Date.now() } },
});
},
step: "input",
check: {
type: "keyboards",
id: "main",
// prop: ["Space", "Enter", "KeyZ"],
// prop: ["Space", "Enter"],
prop: ["KeyE", "Enter"],
becomes: true,
},
}),
whenJumpKeyPressed: itemEffect({
run() {
const { prendyOptions } = meta.assets!;
if (!prendyOptions.hasJumping) return;
setState({ players: { main: { jumpButtonPressTime: Date.now() } } });
},
step: "input",
check: { type: "keyboards", prop: ["Space"], becomes: true },
}),
whenJumpKeyReleased: itemEffect({
run() {
const { prendyOptions } = meta.assets!;
if (!prendyOptions.hasJumping) return;
setState({ players: { main: { jumpButtonReleaseTime: Date.now() } } });
},
step: "input",
check: { type: "keyboards", prop: ["KeyM"], becomes: false },
}),
//
whenJumpPressed: itemEffect({
run({ itemState: playerState, frameDuration }) {
const globalRefs = getRefs().global.main;
const { playerCharacter, playerMovingPaused, gravityValue } = getState().global.main;
const { timerSpeed } = globalRefs;
const { dollRefs, dollState, dollName } = getCharDollStuff(playerCharacter as CharacterName) ?? {};
const { isOnGround, canJump } = playerState;
const { scene } = globalRefs;
const activeCamera = scene?.activeCamera;
if (!dollRefs || !dollState || !dollName || !activeCamera) return;
if (playerMovingPaused || !canJump) return;
dollRefs.positionMoverRefs.velocity.y = 10;
setState({
dolls: {
[dollName as string]: {
// nowAnimation: newAnimationName,
positionMoveMode: "push",
positionIsMoving: true,
},
},
players: { main: { isJumping: true, isOnGround: false } },
});
},
step: "input",
check: { type: "players", id: "main", prop: ["jumpButtonPressTime"] },
}),
whenJumpReleased: itemEffect({
run() {
setState({ players: { main: { jumpButtonReleaseTime: Date.now() } } });
},
step: "input",
check: { type: "players", id: "main", prop: ["jumpButtonPressTime"] },
}),
whenJoystickMoves: itemEffect({
run({ newValue: inputVelocity, itemState: playerState, itemRefs: playerRefs }) {
const { playerCharacter, playerMovingPaused, gravityValue } = getState().global.main;
const globalRefs = getRefs().global.main;
const { timerSpeed } = globalRefs;
const { dollRefs, dollState, dollName } = getCharDollStuff(playerCharacter as CharacterName) ?? {};
const { scene } = globalRefs;
const activeCamera = scene?.activeCamera;
if (!dollRefs || !dollState || !dollName || !activeCamera) return;
const { lastSafeInputAngle } = playerState;
let shouldChangeAngle = true;
if (lastSafeInputAngle !== null) {
const {
angle: newInputAngle,
// speed: newInputSpeed,
} = getSpeedAndAngleFromVector(inputVelocity);
const rotationYDifference = Math.abs(getShortestAngle(lastSafeInputAngle, newInputAngle));
shouldChangeAngle = rotationYDifference > 25;
}
if (!shouldChangeAngle) return;
// if (!walkerRefs.meshRef) return;
//assuming we're only using the single camera:
const camera = activeCamera as TargetCamera;
let currentYRotation = dollState.rotationYGoal;
let currentWalkSpeed = dollState.nowWalkSpeed;
let newYRotation = currentYRotation;
let newWalkSpeed = currentWalkSpeed;
let newAnimationName = dollState.nowAnimation;
let newIsMoving = dollState.positionIsMoving;
let newPositionMoveMode = dollState.positionMoveMode;
const forward = camera.getFrontPosition(-1).subtract(camera.position);
const right = camera.upVector.cross(forward);
//project forward and right vectors on the horizontal plane (y = 0)
forward.y = 0;
right.y = 0;
forward.normalize();
right.normalize();
//this is the direction in the world space we want to move:
let desiredMoveDirection = forward
.multiplyByFloats(inputVelocity.y, inputVelocity.y, inputVelocity.y)
.add(right.multiplyByFloats(-inputVelocity.x, -inputVelocity.x, -inputVelocity.x));
const canMove = !pointIsZero(inputVelocity) && !playerMovingPaused;
if (canMove) {
newAnimationName = playerState.animationNames.walking;
newIsMoving = true;
} else {
if (newAnimationName === playerState.animationNames.walking) {
newAnimationName = playerState.animationNames.idle;
}
// newIsMoving = false;
}
if (playerMovingPaused) {
desiredMoveDirection = new Vector3(0, 0, 0);
// newIsMoving = false;
}
//now we can apply the movement:
// * frameDuration * 0.1
dollRefs.positionMoverRefs.velocity.x = desiredMoveDirection.x * playerRefs.topWalkSpeed * timerSpeed;
dollRefs.positionMoverRefs.velocity.z = desiredMoveDirection.z * playerRefs.topWalkSpeed * timerSpeed;
if (canMove) {
const newSpeedAndRotation = getSpeedAndAngleFromVector({
x: -dollRefs.positionMoverRefs.velocity.z,
y: -dollRefs.positionMoverRefs.velocity.x,
});
newYRotation = newSpeedAndRotation.angle;
newWalkSpeed = newSpeedAndRotation.speed;
}
// dollRefs.positionMoverRefs.velocity.y = -gravityValue * timerSpeed;
// if (shouldChangeAngle) {
if (!playerMovingPaused) newPositionMoveMode = "push";
if (playerMovingPaused) {
dollRefs.positionMoverRefs.velocity.x = 0;
dollRefs.positionMoverRefs.velocity.z = 0;
setState({
dolls: {
[dollName]: {
nowAnimation: newAnimationName,
},
},
players: { main: { lastSafeInputAngle: null } },
});
return;
}
setState({
dolls: {
[dollName]: {
// inputVelocity: newInputVelocity,
rotationYGoal: newYRotation,
nowWalkSpeed: newWalkSpeed,
// nowAnimation: playerMovingPaused ? undefined : newAnimationName,
nowAnimation: newAnimationName,
positionMoveMode: newPositionMoveMode,
positionIsMoving: newIsMoving,
// positionIsMoving: true,
},
},
players: { main: { lastSafeInputAngle: null } },
});
// }
},
check: { type: "players", prop: "inputVelocity" },
step: "input",
atStepEnd: true,
}),
whenVirtualControlsPressed: itemEffect({
run({ itemRefs: playerRefs, itemId: playerName }) {
clearTimeoutSafe(playerRefs.canShowVirtualButtonsTimeout);
playerRefs.canShowVirtualButtonsTimeout = setTimeout(() => {
const { virtualControlsPressTime, virtualControlsReleaseTime } = getState().players[playerName];
if (virtualControlsReleaseTime > virtualControlsPressTime) return;
setState({
players: { [playerName]: { canShowVirtualButtons: true } },
});
}, 200); // wait 200 milliseconds, to prevent buttons showing from small mouse clicks
},
check: { type: "players", prop: "virtualControlsPressTime" },
step: "input",
atStepEnd: true,
}),
whenVirtualControlsReleased: itemEffect({
run({ itemRefs: playerRefs, itemId: playerName }) {
clearTimeoutSafe(playerRefs.canHideVirtualButtonsTimeout);
playerRefs.canHideVirtualButtonsTimeout = setTimeout(() => {
const { virtualControlsPressTime, virtualControlsReleaseTime } = getState().players[playerName];
if (virtualControlsPressTime > virtualControlsReleaseTime) return;
setState({
players: { [playerName]: { canShowVirtualButtons: false } },
});
}, 5000); // wait 5 seconds
},
check: { type: "players", prop: ["virtualControlsReleaseTime"] },
step: "input",
atStepEnd: true,
}),
// Jumping
onEachFrame: itemEffect({
run({
newValue: newElapsedTime,
prevValue: prevElapsedTime,
// itemState: playerState,
// itemRefs: playerRefs,
// frameDuration: timeDuration2,
}) {
const timeDuration = newElapsedTime - prevElapsedTime;
const { placeInfoByName } = meta.assets!;
const globalRefs = getRefs().global.main;
// console.log(parseInt(frameDuration));
// return false;
// NOTE should be a dynamic rule for each player listening to frame
const { playerCharacter, playerMovingPaused, gravityValue, nowPlaceName } = getState().global.main;
const { timerSpeed } = globalRefs;
const { dollRefs, dollState, dollName } = getCharDollStuff(playerCharacter as CharacterName) ?? {};
const dollPosRefs = dollRefs.positionMoverRefs;
const { isJumping, isOnGround, inputVelocity } = getState().players.main;
// if (!dollRefs.canCollide) return;
// const { scene } = globalRefs;
const { meshRef } = dollRefs;
const scene = getScene();
const activeCamera = scene?.activeCamera;
const placeInfo = placeInfoByName[nowPlaceName];
const floorNames = placeInfo.floorNames as readonly string[];
const wallNames = placeInfo.wallNames as readonly string[];
if (!dollRefs || !dollState || !dollName || !activeCamera || !meshRef || !scene) return;
// console.log("player move mode", dollState.positionMoveMode);
let newIsOnGround = isOnGround;
let currentYRotation = dollState.rotationYGoal;
let newAnimationName = dollState.nowAnimation;
let newIsMoving = dollState.positionIsMoving;
let nowWalkSpeed = dollState.nowWalkSpeed;
let newPositionMoveMode = dollState.positionMoveMode;
let newIsJumping = isJumping;
const {
// angle: newInputAngle,
speed: dollSpeed,
} = getSpeedAndAngleFromVector({ x: meshRef?.velocity?.x ?? 0, y: meshRef?.velocity?.z ?? 0 });
// dollPosRefs.velocity.y -=
// (gravityValue * frameDuration) / 160;
// const isGoinDownOrStill = dollPosRefs.velocity.y < 0;
// const isGoinDownOrStill =
// dollPosRefs.velocity.y < 100000000;
const isGoingDownOrStill = dollPosRefs.velocity.y <= 0;
// console.log("dollPosRefs.velocity.y", dollPosRefs.velocity.y);
// if (isGoinDownOrStill ) {
let slopeUnderPlayer = 0;
let slope = 0;
let isAboveDownSlope = false;
let isAboveUpSlope = false;
let isAboveASlope = false;
if (isGoingDownOrStill) {
downRayHelper.attachToMesh(
/*mesh*/ meshRef,
/*direction*/ downRayDirection,
/*relativeOrigin*/ downRayRelativeOrigin, // used to be (0, -1, 0), when the character model origins were higher,but now the character orig should be at the bottom
// /*length*/ 2 // 0.25 meant the bird in eggventure couldn't climb the ~45degree pan, 0.3 meant the player couldn't climb the cave in rodont
/*length*/ 10 // 0.25 meant the bird in eggventure couldn't climb the ~45degree pan, 0.3 meant the player couldn't climb the cave in rodont
);
// For stacked floors, I think it's picking the bottom floor, which is bad
const centerPick = scene.pickWithRay(
downRay,
(mesh) => {
return floorNames.includes(mesh.name) || wallNames.includes(mesh.name);
},
false // if true, then it can pick the bottom of overlapping floors, which is bad
);
if (centerPick) {
let distance = 1000000;
if (centerPick.pickedPoint && meshRef.position) {
const pickedPointY = centerPick.pickedPoint?.y;
const meshYPosition = meshRef.position.y;
distance = Math.abs(pickedPointY - meshYPosition);
}
const isWalking = Math.abs(dollPosRefs.velocity.x) > 0.1 || Math.abs(dollPosRefs.velocity.z) > 0.1;
if (isWalking) {
const RAY_FORWARD_DIST = 0.25;
frontRayHelper.attachToMesh(/*mesh*/ meshRef, frontRayDirection, frontRayRelativeOrigin, /*length*/ 10);
const frontPick = scene.pickWithRay(
frontRay,
(mesh) => floorNames.includes(mesh.name) || wallNames.includes(mesh.name),
true
);
if (frontPick?.hit) {
const heightDiff = (frontPick?.pickedPoint?.y || 0) - (centerPick?.pickedPoint?.y || 0);
// In degrees, negative is down
slopeUnderPlayer = getVectorAngle({
x: RAY_FORWARD_DIST,
y: heightDiff,
});
}
}
slope = slopeUnderPlayer;
// maybe put in prendy assets options
const SLOPE_LIMIT = 45;
isAboveDownSlope = slope < -1 && slope > -SLOPE_LIMIT;
isAboveUpSlope = slope > 1 && slope < SLOPE_LIMIT;
isAboveASlope = isAboveDownSlope || isAboveUpSlope;
newIsOnGround = distance < 0.1; // 0.21
}
}
const safeSlopeDivider = Math.max(Math.abs(slope) * 0.7, 1);
const slopeFallSpeed = (1 / safeSlopeDivider) * timeDuration;
if (isAboveDownSlope && newIsOnGround) {
dollPosRefs.velocity.y = -slopeFallSpeed * nowWalkSpeed; // need to multiply by player walk speed
}
if (newIsOnGround) {
if (!isAboveDownSlope) {
dollPosRefs.velocity.y = Math.max(0, dollPosRefs.velocity.y);
}
if (!isAboveUpSlope) {
// this stops the y velocity from being kept after doing a springDollToSpot
dollPosRefs.velocity.y = Math.min(0, dollPosRefs.velocity.y);
// before it would keep a small y up velocity, but still record isOnGround as true
// may be a better way to cleat the y velocity after a springDollToSpot ends
// wait this might break walking up slopes
// it happened in hug-report when hugging the statue
// maybe its also combined with disabling movement
}
} else {
// is falling
dollPosRefs.velocity.y -= (gravityValue / 160) * timeDuration;
}
if (dollPosRefs.velocity.y !== 0) newIsMoving = true;
// console.log(
// "newIsOnGround",
// newIsOnGround,
// "isAboveDownSlope",
// isAboveDownSlope,
// "isAboveUpSlope",
// isAboveUpSlope
// );
// if (!playerMovingPaused) newPositionMoveMode = "push";
newPositionMoveMode = "push";
setState({
players: {
main: {
isOnGround: newIsOnGround,
positionMoveMode: newPositionMoveMode,
positionIsMoving: newIsMoving,
},
},
});
},
check: { type: "global", prop: "elapsedGameTime" },
step: "input",
atStepEnd: true,
}),
whenIsOnGroundChanges: itemEffect({
run({ newValue: isOnGround, prevValue: prevIsOnGround, itemState: playerState, itemRefs: playerRefs }) {
clearTimeoutSafe(playerRefs.canJumpTimeout);
const { isJumping } = playerState;
const justLeftTheGround = prevIsOnGround && !isOnGround;
const justHitTheGround = !prevIsOnGround && isOnGround;
if (justHitTheGround) {
setState({ players: { main: { canJump: true, isJumping: false } } });
}
if (justLeftTheGround) {
if (isJumping) {
setState({ players: { main: { canJump: false } } });
} else {
// allow jumping for a short time after leaving the ground
playerRefs.canJumpTimeout = setTimeout(() => {
setState({ players: { main: { canJump: false } } });
}, LEAVE_GROUND_CANT_JUMP_DELAY);
}
}
},
check: { type: "players", prop: "isOnGround" },
step: "positionReaction",
atStepEnd: false,
}),
whenAnimationNamesChange: itemEffect({
run({ newValue: newAnimationNames, itemState: playerState }) {
const { playerCharacter, playerMovingPaused } = getState().global.main;
const { inputVelocity } = playerState;
const { dollName } = getCharDollStuff(playerCharacter as CharacterName) ?? {};
if (!dollName) return;
let newAnimationName = newAnimationNames.idle;
if (!pointIsZero(inputVelocity) && !playerMovingPaused) {
newAnimationName = newAnimationNames.walking;
}
setState({
dolls: { [dollName]: { nowAnimation: newAnimationName } },
});
},
step: "input",
check: { type: "players", prop: "animationNames" },
atStepEnd: true,
}),
whenCameraChanges: itemEffect({
run() {
setState((state) => ({
players: {
main: {
lastSafeInputAngle: getSpeedAndAngleFromVector(state.players.main.inputVelocity).angle,
},
},
}));
},
step: "cameraChange",
check: { type: "global", prop: "nowCamName" },
}),
whenPlayerMovementPausedChanges: itemEffect({
run({ newValue: playerMovingPaused }) {
const { playerCharacter } = getState().global.main;
const playerState = getState().players.main;
const { dollRefs, dollName, dollState } = getCharDollStuff(playerCharacter as CharacterName);
let newAnimationName = dollState.nowAnimation;
if (playerMovingPaused) {
if (newAnimationName === playerState.animationNames.walking) {
newAnimationName = playerState.animationNames.idle;
}
dollRefs.positionMoverRefs.velocity.x = 0;
dollRefs.positionMoverRefs.velocity.y = 0;
dollRefs.positionMoverRefs.velocity.z = 0;
setState({
dolls: {
[dollName]: {
nowAnimation: newAnimationName,
// positionMoveMode: "push",
positionIsMoving: false,
},
},
players: {
main: {
// lastSafeInputAngle: null,
inputVelocity: defaultPosition(),
},
},
});
}
// setState((state) => ({
// players: {
// main: {
// lastSafeInputAngle: getSpeedAndAngleFromVector(
// state.players.main.inputVelocity
// ).angle,
// },
// },
// }));
},
// step: "default",
check: { type: "global", prop: "playerMovingPaused" },
step: "storyReaction", // runs at storyReaction flow so it can react after story rules setting playerMovingPaused
// atStepEnd: true,
}),
}));