added compound manager, followers quit their jobs when you run out of money

This commit is contained in:
Rudis Muiznieks 2021-09-11 20:37:33 -05:00
parent 5fd789ca88
commit 2126ed483b
8 changed files with 154 additions and 42 deletions

View File

@ -2,6 +2,7 @@
/// <reference path="./resource/BuildingPermit.ts" /> /// <reference path="./resource/BuildingPermit.ts" />
/// <reference path="./resource/Church.ts" /> /// <reference path="./resource/Church.ts" />
/// <reference path="./resource/Compound.ts" /> /// <reference path="./resource/Compound.ts" />
/// <reference path="./resource/CompoundManager.ts" />
/// <reference path="./resource/Credibility.ts" /> /// <reference path="./resource/Credibility.ts" />
/// <reference path="./resource/CryptoCurrency.ts" /> /// <reference path="./resource/CryptoCurrency.ts" />
/// <reference path="./resource/CryptoMarket.ts" /> /// <reference path="./resource/CryptoMarket.ts" />
@ -58,12 +59,19 @@ class GameConfig {
}; };
public cfgSalary: ResourceNumber = { public cfgSalary: ResourceNumber = {
pastors: 7.5, pastors: 250,
compoundManagers: 1000,
}; };
public cfgCapacity: { [key in ResourceKey]?: ResourceNumber } = { public cfgCapacity: { [key in ResourceKey]?: ResourceNumber } = {
churches: { pastors: 2 }, 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 }, houses: { followers: 10 },
megaChurches: { pastors: 5 }, megaChurches: { pastors: 5 },
tents: { followers: 2 }, tents: { followers: 2 },
@ -78,6 +86,8 @@ class GameConfig {
public cfgCryptoMarketGrowthBias = 0.1; public cfgCryptoMarketGrowthBias = 0.1;
public cfgDefaultSellMultiplier = 0.5; public cfgDefaultSellMultiplier = 0.5;
public cfgFollowerGainLossLogTimer = 10000; public cfgFollowerGainLossLogTimer = 10000;
public cfgNoMoneyQuitRate = 0.2;
public cfgNoMoneyQuitTime = 10000;
public cfgPassiveMax = 100; public cfgPassiveMax = 100;
public cfgPastorRecruitRate = 0.01; public cfgPastorRecruitRate = 0.01;
public cfgPastorTitheCollectionFollowerMax = 100; public cfgPastorTitheCollectionFollowerMax = 100;
@ -184,6 +194,7 @@ class GameConfig {
// add jobs // add jobs
state.addResource(ResourceKey.pastors, new Pastor()); state.addResource(ResourceKey.pastors, new Pastor());
state.addResource(ResourceKey.compoundManagers, new CompoundManager());
// add resources // add resources
state.addResource(ResourceKey.money, new Money(3.5)); state.addResource(ResourceKey.money, new Money(3.5));

View File

@ -14,10 +14,15 @@ function formatNumber(num: number): string {
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
let item: UnitLookup | undefined; let item: UnitLookup | undefined;
for (item of lookup.slice().reverse()) { for (item of lookup.slice().reverse()) {
if (num >= item.value) break; if (Math.abs(num) >= item.value) break;
} }
return item !== undefined const sign = num < 0 ? '-' : '';
? (num / item.value).toFixed(numberFormatDigits).replace(rx, '$1') + const number =
item.symbol item !== undefined
: num.toFixed(numberFormatDigits).replace(rx, '$1'); ? (Math.abs(num) / item.value)
.toFixed(numberFormatDigits)
.replace(rx, '$1') + item.symbol
: Math.abs(num).toFixed(numberFormatDigits).replace(rx, '$1');
return `${sign}${number}`;
} }

View File

@ -0,0 +1,25 @@
/// <reference path="./Job.ts" />
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;
}
}

View File

@ -1,4 +1,5 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
/// <reference path="./Job.ts" />
class Follower implements IResource { class Follower implements IResource {
public readonly resourceType = ResourceType.religion; public readonly resourceType = ResourceType.religion;
@ -24,6 +25,8 @@ class Follower implements IResource {
private _lastRecruitmentLog = 0; private _lastRecruitmentLog = 0;
private _followerSources: ResourceNumber = {}; private _followerSources: ResourceNumber = {};
private _followerDests: ResourceNumber = {}; private _followerDests: ResourceNumber = {};
private _timeSinceLastQuit = 0;
private _quitTracker: ResourceNumber = {};
public max(state: GameState): number { public max(state: GameState): number {
let max = state.config.cfgInitialMax.followers ?? 0; let max = state.config.cfgInitialMax.followers ?? 0;
@ -84,7 +87,7 @@ class Follower implements IResource {
} }
public advanceAction(time: number, state: GameState): void { 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; this._timeSinceLastLost += time;
if (this._timeSinceLastLost > state.config.cfgCredibilityFollowerLossTime) { if (this._timeSinceLastLost > state.config.cfgCredibilityFollowerLossTime) {
if (this.value > 0) { if (this.value > 0) {
@ -104,12 +107,40 @@ class Follower implements IResource {
this._timeSinceLastLost = 0; 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 ( if (
state.now - this._lastRecruitmentLog > state.now - this._lastRecruitmentLog >
state.config.cfgFollowerGainLossLogTimer && state.config.cfgFollowerGainLossLogTimer &&
(Object.keys(this._followerSources).length > 0 || (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) { if (Object.keys(this._followerDests).length > 0) {
let msg = ''; let msg = '';
@ -155,6 +186,28 @@ class Follower implements IResource {
}: ${msg}` }: ${msg}`
); );
} }
if (Object.keys(this._quitTracker).length > 0) {
let msg = '';
let total = 0;
for (const key in this._quitTracker) {
const rkey = <ResourceKey>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; this._lastRecruitmentLog = state.now;
} }
} }

View File

@ -15,7 +15,8 @@ abstract class Job implements IResource {
description: 'Promote one of your followers.', description: 'Promote one of your followers.',
isEnabled: (state: GameState): boolean => isEnabled: (state: GameState): boolean =>
(this.max === undefined || this.value < this.max(state)) && (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 => { performAction: (state: GameState): void => {
this._promoteFollower(state); this._promoteFollower(state);
}, },
@ -40,6 +41,39 @@ abstract class Job implements IResource {
public readonly description: string 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 { public addValue(amount: number): void {
this.value += amount; this.value += amount;
if (this.value < 0) this.value = 0; if (this.value < 0) this.value = 0;
@ -51,39 +85,14 @@ abstract class Job implements IResource {
public advanceAction(_time: number, state: GameState): void { public advanceAction(_time: number, state: GameState): void {
// if we're out of followers then the jobs also vacate // 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) { if (avail < 0 && this.value > 0) {
this.addValue(avail); this.addValue(avail);
} }
return; 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 { protected _hireLog(amount: number, _state: GameState): string {
return amount > 0 return amount > 0
? `You hired ${amount} ${ ? `You hired ${amount} ${
@ -95,7 +104,7 @@ abstract class Job implements IResource {
} }
private _promoteFollower(state: GameState): void { private _promoteFollower(state: GameState): void {
if (this._availableJobs(state) <= 0) return; if (Job.availableJobs(state) <= 0) return;
if ( if (
this.max !== undefined && this.max !== undefined &&
this.value < this.max(state) && this.value < this.max(state) &&

View File

@ -47,6 +47,10 @@ class Money implements IResource {
(state.resource.pastors?.value ?? 0) * (state.resource.pastors?.value ?? 0) *
(state.config.cfgSalary.pastors ?? 0); (state.config.cfgSalary.pastors ?? 0);
inc -=
(state.resource.compoundManagers?.value ?? 0) *
(state.config.cfgSalary.compoundManagers ?? 0);
return inc; return inc;
}; };

View File

@ -20,6 +20,7 @@ enum ResourceKey {
other = 'other', other = 'other',
atheism = 'atheism', atheism = 'atheism',
pastors = 'pastors', pastors = 'pastors',
compoundManagers = 'compoundManagers',
money = 'money', money = 'money',
cryptoCurrency = 'cryptoCurrency', cryptoCurrency = 'cryptoCurrency',
cryptoMarket = 'cryptoMarket', cryptoMarket = 'cryptoMarket',

View File

@ -131,9 +131,13 @@ class DebugRenderer implements IRenderer {
} }
} }
} }
if (resource.inc !== undefined && resource.inc(state) > 0) { const inc =
const elI = el.getElementsByClassName('resource-inc')[0]; resource.inc !== undefined ? resource.inc(state) : undefined;
elI.innerHTML = ` +${formatNumber(resource.inc(state))}/s`; 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) { if (this._handleClick) {
const elC = el.getElementsByClassName('resource-cost'); const elC = el.getElementsByClassName('resource-cost');