UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

436 lines (333 loc) • 12.3 kB
import { assert } from "../../core/assert.js"; import Vector2 from "../../core/geom/Vector2.js"; import { makeCubicCurve } from "../../core/math/spline/makeCubicCurve.js"; import ObservedBoolean from "../../core/model/ObservedBoolean.js"; import { ReactiveAnd } from "../../core/model/reactive/model/logic/ReactiveAnd.js"; import { ReactiveReference } from "../../core/model/reactive/model/terminal/ReactiveReference.js"; import { AchievementNotificationView } from "../../view/game/achievements/AchievementNotificationView.js"; import AnimationTrack from "../animation/keyed2/AnimationTrack.js"; import AnimationTrackPlayback from "../animation/keyed2/AnimationTrackPlayback.js"; import { AnimationBehavior } from "../animation/keyed2/behavior/AnimationBehavior.js"; import TransitionFunctions from "../animation/TransitionFunctions.js"; import { GameAssetType } from "../asset/GameAssetType.js"; import { SerializationMetadata } from "../ecs/components/SerializationMetadata.js"; import Entity from "../ecs/Entity.js"; import GUIElement from "../ecs/gui/GUIElement.js"; import ViewportPosition from "../ecs/gui/position/ViewportPosition.js"; import { Transform } from "../ecs/transform/Transform.js"; import { SequenceBehavior } from "../intelligence/behavior/composite/SequenceBehavior.js"; import { BehaviorComponent } from "../intelligence/behavior/ecs/BehaviorComponent.js"; import { ClockChannelType } from "../intelligence/behavior/ecs/ClockChannelType.js"; import { DieBehavior } from "../intelligence/behavior/ecs/DieBehavior.js"; import { logger } from "../logging/GlobalLogger.js"; import { globalMetrics } from "../metrics/GlobalMetrics.js"; import { MetricsCategory } from "../metrics/MetricsCategory.js"; import { EnginePlugin } from "../plugin/EnginePlugin.js"; import { SoundEmitter } from "../sound/ecs/emitter/SoundEmitter.js"; import { SoundEmitterChannels } from "../sound/ecs/emitter/SoundEmitterSystem.js"; import { Achievement } from "./Achievement.js"; const SLOW_CUBIC = makeCubicCurve(0.04, 0.4, 0.9, 0.99); const OVERSHOT_CUBIC_0 = makeCubicCurve(0.04, 0.4, 1.8, 0.99); /** * * @param {String} id * @returns {string} */ function computeAchievementBlackboardFlag(id) { return `system.achievement.${id}.completed`; } export class AchievementManager extends EnginePlugin { constructor() { super(); this.id = 'achievements'; /** * * @type {AchievementGateway|null} */ this.gateway = null; /** * * @type {Achievement[]} */ this.entries = []; /** * * @type {AssetManager|null} */ this.assetManager = null; /** * * @type {Blackboard|null} */ this.blackboard = null; /** * * @type {EntityManager|null} */ this.entityManager = null; /** * * @type {Localization|null} */ this.localization = null; /** * @readonly * @type {ObservedBoolean} */ this.isStarted = new ObservedBoolean(false); /** * @readonly * @type {ObservedBoolean} */ this.isGatewayInitialized = new ObservedBoolean(false); /** * @readonly * @type {ObservedBoolean} */ this.isBlackboardAttached = new ObservedBoolean(false); /** * @readonly * @type {ReactiveAnd} */ this.isActive = ReactiveAnd.from( ReactiveReference.from(this.isStarted, 'started'), ReactiveReference.from(this.isBlackboardAttached, 'blackboardAttached') ); this.isActive.onChanged.add(v => { if (v) { this.activate(); } else { this.deactivate(); } }); this.handlers = {}; } /** * * @param {String} id * @return {Achievement|undefined} */ getAchievementById(id) { return this.entries.find(a => a.id === id); } /** * * @param {String} id */ unlock(id) { assert.isString(id, 'id'); const key = computeAchievementBlackboardFlag(id); const value = this.blackboard.acquireBoolean(key, false); value.set(true); //release value this.blackboard.release(key); this.gateway.unlock(id); const achievement = this.getAchievementById(id); if (achievement === undefined) { logger.warn(`Attempting to unlock achievement ${id}. Achievement not found`); } if (achievement !== undefined && this.isGatewayInitialized.getValue()) { this.deactivateEntry(achievement); this.present(achievement); } globalMetrics.record("achievement", { category: MetricsCategory.Progression, label: id }); } /** * @private * @param {Achievement} entry */ activateEntry(entry) { entry.trigger.link(this.blackboard); const handler = (v) => { if (v) { this.unlock(entry.id); } }; this.handlers[entry.id] = handler; entry.trigger.getExpression() .process(handler); } /** * @private * @param {Achievement} entry */ deactivateEntry(entry) { entry.trigger.unlink(); const handler = this.handlers[entry.id]; if (handler !== undefined) { entry.trigger.getExpression() .onChanged.remove(handler); } } /** * @private */ activate() { this.entries.forEach(a => { if (!a.enabled) { //ignore return; } this.activateEntry(a); }); } /** * @private */ deactivate() { this.entries.forEach(a => { this.deactivateEntry(a); }); } async initialize(engine) { this.assetManager = engine.assetManager; this.gateway = engine.platform.getAchievementGateway(); this.entityManager = engine.entityManager; this.localization = engine.localization; } /** * * @param {AssetManager} assetManager */ async loadDefinitions(assetManager) { const asset = await assetManager.promise("data/database/achievements/data.json", GameAssetType.JSON) const json = asset.create(); json.forEach(def => { const achievement = new Achievement(); achievement.fromJSON(def); this.entries.push(achievement); }); } /** * Visually present an achievement * @param {Achievement} achievement */ present(achievement) { const ecd = this.entityManager.dataset; if (ecd === null) { //no ECD, skip return; } const localization = this.localization; const achievementView = new AchievementNotificationView({ achievement, localization }); achievementView.size.x = 460; achievementView.size.y = 58; const viewportPosition = ViewportPosition.fromJSON({ position: new Vector2(0.5, 0) }); viewportPosition.anchor.set(0.5, 0.5); const aEntry = new AnimationTrack(['scale', 'alpha', 'v']); aEntry.addKey(0, [1.3, 0.2, 0]); aEntry.addKey(0.4, [1, 1, 0.8]); aEntry.addTransition(0, OVERSHOT_CUBIC_0); const aMain = new AnimationTrack(["value"]); aMain.addKey(0, [0]); aMain.addKey(8.3, [1]); aMain.addTransition(0, TransitionFunctions.Linear); const aExit = new AnimationTrack(['scale', 'alpha', 'v']); aExit.addKey(0, [1, 1, 0]); aExit.addKey(1, [1, 0, 1]); aExit.addTransition(0, SLOW_CUBIC); const builder = new Entity(); const sequenceBehavior = SequenceBehavior.from([ new AnimationBehavior(new AnimationTrackPlayback(aEntry, (scale, alpha, v) => { achievementView.scale.setScalar(scale); achievementView.css({ opacity: alpha }); })), new AnimationBehavior(new AnimationTrackPlayback(aMain, (value) => { })), new AnimationBehavior(new AnimationTrackPlayback(aExit, (scale, alpha, v) => { achievementView.scale.setScalar(scale); achievementView.css({ opacity: alpha }); })), DieBehavior.create() ]); const cBehavior = BehaviorComponent.from(sequenceBehavior); //use system clock for behavior cBehavior.clock = ClockChannelType.Simulation; const guiElement = GUIElement.fromView(achievementView); guiElement.group = "ui-managed-achievements"; const soundEmitter = SoundEmitter.fromJSON({ tracks: [ { url: "data/sounds/effects/Magic_Game_Essentials/Magic_Airy_Alert.wav", startWhenReady: true } ], isPositioned: false, volume: 1, loop: false, channel: SoundEmitterChannels.Effects }); //prevent achievement message from being serialized in game save builder .add(SerializationMetadata.Transient) .add(new Transform()) .add(soundEmitter) .add(cBehavior) .add(guiElement) .add(viewportPosition) .build(ecd); } /** * * @param {Blackboard} blackboard */ attachBlackboard(blackboard) { if (this.blackboard !== null) { throw new Error('Blackboard already attached'); } this.blackboard = blackboard; this.isBlackboardAttached.set(true); } async initializeGateway() { /** * * @type {string[]} */ const unlockedIds = await this.gateway.getUnlocked(); //de-activate unlocked achievements const unlockedAchievements = this.entries.filter(a => unlockedIds.includes(a.id)); unlockedAchievements.forEach(a => a.enabled = false); //try to read unlocked achievements from blackboard if (this.isBlackboardAttached.getValue()) { this.entries.forEach(achievement => { const id = achievement.id; const blackboardKey = computeAchievementBlackboardFlag(id); const blackboardValue = this.blackboard.acquireBoolean(blackboardKey, false); if (!unlockedIds.includes(id)) { // gateway doesn't have this achievement unlocked yet // check blackboard if (blackboardValue.getValue()) { // unlocked in the blackboard, communicate to the gateway this.gateway.unlock(id); //disable achievement tracking achievement.enabled = false; } } else { //not marker as unlocked on gateway this.deactivateEntry(achievement); } //release the value this.blackboard.release(blackboardKey); }); } this.isGatewayInitialized.set(true); } async startup() { assert.equal() //load achievement definitions await this.loadDefinitions(this.assetManager); this.initializeGateway(); this.isStarted.set(true); } async shutdown() { } }