diff --git a/src/model/GameConfig.ts b/src/model/GameConfig.ts index 45ea19f..2f70699 100644 --- a/src/model/GameConfig.ts +++ b/src/model/GameConfig.ts @@ -14,6 +14,7 @@ class GameConfig { public worldPopulation = 790000000; + public numberFormatDigits = 1; // religion configs public relChristianitySharer = 0.325; @@ -25,73 +26,128 @@ class GameConfig { public relOtherShare = 0.02; public relNoneShare = 0.16; - public cfgStartingPlayerMax = 5; - public cfgStartingMoneyMax = 500000; - public cfgStartingTentMax = 5; - public cfgStartingCryptoMax = 1000; - public cfgStartingMegaChurchMax = 2; + // general configs + public cfgPassiveMax = 100; - public cfgTitheAmount = 10; - public cfgTimeBetweenTithes = 30000; - public cfgCryptoReturnAmount = 1; + public cfgCredibilityFollowerLossRatio = 0.04; + public cfgCredibilityFollowerLossTime = 10000; public cfgCredibilityRestoreRate = 0.25; + + public cfgFollowerGainLossLogTimer = 10000; + public cfgFollowerStartingMax = 5; public cfgPastorRecruitRate = 0.01; + public cfgTimeBetweenTithes = 30000; + public cfgTitheAmount = 10; + public cfgCryptoReturnAmount = 1; + public cfgMoneyStartingMax = 500000; + public cfgPastorTitheCollectionFollowerMax = 100; + + public cfgBuildingPermitCost = 250000; + + public cfgChurchCostMultiplier = 1.01; + public cfgChurchPastorCapacity = 2; + public cfgChurchStartingCost = 150000; + public cfgCompoundChurchCapacity = 1; + public cfgCompoundCostMultiplier = 1.5; + public cfgCompoundHouseCapacity = 2; + public cfgCompoundMoneyCapacity = 500000; + public cfgCompoundStartingCost = 15000; + public cfgCompoundTentCapacity = 10; + public cfgCryptoCostMultiplier = 1.1; + public cfgCryptoStartingCost = 100; + public cfgCryptoStartingMax = 1000; + public cfgHouseCostMultiplier = 1.01; + public cfgHouseFollowerCapacity = 10; + public cfgHouseStartingCost = 75000; + public cfgMegaChurchCostMultiplier = 1.01; + public cfgMegaChurchPastorCapacity = 5; + public cfgMegaChurchStartingCost = 7500000; + public cfgMegaChurchStartingMax = 2; + public cfgTentCostMultiplier = 1.05; + public cfgTentFollowerCapacity = 2; + public cfgTentStartingCost = 250; + public cfgTentStartingMax = 5; + public generateState (): GameState { const state = new GameState(this); // create player organization - state.addResource('plorg', new PlayerOrg()); + state.addResource(ResourceKey.playerOrg, new PlayerOrg()); // create world religions - state.addResource('xtian', new Religion( + state.addResource(ResourceKey.christianity, new Religion( 'Christianity', 'God, Jesus, Bible, churches.', this.relChristianitySharer * this.worldPopulation)); - state.addResource('islam', new Religion( + state.addResource(ResourceKey.islam, new Religion( 'Islam', 'God, Muhammad, Quran, mosques.', this.relIslamShare * this.worldPopulation)); - state.addResource('hindu', new Religion( + state.addResource(ResourceKey.hinduism, new Religion( 'Hinduism', 'Dogma-free spiritualism.', this.relHinduismShare * this.worldPopulation)); - state.addResource('buddh', new Religion( + state.addResource(ResourceKey.buddhism, new Religion( 'Buddhism', 'The minimization of suffering.', this.relBuddhismShare * this.worldPopulation)); - state.addResource('sikhi', new Religion( + state.addResource(ResourceKey.sikhism, new Religion( 'Sikhism', 'Meditation and ten Gurus', this.relSikhismShare * this.worldPopulation)); - state.addResource('judah', new Religion( + state.addResource(ResourceKey.judaism, new Religion( 'Judaism', 'God, Abraham, Torah, synagogues.', this.relJudaismShare * this.worldPopulation)); - state.addResource('other', new Religion( + state.addResource(ResourceKey.other, new Religion( 'Other', 'A variety of belief systems.', this.relOtherShare * this.worldPopulation)); - state.addResource('agnos', new Religion( + state.addResource(ResourceKey.atheism, new Religion( 'Non-Religious', 'Atheists and agnostics.', this.relNoneShare * this.worldPopulation)); // add jobs - state.addResource('pstor', new Pastor()); + state.addResource(ResourceKey.pastors, new Pastor()); // add resources - state.addResource('money', new Money(3.50)); - state.addResource('crpto', new CryptoCurrency()); - state.addResource('tents', new Tent()); - state.addResource('house', new House()); - state.addResource('chrch', new Church()); - state.addResource('cmpnd', new Compound()); - state.addResource('blpmt', new BuildingPermit()); - state.addResource('mchch', new MegaChurch()); + state.addResource(ResourceKey.money, new Money(3.50)); + state.addResource(ResourceKey.faithCoin, new CryptoCurrency(this)); + state.addResource(ResourceKey.tents, new Tent(this)); + state.addResource(ResourceKey.houses, new House(this)); + state.addResource(ResourceKey.churches, new Church(this)); + state.addResource(ResourceKey.compounds, new Compound(this)); + state.addResource(ResourceKey.megaChurches, new MegaChurch(this)); + + // add research + state.addResource(ResourceKey.buildingPermit, new BuildingPermit(this)); // add passive resources - state.addResource('creds', new Credibility()); + state.addResource(ResourceKey.credibility, new Credibility(this)); return state; } + + public formatNumber (num: number): string { + type UnitLookup = { value: number, symbol: string }; + const lookup: UnitLookup[] = [ + { 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+$/; + let item: UnitLookup | undefined; + for (item of lookup.slice().reverse()) { + if (num >= item.value) break; + } + return item !== undefined + ? (num / item.value).toFixed( + this.numberFormatDigits).replace(rx, '$1') + item.symbol + : num.toFixed(this.numberFormatDigits).replace(rx, '$1'); + } } diff --git a/src/model/GameState.ts b/src/model/GameState.ts index 37c7137..788468a 100644 --- a/src/model/GameState.ts +++ b/src/model/GameState.ts @@ -3,7 +3,6 @@ class GameState { public onResourceClick: Array<() => void> = []; public logger: ILogger | null = null; - public numberFormatDigits = 1; public now = 0; @@ -13,15 +12,15 @@ class GameState { private _timeSinceSave = 0; private readonly _timeBetweenSaves = 10000; - private _resources: { [key: string]: IResource } = { }; - private readonly _resourceKeys: string[] = []; + private _resources: { [key in ResourceKey]?: IResource } = { }; + private readonly _resourceKeys: ResourceKey[] = []; constructor (config: GameConfig) { this.config = config; } - public addResource (key: string, resource: IResource): void { + public addResource (key: ResourceKey, resource: IResource): void { this._resourceKeys.push(key); this._resources[key] = resource; } @@ -38,7 +37,7 @@ class GameState { // advance each resource for (const rkey of this._resourceKeys) { const resource = this._resources[rkey]; - if (this._resources[rkey].isUnlocked(this)) { + if (resource?.isUnlocked(this) === true) { if (resource.advanceAction !== null) resource.advanceAction(time, this); } @@ -47,34 +46,34 @@ class GameState { // perform auto increments for (const rkey of this._resourceKeys) { const resource = this._resources[rkey]; - if (!resource.isUnlocked(this)) continue; + if (resource === undefined || !resource.isUnlocked(this)) continue; if (resource.inc !== null && (resource.max === null - || this._resources[rkey].value < resource.max(this))) { - this._resources[rkey].addValue(resource.inc(this) * time / 1000, this); + || resource.value < resource.max(this))) { + resource.addValue(resource.inc(this) * time / 1000, this); } if (resource.max !== null && resource.value > resource.max(this)) { - this._resources[rkey].addValue( - (resource.value - resource.max(this)) * -1, this); + resource.addValue((resource.value - resource.max(this)) * -1, this); } if (resource.value < 0) { - this._resources[rkey].addValue(resource.value * -1, this); + resource.addValue(resource.value * -1, this); } } } - public getResources (): string[] { + public getResources (): ResourceKey[] { return this._resourceKeys; } - public getResource (key: string): IResource { - return this._resources[key]; + public getResource (key: ResourceKey): IResource | null { + const resource = this._resources[key]; + return resource !== undefined ? resource : null; } - public performClick (resourceKey: string): void { + public performClick (resourceKey: ResourceKey): void { const resource = this._resources[resourceKey]; - if (!resource.isUnlocked(this)) return; + if (resource === undefined || !resource.isUnlocked(this)) return; if (resource.clickAction !== null) { resource.clickAction(this); @@ -84,47 +83,31 @@ class GameState { } } - public deductCost (cost: { [rkey: string]: number } | null): boolean { + public deductCost (cost: { [key in ResourceKey]?: number } | null): boolean { if (cost === null) return true; if (!this.isPurchasable(cost)) return false; - for (const rkey of Object.keys(cost)) { - this._resources[rkey].addValue(cost[rkey] * -1, this); + for (const key in cost) { + const rkey = key; + const resource = this._resources[rkey]; + const resCost = cost[rkey]; + if (resource === undefined || resCost === undefined) continue; + resource.addValue(resCost * -1, this); } return true; } - public isPurchasable (cost: { [rkey: string]: number } | null): boolean { + public isPurchasable ( + cost: { [key in ResourceKey]?: number } | null): boolean { if (cost === null) return true; - for (const rkey of Object.keys(cost)) { - if (this._resources[rkey].value < cost[rkey]) { + for (const key in cost) { + const rkey = key; + if ((this._resources[rkey]?.value ?? 0) < (cost[rkey] ?? 0)) { return false; } } return true; } - public formatNumber (num: number): string { - type UnitLookup = { value: number, symbol: string }; - const lookup: UnitLookup[] = [ - { 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+$/; - let item: UnitLookup | undefined; - for (item of lookup.slice().reverse()) { - if (num >= item.value) break; - } - return item !== undefined - ? (num / item.value).toFixed( - this.numberFormatDigits).replace(rx, '$1') + item.symbol - : num.toFixed(this.numberFormatDigits).replace(rx, '$1'); - } - public log (text: string): void { if (this.logger !== null) { this.logger.msg(text); @@ -137,10 +120,11 @@ class GameState { maj: this._versionMaj, min: this._versionMin, }; - for (const rkey of this._resourceKeys) { + for (const key in this._resources) { + const rkey = key; saveObj[rkey] = { - value: this._resources[rkey].value, - cost: this._resources[rkey].cost, + value: this._resources[rkey]?.value ?? 0, + cost: this._resources[rkey]?.cost ?? null, }; } const saveStr: string = btoa(JSON.stringify(saveObj)); @@ -153,11 +137,12 @@ class GameState { try { const saveObj: SaveData = JSON.parse(atob(saveStr)); if (this._versionMaj === saveObj.version?.maj) { - for (const rkey of this._resourceKeys) { + for (const key in this._resources) { + const rkey = key; const saveRes = <{ value: number; cost: { [key: string]: number } | null; - } | undefined> saveObj[rkey]; + } | undefined> saveObj[key]; if (saveRes !== undefined) { // @ts-expect-error writing read-only value from save data this._resources[rkey].value = saveRes.value; diff --git a/src/model/resource/BuildingPermit.ts b/src/model/resource/BuildingPermit.ts index 2ed0bd4..ab66fbb 100644 --- a/src/model/resource/BuildingPermit.ts +++ b/src/model/resource/BuildingPermit.ts @@ -1,16 +1,16 @@ /// class BuildingPermit extends Research { - constructor () { + constructor (config: GameConfig) { super('Building Permit', 'Unlocks several new buildings you can build outside of your compounds.'); - this.cost.money = 250000; + this.cost.money = config.cfgBuildingPermitCost; } public isUnlocked (state: GameState): boolean { if (this._isUnlocked) return true; - const compounds = state.getResource('cmpnd'); - if (compounds.value > 0) { + const compounds = state.getResource(ResourceKey.compounds); + if (compounds !== null && compounds.value > 0) { this._isUnlocked = true; } return this._isUnlocked; diff --git a/src/model/resource/Church.ts b/src/model/resource/Church.ts index f95d217..3c767c8 100644 --- a/src/model/resource/Church.ts +++ b/src/model/resource/Church.ts @@ -1,20 +1,20 @@ /// class Church extends Infrastructure { - constructor () { + constructor (config: GameConfig) { super('Churches', - 'Preaching grounds for 2 pastors.'); - this.cost.money = 150000; - this._costMultiplier.money = 1.01; + `Preaching grounds for ${config.formatNumber(config.cfgChurchPastorCapacity)} pastors.`); + this.cost.money = config.cfgChurchStartingCost; + this._costMultiplier.money = config.cfgChurchCostMultiplier; } public max: (state: GameState) => number = (state) => - state.getResource('cmpnd').value; + state.getResource(ResourceKey.compounds)?.value ?? 0; public isUnlocked (state: GameState): boolean { if (this._isUnlocked) return true; - const compounds = state.getResource('cmpnd'); - if (compounds.value > 0) { + const compounds = state.getResource(ResourceKey.compounds); + if (compounds != null && compounds.value > 0) { this._isUnlocked = true; } return this._isUnlocked; diff --git a/src/model/resource/Compound.ts b/src/model/resource/Compound.ts index 14025d9..23ed048 100644 --- a/src/model/resource/Compound.ts +++ b/src/model/resource/Compound.ts @@ -1,17 +1,17 @@ /// class Compound extends Infrastructure { - constructor () { + constructor (config: GameConfig) { super('Compounds', 'Provides space for tents, houses, and churches and a place to hide more money.'); - this.cost.money = 15000; - this._costMultiplier.money = 1.5; + this.cost.money = config.cfgCompoundStartingCost; + this._costMultiplier.money = config.cfgCompoundCostMultiplier; } public isUnlocked (state: GameState): boolean { if (this._isUnlocked) return true; - const tents = state.getResource('tents'); - if (tents.value >= 5) { + const tents = state.getResource(ResourceKey.tents); + if (tents !== null && tents.value >= state.config.cfgTentStartingMax) { this._isUnlocked = true; } return this._isUnlocked; diff --git a/src/model/resource/Credibility.ts b/src/model/resource/Credibility.ts index d928740..f682422 100644 --- a/src/model/resource/Credibility.ts +++ b/src/model/resource/Credibility.ts @@ -1,14 +1,16 @@ /// class Credibility extends Passive { - constructor () { + constructor (config: GameConfig) { super( 'Credibility', 'Affects your ability to recruit and retain followers.'); - this.value = 100; + this.value = config.cfgPassiveMax; } - public max: (state: GameState) => number = (_state) => 100; + public max: (state: GameState) => number = (state) => + state.config.cfgPassiveMax; + public inc: (state: GameState) => number = (state) => state.config.cfgCredibilityRestoreRate; } diff --git a/src/model/resource/CryptoCurrency.ts b/src/model/resource/CryptoCurrency.ts index 10efdba..f96fadd 100644 --- a/src/model/resource/CryptoCurrency.ts +++ b/src/model/resource/CryptoCurrency.ts @@ -1,14 +1,14 @@ /// class CryptoCurrency extends Purchasable { - constructor () { + constructor (config: GameConfig) { super('Faithcoin', "A crypto coin that can't be spent directly, but provides a steady stream of passive income."); - this.cost.money = 100; - this._costMultiplier.money = 1.1; + this.cost.money = config.cfgCryptoStartingCost; + this._costMultiplier.money = config.cfgCryptoCostMultiplier; this.valueInWholeNumbers = false; } public max: (state: GameState) => number = (state) => - state.config.cfgStartingCryptoMax; + state.config.cfgCryptoStartingMax; } diff --git a/src/model/resource/House.ts b/src/model/resource/House.ts index 48618d4..eca4dbf 100644 --- a/src/model/resource/House.ts +++ b/src/model/resource/House.ts @@ -1,21 +1,21 @@ /// class House extends Infrastructure { - constructor () { + constructor (config: GameConfig) { super('Houses', - 'Provides room to house 10 followers.'); - this.cost.money = 75000; - this._costMultiplier.money = 1.01; + `Provides room to house ${config.formatNumber(config.cfgHouseFollowerCapacity)} followers.`); + this.cost.money = config.cfgHouseStartingCost; + this._costMultiplier.money = config.cfgHouseCostMultiplier; } - // two houses per compound public max: (state: GameState) => number = (state) => - state.getResource('cmpnd').value * 2; + (state.getResource(ResourceKey.compounds)?.value ?? 0) + * state.config.cfgCompoundHouseCapacity; public isUnlocked (state: GameState): boolean { if (this._isUnlocked) return true; - const compounds = state.getResource('cmpnd'); - if (compounds.value > 0) { + const compounds = state.getResource(ResourceKey.compounds); + if (compounds !== null && compounds.value > 0) { this._isUnlocked = true; } return this._isUnlocked; diff --git a/src/model/resource/IResource.ts b/src/model/resource/IResource.ts index 4ee0815..91e1985 100644 --- a/src/model/resource/IResource.ts +++ b/src/model/resource/IResource.ts @@ -7,6 +7,28 @@ enum ResourceType { passive = 'passive', } +enum ResourceKey { + playerOrg = 'plorg', + christianity = 'xtian', + islam = 'islam', + hinduism = 'hindu', + buddhism = 'buddh', + sikhism = 'sikhi', + judaism = 'judah', + other = 'other', + atheism = 'agnos', + pastors = 'pstor', + money = 'money', + faithCoin = 'crpto', + tents = 'tents', + houses = 'houses', + churches = 'chrch', + compounds = 'cmpnd', + buildingPermit = 'blpmt', + megaChurches = 'mchch', + credibility = 'creds', +} + interface IResource { readonly resourceType: ResourceType; readonly name: string; @@ -17,7 +39,7 @@ interface IResource { // readonly altClickText?: string; // readonly altClickDescription?: string; readonly value: number; - readonly cost: { [key: string]: number } | null; + readonly cost: { [key in ResourceKey]?: number } | null; max: ((state: GameState) => number) | null; inc: ((state: GameState) => number) | null; diff --git a/src/model/resource/Job.ts b/src/model/resource/Job.ts index 206d30f..1388eeb 100644 --- a/src/model/resource/Job.ts +++ b/src/model/resource/Job.ts @@ -6,12 +6,12 @@ abstract class Job implements IResource { public readonly clickText = 'Hire'; public readonly clickDescription = 'Promote one of your followers.'; public value = 0; - public readonly cost: { [key: string]: number } = { }; + public readonly cost: { [key in ResourceKey]?: number } = { }; public max: ((state: GameState) => number) | null = null; public inc: ((state: GameState) => number) | null = null; - protected _costMultiplier: { [key: string]: number } = { }; + protected _costMultiplier: { [key in ResourceKey]?: number } = { }; protected _isUnlocked = false; constructor ( @@ -29,8 +29,10 @@ abstract class Job implements IResource { && state.deductCost(this.cost)) { this.addValue(1); state.log(this._hireLog(1, state)); - for (const rkey of Object.keys(this._costMultiplier)) { - this.cost[rkey] *= this._costMultiplier[rkey]; + for (const key in this._costMultiplier) { + const rkey = key; + this.cost[rkey] = + (this.cost[rkey] ?? 0) * (this._costMultiplier[rkey] ?? 1); } } } @@ -49,11 +51,11 @@ abstract class Job implements IResource { protected _availableJobs (state: GameState): number { // number of followers minus the number of filled jobs - const followers = state.getResource('plorg').value; + const followers = state.getResource(ResourceKey.playerOrg)?.value ?? 0; const hired = state.getResources().reduce( - (tot: number, rkey: string): number => { + (tot: number, rkey: ResourceKey): number => { const res = state.getResource(rkey); - return res.resourceType === ResourceType.job + return res?.resourceType === ResourceType.job ? tot + res.value : tot; }, 0); diff --git a/src/model/resource/MegaChurch.ts b/src/model/resource/MegaChurch.ts index 11f6d0f..843fd38 100644 --- a/src/model/resource/MegaChurch.ts +++ b/src/model/resource/MegaChurch.ts @@ -1,20 +1,20 @@ /// class MegaChurch extends Infrastructure { - constructor () { + constructor (config: GameConfig) { super('MegaChurches', - 'Room for 5 pastors'); - this.cost.money = 7500000; - this._costMultiplier.money = 1.01; + `Room for ${config.formatNumber(config.cfgMegaChurchPastorCapacity)} pastors`); + this.cost.money = config.cfgMegaChurchStartingCost; + this._costMultiplier.money = config.cfgMegaChurchCostMultiplier; } public max: (state: GameState) => number = (state) => - state.config.cfgStartingMegaChurchMax; + state.config.cfgMegaChurchStartingMax; public isUnlocked (state: GameState): boolean { if (this._isUnlocked) return true; - const permit = state.getResource('blpmt'); - if (permit.value > 0) { + const permit = state.getResource(ResourceKey.buildingPermit); + if (permit !== null && permit.value > 0) { this._isUnlocked = true; } return this._isUnlocked; diff --git a/src/model/resource/Money.ts b/src/model/resource/Money.ts index 80c4950..308e5e0 100644 --- a/src/model/resource/Money.ts +++ b/src/model/resource/Money.ts @@ -16,8 +16,9 @@ class Money extends Purchasable { } public max: (state: GameState) => number = (state: GameState) => { - let max = state.config.cfgStartingMoneyMax; - max += state.getResource('cmpnd').value * 500000; + let max = state.config.cfgMoneyStartingMax; + max += (state.getResource(ResourceKey.compounds)?.value ?? 0) + * state.config.cfgCompoundMoneyCapacity; return max; }; @@ -25,31 +26,32 @@ class Money extends Purchasable { let inc = 0; // crypto currency - inc += state.getResource('crpto').value + inc += (state.getResource(ResourceKey.faithCoin)?.value ?? 0) * state.config.cfgCryptoReturnAmount; + // TODO: job salaries + return inc; }; protected _purchaseAmount (state: GameState): number { - const plorg = state.getResource('plorg'); - if (plorg.value === 0) { + const plorg = state.getResource(ResourceKey.playerOrg); + if (plorg === null || plorg.value === 0) { state.log('You have no followers to collect from!'); return 0; } const diff = state.now - this._lastCollectionTime; if (diff < state.config.cfgTimeBetweenTithes) { const lost = state.config.cfgTimeBetweenTithes / diff / 3; - state.getResource('creds').addValue(lost * -1, state); + state.getResource(ResourceKey.credibility)?.addValue(lost * -1, state); } - // each follower gives you $10 const tithings = plorg.value * state.config.cfgTitheAmount; this._lastCollectionTime = state.now; return tithings; } protected _purchaseLog (amount: number, state: GameState): string { - const followers = state.getResource('plorg').value; - return `You collected $${state.formatNumber(amount)} from ${state.formatNumber(followers)} followers.`; + const followers = state.getResource(ResourceKey.playerOrg)?.value ?? 0; + return `You collected $${state.config.formatNumber(amount)} from ${state.config.formatNumber(followers)} followers.`; } } diff --git a/src/model/resource/Pastor.ts b/src/model/resource/Pastor.ts index 1e243c0..943ef8c 100644 --- a/src/model/resource/Pastor.ts +++ b/src/model/resource/Pastor.ts @@ -9,32 +9,36 @@ class Pastor extends Job { } public max: (state: GameState) => number = (state) => { - let max = state.getResource('chrch').value * 2; - max += state.getResource('mchch').value * 5; + let max = (state.getResource(ResourceKey.churches)?.value ?? 0) + * state.config.cfgChurchPastorCapacity; + max += (state.getResource(ResourceKey.megaChurches)?.value ?? 0) + * state.config.cfgMegaChurchPastorCapacity; return max; }; public isUnlocked (state: GameState): boolean { if (this._isUnlocked) return true; - this._isUnlocked = state.getResource('chrch').isUnlocked(state); + this._isUnlocked = state.getResource( + ResourceKey.churches)?.isUnlocked(state) === true; return this._isUnlocked; } public advanceAction (time: number, state: GameState): void { this._timeSinceLastTithe += time; if (this._timeSinceLastTithe >= state.config.cfgTimeBetweenTithes) { - const money = state.getResource('money'); - const plorg = state.getResource('plorg'); - // each pastor can collect from up to 100 followers - let tithed = this.value * 100; - if (Math.floor(plorg.value) < tithed) - tithed = Math.floor(plorg.value); + const money = state.getResource(ResourceKey.money); + const plorg = state.getResource(ResourceKey.playerOrg); + let tithed = this.value + * state.config.cfgPastorTitheCollectionFollowerMax; + if (Math.floor(plorg?.value ?? 0) < tithed) + tithed = Math.floor(plorg?.value ?? 0); let collected = tithed * state.config.cfgTitheAmount; - if (money.max !== null && collected > money.max(state) - money.value) - collected = money.max(state) - money.value; + if (money?.max !== null + && collected > (money?.max(state) ?? 0) - (money?.value ?? 0)) + collected = (money?.max(state) ?? 0) - (money?.value ?? 0); if (collected > 0) { - money.addValue(collected, state); - state.log(`Your pastors collected $${state.formatNumber(collected)} in tithings from ${state.formatNumber(tithed)} followers.`); + money?.addValue(collected, state); + state.log(`Your pastors collected $${state.config.formatNumber(collected)} in tithings from ${state.config.formatNumber(tithed)} followers.`); } this._timeSinceLastTithe = 0; } diff --git a/src/model/resource/PlayerOrg.ts b/src/model/resource/PlayerOrg.ts index 8b130c0..6eb1c28 100644 --- a/src/model/resource/PlayerOrg.ts +++ b/src/model/resource/PlayerOrg.ts @@ -12,13 +12,15 @@ class PlayerOrg implements IResource { private _timeSinceLastLost = 0; private _lastRecruitmentLog = 0; - private _followerSources: { [key: string]: number } = { }; - private _followerDests: { [key: string]: number } = { }; + private _followerSources: { [key in ResourceKey]?: number } = { }; + private _followerDests: { [key in ResourceKey]?: number } = { }; public max (state: GameState): number { - let max = state.config.cfgStartingPlayerMax; - max += state.getResource('tents').value * 2; - max += state.getResource('house').value * 10; + let max = state.config.cfgFollowerStartingMax; + max += (state.getResource(ResourceKey.tents)?.value ?? 0) + * state.config.cfgTentFollowerCapacity; + max += (state.getResource(ResourceKey.houses)?.value ?? 0) + * state.config.cfgHouseFollowerCapacity; return max; } @@ -26,12 +28,13 @@ class PlayerOrg implements IResource { let inc = 0; // pastor recruiting - const pastors = state.getResource('pstor').value; + const pastors = state.getResource(ResourceKey.pastors)?.value ?? 0; inc += pastors * state.config.cfgPastorRecruitRate; // credibility adjustment - const creds = state.getResource('creds'); - if (creds.max !== null) inc *= creds.value / creds.max(state); + const creds = state.getResource(ResourceKey.credibility); + if (creds?.max !== null) inc *= + (creds?.value ?? 0) / (creds?.max(state) ?? state.config.cfgPassiveMax); return inc; } @@ -44,9 +47,10 @@ class PlayerOrg implements IResource { } // chance to fail increases as credibility decreases - const creds = state.getResource('creds'); - if (creds.max !== null) { - const ratio = Math.ceil(creds.value) / creds.max(state); + const creds = state.getResource(ResourceKey.credibility); + if (creds?.max !== null) { + const ratio = Math.ceil(creds?.value ?? 0) / (creds?.max(state) + ?? state.config.cfgPassiveMax); if (Math.random() > ratio) { state.log('Your recruitment efforts failed.'); return; @@ -66,21 +70,21 @@ class PlayerOrg implements IResource { // gained followers must come from other faiths for (let i = 0; i < diff; i++) { const source = this._getRandomReligion(state); - source[1].addValue(-1, state); - const curFollowers = this._followerSources[source[0]]; - this._followerSources[source[0]] = !isNaN(curFollowers) - ? curFollowers + 1 - : 1; + if (source !== null) { + source[1].addValue(-1, state); + const curFollowers = this._followerSources[source[0]] ?? 0; + this._followerSources[source[0]] = curFollowers + 1; + } } } else { // lost followers must return to other faiths for (let i = 0; i < diff * -1; i++) { const dest = this._getRandomReligion(state); - dest[1].addValue(1, state); - const curFollowers = this._followerDests[dest[0]]; - this._followerDests[dest[0]] = !isNaN(curFollowers) - ? curFollowers + 1 - : 1; + if (dest !== null) { + dest[1].addValue(1, state); + const curFollowers = this._followerDests[dest[0]] ?? 0; + this._followerDests[dest[0]] = curFollowers + 1; + } } } } @@ -92,13 +96,17 @@ class PlayerOrg implements IResource { public advanceAction (time: number, state: GameState): void { // chance to lose some followers every 10s if credibility < 100% this._timeSinceLastLost += time; - if (this._timeSinceLastLost > 10000) { + if (this._timeSinceLastLost > state.config.cfgCredibilityFollowerLossTime) { if (this.value > 0) { - const creds = state.getResource('creds'); - if (creds.max !== null) { - const ratio = Math.ceil(creds.value) / creds.max(state); + const creds = state.getResource(ResourceKey.credibility); + if (creds?.max !== null) { + const ratio = + Math.ceil(creds?.value ?? 0) / (creds?.max(state) + ?? state.config.cfgPassiveMax); if (Math.random() > ratio) { - const lost = Math.ceil(this.value / 25 * (1 - ratio)); + const lost = Math.ceil(this.value + * state.config.cfgCredibilityFollowerLossRatio + * (1 - ratio)); this.addValue(lost * -1, state); } } @@ -107,42 +115,54 @@ class PlayerOrg implements IResource { } // log lost and gained followers every 10s - if (state.now - this._lastRecruitmentLog > 10000 + if (state.now + - this._lastRecruitmentLog > state.config.cfgFollowerGainLossLogTimer && (Object.keys(this._followerSources).length > 0 || Object.keys(this._followerDests).length > 0)) { if (Object.keys(this._followerDests).length > 0) { let msg = ''; let total = 0; - for (const rkey of Object.keys(this._followerDests)) { - if (msg !== '') msg += ', '; + for (const key in this._followerDests) { + const rkey = key; const religion = state.getResource(rkey); - msg += `${state.formatNumber(this._followerDests[rkey])} to ${religion.name}`; - total += this._followerDests[rkey]; - delete this._followerDests[rkey]; + const followers = this._followerDests[rkey]; + if (religion !== null && followers !== undefined) { + if (msg !== '') msg += ', '; + msg += `${state.config.formatNumber(followers)} to ${religion.name}`; + total += followers; + delete this._followerDests[rkey]; + } } - state.log(`You lost ${state.formatNumber(total)} followers: ${msg}`); + state.log(`You lost ${state.config.formatNumber(total)} followers: ${msg}`); } if (Object.keys(this._followerSources).length > 0) { let msg = ''; let total = 0; - for (const rkey of Object.keys(this._followerSources)) { - if (msg !== '') msg += ', '; + for (const key in this._followerSources) { + const rkey = key; const religion = state.getResource(rkey); - msg += - `${state.formatNumber(this._followerSources[rkey])} from ${religion.name}`; - total += this._followerSources[rkey]; - delete this._followerSources[rkey]; + const followers = this._followerSources[rkey]; + if (religion !== null && followers !== undefined) { + if (msg !== '') msg += ', '; + msg += + `${state.config.formatNumber(followers)} from ${religion.name}`; + total += followers; + delete this._followerSources[rkey]; + } } - state.log(`You gained ${state.formatNumber(total)} followers: ${msg}`); + state.log(`You gained ${state.config.formatNumber(total)} followers: ${msg}`); } this._lastRecruitmentLog = state.now; } } - private _getRandomReligion (state: GameState): [string, IResource] { - const religs = ['xtian', 'islam', 'hindu', - 'buddh', 'sikhi', 'judah', 'other', 'agnos']; + private _getRandomReligion ( + state: GameState): [ResourceKey, IResource] | null { + const religs = [ResourceKey.christianity, ResourceKey.islam, + ResourceKey.hinduism, ResourceKey.buddhism, ResourceKey.sikhism, + ResourceKey.judaism, ResourceKey.other, ResourceKey.atheism]; const source = religs[Math.floor(Math.random() * 8)]; - return [source, state.getResource(source)]; + const resource = state.getResource(source); + return resource !== null ? [source, resource] : null; } } diff --git a/src/model/resource/Purchasable.ts b/src/model/resource/Purchasable.ts index 42c4229..710edee 100644 --- a/src/model/resource/Purchasable.ts +++ b/src/model/resource/Purchasable.ts @@ -6,12 +6,12 @@ abstract class Purchasable implements IResource { public clickText = 'Purchase'; public clickDescription = 'Purchase'; public value = 0; - public readonly cost: { [key: string]: number } = { }; + public readonly cost: { [key in ResourceKey]?: number } = { }; public inc: ((state: GameState) => number) | null = null; public max: ((_state: GameState) => number) | null = null; - protected _costMultiplier: { [key: string]: number } = { }; + protected _costMultiplier: { [key in ResourceKey]?: number } = { }; protected _isUnlocked = false; constructor ( @@ -27,8 +27,10 @@ abstract class Purchasable implements IResource { if (amount > 0) { this.value += amount; state.log(this._purchaseLog(amount, state)); - for (const rkey of Object.keys(this._costMultiplier)) { - this.cost[rkey] *= this._costMultiplier[rkey]; + for (const key in this._costMultiplier) { + const rkey = key; + this.cost[rkey] = + (this.cost[rkey] ?? 0) * (this._costMultiplier[rkey] ?? 1); } } } diff --git a/src/model/resource/Tent.ts b/src/model/resource/Tent.ts index ca26272..7caae4d 100644 --- a/src/model/resource/Tent.ts +++ b/src/model/resource/Tent.ts @@ -1,17 +1,18 @@ /// class Tent extends Infrastructure { - constructor () { + constructor (config: GameConfig) { super('Tents', - 'Provides room to house 2 followers.'); - this.cost.money = 250; - this._costMultiplier.money = 1.05; + `Provides room to house ${config.formatNumber(config.cfgTentFollowerCapacity)} followers.`); + this.cost.money = config.cfgTentStartingCost; + this._costMultiplier.money = config.cfgTentCostMultiplier; } public max: (state: GameState) => number = (state) => { // ten extra tents per compound - let max = state.config.cfgStartingTentMax; - max += state.getResource('cmpnd').value * 10; + let max = state.config.cfgTentStartingMax; + max += (state.getResource(ResourceKey.compounds)?.value ?? 0) + * state.config.cfgCompoundTentCapacity; return max; }; } diff --git a/src/render/DebugRenderer.ts b/src/render/DebugRenderer.ts index 51ed772..56dacc9 100644 --- a/src/render/DebugRenderer.ts +++ b/src/render/DebugRenderer.ts @@ -5,7 +5,7 @@ class DebugRenderer implements IRenderer { private _handleClick = true; public render (state: GameState): void { - const rkeys: string[] = state.getResources(); + const rkeys = state.getResources(); const container = document.getElementById('irreligious-game'); if (!this._initialized) { if (container === null) { @@ -42,7 +42,8 @@ class DebugRenderer implements IRenderer { } // create containers for each resource for (const rkey of rkeys) { - const resource: IResource = state.getResource(rkey); + const resource = state.getResource(rkey); + if (resource === null) continue; const resContainer = document.getElementById( `resource-container-${resource.resourceType}`); if (resContainer === null) continue; @@ -70,8 +71,7 @@ class DebugRenderer implements IRenderer { el.innerHTML = content; resContainer.appendChild(el); if (resource.clickAction !== null) { - const btn: Element = - el.getElementsByClassName('resource-btn')[0]; + const btn = el.getElementsByClassName('resource-btn')[0]; btn.addEventListener('click', (): void => { state.performClick(rkey); }); @@ -92,38 +92,34 @@ class DebugRenderer implements IRenderer { }); } for (const rkey of rkeys) { - const resource: IResource = state.getResource(rkey); + const resource = state.getResource(rkey); + if (resource === null) continue; const el = document.getElementById(`resource-details-${rkey}`); if (el !== null && resource.isUnlocked(state)) { if (el.className !== 'resource') el.className = 'resource'; - const elV: Element = - el.getElementsByClassName('resource-value')[0]; - const elT: Element = - el.getElementsByClassName('resource-max')[0]; - const value: number = resource.valueInWholeNumbers + const elV = el.getElementsByClassName('resource-value')[0]; + const elT = el.getElementsByClassName('resource-max')[0]; + const value = resource.valueInWholeNumbers ? Math.floor(resource.value) : resource.value; - elV.innerHTML = state.formatNumber(value); + elV.innerHTML = state.config.formatNumber(value); elT.innerHTML = resource.max !== null - ? ` / ${state.formatNumber(resource.max(state))}` + ? ` / ${state.config.formatNumber(resource.max(state))}` : ''; - const elB: HTMLCollectionOf = - el.getElementsByClassName('resource-btn'); + const elB = el.getElementsByClassName('resource-btn'); if (elB.length > 0) { - const enabled: boolean = state.isPurchasable(resource.cost) + const enabled = state.isPurchasable(resource.cost) && (resource.max === null || resource.value < resource.max(state)); if (enabled) elB[0].removeAttribute('disabled'); else elB[0].setAttribute('disabled', 'disabled'); } if (resource.inc !== null && resource.inc(state) > 0) { - const elI: Element = - el.getElementsByClassName('resource-inc')[0]; + const elI = el.getElementsByClassName('resource-inc')[0]; elI.innerHTML = - ` +${state.formatNumber(resource.inc(state))}/s`; + ` +${state.config.formatNumber(resource.inc(state))}/s`; } if (this._handleClick) { - const elC: HTMLCollectionOf = - el.getElementsByClassName('resource-cost'); + const elC = el.getElementsByClassName('resource-cost'); if (elC.length > 0) { elC[0].innerHTML = this._getCostStr(resource, state); } @@ -152,15 +148,14 @@ class DebugRenderer implements IRenderer { private _getCostStr (resource: IResource, state: GameState): string { let cost = ''; - if (resource.cost !== null) { - for (const rkey of state.getResources()) { - if (isNaN(resource.cost[rkey])) continue; + for (const rkey of state.getResources()) { + if (resource.cost?.[rkey] !== undefined) { if (cost !== '') cost += ', '; - if (rkey === 'money') { - cost += `$${state.formatNumber(resource.cost[rkey])}`; + if (rkey === ResourceKey.money) { + cost += `$${state.config.formatNumber(resource.cost[rkey] ?? 0)}`; } else { - cost += `${state.formatNumber(resource.cost[rkey])} - ${state.getResource(rkey).name}`; + cost += `${state.config.formatNumber(resource.cost[rkey] ?? 0)} + ${state.getResource(rkey)?.name ?? rkey}`; } } }