diff --git a/src/main.ts b/src/main.ts index 2f747aa..990aa85 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,12 +26,12 @@ function gameLoop (state: GameState, renderer: IRenderer): void { const state = config.generateState(); // re-run main loop immediately on user clicks - state.onResourceClick = () => { + state.onResourceClick.push(() => { 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 75e20ef..25791c9 100644 --- a/src/model/GameConfig.ts +++ b/src/model/GameConfig.ts @@ -1,7 +1,7 @@ /// /// -/// /// +/// class GameConfig { public worldPopulation: number = 790000000; @@ -20,7 +20,8 @@ class GameConfig { const state = new GameState(); // create player organization - state.addResource('plorg', new PlayerOrganization()); + state.addResource('plorg', new Religion( + 'Player', 'In you they trust.', 0)); // create world religions state.addResource('xtian', new Religion( @@ -55,8 +56,9 @@ class GameConfig { 'Non-Religious', 'Atheists and agnostics.', this.relNoneShare * this.worldPopulation)); - // add crafting resources + // add purchasable resources state.addResource('money', new Money(0, 1000)); + state.addResource('bonds', new SavingsBonds(0)); return state; } diff --git a/src/model/GameState.ts b/src/model/GameState.ts index af3ec18..daccdb6 100644 --- a/src/model/GameState.ts +++ b/src/model/GameState.ts @@ -4,7 +4,7 @@ class GameState { private _resources: Record = { }; private _resourceKeys: string[] = []; - public onResourceClick: () => void = null; + public onResourceClick: (() => void)[] = []; public addResource (key: string, resource: IResource): void { this._resourceKeys.push(key); @@ -30,22 +30,28 @@ class GameState { public performClick (resourceKey: string): void { if (this._resources[resourceKey].clickAction !== null) { this._resources[resourceKey].clickAction(this); - if (this.onResourceClick !== null) { - this.onResourceClick(); + for (const callback of this.onResourceClick) { + callback(); } } } 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; - } - } + if (!this.isPurchasable(cost)) return false; for (const rkey of Object.keys(cost)) { this._resources[rkey].value -= cost[rkey]; } return true; } + + public isPurchasable (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; + } + } + return true; + } } diff --git a/src/model/resource/IResource.ts b/src/model/resource/IResource.ts index 2eb8937..7ded881 100644 --- a/src/model/resource/IResource.ts +++ b/src/model/resource/IResource.ts @@ -11,7 +11,9 @@ interface IResource { resourceType: ResourceType; value: number; max?: number; - unlocked: boolean; + cost: { [key: string]: number }; + + isUnlocked: (state: GameState) => boolean; clickText: string; clickDescription: string; diff --git a/src/model/resource/Money.ts b/src/model/resource/Money.ts index 9545106..043c244 100644 --- a/src/model/resource/Money.ts +++ b/src/model/resource/Money.ts @@ -5,9 +5,12 @@ class Money extends Purchasable { public value: number, public max: number ) { - super('Money', 'Used to purchase goods and services.', null); + super('Money', 'Used to purchase goods and services.'); this.clickText = 'Beg'; this.clickDescription = 'Alms for the poor.'; - this.unlocked = true; + } + + public isUnlocked (state: GameState): boolean { + return true; } } diff --git a/src/model/resource/PlayerOrganization.ts b/src/model/resource/PlayerOrganization.ts deleted file mode 100644 index 8f6c628..0000000 --- a/src/model/resource/PlayerOrganization.ts +++ /dev/null @@ -1,18 +0,0 @@ -/// - -class PlayerOrganization implements IResource { - public readonly name = 'Player'; - public readonly description = 'In you they trust.'; - 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; - - public value = 0; - -} diff --git a/src/model/resource/Purchasable.ts b/src/model/resource/Purchasable.ts index 8e1d1b8..e4bd662 100644 --- a/src/model/resource/Purchasable.ts +++ b/src/model/resource/Purchasable.ts @@ -4,25 +4,36 @@ 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; + public clickText: string = 'Purchase'; + public clickDescription: string = 'Purchase'; + + public cost: { [key: string]: number } = null; + protected _costMultiplier: { [key: string]: number } = null; constructor ( public readonly name: string, - public readonly description: string, - private _cost: { [key: string]: number } + public readonly description: string ) { } public clickAction (state: GameState) { if (this.max !== null && this.value >= this.max) return; - if (state.deductCost(this._cost)) { + if (state.deductCost(this.cost)) { this.value += 1; + if (this._costMultiplier !== null + && Object.keys(this._costMultiplier !== null)) { + for (const rkey of Object.keys(this._costMultiplier)) { + this.cost[rkey] *= this._costMultiplier[rkey]; + } + } } } - public advanceAction (time: number, state: GameState) { - // do nothing + public advanceAction (time: number, state: GameState): void { + return; + } + + public isUnlocked (state: GameState): boolean { + return false; } } diff --git a/src/model/resource/Religion.ts b/src/model/resource/Religion.ts index 6635cc0..8e4af0e 100644 --- a/src/model/resource/Religion.ts +++ b/src/model/resource/Religion.ts @@ -3,16 +3,19 @@ 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; + public readonly cost: { [key: string]: number } = null; constructor ( public readonly name: string, public readonly description: string, public value: number, - ) { + ) { } + + public isUnlocked (state: GameState): boolean { + return true; } } diff --git a/src/model/resource/SavingsBonds.ts b/src/model/resource/SavingsBonds.ts new file mode 100644 index 0000000..bff43fd --- /dev/null +++ b/src/model/resource/SavingsBonds.ts @@ -0,0 +1,23 @@ +/// + +class SavingsBonds extends Purchasable { + public max?: number = null; + private _isUnlocked = false; + + constructor ( + public value: number, + ) { + super('Savings Bonds', 'Grows money by a small amount over time.'); + this.cost = { 'money': 25 }; + this._costMultiplier = { 'money': 1.1 }; + } + + public isUnlocked (state: GameState): boolean { + if (this._isUnlocked) return true; + if (state.getResource('money').value >= 25) { + this._isUnlocked = true; + return true; + } + return false; + } +} diff --git a/src/render/DebugRenderer.ts b/src/render/DebugRenderer.ts index 6d0de8a..f74fec8 100644 --- a/src/render/DebugRenderer.ts +++ b/src/render/DebugRenderer.ts @@ -3,6 +3,7 @@ class DebugRenderer implements IRenderer { private _initialized = false; + private _handleClick = true; public render (state: GameState) { const container = document.getElementById('irreligious-game'); @@ -11,15 +12,17 @@ class DebugRenderer implements IRenderer { } else { if (!this._initialized) { this._initialized = true; + state.onResourceClick.push(() => this._handleClick = 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 rkeys = state.getResources(); + for (const rkey of rkeys) { const resource = state.getResource(rkey); - if (resource.unlocked) { + if (resource.isUnlocked(state)) { let el = document.getElementById(`r_${rkey}`); if (el === null) { el = document.createElement('div'); @@ -27,11 +30,14 @@ class DebugRenderer implements IRenderer { el.id = `r_${rkey}`; let content = ` ${resource.name}
- + `; if (resource.clickText !== null) { content += `
`; } + if (resource.cost !== null && Object.keys(resource.cost) !== null) { + content += `
Cost: `; + } el.innerHTML = content; container.appendChild(el); if (resource.clickAction !== null) { @@ -43,9 +49,31 @@ class DebugRenderer implements IRenderer { const elT = el.getElementsByClassName('max')[0]; elV.innerHTML = this.formatNumber(resource.value, 1); elT.innerHTML = resource.max !== null ? ` / ${this.formatNumber(resource.max, 2)}` : ''; + if (this._handleClick) { + const elC = el.getElementsByClassName('cost'); + if (elC.length > 0) { + elC[0].innerHTML = this.getCostStr(resource, state); + } + } + } + } + this._handleClick = false; + } + } + + private getCostStr (resource: IResource, state: GameState) { + let cost = ''; + for (const rkey of state.getResources()) { + if (resource.cost[rkey] !== undefined) { + if (cost !== '') cost += ', '; + if (rkey === 'money') { + cost += `$${this.formatNumber(resource.cost[rkey], 1)}`; + } else { + cost += `${this.formatNumber(resource.cost[rkey], 1)} ${state.getResource(rkey).name}`; } } } + return cost; } private formatNumber (num: number, digits: number): string {