UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

544 lines (417 loc) • 13.2 kB
import LabelView from "../../common/LabelView.js"; import EmptyView from "../../elements/EmptyView.js"; import { TooltipParser } from "./TooltipParser.js"; import { TooltipTokenType } from "./parser/TooltipTokenType.js"; class GMLContextFrame { constructor() { this.enableTooltips = true; } } const RECURSION_LIMIT = 50; /** * Game Markup Language */ export class GMLEngine { constructor() { /** * * @type {StaticKnowledgeDatabase} */ this.database = null; /** * * @type {Localization} */ this.localization = null; /** * * @type {DomTooltipManager} */ this.tooltips = null; /** * Used to prevent infinite recursion * @type {number} * @private */ this.__recursionCount = 0; /** * * @type {Array} * @private */ this.__recursionReferencePath = []; /** * * @type {GMLContextFrame[]} */ this.__contextStack = []; /** * * @type {boolean} * @private */ this.__tooltipsEnabled = true; /** * * @type {Map<string, GMLReferenceCompiler>} * @private */ this.__reference_compilers_visual = new Map(); /** * * @type {Map<string, GMLReferenceCompiler>} * @private */ this.__reference_compilers_text = new Map(); /** * * @type {TooltipParser} * @private */ this.__parser = new TooltipParser(); } /** * * @param {string} key * @param {GMLReferenceCompiler} compiler * @returns {boolean} */ addReferenceCompilerVisual(key, compiler) { if (this.__reference_compilers_visual.has(key)) { return false; } this.__reference_compilers_visual.set(key, compiler); return true; } /** * * @param {string} key * @returns {boolean} */ removeReferenceCompilerVisual(key) { if (!this.__reference_compilers_visual.has(key)) { return false; } this.__reference_compilers_visual.delete(key); return true; } /** * * @param {string} key * @param {GMLReferenceCompiler} compiler * @returns {boolean} */ addReferenceCompilerText(key, compiler) { if (this.__reference_compilers_text.has(key)) { return false; } this.__reference_compilers_text.set(key, compiler); return true; } /** * * @param {string} key * @returns {boolean} */ removeReferenceCompilerText(key) { if (!this.__reference_compilers_text.has(key)) { return false; } this.__reference_compilers_text.delete(key); return true; } /** * * @returns {boolean} */ getTooltipsEnabled() { return this.__tooltipsEnabled; } /** * * @param {boolean} v */ setTooltipsEnabled(v) { this.__tooltipsEnabled = v; } /** * @param {StaticKnowledgeDatabase} database * @param {Localization} localization */ initialize(database, localization) { /** * * @type {StaticKnowledgeDatabase} */ this.database = database; this.localization = localization; } /** * * @returns {Promise<any>} */ startup() { const self = this; return new Promise(function (resolve, reject) { if (self.database === null) { throw new Error('Database not set; probably not initialized'); } self.database.promise().then(resolve, reject); }); } /** * * @param {String} type * @returns {number} */ getReferenceDepth(type) { let result = 0; const path = this.__recursionReferencePath; const pathLength = path.length; for (let i = 0; i < pathLength; i++) { const r = path[i]; if (r.type === type) { result++; } } return result; } /** * @private * @param {Token[]} tokens * @returns {string} */ compileTokensToText(tokens) { const database = this.database; const localization = this.localization; const tooltips = this.tooltips; const gml = this; /** * * @param {Token} token */ function compileTextToken(token) { return token.value; } function compileReferenceToken(token) { /** * @type {TooltipReferenceValue} */ const reference = token.value; const refType = reference.type.toLocaleLowerCase(); const referenceCompiler = gml.__reference_compilers_text.get(refType); if (referenceCompiler === undefined) { //unknown reference type console.error(`unknown reference type '${refType}'`); return; } let result; gml.__recursionReferencePath.push(reference); try { result = referenceCompiler.compile(reference.values, database, localization, gml, tooltips); } catch (e) { console.error(`Failed to compile reference token`, token, 'ERROR:', e, 'token stream:', tokens); result = 'ERROR'; } gml.__recursionReferencePath.pop(); return result; } const result = tokens.map(t => { let v; const tokenType = t.type; if (tokenType === TooltipTokenType.Reference) { v = compileReferenceToken(t); } else if (tokenType === TooltipTokenType.Text) { v = compileTextToken(t); } else if (tokenType === TooltipTokenType.StyleStart) { //do nothing } else if (tokenType === TooltipTokenType.StyleEnd) { //do nothing } else { throw new TypeError(`Unsupported token type '${tokenType}'`); } return v; }) .filter(v => v !== undefined) .join(''); return result; } /** * * @param {Token[]} tokens * @param {View} [target] * @returns {View} * @private */ compileTokensToVisual(tokens, target = new EmptyView({ tag: 'span' })) { const database = this.database; const localization = this.localization; const tooltips = this.tooltips; const gml = this; /** * * @param {Token} token * @param {View[]} result */ function compileTextToken(token, result) { /** * @type {string} */ const tokenValue = token.value; const view = new LabelView(tokenValue, { tag: 'span' }); result.push(view); } /** * * @param {Token} token * @param {View[]} result */ function compileReferenceToken(token, result) { /** * @type {TooltipReferenceValue} */ const reference = token.value; const refType = reference.type.toLocaleLowerCase(); const referenceCompiler = gml.__reference_compilers_visual.get(refType); if (referenceCompiler === undefined) { //unknown reference type console.error(`No compiler registered for reference type '${refType}'`); return; } let view; gml.__recursionReferencePath.push(reference); try { view = referenceCompiler.compile(reference.values, database, localization, gml, tooltips); } catch (e) { console.error(`Failed to compile reference token`, token, 'ERROR:', e, 'token stream:', tokens); view = new LabelView('ERROR'); } gml.__recursionReferencePath.pop(); view.addClass('reference-type-' + refType); result.push(view); } //style stack const styleSet = []; let containerElement = target; function makeStyleContainer() { const view = new EmptyView({ tag: 'span' }); styleSet.forEach(n => view.addClass(n)); return view; } function pushStyle(name) { styleSet.push(name); const el = makeStyleContainer(); target.addChild(el); containerElement = el; } function popStyle(name) { const i = styleSet.indexOf(name); if (i === -1) { console.error(`encountered closing token for a style(name=${name}) that is not open. Current style set: [${styleSet}]`); //bail return; } styleSet.splice(i, 1); if (styleSet.length === 0) { containerElement = target; } else { //close current container and start a new one const el = makeStyleContainer(); target.addChild(el); containerElement = el; } } const tokenCount = tokens.length; for (let i = 0; i < tokenCount; i++) { let t = tokens[i]; /** * @type {View[]} */ const childViews = []; const tokenType = t.type; if (tokenType === TooltipTokenType.Reference) { compileReferenceToken(t, childViews); } else if (tokenType === TooltipTokenType.Text) { compileTextToken(t, childViews); } else if (tokenType === TooltipTokenType.StyleStart) { pushStyle(t.value); } else if (tokenType === TooltipTokenType.StyleEnd) { popStyle(t.value); } else { throw new TypeError(`Unsupported token type '${tokenType}'`); } const nChildren = childViews.length; for (let j = 0; j < nChildren; j++) { const childView = childViews[j]; containerElement.addChild(childView); } } return target; } pushState() { const frame = new GMLContextFrame(); frame.enableTooltips = this.__tooltipsEnabled; this.__contextStack.push(frame); } popState() { const frame = this.__contextStack.pop(); this.__tooltipsEnabled = frame.enableTooltips; } /** * * @param {string} code * @return {string} */ compileAsText(code) { if (this.__recursionCount >= RECURSION_LIMIT) { console.error(`Hit recursion limit(=${RECURSION_LIMIT}), returning empty view`); return ""; } this.__recursionCount++; this.pushState(); try { const tokens = this.__parser.parse(code); const result = this.compileTokensToText(tokens); return result; } finally { //restore frame this.popState(); this.__recursionCount--; } } /** * * @param {string} code * @param {View} [target] * @returns {View} */ compile(code, target) { if (this.__recursionCount >= RECURSION_LIMIT) { console.error(`Hit recursion limit(=${RECURSION_LIMIT}), returning empty view`); return new EmptyView(); } else { this.__recursionCount++; this.pushState(); try { const tokens = this.__parser.parse(code); const view = this.compileTokensToVisual(tokens, target); return view; } finally { //restore frame this.popState(); this.__recursionCount--; } } } /** * * @param {string} key * @param {View} [target] * @param {Object} [seed] * @returns {View} */ compile_localized(key, target, seed) { const text = this.localization.getString(key, seed); return this.compile(text, target); } }