diff --git a/public/css/debugger.css b/public/css/debugger.css index 8fd04d6..cf30440 100644 --- a/public/css/debugger.css +++ b/public/css/debugger.css @@ -42,6 +42,9 @@ body, html { #resource-container-religion .resource { background-color: #ccf; } +#resource-container-job .resource { + background-color: #fcf; +} #resource-container-consumable .resource { background-color: #cfc; } diff --git a/src/main.ts b/src/main.ts index bc2a1f2..253178e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,9 +29,11 @@ function startGame (state: GameState, renderer: IRenderer): void { const config: GameConfig = new GameConfig(); // debug values to make the game play faster while testing - config.baseTitheAmount = 1000; - config.baseCryptoReturnAmount = 100; - config.baseCredibilityRestoreRate = 5; + config.cfgTitheAmount = 1000; + config.cfgTimeBetweenTithes = 5000; + config.cfgCryptoReturnAmount = 100; + config.cfgCredibilityRestoreRate = 5; + config.cfgPastorRecruitRate = 0.5; const renderer: IRenderer = new DebugRenderer(); const state: GameState = config.generateState(); diff --git a/src/model/GameConfig.ts b/src/model/GameConfig.ts index ff655d5..0dabdbd 100644 --- a/src/model/GameConfig.ts +++ b/src/model/GameConfig.ts @@ -1,12 +1,14 @@ /// +/// +/// /// /// +/// /// +/// /// /// /// -/// -/// class GameConfig { public worldPopulation: number = 790000000; @@ -21,12 +23,15 @@ class GameConfig { public relOtherShare: number = 0.02; public relNoneShare: number = 0.16; - public baseTitheAmount: number = 10; - public baseCryptoReturnAmount: number = 1; - public baseCredibilityRestoreRate: number = 0.25; + public cfgTitheAmount: number = 10; + public cfgTimeBetweenTithes: number = 30000; + public cfgCryptoReturnAmount: number = 1; + public cfgCredibilityRestoreRate: number = 0.25; + public cfgPastorRecruitRate: number = 0.01; public generateState (): GameState { const state: GameState = new GameState(); + state.config = this; // create player organization state.addResource('plorg', new PlayerOrg()); @@ -64,17 +69,19 @@ class GameConfig { 'Non-Religious', 'Atheists and agnostics.', this.relNoneShare * this.worldPopulation)); + // add jobs + state.addResource('pstor', new Pastor()); + // add resources - state.addResource('money', new Money(3.50, - this.baseTitheAmount, this.baseCryptoReturnAmount)); + state.addResource('money', new Money(3.50)); state.addResource('crpto', new CryptoCurrency()); state.addResource('tents', new Tent()); state.addResource('house', new House()); state.addResource('cmpnd', new Compound()); + state.addResource('chrch', new Church()); // add passive resources - state.addResource('creds', new Credibility( - this.baseCredibilityRestoreRate)); + state.addResource('creds', new Credibility()); return state; } diff --git a/src/model/GameState.ts b/src/model/GameState.ts index 3d62599..08263f0 100644 --- a/src/model/GameState.ts +++ b/src/model/GameState.ts @@ -2,6 +2,8 @@ class GameState { private _versionMaj: number = 0; private _versionMin: number = 1; + public config: GameConfig; + private _timeSinceSave: number = 0; private readonly _timeBetweenSaves: number = 10000; diff --git a/src/model/resource/Church.ts b/src/model/resource/Church.ts new file mode 100644 index 0000000..f65964b --- /dev/null +++ b/src/model/resource/Church.ts @@ -0,0 +1,11 @@ +/// + +class Church extends Infrastructure { + constructor () { + super('Churches', + 'Preaching grounds for 2 pastors.'); + this.cost.money = 10000; + this._costMultiplier.money = 1.01; + this._baseMax = 2; + } +} diff --git a/src/model/resource/Compound.ts b/src/model/resource/Compound.ts index 05af056..0195089 100644 --- a/src/model/resource/Compound.ts +++ b/src/model/resource/Compound.ts @@ -11,7 +11,7 @@ class Compound extends Infrastructure { public isUnlocked (state: GameState): boolean { if (this._isUnlocked) return true; const tents: IResource = state.getResource('tents'); - if (tents.value === tents.max(state)) { + if (tents.value >= 5) { this._isUnlocked = true; } return this._isUnlocked; diff --git a/src/model/resource/Credibility.ts b/src/model/resource/Credibility.ts index 0ac71a4..a01e024 100644 --- a/src/model/resource/Credibility.ts +++ b/src/model/resource/Credibility.ts @@ -3,8 +3,7 @@ class Credibility extends Passive { private _lastValue: number = 100; - constructor ( - private _baseRestoreRate: number) { + constructor () { super( 'Credibility', 'Affects your ability to recruit and retain followers.', @@ -16,6 +15,6 @@ class Credibility extends Passive { } public inc (state: GameState): number { - return this._baseRestoreRate; + return state.config.cfgCredibilityRestoreRate; } } diff --git a/src/model/resource/CryptoCurrency.ts b/src/model/resource/CryptoCurrency.ts index 9586e66..3455663 100644 --- a/src/model/resource/CryptoCurrency.ts +++ b/src/model/resource/CryptoCurrency.ts @@ -1,6 +1,8 @@ /// class CryptoCurrency extends Purchasable { + public readonly valueInWholeNumbers: boolean = false; + constructor () { super('Faithcoin', "A crypto coin that can't be spent directly, but provides a steady stream of passive income."); diff --git a/src/model/resource/House.ts b/src/model/resource/House.ts index d04c919..2ab42c9 100644 --- a/src/model/resource/House.ts +++ b/src/model/resource/House.ts @@ -17,8 +17,8 @@ class House extends Infrastructure { public isUnlocked (state: GameState): boolean { if (this._isUnlocked) return true; - const tents: IResource = state.getResource('tents'); - if (tents.value === tents.max(state)) { + const compounds: IResource = state.getResource('cmpnd'); + if (compounds.value > 0) { this._isUnlocked = true; } return this._isUnlocked; diff --git a/src/model/resource/IResource.ts b/src/model/resource/IResource.ts index 8c58c44..537aebe 100644 --- a/src/model/resource/IResource.ts +++ b/src/model/resource/IResource.ts @@ -1,5 +1,6 @@ enum ResourceType { Religion = 'religion', + Job = 'job', Consumable = 'consumable', Infrastructure = 'infrastructure', Passive = 'passive' @@ -11,6 +12,7 @@ interface IResource { resourceType: ResourceType; value: number; + valueInWholeNumbers: boolean; clickText: string; clickDescription: string; diff --git a/src/model/resource/Job.ts b/src/model/resource/Job.ts new file mode 100644 index 0000000..081ffff --- /dev/null +++ b/src/model/resource/Job.ts @@ -0,0 +1,69 @@ +/// + +abstract class Job implements IResource { + public readonly resourceType: ResourceType = ResourceType.Job; + public value: number = 0; + public readonly valueInWholeNumbers: boolean = true; + + public clickText: string = 'Hire'; + public clickDescription: string = 'Promote one of your followers.'; + + public cost: { [key: string]: number } = { }; + + protected _costMultiplier: { [key: string]: number } = { }; + protected _isUnlocked: boolean = false; + + constructor ( + public readonly name: string, + public readonly description: string + ) { } + + public clickAction (state: GameState): void { + if (this._availableJobs(state) <= 0) { + state.log('You have no unemployed followers to promote.'); + return; + } + if (this.value < this.max(state) && state.deductCost(this.cost)) { + this.value++; + state.log(this._hireLog(1, state)); + for (const rkey of Object.keys(this._costMultiplier)) { + this.cost[rkey] *= this._costMultiplier[rkey]; + } + } + } + + public inc (state: GameState): number | null { + return null; + } + + public max (state: GameState): number | null { + return null; + } + + public advanceAction (time: number, state: GameState): void { + return; + } + + public isUnlocked (state: GameState): boolean { + return this._isUnlocked; + } + + protected _availableJobs (state: GameState): number { + // number of followers minus the number of filled jobs + const followers: number = state.getResource('plorg').value; + const hired: number = state.getResources() + .reduce((tot: number, rkey: string): number => { + const res: IResource = state.getResource(rkey); + return res.resourceType === ResourceType.Job + ? tot + res.value + : tot; + }, 0); + let max: number = followers - hired; + if (max < 0) max = 0; + return max; + } + + protected _hireLog (amount: number, state: GameState): string { + return `You hired ${amount} x ${this.name}.`; + } +} diff --git a/src/model/resource/Money.ts b/src/model/resource/Money.ts index 2cf2f94..1ae8a54 100644 --- a/src/model/resource/Money.ts +++ b/src/model/resource/Money.ts @@ -5,11 +5,10 @@ class Money extends Purchasable { public resourceType: ResourceType = ResourceType.Consumable; public cost: { [key: string]: number } = { }; + public readonly valueInWholeNumbers: boolean = false; constructor ( - public value: number, - private readonly _baseTitheAmount: number, - private readonly _baseCryptoReturnAmount: number + public value: number ) { super('Money', 'Used to purchase goods and services.'); this.clickText = 'Collect Tithes'; @@ -22,8 +21,13 @@ class Money extends Purchasable { } public inc (state: GameState): number { + let inc: number = 0; + // crypto currency - return state.getResource('crpto').value * this._baseCryptoReturnAmount; + inc += state.getResource('crpto').value + * state.config.cfgCryptoReturnAmount; + + return inc; } protected _purchaseAmount (state: GameState): number { @@ -33,12 +37,12 @@ class Money extends Purchasable { return 0; } const diff: number = state.now - this._lastCollectionTime; - if (diff < 30000) { - const lost: number = 30000 / diff / 3; + if (diff < state.config.cfgTimeBetweenTithes) { + const lost: number = state.config.cfgTimeBetweenTithes / diff / 3; state.getResource('creds').value -= lost; } // each follower gives you $10 - const tithings: number = plorg.value * this._baseTitheAmount; + const tithings: number = plorg.value * state.config.cfgTitheAmount; this._lastCollectionTime = state.now; return tithings; } diff --git a/src/model/resource/Passive.ts b/src/model/resource/Passive.ts index fe5a4e9..63a91c3 100644 --- a/src/model/resource/Passive.ts +++ b/src/model/resource/Passive.ts @@ -6,6 +6,7 @@ abstract class Passive implements IResource { public readonly clickDescription: null = null; public readonly cost: null = null; public readonly clickAction: null = null; + public readonly valueInWholeNumbers: boolean = false; protected _baseMax: number | null; protected _baseInc: number | null; diff --git a/src/model/resource/Pastor.ts b/src/model/resource/Pastor.ts new file mode 100644 index 0000000..1b662cd --- /dev/null +++ b/src/model/resource/Pastor.ts @@ -0,0 +1,37 @@ +/// + +class Pastor extends Job { + private _timeSinceLastTithe: number = 0; + + constructor () { + super('Pastors', 'Leaders of the faith.'); + } + + public max (state: GameState): number { + return state.getResource('chrch').value * 2; + } + + public isUnlocked (state: GameState): boolean { + if (this._isUnlocked) return true; + this._isUnlocked = state.getResource('chrch').isUnlocked(state); + } + + public advanceAction (time: number, state: GameState): void { + this._timeSinceLastTithe += time; + if (this._timeSinceLastTithe >= state.config.cfgTimeBetweenTithes) { + const money: IResource = state.getResource('money'); + const plorg: IResource = state.getResource('plorg'); + // each pastor can collect from up to 100 followers + let tithed: number = this.value * 100; + if (plorg.value < tithed) tithed = plorg.value; + let collected: number = tithed * state.config.cfgTitheAmount; + if (collected > money.max(state) - money.value) + collected = money.max(state) - money.value; + if (collected > 0) { + money.value += collected; + state.log(`Your pastors collected $${state.formatNumber(collected)} in tithings from ${state.formatNumber(tithed)} followers.`); + } + this._timeSinceLastTithe = 0; + } + } +} diff --git a/src/model/resource/PlayerOrg.ts b/src/model/resource/PlayerOrg.ts index fd309da..9131471 100644 --- a/src/model/resource/PlayerOrg.ts +++ b/src/model/resource/PlayerOrg.ts @@ -6,9 +6,9 @@ class PlayerOrg implements IResource { public readonly description: string = 'In you they trust.'; public cost: { [key: string]: number } = { }; - public readonly inc: null = null; public value: number = 0; + public readonly valueInWholeNumbers: boolean = true; public clickText: string = 'Recruit'; public clickDescription: string = 'Gather new followers.'; @@ -53,6 +53,16 @@ class PlayerOrg implements IResource { } } + public inc (state: GameState): number { + let inc: number = 0; + + // pastor auto-recruit + inc += state.getResource('pstor').value + * state.config.cfgPastorRecruitRate; + + return inc; + } + public advanceAction (time: number, state: GameState): void { // chance to lose some followers every 10s if credibility < 100% if (state.now - this._lastLostTime > 10000) { diff --git a/src/model/resource/Purchasable.ts b/src/model/resource/Purchasable.ts index fa40c5c..0620059 100644 --- a/src/model/resource/Purchasable.ts +++ b/src/model/resource/Purchasable.ts @@ -3,6 +3,7 @@ abstract class Purchasable implements IResource { public readonly resourceType: ResourceType = ResourceType.Consumable; public value: number = 0; + public valueInWholeNumbers: boolean = true; public clickText: string = 'Purchase'; public clickDescription: string = 'Purchase'; diff --git a/src/model/resource/Religion.ts b/src/model/resource/Religion.ts index b645147..4fa83cd 100644 --- a/src/model/resource/Religion.ts +++ b/src/model/resource/Religion.ts @@ -9,6 +9,7 @@ class Religion implements IResource { public readonly cost: null = null; public readonly max: null = null; public readonly inc: null = null; + public readonly valueInWholeNumbers: boolean = true; constructor ( public readonly name: string, diff --git a/src/render/DebugRenderer.ts b/src/render/DebugRenderer.ts index f10458d..1649e41 100644 --- a/src/render/DebugRenderer.ts +++ b/src/render/DebugRenderer.ts @@ -82,7 +82,10 @@ class DebugRenderer implements IRenderer { el.getElementsByClassName('resource-value')[0]; const elT: Element = el.getElementsByClassName('resource-max')[0]; - elV.innerHTML = state.formatNumber(resource.value); + const value: number = resource.valueInWholeNumbers + ? Math.floor(resource.value) + : resource.value; + elV.innerHTML = state.formatNumber(value); elT.innerHTML = resource.max !== null && resource.max(state) !== null ? ` / ${state.formatNumber(resource.max(state))}`