@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
253 lines (201 loc) • 7.89 kB
JavaScript
import { SerializationMetadata } from "../ecs/components/SerializationMetadata.js";
import Timer from "../ecs/components/Timer.js";
import Entity from "../ecs/Entity.js";
import { Transform } from "../ecs/transform/Transform.js";
import { whenAllEntitiesDestroyed, whenEntityDestroyed } from "../ecs/util/EntityBuilderUtils.js";
import { removeComponentsExcept } from "../ecs/util/removeComponentsExcept.js";
import { createSound, createTimer } from "../EntityCreator.js";
import Mesh from "../graphics/ecs/mesh/Mesh.js";
import Trail2D from "../graphics/ecs/trail2d/Trail2D.js";
import { Trail2DFlags } from "../graphics/ecs/trail2d/Trail2DFlags.js";
import { ParticleEmitter } from "../graphics/particles/particular/engine/emitter/ParticleEmitter.js";
import { ParticleEmitterFlag } from "../graphics/particles/particular/engine/emitter/ParticleEmitterFlag.js";
import { SequenceBehavior } from "../intelligence/behavior/composite/SequenceBehavior.js";
import { BehaviorComponent } from "../intelligence/behavior/ecs/BehaviorComponent.js";
import { ActionBehavior } from "../intelligence/behavior/primitive/ActionBehavior.js";
import { DelayBehavior } from "../intelligence/behavior/util/DelayBehavior.js";
import { stopEntityAndNotifyWhenStopped } from "./AnimatedActions.js";
import AnimationTrack from "./keyed2/AnimationTrack.js";
import AnimationTrackPlayback from "./keyed2/AnimationTrackPlayback.js";
import { playAnimationTrack } from "./playAnimationTrack.js";
import TransitionFunctions from "./TransitionFunctions.js";
/**
*
* @param {number} entity
* @param {EntityComponentDataset} ecd
* @returns {Promise}
*/
export function stopTrailAndNotifyOnceFinished(entity, ecd) {
return new Promise((resolve, reject) => {
if (!ecd.entityExists(entity)) {
resolve();
return;
}
const trail = ecd.getComponent(entity, Trail2D);
if (trail === undefined) {
resolve();
return;
}
trail.clearFlag(Trail2DFlags.Spawning);
new Entity()
.add(BehaviorComponent.from(SequenceBehavior.from([
DelayBehavior.fromJSON({ value: trail.maxAge }),
new ActionBehavior(resolve)
])))
.build(ecd);
});
}
/**
*
* @param {number} entity
* @param {EntityComponentDataset} ecd
* @returns {Promise}
*/
export function stopEmitterAndNotifyOnceFinished(entity, ecd) {
return new Promise(function (resolve, reject) {
if (!ecd.entityExists(entity)) {
console.warn(`Entity ${entity} doesn't exist`);
resolve();
return;
}
/**
*
* @type {ParticleEmitter}
*/
const emitter = ecd.getComponent(entity, ParticleEmitter);
if (emitter === undefined) {
console.warn(`Entity ${entity} doesn't have ParticleEmitter component`);
resolve();
return;
}
//stop emission
emitter.clearFlag(ParticleEmitterFlag.Emitting);
//figure out how long the emitter should stay alive
const maxLife = emitter.computeMaxEmittingParticleLife();
const entityBuilder = new Entity();
//create a timer to remove emitter
const timer = new Timer();
timer.timeout = maxLife;
timer.actions.push(function () {
//confirm that entity still exists
if (!ecd.entityExists(entity)) {
//nothing to do
return;
}
//confirm that it's the same entity
const component = ecd.getComponent(entity, ParticleEmitter);
if (component !== emitter) {
//entity seems to have changed, do nothing
return;
}
}, function () {
//kill self
entityBuilder.destroy();
resolve();
});
entityBuilder.add(timer);
entityBuilder.build(ecd);
});
}
/**
*
* @param {number} entity
* @param {EntityComponentDataset} ecd
* @returns {Promise}
*/
export function shutdownParticleEmitter(entity, ecd) {
const promise = stopEmitterAndNotifyOnceFinished(entity, ecd);
const MAX_PARTICLE_LIFE = 10;
/**
*
* @type {ParticleEmitter}
*/
const particleEmitter = ecd.getComponent(entity, ParticleEmitter);
if (particleEmitter !== undefined) {
//destroy any particles that have very long lifespan
particleEmitter.traverseLayers((layer) => {
const maxParticleLife = layer.particleLife.max;
if (layer.emissionRate <= 0 && layer.emissionImmediate <= 0) {
//skip layers with no emission
return;
}
if (maxParticleLife > MAX_PARTICLE_LIFE) {
particleEmitter.destroyParticlesFromLayer(layer);
}
});
}
return promise;
}
/**
*
* @param entity
* @param {EntityComponentDataset} ecd
* @param {ParticleEmitter} emitter
* @param {String} soundEffect
* @param {number} [timeout]
* @returns {Promise}
*/
export function removeEntityWithEffect({ entity, ecd, emitter, soundEffect, timeout }) {
/**
*
* @type {Transform}
*/
const transform = ecd.getComponent(entity, Transform);
//prevent further interactions
removeComponentsExcept(ecd, entity, [Mesh, ParticleEmitter, Trail2D, Transform]);
//make entity volatile so it does not end up in game saves
ecd.addComponentToEntity(entity, SerializationMetadata.Transient);
return new Promise((resolve, reject) => {
const entityStopped = stopEntityAndNotifyWhenStopped(entity, ecd);
const animationTrack = new AnimationTrack(['scale']);
animationTrack.addKey(0, [1]);
animationTrack.addKey(0.05, [1]);
animationTrack.addKey(0.27, [0]);
animationTrack.addTransition(0, TransitionFunctions.EaseOut);
animationTrack.addTransition(1, TransitionFunctions.EaseOut);
const originalScale = transform.scale.clone();
const trackPlayback = new AnimationTrackPlayback(animationTrack, function (s) {
transform.scale.copy(originalScale.clone().multiplyScalar(s));
}, null);
trackPlayback.on.ended.add(() => {
//remove mesh if it exists since scale is 0
ecd.removeComponentFromEntity(entity, Mesh);
});
const eAnimation = playAnimationTrack(trackPlayback, ecd);
const entityRemoved = Promise.all([
whenEntityDestroyed(eAnimation),
entityStopped
])
.then(() => {
//check that the entity exists
if (ecd.entityExists(entity)) {
ecd.removeEntity(entity);
}
});
const t = new Transform();
t.copy(transform);
const emitterBuilder = new Entity();
emitterBuilder.add(t).add(emitter);
emitterBuilder.build(ecd);
const timer = createTimer({
timeout: emitter.layers.reduce((s, l) => {
return Math.max(s, l.particleLife.max)
}, 0),
action() {
emitterBuilder.destroy()
}
});
const sound = createSound({
position: transform.position,
url: soundEffect,
positioned: true
});
Promise.all([
entityRemoved,
whenAllEntitiesDestroyed([timer, sound])
])
.then(resolve, reject);
timer.build(ecd);
sound.build(ecd);
});
}