diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..97bbaac --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +all: lint build + +build: + tsc + +lint: + tslint --project . + +clean: + rm -f public/js/irreligious.js + +run: + firefox public/index.html diff --git a/public/css/debugger.css b/public/css/debugger.css new file mode 100644 index 0000000..b9946d2 --- /dev/null +++ b/public/css/debugger.css @@ -0,0 +1,10 @@ +#irreligious-game { + overflow: auto; +} +div.resource { + float: left; + border: 2px solid black; + margin-right: 5px; + margin-bottom: 5px; + padding: 5px 10px; +} diff --git a/public/index.html b/public/index.html index 0016c3d..11da3d2 100644 --- a/public/index.html +++ b/public/index.html @@ -6,5 +6,6 @@ +
diff --git a/src/main.ts b/src/main.ts index ddcc415..2f747aa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,25 +1,38 @@ /// /// +/// +/// let globalStartTime = 0; +let globalTimeout: number = null; -function gameLoop (state: GameState): void { +function gameLoop (state: GameState, renderer: IRenderer): void { // figure out how much actual time has passed const elapsedTime = globalStartTime > 0 ? (new Date()).getTime() - globalStartTime : 0; + renderer.render(state); state.advance(elapsedTime); // run again in 1sec globalStartTime = (new Date()).getTime(); - setTimeout(() => gameLoop(state), 1000); + globalTimeout = setTimeout(() => gameLoop(state, renderer), 1000); } // run with default config at startup (() => { const config = new GameConfig(); + const renderer = new DebugRenderer(); const state = config.generateState(); - if (document.readyState !== 'loading') gameLoop(state); - else document.addEventListener('DOMContentLoaded', () => gameLoop(state)); + // re-run main loop immediately on user clicks + state.onResourceClick = () => { + if (globalTimeout !== null) { + clearTimeout(globalTimeout); + gameLoop(state, renderer); + } + } + + if (document.readyState !== 'loading') gameLoop(state, renderer); + else document.addEventListener('DOMContentLoaded', () => gameLoop(state, renderer)); })(); diff --git a/src/model/GameConfig.ts b/src/model/GameConfig.ts index 6999f14..75e20ef 100644 --- a/src/model/GameConfig.ts +++ b/src/model/GameConfig.ts @@ -13,10 +13,10 @@ class GameConfig { public relBuddhismShare: number = 0.06; public relSikhismShare: number = 0.04; public relJudaismShare: number = 0.02; - public relOtherShare:number = 0.02; + public relOtherShare: number = 0.02; public relNoneShare: number = 0.16; - public generateState(): GameState { + public generateState (): GameState { const state = new GameState(); // create player organization diff --git a/src/model/GameState.ts b/src/model/GameState.ts index cdc7f27..af3ec18 100644 --- a/src/model/GameState.ts +++ b/src/model/GameState.ts @@ -1,13 +1,51 @@ -/// +/// class GameState { - private _resources: {[key: string]: IResource} = {}; + private _resources: Record = { }; + private _resourceKeys: string[] = []; + + public onResourceClick: () => void = null; public addResource (key: string, resource: IResource): void { + this._resourceKeys.push(key); this._resources[key] = resource; } public advance (time: number): void { - console.log(`Advancing state by ${time}ms...`); + for (const rkey of this._resourceKeys) { + if (this._resources[rkey].advanceAction !== null) { + this._resources[rkey].advanceAction(time, this); + } + } + } + + public getResources (): string[] { + return this._resourceKeys; + } + + public getResource (key: string): IResource { + return this._resources[key]; + } + + public performClick (resourceKey: string): void { + if (this._resources[resourceKey].clickAction !== null) { + this._resources[resourceKey].clickAction(this); + if (this.onResourceClick !== null) { + this.onResourceClick(); + } + } + } + + public deductCost (cost: { [rkey: string]: number }): boolean { + if (cost === null || Object.keys(cost) === null) return true; + for (const rkey of Object.keys(cost)) { + if (this._resources[rkey].value < cost[rkey]) { + return false; + } + } + for (const rkey of Object.keys(cost)) { + this._resources[rkey].value -= cost[rkey]; + } + return true; } } diff --git a/src/model/IResource.ts b/src/model/IResource.ts deleted file mode 100644 index 6710231..0000000 --- a/src/model/IResource.ts +++ /dev/null @@ -1,14 +0,0 @@ -enum ResourceType { - Religion, - Consumable -} - -interface IResource { - name: string; - description: string; - - resourceType: ResourceType; - value: number; - max?: number; - unlocked: boolean; -} diff --git a/src/model/resource/Consumable.ts b/src/model/resource/Consumable.ts deleted file mode 100644 index 23c8f9e..0000000 --- a/src/model/resource/Consumable.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// - -class Consumable implements IResource { - public readonly resourceType = ResourceType.Consumable; - - constructor ( - public readonly name: string, - public readonly description: string, - public value: number, - public unlocked: boolean, - public max?: number, - ) { - } -} diff --git a/src/model/resource/IResource.ts b/src/model/resource/IResource.ts new file mode 100644 index 0000000..2eb8937 --- /dev/null +++ b/src/model/resource/IResource.ts @@ -0,0 +1,21 @@ +enum ResourceType { + Religion, + Consumable, + Infrastructure +} + +interface IResource { + name: string; + description: string; + + resourceType: ResourceType; + value: number; + max?: number; + unlocked: boolean; + + clickText: string; + clickDescription: string; + clickAction: (state: GameState) => void; + + advanceAction: (time: number, state: GameState) => void; +} diff --git a/src/model/resource/Money.ts b/src/model/resource/Money.ts index 6497169..9545106 100644 --- a/src/model/resource/Money.ts +++ b/src/model/resource/Money.ts @@ -1,11 +1,13 @@ -/// +/// -class Money extends Consumable { +class Money extends Purchasable { constructor ( public value: number, public max: number ) { - super('Money', 'Used to purchase goods and services.', - value, true, max); + super('Money', 'Used to purchase goods and services.', null); + this.clickText = 'Beg'; + this.clickDescription = 'Alms for the poor.'; + this.unlocked = true; } } diff --git a/src/model/resource/PlayerOrganization.ts b/src/model/resource/PlayerOrganization.ts index d5b738e..8f6c628 100644 --- a/src/model/resource/PlayerOrganization.ts +++ b/src/model/resource/PlayerOrganization.ts @@ -1,4 +1,4 @@ -/// +/// class PlayerOrganization implements IResource { public readonly name = 'Player'; @@ -7,5 +7,12 @@ class PlayerOrganization implements IResource { public readonly max?: number = null; public readonly unlocked = true; + public readonly clickText: string = null; + public readonly clickDescription: string = null; + public readonly clickAction: () => void = null; + + public readonly advanceAction: (time: number) => void = null; + public value = 0; + } diff --git a/src/model/resource/Purchasable.ts b/src/model/resource/Purchasable.ts new file mode 100644 index 0000000..8e1d1b8 --- /dev/null +++ b/src/model/resource/Purchasable.ts @@ -0,0 +1,28 @@ +/// + +abstract class Purchasable implements IResource { + public readonly resourceType = ResourceType.Infrastructure; + public readonly max?: number = null; + public value: number = 0; + public unlocked: boolean = false; + + public clickText: string = "Purchase"; + public clickDescription: string = null; + + constructor ( + public readonly name: string, + public readonly description: string, + private _cost: { [key: string]: number } + ) { } + + public clickAction (state: GameState) { + if (this.max !== null && this.value >= this.max) return; + if (state.deductCost(this._cost)) { + this.value += 1; + } + } + + public advanceAction (time: number, state: GameState) { + // do nothing + } +} diff --git a/src/model/resource/Religion.ts b/src/model/resource/Religion.ts index aa9b3e7..6635cc0 100644 --- a/src/model/resource/Religion.ts +++ b/src/model/resource/Religion.ts @@ -1,9 +1,13 @@ -/// +/// class Religion implements IResource { public readonly resourceType = ResourceType.Religion; public readonly max?: number = null; public readonly unlocked = true; + public readonly clickText: string = null; + public readonly clickDescription: string = null; + public readonly clickAction: () => void = null; + public readonly advanceAction: (time: number) => void = null; constructor ( public readonly name: string, diff --git a/src/render/DebugRenderer.ts b/src/render/DebugRenderer.ts new file mode 100644 index 0000000..6d0de8a --- /dev/null +++ b/src/render/DebugRenderer.ts @@ -0,0 +1,67 @@ +/// +/// + +class DebugRenderer implements IRenderer { + private _initialized = false; + + public render (state: GameState) { + const container = document.getElementById('irreligious-game'); + if (container === null) { + console.log('Cannot find #irreligious-game container.'); // tslint:disable-line + } else { + if (!this._initialized) { + this._initialized = true; + const style = document.createElement('link'); + style.setAttribute('rel', 'stylesheet'); + style.setAttribute('href', 'css/debugger.css'); + const head = document.getElementsByTagName('head')[0]; + head.appendChild(style); + } + for (const rkey of state.getResources()) { + const resource = state.getResource(rkey); + if (resource.unlocked) { + let el = document.getElementById(`r_${rkey}`); + if (el === null) { + el = document.createElement('div'); + el.className = 'resource'; + el.id = `r_${rkey}`; + let content = ` + ${resource.name}
+ + `; + if (resource.clickText !== null) { + content += `
`; + } + el.innerHTML = content; + container.appendChild(el); + if (resource.clickAction !== null) { + const btn = el.getElementsByClassName('btn')[0]; + btn.addEventListener('click', () => state.performClick(rkey)); + } + } + const elV = el.getElementsByClassName('value')[0]; + const elT = el.getElementsByClassName('max')[0]; + elV.innerHTML = this.formatNumber(resource.value, 1); + elT.innerHTML = resource.max !== null ? ` / ${this.formatNumber(resource.max, 2)}` : ''; + } + } + } + } + + private formatNumber (num: number, digits: number): string { + const lookup = [ + { value: 1, symbol: "" }, + { value: 1e3, symbol: "k" }, + { value: 1e6, symbol: "M" }, + { value: 1e9, symbol: "G" }, + { value: 1e12, symbol: "T" }, + { value: 1e15, symbol: "P" }, + { value: 1e18, symbol: "E" } + ]; + const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; + const item = lookup.slice().reverse().find((i) => num >= i.value); + return item + ? (num / item.value).toFixed(digits).replace(rx, "$1") + item.symbol + : "0"; + } +} diff --git a/src/render/IRenderer.ts b/src/render/IRenderer.ts new file mode 100644 index 0000000..ed0ce79 --- /dev/null +++ b/src/render/IRenderer.ts @@ -0,0 +1,5 @@ +/// + +interface IRenderer { + render (state: GameState); +} diff --git a/tsconfig.json b/tsconfig.json index d0e2d28..dc50e43 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "ES5", + "target": "ES6", "module": "none" } } diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..d1c81ab --- /dev/null +++ b/tslint.json @@ -0,0 +1,19 @@ +{ + "extends": "tslint:recommended", + "rules": { + "no-reference": false, + "space-before-function-paren": true, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-rest-spread", + "check-type", + "check-typecast", + "check-type-operator", + "check-preblock", + "check-postbrace" + ] + } +}