UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

253 lines (201 loc) • 7.89 kB
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); }); }