UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

556 lines (427 loc) • 14.3 kB
import List from '../../core/collection/list/List.js'; import { noop } from "../../core/function/noop.js"; import ObservedString from "../../core/model/ObservedString.js"; import { SimpleLifecycle, SimpleLifecycleStateType } from "../../core/process/SimpleLifecycle.js"; import ConfirmationDialogView from '../../view/elements/ConfirmationDialogView.js'; import EmptyView from "../../view/elements/EmptyView.js"; import SimpleWindowView from '../../view/elements/SimpleWindow.js'; import { DomTooltipManager } from "../../view/tooltip/DomTooltipManager.js"; import { GMLEngine } from "../../view/tooltip/gml/GMLEngine.js"; import { TooltipManager } from "../../view/tooltip/TooltipManager.js"; import View from '../../view/View.js'; import AnimationTrack from "../animation/keyed2/AnimationTrack.js"; import AnimationTrackPlayback from "../animation/keyed2/AnimationTrackPlayback.js"; import { playTrackRealTime } from "../animation/playTrackRealTime.js"; import TransitionFunctions from "../animation/TransitionFunctions.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 Ticker from "../simulation/Ticker.js"; import { CursorType } from "./cursor/CursorType.js"; import { SceneGUIContext } from "./scene/SceneGUIContext.js"; class GUIEngine { windows = new List(); /** * * @type {EntityManager|null} */ entityManager = null; /** * * @type {Engine} */ engine = null; /** * * @type {WeakMap<Scene, SceneGUIContext>} */ sceneContexts = new WeakMap(); /** * * @type {TooltipManager} */ tooltips = new TooltipManager(); /** * * @type {DomTooltipManager} */ viewTooltips = new DomTooltipManager(this.tooltips); /** * * @type {Ticker} */ ticker = new Ticker(); view = new EmptyView({ classList: ['gui-engine-root'], css: { position: "absolute", left: "0", top: "0" } }); /** * * @type {GMLEngine} */ gml = new GMLEngine(); /** * * @type {ObservedString} */ cursor = new ObservedString(CursorType.Normal); /** * * @type {Localization|null} */ localization = null; constructor() { this.ticker.onTick.add(d => { let ctx = null; try { ctx = this.getActiveSceneContext(); } catch (e) { //skip } if (ctx !== null) { ctx.tick(d); } }); } /** * @param {boolean} closeable * @param {View} content * @param {string} title * @param {View} [wrapper] * @returns {Entity} */ openWindow({closeable, content, title, wrapper}) { const entityBuilder = new Entity(); function closeAction() { entityBuilder.destroy(); } const windowView = new SimpleWindowView(content, { closeAction, title, closeable }); entityBuilder.add(new ViewportPosition()); let vElement; if (wrapper !== undefined) { vElement = wrapper; wrapper.addChild(windowView); } else { vElement = windowView; } const guiElement = GUIElement.fromView(vElement); entityBuilder.add(guiElement) .add(SerializationMetadata.Transient); const dataset = this.entityManager.dataset; animateView(windowView, dataset); entityBuilder.build(dataset); return entityBuilder; } /** * * @param {View} content * @param {string} title * @param {number} priority * @returns {SimpleLifecycle} */ createModal({content, title, priority = 0}) { const entityManager = this.entityManager; const self = this; let window = null; let overlay = null; function destroy() { window.destroy(); overlay.destroy(); } function makeOverlay() { const overlay = new View(); overlay.el = document.createElement('div'); overlay.el.classList.add('ui-modal-overlay'); //make overlay dismiss modal overlay.el.addEventListener('click', function (event) { event.stopPropagation(); lifecycle.makeDestroyed(); }); const builder = new Entity(); builder.add(SerializationMetadata.Transient); builder.add(GUIElement.fromView(overlay)); return builder; } function build() { overlay = makeOverlay(); overlay.build(entityManager.dataset); const view = content; const vModalContainer = new EmptyView({classList: ['ui-modal-window-container']}); window = self.openWindow({ title: title, content: view, closeable: false, wrapper: vModalContainer }); const windowGuiElement = window.getComponent(GUIElement); windowGuiElement.anchor.set(0.5, 0.5); window.removeComponent(ViewportPosition); } const lifecycle = new SimpleLifecycle({priority}); lifecycle.sm.addEventHandlerStateEntry(SimpleLifecycleStateType.Active, build); lifecycle.sm.addEventHandlerStateExit(SimpleLifecycleStateType.Active, destroy); this.getActiveSceneContext().modals.add(lifecycle); return lifecycle; } /** * * @param {string} title * @param {View} content * @param {ObservedBoolean|ReactiveExpression} [confirmationEnabled] * @returns {Promise<any>} */ createModalConfirmation({title, content, confirmationEnabled}) { const self = this; let lifecycle = null; const result = new Promise(function (resolve, reject) { //make view let resolved = false; function clear() { lifecycle.makeDestroyed(); } function callbackYes() { resolved = true; clear(); resolve(); } function callbackNo() { resolved = true; clear(); reject(); } const view = new ConfirmationDialogView(content, [{ name: "yes", displayName: self.localization.getString("system_confirmation_confirm"), callback: callbackYes, enabled: confirmationEnabled }, { name: "no", displayName: self.localization.getString("system_confirmation_cancel"), callback: callbackNo }] ); lifecycle = self.createModal({ content: view, title: title }); lifecycle.sm.addEventHandlerStateEntry(SimpleLifecycleStateType.Destroyed, function () { if (!resolved) { //if destroyed without resolution, reject the promise reject(); } }); }); return result; } /** * @param {string} text * @param {string} title * @returns {Promise} will be resolved or rejected based on user choice */ confirmTextDialog({text, title}) { const content = createTextView(text); return this.createModalConfirmation({ title, content: content }); } /** * * @param {string} text * @param {string} title * @returns {Promise} */ createTextAlert({text, title}) { const content = createTextView(text); return this.createAlert({ content, title }); } /** * * @param {View} content * @param {string} title * @param {View[]} [marks] * @param {number} priority * @param {function(SimpleLifecycle)} [lifecycleHook] * @returns {Promise} */ createAlert( { content, title, marks = [], priority = 0, lifecycleHook = noop } ) { /** * * @type {SimpleLifecycle|null} */ let lifecycle = null; function clear() { lifecycle.makeDestroyed(); } const localization = this.localization; const view = new ConfirmationDialogView(content, [{ name: "ok", displayName: localization.getString("system_confirmation_continue"), callback: clear }] ); if (marks.length > 0) { const vMarks = new EmptyView({classList: ['marks']}); marks.forEach(vMarks.addChild, vMarks); view.addChild(vMarks); } lifecycle = this.createModal({ content: view, title, priority }); const result = new Promise(function (resolve, reject) { lifecycle.sm.addEventHandlerStateEntry(SimpleLifecycleStateType.Destroyed, resolve); }); lifecycleHook(lifecycle); return result; } /** * * @param {Scene} scene * @return {SceneGUIContext} */ obtainSceneContext(scene) { let context = this.sceneContexts.get(scene); if (context === undefined) { context = new SceneGUIContext(); context.initialize(scene); context.startup(); this.sceneContexts.set(scene, context); } return context; } /** * @returns {SceneGUIContext|null} */ getActiveSceneContext() { const engine = this.engine; if (engine === null) { throw new Error(`Engine is not set`); } const sm = engine.sceneManager; const scene = sm.current_scene; if (scene === null) { return null; } return this.obtainSceneContext(scene); } /** * Invoked when locale is updated * @private */ __update_localization() { // write locale to the view class so that CSS can be modifier on per-locale basis this.view.removeClassesByPattern(/locale-/) this.view.addClass(`locale-${this.localization.locale.getValue()}`); } /** * * @param {Engine} engine */ startup(engine) { this.engine = engine; this.entityManager = engine.entityManager; const self = this; /** * * @type {Localization} */ const localization = engine.localization; this.gml.initialize(engine.staticKnowledge, localization); this.tooltips.initialize(this.gml, engine.devices.pointer); //attach tooltips to GML this.gml.tooltips = this.viewTooltips; this.view.addChild(this.tooltips.contextView); engine.gameView.addChild(this.view); engine.gameView.size.process(function (x, y) { self.view.size.set(x, y); self.tooltips.contextView.size.set(x, y); }); this.ticker.start(); this.localization = localization; //register cursor propagation this.cursor.process(function (newValue, oldValue) { function className(cursorName) { return `cursor-${cursorName}`; } const classList = engine.graphics.domElement.classList; if (typeof oldValue === 'string') { classList.remove(className(oldValue)); } if (typeof newValue === 'string') { classList.add(className(newValue)); } }); // subscribe to localization changes this.localization.locale.onChanged.add(this.__update_localization, this); this.__update_localization(); return Promise.all([ this.tooltips.startup() ]); } shutdown() { this.windows.reset(); this.entityManager = null; const pTooltips = this.tooltips.shutdown() // unsubscribe from localization changes this.localization.locale.onChanged.remove(this.__update_localization, this); return Promise.all([ pTooltips ]); } } /** * * @param {View} view * @param {EntityComponentDataset} ecd */ function animateView(view, ecd) { const animationTrack = new AnimationTrack(["alpha", "scale"]); animationTrack.addKey(0, [0, 0.95]); animationTrack.addKey(0.2, [1, 1]); animationTrack.addTransition(0, TransitionFunctions.Linear); const playback = new AnimationTrackPlayback(animationTrack, function (alpha, scale) { this.el.style.opacity = alpha; this.scale.set(scale, scale); }, view); //force view status to initial key of animation playback.update(); playTrackRealTime(playback, ecd); } /** * * @param {string} text * @return {View} */ function createTextView(text) { const content = new View(); content.el = document.createElement('div'); content.el.classList.add('text'); content.el.innerText = text; content.size.set(300, 100); return content; } export default GUIEngine;