forked from sent/waves
553 lines
19 KiB
JavaScript
553 lines
19 KiB
JavaScript
/**
|
|
* @file ghost.class.js
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
import AudioSystem from '../framework/audiosystem.class.js';
|
|
import Entity from '../framework/entity.class.js';
|
|
import ModelManager from '../framework/modelmanager.class.js';
|
|
import Animator from '../framework/three.js/animator.class.js';
|
|
|
|
import Utility from '../game/utility.class.js';
|
|
|
|
import * as THREE from '../lib/three.js/three.module.js';
|
|
|
|
export default class Ghost extends Entity {
|
|
static _init() {
|
|
Ghost._tmpV20 = new THREE.Vector2();
|
|
}
|
|
|
|
constructor(context, scene, options = Object.create(null)) {
|
|
options.updatePriority = 5;
|
|
|
|
super(context, scene, options);
|
|
|
|
const tScene = scene.getThreeScene();
|
|
|
|
const gltf = ModelManager.getModel('characters');
|
|
|
|
const animations = Animator.filterAnimations(gltf.animations, [
|
|
'GhostReveal', 'GhostWalkNormal', 'GhostWalkCreepy', 'GhostWalkHands', 'GhostBiking',
|
|
'GhostStrangle2', 'GhostStrangle1', 'GhostCrawlStart', 'GhostCrawl', 'GhostCrawlEat', 'GhostBehind', 'GhostTrap', 'GhostJump',
|
|
], null, null, true, false, true);
|
|
|
|
this._rigGhost = gltf.scene.getObjectByName('Ghost');
|
|
|
|
this._head = this._rigGhost.getObjectByName('Head');
|
|
|
|
this._animator = new Animator(this._rigGhost, animations);
|
|
|
|
this._action = null;
|
|
|
|
this._group = new THREE.Group();
|
|
this._group.name = 'Ghost';
|
|
this._group.add(this._rigGhost);
|
|
this._group.add(gltf.scene.getObjectByName('BikeStatic'));
|
|
tScene.add(this._group);
|
|
|
|
this._group.traverse(obj => {
|
|
obj.matrixAutoUpdate = true;
|
|
obj.frustumCulled = false;
|
|
|
|
obj.material = Utility.replaceMaterial(obj.material);
|
|
});
|
|
|
|
this._player = scene.findEntityOfType('Player');
|
|
this._road = scene.findEntityOfType('Road');
|
|
this._train = scene.findEntityOfType('Train');
|
|
this._overlay = scene.findEntityOfType('Overlay');
|
|
|
|
this._sceneName = null;
|
|
this._sceneSeconds = null;
|
|
|
|
this._stopping = null;
|
|
this._huntIndex = null;
|
|
this._boostSeconds = null;
|
|
this._walkAngle = null;
|
|
this._soundPlayed = null;
|
|
|
|
this._roadPosition = new THREE.Vector2();
|
|
|
|
this._lastActionTime = null;
|
|
}
|
|
|
|
reset(context) {
|
|
this._animator.stopActions();
|
|
|
|
this._sceneName = null;
|
|
|
|
this._stopping = false;
|
|
this._boostSeconds = 2;
|
|
this._walkAngle = Math.PI;
|
|
this._soundPlayed = false;
|
|
|
|
this._lastActionTime = null;
|
|
|
|
this._group.visible = false;
|
|
|
|
this._rigGhost.getObjectByName('GhostNormalMesh').visible = false;
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = false;
|
|
this._rigGhost.getObjectByName('GhostAngryMesh').visible = false;
|
|
this._rigGhost.getObjectByName('GhostCyclistMesh').visible = false;
|
|
|
|
this._group.getObjectByName('BikeStatic').visible = false;
|
|
}
|
|
|
|
update(context) {
|
|
super.update(context);
|
|
|
|
if (this._sceneName === null) {
|
|
return;
|
|
}
|
|
|
|
const dt = context.time.elapsedSeconds;
|
|
|
|
const lastSceneSeconds = this._sceneSeconds;
|
|
this._sceneSeconds += dt;
|
|
|
|
const distanceMin = this._road.getRoadDistanceMin();
|
|
const distanceMax = this._road.getRoadDistanceMax();
|
|
const playerRoadPosition = this._road.getPlayerRoadPosition();
|
|
const playerBiking = this._player.getBiking();
|
|
const pdx = this._roadPosition.x - playerRoadPosition.x;
|
|
const pdd = this._road.getRoadDistanceBetween(playerRoadPosition.y, this._roadPosition.y);
|
|
|
|
let moving = false;
|
|
let shakeHead = 0;
|
|
let t;
|
|
|
|
switch (this._sceneName) {
|
|
case 'walk_away':
|
|
moving = true;
|
|
|
|
if (pdd < 14) {
|
|
this._action.timeScale = 1.5;
|
|
|
|
this._player.slowDown(1);
|
|
|
|
if (!this._soundPlayed) {
|
|
this._soundPlayed = true;
|
|
|
|
AudioSystem.play('effect__ghost_laugh');
|
|
}
|
|
}
|
|
|
|
if (this._action.timeScale > 0) {
|
|
this._roadPosition.y += 0.07 * dt;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'reveal':
|
|
moving = true;
|
|
|
|
this._roadPosition.x = playerRoadPosition.x;
|
|
|
|
if (this._sceneSeconds > 8 && this._sceneSeconds < 22) {
|
|
this._roadPosition.y += 0.02 * dt;
|
|
}
|
|
|
|
if (this._sceneSeconds > 4 && !this._soundPlayed) {
|
|
this._soundPlayed = true;
|
|
|
|
AudioSystem.play('effect__ghost_laugh');
|
|
}
|
|
|
|
if (this._sceneSeconds >= 20 && lastSceneSeconds < 20) {
|
|
AudioSystem.play('effect__ghost_reveal', 1, false, null, 3.8);
|
|
}
|
|
|
|
if (this._sceneSeconds >= 18.7 && lastSceneSeconds < 18.7) {
|
|
this._rigGhost.getObjectByName('GhostNormalMesh').visible = false;
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = true;
|
|
|
|
this._train.makeBloody();
|
|
|
|
this._animator.playAction('GhostJump', 1, 0, 1);
|
|
|
|
this._roadPosition.y += 0.19;
|
|
}
|
|
|
|
if (this._sceneSeconds > 19.5 && this._sceneSeconds < 21.5) {
|
|
this._player.shakeCamera(0.5, 0.01);
|
|
}
|
|
|
|
if (this._sceneSeconds >= 22 && lastSceneSeconds < 22) {
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = false;
|
|
}
|
|
|
|
if (this._sceneSeconds >= 25 && lastSceneSeconds < 25) {
|
|
this._train.stop();
|
|
}
|
|
|
|
if (this._sceneSeconds >= 26 && lastSceneSeconds < 26) {
|
|
this.playScene(context, 'hunting_start');
|
|
|
|
this._roadPosition.set(playerRoadPosition.x, playerRoadPosition.y + 1.15);
|
|
}
|
|
|
|
break;
|
|
|
|
case 'hunting':
|
|
const angleToPlayer = Math.atan2(pdx, pdd) + this._stopping * Math.PI;
|
|
const closestAngleToPlayer = Utility.getClosestAngle(this._walkAngle, angleToPlayer);
|
|
const distanceToPlayer = Math.sqrt(pdx * pdx + pdd * pdd);
|
|
const playerColliding = playerBiking && distanceToPlayer < 0.55;
|
|
const walking = this._huntIndex % 4 !== 3;
|
|
|
|
this._walkAngle += closestAngleToPlayer * (1 - 0.1 ** dt);
|
|
|
|
if (walking) {
|
|
this._boostSeconds -= dt;
|
|
|
|
if (this._boostSeconds < -0.2) {
|
|
this._boostSeconds = Math.random() * 3;
|
|
}
|
|
|
|
this._action.timeScale = this._boostSeconds < 0 ? 8 : 1;
|
|
shakeHead = this._boostSeconds < 0 ? 0.5 : 0;
|
|
}
|
|
|
|
if (playerColliding && !this._stopping) {
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = false;
|
|
this._rigGhost.getObjectByName('GhostAngryMesh').visible = true;
|
|
|
|
this._animator.fadeOutActions(0.3);
|
|
this._animator.playAction(this._player.getHurt() ? 'GhostStrangle2' : 'GhostStrangle1', 1, 0.1, 1, () => {
|
|
this.playScene(context, 'hunting');
|
|
});
|
|
|
|
this._player.playScene(context, 'strangled');
|
|
|
|
const playerGroup = this._player.getGroup();
|
|
|
|
this._group.position.copy(playerGroup.position);
|
|
this._group.rotation.copy(playerGroup.rotation);
|
|
|
|
this._sceneName = 'strangling';
|
|
this._sceneSeconds = 0;
|
|
} else {
|
|
moving = true;
|
|
|
|
if (pdd < -8) {
|
|
if (this._stopping) {
|
|
this.reset(context);
|
|
} else {
|
|
this.playScene(context, 'hunting');
|
|
}
|
|
} else if (this._sceneSeconds > 15 && pdd > 8) {
|
|
this._huntIndex = 2;
|
|
|
|
if (this._stopping) {
|
|
this.reset(context);
|
|
} else {
|
|
this.playScene(context, 'hunting');
|
|
}
|
|
|
|
this._roadPosition.y = playerRoadPosition.y + 0.7;
|
|
}
|
|
}
|
|
|
|
const velocity = !playerBiking ? 0.1 : !walking ? 1.5 : 0.5 * this._action.timeScale;
|
|
const vx = Math.sin(-this._walkAngle) * velocity * dt;
|
|
const vy = -Math.cos(-this._walkAngle) * velocity * dt / 13;
|
|
|
|
this._roadPosition.set(this._roadPosition.x + vx,
|
|
THREE.MathUtils.clamp(this._roadPosition.y + vy, distanceMin, distanceMax));
|
|
|
|
break;
|
|
|
|
case 'strangling':
|
|
shakeHead = 0.25;
|
|
|
|
break;
|
|
|
|
case 'chase':
|
|
moving = true;
|
|
shakeHead = 0.1;
|
|
|
|
break;
|
|
|
|
case 'chase_started':
|
|
moving = true;
|
|
shakeHead = 0.1;
|
|
|
|
this._action.timeScale = this._stopping ? this._action.timeScale * 0.95
|
|
: Math.min(this._sceneSeconds / 2, 3, Math.max(0.2, -pdd / 9));
|
|
|
|
this._roadPosition.y = Math.max(distanceMin, this._roadPosition.y + dt * this._action.timeScale * 0.15);
|
|
|
|
if (this._lastActionTime !== null && (
|
|
this._lastActionTime < 1.05 && this._action.time >= 1.05 ||
|
|
this._lastActionTime < 2.35 && this._action.time >= 2.35)) {
|
|
AudioSystem.play('effect__giant_step');
|
|
|
|
this._player.shakeCamera(0.2);
|
|
}
|
|
|
|
this._lastActionTime = this._action.time;
|
|
|
|
if (playerBiking && pdd > -10) {
|
|
this._animator.fadeOutActions(0.5);
|
|
this._animator.playAction('GhostCrawlEat', 1, 0.5, 0.8);
|
|
|
|
this._player.playScene(context, 'eaten_by_ghost');
|
|
|
|
const playerGroup = this._player.getGroup();
|
|
|
|
this._road.roadToWorldPosition(Ghost._tmpV20.set(
|
|
0, this._roadPosition.y + 0.65), playerGroup.position);
|
|
|
|
this._sceneName = 'chase_ended';
|
|
this._sceneSeconds = 0;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'chase_ended':
|
|
shakeHead = 0.2;
|
|
|
|
break;
|
|
|
|
case 'biking':
|
|
this._roadPosition.y += dt * (1 - Math.min(0.6, Math.max(0, pdd / 5 + 0.3)));
|
|
|
|
if (playerRoadPosition.x < 0 || Math.abs(pdd) > 3) {
|
|
this._roadPosition.x = Math.min(2, this._roadPosition.x + dt * 2);
|
|
} else {
|
|
this._roadPosition.x = Math.max(-2, this._roadPosition.x - dt * 2);
|
|
}
|
|
|
|
if (this._roadPosition.y >= distanceMax) {
|
|
this.reset(context);
|
|
} else {
|
|
moving = true;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'behind_player':
|
|
if (!playerBiking) {
|
|
this.reset(context);
|
|
} else {
|
|
const playerGroup = this._player.getGroup();
|
|
|
|
if (this._action.time < 1) {
|
|
this._group.position.copy(playerGroup.position);
|
|
this._group.rotation.copy(playerGroup.rotation);
|
|
}
|
|
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = this._action.time <= 0.2;
|
|
this._rigGhost.getObjectByName('GhostAngryMesh').visible = this._action.time > 0.2;
|
|
|
|
if (this._action.timeScale === 0) {
|
|
if (this._player.getLookRotation().x > 1.45) {
|
|
AudioSystem.play('effect__ghost_catch');
|
|
AudioSystem.play('effect__stinger_2');
|
|
|
|
this._action.timeScale = 0.7;
|
|
} else if (this._player.getLookRotation().x < 0) {
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (moving) {
|
|
this._road.roadToWorldPosition(this._roadPosition, this._group.position);
|
|
this._group.rotation.set(0, -this._walkAngle
|
|
- this._road.getRoadAngle(this._roadPosition.y) + Math.PI, 0);
|
|
}
|
|
|
|
this._animator.update(dt);
|
|
|
|
if (shakeHead) {
|
|
this._head.rotation.x += (Math.random() * 1 - 0.5) * shakeHead;
|
|
this._head.rotation.y += (Math.random() * 1 - 0.5) * shakeHead;
|
|
this._head.rotation.z += (Math.random() * 1 - 0.5) * shakeHead;
|
|
}
|
|
}
|
|
|
|
playScene(context, sceneName, segment = null) {
|
|
if (sceneName === 'hunting_start' && this._sceneName === 'hunting') {
|
|
return;
|
|
}
|
|
|
|
this.reset(context);
|
|
|
|
const playerRoadPosition = this._road.getPlayerRoadPosition();
|
|
|
|
this._sceneName = sceneName;
|
|
this._sceneSeconds = 0;
|
|
|
|
this._group.visible = true;
|
|
this._group.scale.set(1, 1, 1);
|
|
|
|
switch (sceneName) {
|
|
case 'walk_away':
|
|
this._rigGhost.getObjectByName('GhostNormalMesh').visible = true;
|
|
|
|
this._action = this._animator.playAction('GhostWalkNormal', 2, 0, 0, () => {
|
|
this.reset(context);
|
|
});
|
|
|
|
this._roadPosition.set(1, segment.index + 0.6);
|
|
|
|
break;
|
|
|
|
case 'reveal':
|
|
const secondsUntilLoop = this._train.getSecondsUntilLoop();
|
|
|
|
this._player.pause(10);
|
|
|
|
this._sceneSeconds = -secondsUntilLoop;
|
|
|
|
context.time.setTimeout(() => {
|
|
this._overlay.playScene(context, 'letterbox', 27);
|
|
|
|
this._player.lookAt(this._group, 27);
|
|
|
|
this._roadPosition.set(0, playerRoadPosition.y - 0.6);
|
|
|
|
this._animator.playAction('GhostReveal', 1, 0, 0.5, () => {
|
|
this._animator.fadeOutActions(0.5);
|
|
|
|
this._animator.playAction('GhostWalkNormal', Infinity, 0.5, 1);
|
|
});
|
|
|
|
this._rigGhost.getObjectByName('GhostNormalMesh').visible = true;
|
|
}, (secondsUntilLoop + 3.5) * 1000);
|
|
|
|
break;
|
|
|
|
case 'hunting_start':
|
|
this._overlay.setContrast(0.6, 0.5, 0.5, 20);
|
|
|
|
AudioSystem.stop('ambiance', 0.5);
|
|
AudioSystem.play('ambiance__ghost_hunt', 1, true, 'ambiance');
|
|
|
|
case 'hunting':
|
|
this._huntIndex = this._huntIndex === null ? 0 : this._huntIndex + 1;
|
|
|
|
if (this._huntIndex % 4 === 3) {
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = true;
|
|
|
|
this._animator.playAction('GhostWalkHands', Infinity, 0, 5);
|
|
} else {
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = true;
|
|
|
|
this._action = this._animator.playAction('GhostWalkCreepy', Infinity);
|
|
}
|
|
|
|
this._roadPosition.set(
|
|
Math.random() < 0.4 ? playerRoadPosition.x : -4 + Math.random() * 8,
|
|
playerRoadPosition.y + 1 + Math.random() * 7);
|
|
|
|
this._sceneName = 'hunting';
|
|
|
|
break;
|
|
|
|
case 'chase':
|
|
this._rigGhost.getObjectByName('GhostAngryMesh').visible = true;
|
|
|
|
this._animator.playAction('GhostCrawlStart', 1, 0, 0.8, () => {
|
|
this._animator.fadeOutActions(1);
|
|
this._action = this._animator.playAction('GhostCrawl', Infinity, 0.5);
|
|
this._action.time = 1.8;
|
|
|
|
this._sceneName = 'chase_started';
|
|
this._sceneSeconds = 0;
|
|
});
|
|
|
|
this._roadPosition.set(0, segment.index - 0.3);
|
|
|
|
this._group.scale.set(12, 12, 12);
|
|
|
|
this._player.pause(3);
|
|
|
|
context.time.setTimeout(() => {
|
|
this._player.lookAt(this._group, 10);
|
|
|
|
this._overlay.playScene(context, 'letterbox', 10);
|
|
}, 2000);
|
|
|
|
[0, 1, 2, 3].forEach(seconds => {
|
|
context.time.setTimeout(() => {
|
|
AudioSystem.play('effect__giant_step');
|
|
|
|
this._player.shakeCamera(0.3);
|
|
}, seconds * 1000);
|
|
});
|
|
|
|
break;
|
|
|
|
case 'biking':
|
|
this._rigGhost.getObjectByName('GhostAngryMesh').visible = true;
|
|
this._group.getObjectByName('BikeStatic').visible = true;
|
|
|
|
this._animator.playAction('GhostBiking', Infinity, 0, 1);
|
|
|
|
this._roadPosition.set(0, playerRoadPosition.y - 0.5);
|
|
|
|
context.time.setTimeout(() => {
|
|
AudioSystem.play('effect__ghost_reveal');
|
|
}, 500);
|
|
|
|
break;
|
|
|
|
case 'behind_player':
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = true;
|
|
|
|
this._action = this._animator.playAction('GhostBehind', 1, 0, 0, () => {
|
|
this.reset(context);
|
|
});
|
|
|
|
break;
|
|
|
|
case 'trap':
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = true;
|
|
|
|
this._animator.playAction('GhostTrap');
|
|
|
|
const throneSegment = this._road.getSegmentInstance('TuonelaThrone');
|
|
const chains = throneSegment.mesh.getObjectByName('Chains');
|
|
|
|
chains.getWorldPosition(this._group.position);
|
|
chains.getWorldQuaternion(this._group.quaternion);
|
|
|
|
context.time.setTimeout(() => {
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = false;
|
|
this._rigGhost.getObjectByName('GhostAngryMesh').visible = true;
|
|
}, 6000);
|
|
|
|
context.time.setTimeout(() => {
|
|
this._rigGhost.getObjectByName('GhostAngryMesh').visible = false;
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = true;
|
|
}, 14200);
|
|
|
|
context.time.setTimeout(() => {
|
|
this._rigGhost.getObjectByName('GhostHappyMesh').visible = false;
|
|
this._rigGhost.getObjectByName('GhostCyclistMesh').visible = true;
|
|
|
|
this._overlay.playScene(context, 'fade_in_white', 1);
|
|
|
|
this._player.shakeCamera(0.1);
|
|
}, 18000);
|
|
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Ghost scene "${sceneName}" does not exist`);
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
this._stopping = true;
|
|
}
|
|
}
|
|
|
|
Ghost._init();
|
|
Ghost.p_register();
|