diff --git a/src/App.ts b/src/App.ts index 9f8f5e6..68fcdfc 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,16 +1,34 @@ -import { v, w } from '@dojo/widget-core/d'; +import { w } from '@dojo/widget-core/d'; import { DNode, WidgetProperties } from '@dojo/widget-core/interfaces'; import { WidgetBase } from '@dojo/widget-core/WidgetBase'; -import Controls from './widgets/Controls'; -import AssetContainer from './containers/AssetContainer'; -import OutsideContainer from './containers/OutsideContainer'; +import { ApplicationState } from './context/AppContext'; +import { Scene } from 'three'; +import SceneContainer from './containers/SceneContainer'; + +export interface AppProperties extends WidgetProperties { + isLoadingState: boolean; + state: ApplicationState; + + initialize(): void; +} + +export default class App extends WidgetBase { + manageState() { + const { + isLoadingState, + state, + + initialize + } = this.properties; + + if (!isLoadingState && state === ApplicationState.Initial) { + initialize(); + } + } -export default class App extends WidgetBase { protected render(): DNode { - return v('a-scene', [ - w(AssetContainer, {}), - w(Controls, {}), - w(OutsideContainer, {}), - ]); + this.manageState(); + + return w(SceneContainer, {}); } } diff --git a/src/commands/initialize.ts b/src/commands/initialize.ts new file mode 100644 index 0000000..e2f3549 --- /dev/null +++ b/src/commands/initialize.ts @@ -0,0 +1,28 @@ +import Executor, { Action } from '../framework/Executor'; +import AppContext, { ApplicationState } from '../context/AppContext'; +import { ActionType } from '../initialize'; +import registerHeightComponent from '../components/heightComponent'; + +export type InitializeAction = Action; + +export default function initialize({ state: [ app, executor ] }: InitializeAction) { + if (app.state !== ApplicationState.Initial) { + throw new Error('Application already initialized'); + } + + app.isLoadingState = true; + + if (!app.initialized.monsters) { + executor.execute(ActionType.LoadMonsters); + } + if (!app.initialized.aframe) { + registerHeightComponent(); + app.initialized.aframe = true; + } + + if (app.initialized.monsters && app.initialized.aframe) { + executor.execute(ActionType.RandomizeEncounter); + app.isLoadingState = false; + app.state = ApplicationState.Outside; + } +}; diff --git a/src/commands/loadMonsters.ts b/src/commands/loadMonsters.ts new file mode 100644 index 0000000..8f57300 --- /dev/null +++ b/src/commands/loadMonsters.ts @@ -0,0 +1,9 @@ +import Executor, { Action } from '../framework/Executor'; +import monsters from '../configuration/monsters'; + +export type LoadMonsterAction = Action; + +export default function loadMonsters({ state: executor }: LoadMonsterAction) { + // TODO eventually monsters can come from an async load + executor.execute('loadedMonsters', monsters); +} diff --git a/src/commands/loadedMonsters.ts b/src/commands/loadedMonsters.ts new file mode 100644 index 0000000..78910c1 --- /dev/null +++ b/src/commands/loadedMonsters.ts @@ -0,0 +1,10 @@ +import Executor, { Action } from '../framework/Executor'; +import monsters from '../configuration/monsters'; +import AppContext from '../context/AppContext'; + +export type LoadedMonsterAction = Action; + +export default function loadedMonsters({ state: [ app, executor ] }: LoadedMonsterAction) { + app.initialized.monsters = true; + executor.execute('registerMonsters', monsters); +} diff --git a/src/commands/randomizeEncounter.ts b/src/commands/randomizeEncounter.ts new file mode 100644 index 0000000..276713c --- /dev/null +++ b/src/commands/randomizeEncounter.ts @@ -0,0 +1,8 @@ +import { Action } from '../framework/Executor'; +import OutsideContext from '../context/OutsideContext'; + +export type RandomizeEncounterAction = Action; + +export default function randomizeEncounter({ state: outsideContext }: RandomizeEncounterAction) { + outsideContext.randomizeEncounter(); +}; diff --git a/src/commands/registerMonsters.ts b/src/commands/registerMonsters.ts new file mode 100644 index 0000000..c1f0af3 --- /dev/null +++ b/src/commands/registerMonsters.ts @@ -0,0 +1,21 @@ +import { Action } from '../framework/Executor'; +import { MonsterConfigurationItem } from '../configuration/monsters'; +import OutsideContext from '../context/OutsideContext'; +import { throws } from '../util/properties'; +import AssetContext from '../context/AssetContext'; + +export type RegisterMonstersAction = Action, [ AssetContext, OutsideContext ]>; + +export default function registerMonsters({ + payload: monsters = throws(), + state: [ appContext, outsideContext ] = throws() +}: RegisterMonstersAction) { + for (let monster of monsters) { + appContext.addObjMtlAssets(monster.name, monster.obj, monster.mtl); + outsideContext.addMonster({ + name: monster.name, + heights: monster.heights, + environment: monster.environment + }); + } +}; diff --git a/src/configuration/monsters.ts b/src/configuration/monsters.ts index 600c1a1..360a204 100644 --- a/src/configuration/monsters.ts +++ b/src/configuration/monsters.ts @@ -2,7 +2,7 @@ import { Environment, MonsterDefinition } from '../context/OutsideContext'; import { ObjModelAsset } from '../interfaces'; import { assign } from '@dojo/shim/object'; -declare type MonsterConfigurationItem = MonsterDefinition & ObjModelAsset; +export type MonsterConfigurationItem = MonsterDefinition & ObjModelAsset; export const enum MonsterName { CharDerp = 'charderp', diff --git a/src/containers/AppContainer.ts b/src/containers/AppContainer.ts new file mode 100644 index 0000000..2a465c0 --- /dev/null +++ b/src/containers/AppContainer.ts @@ -0,0 +1,20 @@ +import Container from '../framework/Container'; +import App, { AppProperties } from '../App'; +import AppContext from '../context/AppContext'; +import { ActionType, State } from '../initialize'; +import Executor from '../framework/Executor'; + +const AppContainer = Container(App, [ State.App, State.Executor ], { + getProperties([ app, executor ]: [ AppContext, Executor ]): AppProperties { + return { + isLoadingState: app.isLoadingState, + state: app.state, + + initialize() { + executor.execute(ActionType.Initialize); + } + } + } +}); + +export default AppContainer; diff --git a/src/containers/AssetContainer.ts b/src/containers/AssetContainer.ts index 9932895..ab6755d 100644 --- a/src/containers/AssetContainer.ts +++ b/src/containers/AssetContainer.ts @@ -1,9 +1,10 @@ import Container from '../framework/Container'; import Assets from '../widgets/Assets'; -import AppContext from '../context/AppContext'; +import AssetContext from '../context/AssetContext'; +import { State } from '../initialize'; -const AssetContainer = Container(Assets, 'app-state', { - getProperties(context: AppContext) { +const AssetContainer = Container(Assets, State.Asset, { + getProperties(context: AssetContext) { return { assets: context.assets } diff --git a/src/containers/OutsideContainer.ts b/src/containers/OutsideContainer.ts index 899fbbf..6ecdc5b 100644 --- a/src/containers/OutsideContainer.ts +++ b/src/containers/OutsideContainer.ts @@ -2,10 +2,11 @@ import Outside, { OutsideProperties } from '../widgets/Outside'; import Container from '../framework/Container'; import { throws } from '../util/properties'; import OutsideContext from '../context/OutsideContext'; -import AppContext from '../context/AppContext'; +import { State } from '../initialize'; +import AssetContext from '../context/AssetContext'; -const OutsideContainer = Container(Outside, [ 'outside', 'app-state' ], { - getProperties(payload: [OutsideContext, AppContext]): OutsideProperties { +const OutsideContainer = Container(Outside, [ State.Outside, State.Asset ], { + getProperties(payload: [ OutsideContext, AssetContext ]): OutsideProperties { const [ outside = throws(), appContext = throws() diff --git a/src/containers/SceneContainer.ts b/src/containers/SceneContainer.ts new file mode 100644 index 0000000..758b262 --- /dev/null +++ b/src/containers/SceneContainer.ts @@ -0,0 +1,15 @@ +import Container from '../framework/Container'; +import AppContext from '../context/AppContext'; +import Scene, { SceneProperties } from '../widgets/Scene'; +import { State } from '../initialize'; + +const SceneContainer = Container(Scene, State.App, { + getProperties(state: AppContext): SceneProperties { + return { + debug: state.debug, + state: state.state + } + } +}); + +export default SceneContainer; diff --git a/src/context/AppContext.ts b/src/context/AppContext.ts index a5243f5..0c01806 100644 --- a/src/context/AppContext.ts +++ b/src/context/AppContext.ts @@ -1,48 +1,19 @@ import { InjectorBase } from '../framework/InjectorBase'; -/** - * Application Asset used for loading models, video, and images by A-Frame's - */ -export interface Asset { - id: string; - src: string; +export const enum ApplicationState { + Initial = 'initial', + Outside = 'outside' } export default class AppContext extends InjectorBase { - private _assets: Map = new Map(); + debug = true; - /** - * @return a list of all active assets - */ - get assets(): Asset[] { - return Array.from(this._assets.values()); - } + initialized = { + aframe: false, + monsters: false + }; - /** - * Adds an asset - */ - addAsset(id: string, src: string) { - if (!this._assets.has(id)) { - this._assets.set(id, { - id, - src - }); - this.emitInvalidate(); - } - } + isLoadingState = false; - /** - * Adds an Obj model asset - */ - addObjMtlAssets(name: string, objSrc: string, mtlSrc?: string) { - this.addAsset(`${ name }-obj`, objSrc); - mtlSrc && this.addAsset(`${ name }-mtl`, mtlSrc); - } - - getObjMtlAssets(name: string): { mtl?: Asset, obj?: Asset } { - return { - mtl: this._assets.get(`${ name }-mtl`), - obj: this._assets.get(`${ name }-obj`) - } - } + state: ApplicationState = ApplicationState.Initial; } diff --git a/src/context/AssetContext.ts b/src/context/AssetContext.ts new file mode 100644 index 0000000..c0564a3 --- /dev/null +++ b/src/context/AssetContext.ts @@ -0,0 +1,48 @@ +import { InjectorBase } from '../framework/InjectorBase'; + +/** + * Application Asset used for loading models, video, and images by A-Frame's + */ +export interface Asset { + id: string; + src: string; +} + +export default class AssetContext extends InjectorBase { + private _assets: Map = new Map(); + + /** + * @return a list of all active assets + */ + get assets(): Asset[] { + return Array.from(this._assets.values()); + } + + /** + * Adds an asset + */ + addAsset(id: string, src: string) { + if (!this._assets.has(id)) { + this._assets.set(id, { + id, + src + }); + this.emitInvalidate(); + } + } + + /** + * Adds an Obj model asset + */ + addObjMtlAssets(name: string, objSrc: string, mtlSrc?: string) { + this.addAsset(`${ name }-obj`, objSrc); + mtlSrc && this.addAsset(`${ name }-mtl`, mtlSrc); + } + + getObjMtlAssets(name: string): { mtl?: Asset, obj?: Asset } { + return { + mtl: this._assets.get(`${ name }-mtl`), + obj: this._assets.get(`${ name }-obj`) + } + } +} diff --git a/src/context/OutsideContext.ts b/src/context/OutsideContext.ts index 85c25b6..49b9e1c 100644 --- a/src/context/OutsideContext.ts +++ b/src/context/OutsideContext.ts @@ -1,5 +1,4 @@ import { InjectorBase } from '../framework/InjectorBase'; -import { ObjModelAsset } from '../interfaces'; interface Monster { distance: number; diff --git a/src/createDerpymonElement.ts b/src/createDerpymonElement.ts index 23631e6..be5f334 100644 --- a/src/createDerpymonElement.ts +++ b/src/createDerpymonElement.ts @@ -1,15 +1,11 @@ import { CustomElementDescriptor } from '@dojo/widget-core/customElements'; -import App from './App'; import initialize from './initialize'; - -// Require globals -require('aframe'); -require('aframe-environment-component'); +import AppContainer from './containers/AppContainer'; export default function createDerpymonElement(): CustomElementDescriptor { return { tagName: 'go-derpy', - widgetConstructor: App, + widgetConstructor: AppContainer, events: [], properties: [], initialization() { diff --git a/src/framework/Executor.ts b/src/framework/Executor.ts index e69de29..898ad80 100644 --- a/src/framework/Executor.ts +++ b/src/framework/Executor.ts @@ -0,0 +1,118 @@ +import Registry from '@dojo/widget-core/Registry'; +import { RegistryLabel } from '@dojo/widget-core/interfaces'; +import { InjectorBase } from './InjectorBase'; +import Injector from '@dojo/widget-core/Injector'; + +export interface Command { + (action: T): void; +} + +export interface Action { + type: string; + payload?: T; + state: U; +} + +interface CommandMapping { + handler: Command; + state?: RegistryLabel | RegistryLabel[]; +} + +export interface CommandDescriptor extends CommandMapping { + type: string +} + +declare type InjectorState = Injector | Array | null; + +export default class Executor extends InjectorBase { + readonly registry: Registry; + + private _commands: Map = new Map(); + + constructor(registry: Registry, commands?: CommandDescriptor[]) { + super(); + this.registry = registry; + + if (commands) { + for (let command of commands) { + this._setCommand(command.type, command.handler, command.state); + } + } + } + + get actions() { + return Array.from(this._commands.keys()); + } + + execute(type: string, payload?: any) { + if (this._commands.has(type)) { + const { + handler, + state + } = this._commands.get(type)!; + + const injectors = this._getInjectors(state); + + console.log(`action: ${ type }`); + try { + handler({ + type, + payload, + state: injectors + }); + } + catch (e) { + console.error(`[Executor]: action ${ type } failed.`, e); + } + console.log(`completed ${ type }`); + + this._invalidate(injectors); + } + else { + console.warn(`[Executor]: missing action for ${ type }`); + } + } + + register(name: string, handler: Command, state?: RegistryLabel | RegistryLabel[]) { + if (this._commands.has(name)) { + throw new Error(`Command "${ name }" already registered`); + } + + this._setCommand(name, handler, state); + this.emitInvalidate(); + } + + private _getInjectors(labels?: RegistryLabel | RegistryLabel[]): InjectorState { + if (!labels) { + return null; + } + else if (Array.isArray(labels)) { + return labels.map((label) => { + return this.registry.getInjector(label); + }); + } + else { + return this.registry.getInjector(labels); + } + } + + private _invalidate(injectors: InjectorState) { + if (injectors) { + if (Array.isArray(injectors)) { + for (let injector of injectors) { + injector && injector.emit({ type: 'invalidate' }); + } + } + else { + injectors.emit({ type: 'invalidate' }); + } + } + } + + private _setCommand(name: string, handler: Command, state?: RegistryLabel | RegistryLabel[]) { + this._commands.set(name, { + handler, + state: Array.isArray(state) ? Array.from(state) : state + }); + } +} diff --git a/src/framework/InjectorBase.ts b/src/framework/InjectorBase.ts index b5b059c..2e1284d 100644 --- a/src/framework/InjectorBase.ts +++ b/src/framework/InjectorBase.ts @@ -5,6 +5,10 @@ export class InjectorBase extends Injector { super({}); } + emitInvalidate() { + this.emit({type: 'invalidate'}); + } + get(): this { return this; } @@ -12,8 +16,4 @@ export class InjectorBase extends Injector { set() { throw new Error('not implemented'); } - - protected emitInvalidate() { - this.emit({type: 'invalidate'}); - } } diff --git a/src/initialize.ts b/src/initialize.ts index 48cf960..16a455a 100644 --- a/src/initialize.ts +++ b/src/initialize.ts @@ -1,29 +1,51 @@ -import registerHeightComponent from './components/heightComponent'; import Registry from '@dojo/widget-core/Registry'; import AppContext from './context/AppContext'; import OutsideContext from './context/OutsideContext'; -import monsters from './configuration/monsters'; +import Executor from './framework/Executor'; +import initializeApp from './commands/initialize'; +import loadMonsters from './commands/loadMonsters'; +import randomizeEncounter from './commands/randomizeEncounter'; +import registerMonsters from './commands/registerMonsters'; +import AssetContext from './context/AssetContext'; +import loadedMonsters from './commands/loadedMonsters'; + +// Require globals +require('aframe'); +require('aframe-environment-component'); +require('aframe-physics-system'); + +export const enum State { + App = 'app-state', + Asset = 'assets', + Executor = 'executor', + Outside = 'outside' +} + +export const enum ActionType { + Initialize = 'initialize', + LoadMonsters = 'loadMonsters', + LoadedMonsters = 'loadedMonsters', + RandomizeEncounter = 'randomizeEncounter', + RegisterMonsters = 'registerMonsters' +} export default function initialize() { const registry = new Registry(); + const executor = new Executor(registry, [ + { type: ActionType.Initialize, handler: initializeApp, state: [ State.App, State.Executor ] }, + { type: ActionType.LoadMonsters, handler: loadMonsters, state: State.Executor }, + { type: ActionType.LoadedMonsters, handler: loadedMonsters, state: [ State.App, State.Executor ] }, + { type: ActionType.RandomizeEncounter, handler: randomizeEncounter, state: State.Outside }, + { type: ActionType.RegisterMonsters, handler: registerMonsters, state: [ State.Asset, State.Outside ] } + ]); const appContext = new AppContext(); + const assetContext = new AssetContext(); const outsideContext = new OutsideContext(); - registry.defineInjector('app-state', appContext); - registry.defineInjector('outside', outsideContext); - initializeMonsters(appContext, outsideContext); - registerHeightComponent(); - outsideContext.randomizeEncounter(); - return registry; -} + registry.defineInjector(State.App, appContext); + registry.defineInjector(State.Asset, assetContext); + registry.defineInjector(State.Executor, executor); + registry.defineInjector(State.Outside, outsideContext); -function initializeMonsters(appContext: AppContext, outsideContext: OutsideContext) { - for (let monster of monsters) { - appContext.addObjMtlAssets(monster.name, monster.obj, monster.mtl); - outsideContext.addMonster({ - name: monster.name, - heights: monster.heights, - environment: monster.environment - }) - } + return registry; } diff --git a/src/main.ts b/src/main.ts index 28f83d5..3600463 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,6 @@ import { ProjectorMixin } from '@dojo/widget-core/mixins/Projector'; -import App from './App'; import initialize from './initialize'; - -// Require globals -require('aframe'); -require('aframe-environment-component'); +import AppContainer from './containers/AppContainer'; const root = document.querySelector('go-derpy') || undefined; @@ -13,8 +9,10 @@ if (!root) { } const registry = initialize(); - -const Projector = ProjectorMixin(App); +const Projector = ProjectorMixin(AppContainer); const projector = new Projector(); projector.setProperties({ registry }); projector.append(root); + +( root.ownerDocument).registry = registry; +( root.ownerDocument).projector = projector; diff --git a/src/widgets/Assets.ts b/src/widgets/Assets.ts index 35ebdfe..2968d96 100644 --- a/src/widgets/Assets.ts +++ b/src/widgets/Assets.ts @@ -1,10 +1,10 @@ import { WidgetBase } from '@dojo/widget-core/WidgetBase'; import { DNode, WidgetProperties } from '@dojo/widget-core/interfaces'; import { v } from '@dojo/widget-core/d'; -import AppContext from '../context/AppContext'; +import AssetContext from '../context/AssetContext'; export interface AssetProperties extends WidgetProperties { - assets?: AppContext['assets']; + assets?: AssetContext['assets']; } export default class Assets extends WidgetBase { diff --git a/src/widgets/Outside.ts b/src/widgets/Outside.ts index 4ec2414..0c98c1f 100644 --- a/src/widgets/Outside.ts +++ b/src/widgets/Outside.ts @@ -26,12 +26,14 @@ export default class Outside extends WidgetBase { return [ v('a-entity', { - environment: `preset: ${ environment }` + environment: `preset: ${ environment }`, + 'static-body': '' }), monster ? w(ObjModel, { mtl: monster.mtl, src: monster.obj, position: `0 0 -${ monster.distance }`, + 'static-body': 'shape: box', [objHeight]: `${ monster.height }` }) : null ]; diff --git a/src/widgets/Scene.ts b/src/widgets/Scene.ts new file mode 100644 index 0000000..3f19cdc --- /dev/null +++ b/src/widgets/Scene.ts @@ -0,0 +1,48 @@ +import { v, w } from '@dojo/widget-core/d'; +import { DNode, WidgetProperties } from '@dojo/widget-core/interfaces'; +import { WidgetBase } from '@dojo/widget-core/WidgetBase'; +import Controls from '../widgets/Controls'; +import AssetContainer from '../containers/AssetContainer'; +import OutsideContainer from '../containers/OutsideContainer'; +import { ApplicationState } from '../context/AppContext'; + +export interface SceneProperties extends WidgetProperties { + debug?: boolean; + state: string; +} + +export default class Scene extends WidgetBase { + protected renderInitial() { + return v('div', [ + 'loading' + ]); + } + + protected renderOutside() { + const { + debug = false, + } = this.properties; + + return v('a-scene', { + physics: `debug: ${ debug }` + }, [ + w(AssetContainer, {}), + w(Controls, {}), + w(OutsideContainer, {}), + ]); + } + + protected render(): DNode { + const { + debug = false, + state = ApplicationState.Initial + } = this.properties; + + switch (state) { + case ApplicationState.Outside: + return this.renderOutside(); + default: + return this.renderInitial(); + } + } +}