diff --git a/src/model/GameConfig.ts b/src/model/GameConfig.ts index 9c04cf8..bb1f5db 100644 --- a/src/model/GameConfig.ts +++ b/src/model/GameConfig.ts @@ -2,6 +2,7 @@ /// /// /// +/// /// /// /// @@ -58,12 +59,19 @@ class GameConfig { }; public cfgSalary: ResourceNumber = { - pastors: 7.5, + pastors: 250, + compoundManagers: 1000, }; public cfgCapacity: { [key in ResourceKey]?: ResourceNumber } = { churches: { pastors: 2 }, - compounds: { churches: 1, houses: 2, money: 500000, tents: 10 }, + compounds: { + churches: 1, + compoundManagers: 1, + houses: 2, + money: 500000, + tents: 10, + }, houses: { followers: 10 }, megaChurches: { pastors: 5 }, tents: { followers: 2 }, @@ -78,6 +86,8 @@ class GameConfig { public cfgCryptoMarketGrowthBias = 0.1; public cfgDefaultSellMultiplier = 0.5; public cfgFollowerGainLossLogTimer = 10000; + public cfgNoMoneyQuitRate = 0.2; + public cfgNoMoneyQuitTime = 10000; public cfgPassiveMax = 100; public cfgPastorRecruitRate = 0.01; public cfgPastorTitheCollectionFollowerMax = 100; @@ -184,6 +194,7 @@ class GameConfig { // add jobs state.addResource(ResourceKey.pastors, new Pastor()); + state.addResource(ResourceKey.compoundManagers, new CompoundManager()); // add resources state.addResource(ResourceKey.money, new Money(3.5)); diff --git a/src/model/Utils.ts b/src/model/Utils.ts index 3ac5d80..c7f096e 100644 --- a/src/model/Utils.ts +++ b/src/model/Utils.ts @@ -14,10 +14,15 @@ function formatNumber(num: number): string { const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; let item: UnitLookup | undefined; for (item of lookup.slice().reverse()) { - if (num >= item.value) break; + if (Math.abs(num) >= item.value) break; } - return item !== undefined - ? (num / item.value).toFixed(numberFormatDigits).replace(rx, '$1') + - item.symbol - : num.toFixed(numberFormatDigits).replace(rx, '$1'); + const sign = num < 0 ? '-' : ''; + const number = + item !== undefined + ? (Math.abs(num) / item.value) + .toFixed(numberFormatDigits) + .replace(rx, '$1') + item.symbol + : Math.abs(num).toFixed(numberFormatDigits).replace(rx, '$1'); + + return `${sign}${number}`; } diff --git a/src/model/resource/CompoundManager.ts b/src/model/resource/CompoundManager.ts new file mode 100644 index 0000000..7941f5c --- /dev/null +++ b/src/model/resource/CompoundManager.ts @@ -0,0 +1,25 @@ +/// + +class CompoundManager extends Job { + constructor() { + super( + 'Compound Managers', + 'compound manager', + 'compound managers', + 'Automatically purchase tents, houses, and churches when money and compound space permits.' + ); + } + + public max: (state: GameState) => number = (state) => { + return ( + (state.resource.compounds?.value ?? 0) * + (state.config.cfgCapacity.compounds?.compoundManagers ?? 0) + ); + }; + + public isUnlocked(state: GameState): boolean { + if (this._isUnlocked) return true; + this._isUnlocked = state.resource.compounds?.isUnlocked(state) === true; + return this._isUnlocked; + } +} diff --git a/src/model/resource/Follower.ts b/src/model/resource/Follower.ts index dc3542c..934df5e 100644 --- a/src/model/resource/Follower.ts +++ b/src/model/resource/Follower.ts @@ -1,4 +1,5 @@ /// +/// class Follower implements IResource { public readonly resourceType = ResourceType.religion; @@ -24,6 +25,8 @@ class Follower implements IResource { private _lastRecruitmentLog = 0; private _followerSources: ResourceNumber = {}; private _followerDests: ResourceNumber = {}; + private _timeSinceLastQuit = 0; + private _quitTracker: ResourceNumber = {}; public max(state: GameState): number { let max = state.config.cfgInitialMax.followers ?? 0; @@ -84,7 +87,7 @@ class Follower implements IResource { } public advanceAction(time: number, state: GameState): void { - // chance to lose some followers every 10s if credibility < 100% + // chance to lose some followers if credibility < 100% this._timeSinceLastLost += time; if (this._timeSinceLastLost > state.config.cfgCredibilityFollowerLossTime) { if (this.value > 0) { @@ -104,12 +107,40 @@ class Follower implements IResource { this._timeSinceLastLost = 0; } - // log lost and gained followers every 10s + // chance for some followers to quit their jobs if money === 0 + const money = state.resource.money; + const totalJobs = Job.totalJobs(state); + if (money !== undefined && money.value <= 0 && totalJobs > 0) { + this._timeSinceLastQuit += time; + if (this._timeSinceLastQuit > state.config.cfgNoMoneyQuitTime) { + let lost = Math.ceil(totalJobs * state.config.cfgNoMoneyQuitRate); + for (const rkey of Job.jobResources(state)) { + const job = state.resource[rkey]; + if (job !== undefined && job.value > 0) { + if (job.value >= lost) { + job.addValue(lost * -1, state); + this._quitTracker[rkey] = lost; + break; + } else { + job.addValue(job.value * -1, state); + this._quitTracker[rkey] = job.value; + lost -= job.value; + } + } + } + this._timeSinceLastQuit = 0; + } + } else { + this._timeSinceLastQuit = 0; + } + + // log lost, gained, and quit followers at regular intervals if ( state.now - this._lastRecruitmentLog > state.config.cfgFollowerGainLossLogTimer && (Object.keys(this._followerSources).length > 0 || - Object.keys(this._followerDests).length > 0) + Object.keys(this._followerDests).length > 0 || + Object.keys(this._quitTracker).length > 0) ) { if (Object.keys(this._followerDests).length > 0) { let msg = ''; @@ -155,6 +186,28 @@ class Follower implements IResource { }: ${msg}` ); } + if (Object.keys(this._quitTracker).length > 0) { + let msg = ''; + let total = 0; + for (const key in this._quitTracker) { + const rkey = key; + const job = state.resource[rkey]; + const followers = this._quitTracker[rkey]; + if (job !== undefined && followers !== undefined) { + if (msg !== '') msg += ', '; + msg += `${formatNumber(followers)} ${ + followers > 1 ? job.pluralName : job.singularName + }`; + total += followers; + delete this._quitTracker[rkey]; + } + } + state.log( + `${formatNumber(total)} ${ + total > 1 ? this.pluralName : this.singularName + } quit their jobs: ${msg}` + ); + } this._lastRecruitmentLog = state.now; } } diff --git a/src/model/resource/Job.ts b/src/model/resource/Job.ts index ad812c3..c55f0e2 100644 --- a/src/model/resource/Job.ts +++ b/src/model/resource/Job.ts @@ -15,7 +15,8 @@ abstract class Job implements IResource { description: 'Promote one of your followers.', isEnabled: (state: GameState): boolean => (this.max === undefined || this.value < this.max(state)) && - this._availableJobs(state) > 0, + Job.availableJobs(state) > 0 && + (state.resource.money?.value ?? 0) > 0, performAction: (state: GameState): void => { this._promoteFollower(state); }, @@ -40,6 +41,39 @@ abstract class Job implements IResource { public readonly description: string ) {} + public static jobResources(state: GameState): ResourceKey[] { + return state.resources.filter((rkey) => { + const res = state.resource[rkey]; + return res !== undefined && res.resourceType === ResourceType.job; + }); + } + + public static availableJobs(state: GameState): number { + // number of followers minus the number of filled jobs + const followers = state.resource.followers?.value ?? 0; + const hired = state.resources.reduce( + (tot: number, rkey: ResourceKey): number => { + const res = state.resource[rkey]; + return res?.resourceType === ResourceType.job ? tot + res.value : tot; + }, + 0 + ); + return followers - hired; + } + + public static totalJobs(state: GameState): number { + // number of followers minus the number of filled jobs + const followers = state.resource.followers?.value ?? 0; + const hired = state.resources.reduce( + (tot: number, rkey: ResourceKey): number => { + const res = state.resource[rkey]; + return res?.resourceType === ResourceType.job ? tot + res.value : tot; + }, + 0 + ); + return followers - hired; + } + public addValue(amount: number): void { this.value += amount; if (this.value < 0) this.value = 0; @@ -51,39 +85,14 @@ abstract class Job implements IResource { public advanceAction(_time: number, state: GameState): void { // if we're out of followers then the jobs also vacate - const avail = this._availableJobs(state); + const avail = Job.availableJobs(state); if (avail < 0 && this.value > 0) { this.addValue(avail); } + return; } - protected _availableJobs(state: GameState): number { - // number of followers minus the number of filled jobs - const followers = state.resource.followers?.value ?? 0; - const hired = state.resources.reduce( - (tot: number, rkey: ResourceKey): number => { - const res = state.resource[rkey]; - return res?.resourceType === ResourceType.job ? tot + res.value : tot; - }, - 0 - ); - return followers - hired; - } - - protected _totalPayroll(state: GameState): number { - // number of followers minus the number of filled jobs - const followers = state.resource.followers?.value ?? 0; - const hired = state.resources.reduce( - (tot: number, rkey: ResourceKey): number => { - const res = state.resource[rkey]; - return res?.resourceType === ResourceType.job ? tot + res.value : tot; - }, - 0 - ); - return followers - hired; - } - protected _hireLog(amount: number, _state: GameState): string { return amount > 0 ? `You hired ${amount} ${ @@ -95,7 +104,7 @@ abstract class Job implements IResource { } private _promoteFollower(state: GameState): void { - if (this._availableJobs(state) <= 0) return; + if (Job.availableJobs(state) <= 0) return; if ( this.max !== undefined && this.value < this.max(state) && diff --git a/src/model/resource/Money.ts b/src/model/resource/Money.ts index abe21c8..9394941 100644 --- a/src/model/resource/Money.ts +++ b/src/model/resource/Money.ts @@ -47,6 +47,10 @@ class Money implements IResource { (state.resource.pastors?.value ?? 0) * (state.config.cfgSalary.pastors ?? 0); + inc -= + (state.resource.compoundManagers?.value ?? 0) * + (state.config.cfgSalary.compoundManagers ?? 0); + return inc; }; diff --git a/src/model/resource/SharedTypes.ts b/src/model/resource/SharedTypes.ts index 63a2c87..92551c1 100644 --- a/src/model/resource/SharedTypes.ts +++ b/src/model/resource/SharedTypes.ts @@ -20,6 +20,7 @@ enum ResourceKey { other = 'other', atheism = 'atheism', pastors = 'pastors', + compoundManagers = 'compoundManagers', money = 'money', cryptoCurrency = 'cryptoCurrency', cryptoMarket = 'cryptoMarket', diff --git a/src/render/DebugRenderer.ts b/src/render/DebugRenderer.ts index 6c68479..1be2d6e 100644 --- a/src/render/DebugRenderer.ts +++ b/src/render/DebugRenderer.ts @@ -131,9 +131,13 @@ class DebugRenderer implements IRenderer { } } } - if (resource.inc !== undefined && resource.inc(state) > 0) { - const elI = el.getElementsByClassName('resource-inc')[0]; - elI.innerHTML = ` +${formatNumber(resource.inc(state))}/s`; + const inc = + resource.inc !== undefined ? resource.inc(state) : undefined; + const elI = el.getElementsByClassName('resource-inc')[0]; + if (inc !== undefined && inc !== 0) { + elI.innerHTML = ` ${inc > 0 ? '+' : ''}${formatNumber(inc)}/s`; + } else if (elI.innerHTML !== '') { + elI.innerHTML = ''; } if (this._handleClick) { const elC = el.getElementsByClassName('resource-cost');