@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
556 lines (427 loc) • 14.3 kB
JavaScript
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;