replaced hard-coded values with configs

This commit is contained in:
Rudis Muiznieks 2021-09-05 17:33:28 -05:00
parent caf5152551
commit 3f57d6ce91
17 changed files with 316 additions and 225 deletions

View File

@ -14,6 +14,7 @@
class GameConfig { class GameConfig {
public worldPopulation = 790000000; public worldPopulation = 790000000;
public numberFormatDigits = 1;
// religion configs // religion configs
public relChristianitySharer = 0.325; public relChristianitySharer = 0.325;
@ -25,73 +26,128 @@ class GameConfig {
public relOtherShare = 0.02; public relOtherShare = 0.02;
public relNoneShare = 0.16; public relNoneShare = 0.16;
public cfgStartingPlayerMax = 5; // general configs
public cfgStartingMoneyMax = 500000; public cfgPassiveMax = 100;
public cfgStartingTentMax = 5;
public cfgStartingCryptoMax = 1000;
public cfgStartingMegaChurchMax = 2;
public cfgTitheAmount = 10; public cfgCredibilityFollowerLossRatio = 0.04;
public cfgTimeBetweenTithes = 30000; public cfgCredibilityFollowerLossTime = 10000;
public cfgCryptoReturnAmount = 1;
public cfgCredibilityRestoreRate = 0.25; public cfgCredibilityRestoreRate = 0.25;
public cfgFollowerGainLossLogTimer = 10000;
public cfgFollowerStartingMax = 5;
public cfgPastorRecruitRate = 0.01; 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 { public generateState (): GameState {
const state = new GameState(this); const state = new GameState(this);
// create player organization // create player organization
state.addResource('plorg', new PlayerOrg()); state.addResource(ResourceKey.playerOrg, new PlayerOrg());
// create world religions // create world religions
state.addResource('xtian', new Religion( state.addResource(ResourceKey.christianity, new Religion(
'Christianity', 'God, Jesus, Bible, churches.', 'Christianity', 'God, Jesus, Bible, churches.',
this.relChristianitySharer * this.worldPopulation)); this.relChristianitySharer * this.worldPopulation));
state.addResource('islam', new Religion( state.addResource(ResourceKey.islam, new Religion(
'Islam', 'God, Muhammad, Quran, mosques.', 'Islam', 'God, Muhammad, Quran, mosques.',
this.relIslamShare * this.worldPopulation)); this.relIslamShare * this.worldPopulation));
state.addResource('hindu', new Religion( state.addResource(ResourceKey.hinduism, new Religion(
'Hinduism', 'Dogma-free spiritualism.', 'Hinduism', 'Dogma-free spiritualism.',
this.relHinduismShare * this.worldPopulation)); this.relHinduismShare * this.worldPopulation));
state.addResource('buddh', new Religion( state.addResource(ResourceKey.buddhism, new Religion(
'Buddhism', 'The minimization of suffering.', 'Buddhism', 'The minimization of suffering.',
this.relBuddhismShare * this.worldPopulation)); this.relBuddhismShare * this.worldPopulation));
state.addResource('sikhi', new Religion( state.addResource(ResourceKey.sikhism, new Religion(
'Sikhism', 'Meditation and ten Gurus', 'Sikhism', 'Meditation and ten Gurus',
this.relSikhismShare * this.worldPopulation)); this.relSikhismShare * this.worldPopulation));
state.addResource('judah', new Religion( state.addResource(ResourceKey.judaism, new Religion(
'Judaism', 'God, Abraham, Torah, synagogues.', 'Judaism', 'God, Abraham, Torah, synagogues.',
this.relJudaismShare * this.worldPopulation)); this.relJudaismShare * this.worldPopulation));
state.addResource('other', new Religion( state.addResource(ResourceKey.other, new Religion(
'Other', 'A variety of belief systems.', 'Other', 'A variety of belief systems.',
this.relOtherShare * this.worldPopulation)); this.relOtherShare * this.worldPopulation));
state.addResource('agnos', new Religion( state.addResource(ResourceKey.atheism, new Religion(
'Non-Religious', 'Atheists and agnostics.', 'Non-Religious', 'Atheists and agnostics.',
this.relNoneShare * this.worldPopulation)); this.relNoneShare * this.worldPopulation));
// add jobs // add jobs
state.addResource('pstor', new Pastor()); state.addResource(ResourceKey.pastors, new Pastor());
// add resources // add resources
state.addResource('money', new Money(3.50)); state.addResource(ResourceKey.money, new Money(3.50));
state.addResource('crpto', new CryptoCurrency()); state.addResource(ResourceKey.faithCoin, new CryptoCurrency(this));
state.addResource('tents', new Tent()); state.addResource(ResourceKey.tents, new Tent(this));
state.addResource('house', new House()); state.addResource(ResourceKey.houses, new House(this));
state.addResource('chrch', new Church()); state.addResource(ResourceKey.churches, new Church(this));
state.addResource('cmpnd', new Compound()); state.addResource(ResourceKey.compounds, new Compound(this));
state.addResource('blpmt', new BuildingPermit()); state.addResource(ResourceKey.megaChurches, new MegaChurch(this));
state.addResource('mchch', new MegaChurch());
// add research
state.addResource(ResourceKey.buildingPermit, new BuildingPermit(this));
// add passive resources // add passive resources
state.addResource('creds', new Credibility()); state.addResource(ResourceKey.credibility, new Credibility(this));
return state; 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');
}
} }

View File

@ -3,7 +3,6 @@ class GameState {
public onResourceClick: Array<() => void> = []; public onResourceClick: Array<() => void> = [];
public logger: ILogger | null = null; public logger: ILogger | null = null;
public numberFormatDigits = 1;
public now = 0; public now = 0;
@ -13,15 +12,15 @@ class GameState {
private _timeSinceSave = 0; private _timeSinceSave = 0;
private readonly _timeBetweenSaves = 10000; private readonly _timeBetweenSaves = 10000;
private _resources: { [key: string]: IResource } = { }; private _resources: { [key in ResourceKey]?: IResource } = { };
private readonly _resourceKeys: string[] = []; private readonly _resourceKeys: ResourceKey[] = [];
constructor (config: GameConfig) { constructor (config: GameConfig) {
this.config = config; this.config = config;
} }
public addResource (key: string, resource: IResource): void { public addResource (key: ResourceKey, resource: IResource): void {
this._resourceKeys.push(key); this._resourceKeys.push(key);
this._resources[key] = resource; this._resources[key] = resource;
} }
@ -38,7 +37,7 @@ class GameState {
// advance each resource // advance each resource
for (const rkey of this._resourceKeys) { for (const rkey of this._resourceKeys) {
const resource = this._resources[rkey]; const resource = this._resources[rkey];
if (this._resources[rkey].isUnlocked(this)) { if (resource?.isUnlocked(this) === true) {
if (resource.advanceAction !== null) if (resource.advanceAction !== null)
resource.advanceAction(time, this); resource.advanceAction(time, this);
} }
@ -47,34 +46,34 @@ class GameState {
// perform auto increments // perform auto increments
for (const rkey of this._resourceKeys) { for (const rkey of this._resourceKeys) {
const resource = this._resources[rkey]; const resource = this._resources[rkey];
if (!resource.isUnlocked(this)) continue; if (resource === undefined || !resource.isUnlocked(this)) continue;
if (resource.inc !== null && (resource.max === null if (resource.inc !== null && (resource.max === null
|| this._resources[rkey].value < resource.max(this))) { || resource.value < resource.max(this))) {
this._resources[rkey].addValue(resource.inc(this) * time / 1000, this); resource.addValue(resource.inc(this) * time / 1000, this);
} }
if (resource.max !== null && resource.value > resource.max(this)) { if (resource.max !== null && resource.value > resource.max(this)) {
this._resources[rkey].addValue( resource.addValue((resource.value - resource.max(this)) * -1, this);
(resource.value - resource.max(this)) * -1, this);
} }
if (resource.value < 0) { 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; return this._resourceKeys;
} }
public getResource (key: string): IResource { public getResource (key: ResourceKey): IResource | null {
return this._resources[key]; 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]; const resource = this._resources[resourceKey];
if (!resource.isUnlocked(this)) return; if (resource === undefined || !resource.isUnlocked(this)) return;
if (resource.clickAction !== null) { if (resource.clickAction !== null) {
resource.clickAction(this); 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 (cost === null) return true;
if (!this.isPurchasable(cost)) return false; if (!this.isPurchasable(cost)) return false;
for (const rkey of Object.keys(cost)) { for (const key in cost) {
this._resources[rkey].addValue(cost[rkey] * -1, this); const rkey = <ResourceKey>key;
const resource = this._resources[rkey];
const resCost = cost[rkey];
if (resource === undefined || resCost === undefined) continue;
resource.addValue(resCost * -1, this);
} }
return true; return true;
} }
public isPurchasable (cost: { [rkey: string]: number } | null): boolean { public isPurchasable (
cost: { [key in ResourceKey]?: number } | null): boolean {
if (cost === null) return true; if (cost === null) return true;
for (const rkey of Object.keys(cost)) { for (const key in cost) {
if (this._resources[rkey].value < cost[rkey]) { const rkey = <ResourceKey>key;
if ((this._resources[rkey]?.value ?? 0) < (cost[rkey] ?? 0)) {
return false; return false;
} }
} }
return true; 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 { public log (text: string): void {
if (this.logger !== null) { if (this.logger !== null) {
this.logger.msg(text); this.logger.msg(text);
@ -137,10 +120,11 @@ class GameState {
maj: this._versionMaj, maj: this._versionMaj,
min: this._versionMin, min: this._versionMin,
}; };
for (const rkey of this._resourceKeys) { for (const key in this._resources) {
const rkey = <ResourceKey>key;
saveObj[rkey] = { saveObj[rkey] = {
value: this._resources[rkey].value, value: this._resources[rkey]?.value ?? 0,
cost: this._resources[rkey].cost, cost: this._resources[rkey]?.cost ?? null,
}; };
} }
const saveStr: string = btoa(JSON.stringify(saveObj)); const saveStr: string = btoa(JSON.stringify(saveObj));
@ -153,11 +137,12 @@ class GameState {
try { try {
const saveObj: SaveData = <SaveData>JSON.parse(atob(saveStr)); const saveObj: SaveData = <SaveData>JSON.parse(atob(saveStr));
if (this._versionMaj === saveObj.version?.maj) { if (this._versionMaj === saveObj.version?.maj) {
for (const rkey of this._resourceKeys) { for (const key in this._resources) {
const rkey = <ResourceKey>key;
const saveRes = <{ const saveRes = <{
value: number; value: number;
cost: { [key: string]: number } | null; cost: { [key: string]: number } | null;
} | undefined> saveObj[rkey]; } | undefined> saveObj[key];
if (saveRes !== undefined) { if (saveRes !== undefined) {
// @ts-expect-error writing read-only value from save data // @ts-expect-error writing read-only value from save data
this._resources[rkey].value = saveRes.value; this._resources[rkey].value = saveRes.value;

View File

@ -1,16 +1,16 @@
/// <reference path="./Research.ts" /> /// <reference path="./Research.ts" />
class BuildingPermit extends Research { class BuildingPermit extends Research {
constructor () { constructor (config: GameConfig) {
super('Building Permit', super('Building Permit',
'Unlocks several new buildings you can build outside of your compounds.'); '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 { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;
const compounds = state.getResource('cmpnd'); const compounds = state.getResource(ResourceKey.compounds);
if (compounds.value > 0) { if (compounds !== null && compounds.value > 0) {
this._isUnlocked = true; this._isUnlocked = true;
} }
return this._isUnlocked; return this._isUnlocked;

View File

@ -1,20 +1,20 @@
/// <reference path="./Infrastructure.ts" /> /// <reference path="./Infrastructure.ts" />
class Church extends Infrastructure { class Church extends Infrastructure {
constructor () { constructor (config: GameConfig) {
super('Churches', super('Churches',
'Preaching grounds for 2 pastors.'); `Preaching grounds for ${config.formatNumber(config.cfgChurchPastorCapacity)} pastors.`);
this.cost.money = 150000; this.cost.money = config.cfgChurchStartingCost;
this._costMultiplier.money = 1.01; this._costMultiplier.money = config.cfgChurchCostMultiplier;
} }
public max: (state: GameState) => number = (state) => public max: (state: GameState) => number = (state) =>
state.getResource('cmpnd').value; state.getResource(ResourceKey.compounds)?.value ?? 0;
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;
const compounds = state.getResource('cmpnd'); const compounds = state.getResource(ResourceKey.compounds);
if (compounds.value > 0) { if (compounds != null && compounds.value > 0) {
this._isUnlocked = true; this._isUnlocked = true;
} }
return this._isUnlocked; return this._isUnlocked;

View File

@ -1,17 +1,17 @@
/// <reference path="./Infrastructure.ts" /> /// <reference path="./Infrastructure.ts" />
class Compound extends Infrastructure { class Compound extends Infrastructure {
constructor () { constructor (config: GameConfig) {
super('Compounds', super('Compounds',
'Provides space for tents, houses, and churches and a place to hide more money.'); 'Provides space for tents, houses, and churches and a place to hide more money.');
this.cost.money = 15000; this.cost.money = config.cfgCompoundStartingCost;
this._costMultiplier.money = 1.5; this._costMultiplier.money = config.cfgCompoundCostMultiplier;
} }
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;
const tents = state.getResource('tents'); const tents = state.getResource(ResourceKey.tents);
if (tents.value >= 5) { if (tents !== null && tents.value >= state.config.cfgTentStartingMax) {
this._isUnlocked = true; this._isUnlocked = true;
} }
return this._isUnlocked; return this._isUnlocked;

View File

@ -1,14 +1,16 @@
/// <reference path="./Passive.ts" /> /// <reference path="./Passive.ts" />
class Credibility extends Passive { class Credibility extends Passive {
constructor () { constructor (config: GameConfig) {
super( super(
'Credibility', 'Credibility',
'Affects your ability to recruit and retain followers.'); '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) => public inc: (state: GameState) => number = (state) =>
state.config.cfgCredibilityRestoreRate; state.config.cfgCredibilityRestoreRate;
} }

View File

@ -1,14 +1,14 @@
/// <reference path="./Purchasable.ts" /> /// <reference path="./Purchasable.ts" />
class CryptoCurrency extends Purchasable { class CryptoCurrency extends Purchasable {
constructor () { constructor (config: GameConfig) {
super('Faithcoin', super('Faithcoin',
"A crypto coin that can't be spent directly, but provides a steady stream of passive income."); "A crypto coin that can't be spent directly, but provides a steady stream of passive income.");
this.cost.money = 100; this.cost.money = config.cfgCryptoStartingCost;
this._costMultiplier.money = 1.1; this._costMultiplier.money = config.cfgCryptoCostMultiplier;
this.valueInWholeNumbers = false; this.valueInWholeNumbers = false;
} }
public max: (state: GameState) => number = (state) => public max: (state: GameState) => number = (state) =>
state.config.cfgStartingCryptoMax; state.config.cfgCryptoStartingMax;
} }

View File

@ -1,21 +1,21 @@
/// <reference path="./Infrastructure.ts" /> /// <reference path="./Infrastructure.ts" />
class House extends Infrastructure { class House extends Infrastructure {
constructor () { constructor (config: GameConfig) {
super('Houses', super('Houses',
'Provides room to house 10 followers.'); `Provides room to house ${config.formatNumber(config.cfgHouseFollowerCapacity)} followers.`);
this.cost.money = 75000; this.cost.money = config.cfgHouseStartingCost;
this._costMultiplier.money = 1.01; this._costMultiplier.money = config.cfgHouseCostMultiplier;
} }
// two houses per compound
public max: (state: GameState) => number = (state) => 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 { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;
const compounds = state.getResource('cmpnd'); const compounds = state.getResource(ResourceKey.compounds);
if (compounds.value > 0) { if (compounds !== null && compounds.value > 0) {
this._isUnlocked = true; this._isUnlocked = true;
} }
return this._isUnlocked; return this._isUnlocked;

View File

@ -7,6 +7,28 @@ enum ResourceType {
passive = 'passive', 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 { interface IResource {
readonly resourceType: ResourceType; readonly resourceType: ResourceType;
readonly name: string; readonly name: string;
@ -17,7 +39,7 @@ interface IResource {
// readonly altClickText?: string; // readonly altClickText?: string;
// readonly altClickDescription?: string; // readonly altClickDescription?: string;
readonly value: number; readonly value: number;
readonly cost: { [key: string]: number } | null; readonly cost: { [key in ResourceKey]?: number } | null;
max: ((state: GameState) => number) | null; max: ((state: GameState) => number) | null;
inc: ((state: GameState) => number) | null; inc: ((state: GameState) => number) | null;

View File

@ -6,12 +6,12 @@ abstract class Job implements IResource {
public readonly clickText = 'Hire'; public readonly clickText = 'Hire';
public readonly clickDescription = 'Promote one of your followers.'; public readonly clickDescription = 'Promote one of your followers.';
public value = 0; public value = 0;
public readonly cost: { [key: string]: number } = { }; public readonly cost: { [key in ResourceKey]?: number } = { };
public max: ((state: GameState) => number) | null = null; public max: ((state: GameState) => number) | null = null;
public inc: ((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; protected _isUnlocked = false;
constructor ( constructor (
@ -29,8 +29,10 @@ abstract class Job implements IResource {
&& state.deductCost(this.cost)) { && state.deductCost(this.cost)) {
this.addValue(1); this.addValue(1);
state.log(this._hireLog(1, state)); state.log(this._hireLog(1, state));
for (const rkey of Object.keys(this._costMultiplier)) { for (const key in this._costMultiplier) {
this.cost[rkey] *= this._costMultiplier[rkey]; const rkey = <ResourceKey>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 { protected _availableJobs (state: GameState): number {
// number of followers minus the number of filled jobs // 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( const hired = state.getResources().reduce(
(tot: number, rkey: string): number => { (tot: number, rkey: ResourceKey): number => {
const res = state.getResource(rkey); const res = state.getResource(rkey);
return res.resourceType === ResourceType.job return res?.resourceType === ResourceType.job
? tot + res.value ? tot + res.value
: tot; : tot;
}, 0); }, 0);

View File

@ -1,20 +1,20 @@
/// <reference path="./Infrastructure.ts" /> /// <reference path="./Infrastructure.ts" />
class MegaChurch extends Infrastructure { class MegaChurch extends Infrastructure {
constructor () { constructor (config: GameConfig) {
super('MegaChurches', super('MegaChurches',
'Room for 5 pastors'); `Room for ${config.formatNumber(config.cfgMegaChurchPastorCapacity)} pastors`);
this.cost.money = 7500000; this.cost.money = config.cfgMegaChurchStartingCost;
this._costMultiplier.money = 1.01; this._costMultiplier.money = config.cfgMegaChurchCostMultiplier;
} }
public max: (state: GameState) => number = (state) => public max: (state: GameState) => number = (state) =>
state.config.cfgStartingMegaChurchMax; state.config.cfgMegaChurchStartingMax;
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;
const permit = state.getResource('blpmt'); const permit = state.getResource(ResourceKey.buildingPermit);
if (permit.value > 0) { if (permit !== null && permit.value > 0) {
this._isUnlocked = true; this._isUnlocked = true;
} }
return this._isUnlocked; return this._isUnlocked;

View File

@ -16,8 +16,9 @@ class Money extends Purchasable {
} }
public max: (state: GameState) => number = (state: GameState) => { public max: (state: GameState) => number = (state: GameState) => {
let max = state.config.cfgStartingMoneyMax; let max = state.config.cfgMoneyStartingMax;
max += state.getResource('cmpnd').value * 500000; max += (state.getResource(ResourceKey.compounds)?.value ?? 0)
* state.config.cfgCompoundMoneyCapacity;
return max; return max;
}; };
@ -25,31 +26,32 @@ class Money extends Purchasable {
let inc = 0; let inc = 0;
// crypto currency // crypto currency
inc += state.getResource('crpto').value inc += (state.getResource(ResourceKey.faithCoin)?.value ?? 0)
* state.config.cfgCryptoReturnAmount; * state.config.cfgCryptoReturnAmount;
// TODO: job salaries
return inc; return inc;
}; };
protected _purchaseAmount (state: GameState): number { protected _purchaseAmount (state: GameState): number {
const plorg = state.getResource('plorg'); const plorg = state.getResource(ResourceKey.playerOrg);
if (plorg.value === 0) { if (plorg === null || plorg.value === 0) {
state.log('You have no followers to collect from!'); state.log('You have no followers to collect from!');
return 0; return 0;
} }
const diff = state.now - this._lastCollectionTime; const diff = state.now - this._lastCollectionTime;
if (diff < state.config.cfgTimeBetweenTithes) { if (diff < state.config.cfgTimeBetweenTithes) {
const lost = state.config.cfgTimeBetweenTithes / diff / 3; 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; const tithings = plorg.value * state.config.cfgTitheAmount;
this._lastCollectionTime = state.now; this._lastCollectionTime = state.now;
return tithings; return tithings;
} }
protected _purchaseLog (amount: number, state: GameState): string { protected _purchaseLog (amount: number, state: GameState): string {
const followers = state.getResource('plorg').value; const followers = state.getResource(ResourceKey.playerOrg)?.value ?? 0;
return `You collected $${state.formatNumber(amount)} from ${state.formatNumber(followers)} followers.`; return `You collected $${state.config.formatNumber(amount)} from ${state.config.formatNumber(followers)} followers.`;
} }
} }

View File

@ -9,32 +9,36 @@ class Pastor extends Job {
} }
public max: (state: GameState) => number = (state) => { public max: (state: GameState) => number = (state) => {
let max = state.getResource('chrch').value * 2; let max = (state.getResource(ResourceKey.churches)?.value ?? 0)
max += state.getResource('mchch').value * 5; * state.config.cfgChurchPastorCapacity;
max += (state.getResource(ResourceKey.megaChurches)?.value ?? 0)
* state.config.cfgMegaChurchPastorCapacity;
return max; return max;
}; };
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;
this._isUnlocked = state.getResource('chrch').isUnlocked(state); this._isUnlocked = state.getResource(
ResourceKey.churches)?.isUnlocked(state) === true;
return this._isUnlocked; return this._isUnlocked;
} }
public advanceAction (time: number, state: GameState): void { public advanceAction (time: number, state: GameState): void {
this._timeSinceLastTithe += time; this._timeSinceLastTithe += time;
if (this._timeSinceLastTithe >= state.config.cfgTimeBetweenTithes) { if (this._timeSinceLastTithe >= state.config.cfgTimeBetweenTithes) {
const money = state.getResource('money'); const money = state.getResource(ResourceKey.money);
const plorg = state.getResource('plorg'); const plorg = state.getResource(ResourceKey.playerOrg);
// each pastor can collect from up to 100 followers let tithed = this.value
let tithed = this.value * 100; * state.config.cfgPastorTitheCollectionFollowerMax;
if (Math.floor(plorg.value) < tithed) if (Math.floor(plorg?.value ?? 0) < tithed)
tithed = Math.floor(plorg.value); tithed = Math.floor(plorg?.value ?? 0);
let collected = tithed * state.config.cfgTitheAmount; let collected = tithed * state.config.cfgTitheAmount;
if (money.max !== null && collected > money.max(state) - money.value) if (money?.max !== null
collected = money.max(state) - money.value; && collected > (money?.max(state) ?? 0) - (money?.value ?? 0))
collected = (money?.max(state) ?? 0) - (money?.value ?? 0);
if (collected > 0) { if (collected > 0) {
money.addValue(collected, state); money?.addValue(collected, state);
state.log(`Your pastors collected $${state.formatNumber(collected)} in tithings from ${state.formatNumber(tithed)} followers.`); state.log(`Your pastors collected $${state.config.formatNumber(collected)} in tithings from ${state.config.formatNumber(tithed)} followers.`);
} }
this._timeSinceLastTithe = 0; this._timeSinceLastTithe = 0;
} }

View File

@ -12,13 +12,15 @@ class PlayerOrg implements IResource {
private _timeSinceLastLost = 0; private _timeSinceLastLost = 0;
private _lastRecruitmentLog = 0; private _lastRecruitmentLog = 0;
private _followerSources: { [key: string]: number } = { }; private _followerSources: { [key in ResourceKey]?: number } = { };
private _followerDests: { [key: string]: number } = { }; private _followerDests: { [key in ResourceKey]?: number } = { };
public max (state: GameState): number { public max (state: GameState): number {
let max = state.config.cfgStartingPlayerMax; let max = state.config.cfgFollowerStartingMax;
max += state.getResource('tents').value * 2; max += (state.getResource(ResourceKey.tents)?.value ?? 0)
max += state.getResource('house').value * 10; * state.config.cfgTentFollowerCapacity;
max += (state.getResource(ResourceKey.houses)?.value ?? 0)
* state.config.cfgHouseFollowerCapacity;
return max; return max;
} }
@ -26,12 +28,13 @@ class PlayerOrg implements IResource {
let inc = 0; let inc = 0;
// pastor recruiting // pastor recruiting
const pastors = state.getResource('pstor').value; const pastors = state.getResource(ResourceKey.pastors)?.value ?? 0;
inc += pastors * state.config.cfgPastorRecruitRate; inc += pastors * state.config.cfgPastorRecruitRate;
// credibility adjustment // credibility adjustment
const creds = state.getResource('creds'); const creds = state.getResource(ResourceKey.credibility);
if (creds.max !== null) inc *= creds.value / creds.max(state); if (creds?.max !== null) inc *=
(creds?.value ?? 0) / (creds?.max(state) ?? state.config.cfgPassiveMax);
return inc; return inc;
} }
@ -44,9 +47,10 @@ class PlayerOrg implements IResource {
} }
// chance to fail increases as credibility decreases // chance to fail increases as credibility decreases
const creds = state.getResource('creds'); const creds = state.getResource(ResourceKey.credibility);
if (creds.max !== null) { if (creds?.max !== null) {
const ratio = Math.ceil(creds.value) / creds.max(state); const ratio = Math.ceil(creds?.value ?? 0) / (creds?.max(state)
?? state.config.cfgPassiveMax);
if (Math.random() > ratio) { if (Math.random() > ratio) {
state.log('Your recruitment efforts failed.'); state.log('Your recruitment efforts failed.');
return; return;
@ -66,21 +70,21 @@ class PlayerOrg implements IResource {
// gained followers must come from other faiths // gained followers must come from other faiths
for (let i = 0; i < diff; i++) { for (let i = 0; i < diff; i++) {
const source = this._getRandomReligion(state); const source = this._getRandomReligion(state);
if (source !== null) {
source[1].addValue(-1, state); source[1].addValue(-1, state);
const curFollowers = this._followerSources[source[0]]; const curFollowers = this._followerSources[source[0]] ?? 0;
this._followerSources[source[0]] = !isNaN(curFollowers) this._followerSources[source[0]] = curFollowers + 1;
? curFollowers + 1 }
: 1;
} }
} else { } else {
// lost followers must return to other faiths // lost followers must return to other faiths
for (let i = 0; i < diff * -1; i++) { for (let i = 0; i < diff * -1; i++) {
const dest = this._getRandomReligion(state); const dest = this._getRandomReligion(state);
if (dest !== null) {
dest[1].addValue(1, state); dest[1].addValue(1, state);
const curFollowers = this._followerDests[dest[0]]; const curFollowers = this._followerDests[dest[0]] ?? 0;
this._followerDests[dest[0]] = !isNaN(curFollowers) this._followerDests[dest[0]] = curFollowers + 1;
? curFollowers + 1 }
: 1;
} }
} }
} }
@ -92,13 +96,17 @@ class PlayerOrg 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 every 10s if credibility < 100%
this._timeSinceLastLost += time; this._timeSinceLastLost += time;
if (this._timeSinceLastLost > 10000) { if (this._timeSinceLastLost > state.config.cfgCredibilityFollowerLossTime) {
if (this.value > 0) { if (this.value > 0) {
const creds = state.getResource('creds'); const creds = state.getResource(ResourceKey.credibility);
if (creds.max !== null) { if (creds?.max !== null) {
const ratio = Math.ceil(creds.value) / creds.max(state); const ratio =
Math.ceil(creds?.value ?? 0) / (creds?.max(state)
?? state.config.cfgPassiveMax);
if (Math.random() > ratio) { 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); this.addValue(lost * -1, state);
} }
} }
@ -107,42 +115,54 @@ class PlayerOrg implements IResource {
} }
// log lost and gained followers every 10s // 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._followerSources).length > 0
|| Object.keys(this._followerDests).length > 0)) { || Object.keys(this._followerDests).length > 0)) {
if (Object.keys(this._followerDests).length > 0) { if (Object.keys(this._followerDests).length > 0) {
let msg = ''; let msg = '';
let total = 0; let total = 0;
for (const rkey of Object.keys(this._followerDests)) { for (const key in this._followerDests) {
if (msg !== '') msg += ', '; const rkey = <ResourceKey>key;
const religion = state.getResource(rkey); const religion = state.getResource(rkey);
msg += `${state.formatNumber(this._followerDests[rkey])} to ${religion.name}`; const followers = this._followerDests[rkey];
total += 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]; 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) { if (Object.keys(this._followerSources).length > 0) {
let msg = ''; let msg = '';
let total = 0; let total = 0;
for (const rkey of Object.keys(this._followerSources)) { for (const key in this._followerSources) {
if (msg !== '') msg += ', '; const rkey = <ResourceKey>key;
const religion = state.getResource(rkey); const religion = state.getResource(rkey);
const followers = this._followerSources[rkey];
if (religion !== null && followers !== undefined) {
if (msg !== '') msg += ', ';
msg += msg +=
`${state.formatNumber(this._followerSources[rkey])} from ${religion.name}`; `${state.config.formatNumber(followers)} from ${religion.name}`;
total += this._followerSources[rkey]; total += followers;
delete this._followerSources[rkey]; 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; this._lastRecruitmentLog = state.now;
} }
} }
private _getRandomReligion (state: GameState): [string, IResource] { private _getRandomReligion (
const religs = ['xtian', 'islam', 'hindu', state: GameState): [ResourceKey, IResource] | null {
'buddh', 'sikhi', 'judah', 'other', 'agnos']; 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)]; const source = religs[Math.floor(Math.random() * 8)];
return [source, state.getResource(source)]; const resource = state.getResource(source);
return resource !== null ? [source, resource] : null;
} }
} }

View File

@ -6,12 +6,12 @@ abstract class Purchasable implements IResource {
public clickText = 'Purchase'; public clickText = 'Purchase';
public clickDescription = 'Purchase'; public clickDescription = 'Purchase';
public value = 0; public value = 0;
public readonly cost: { [key: string]: number } = { }; public readonly cost: { [key in ResourceKey]?: number } = { };
public inc: ((state: GameState) => number) | null = null; public inc: ((state: GameState) => number) | null = null;
public max: ((_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; protected _isUnlocked = false;
constructor ( constructor (
@ -27,8 +27,10 @@ abstract class Purchasable implements IResource {
if (amount > 0) { if (amount > 0) {
this.value += amount; this.value += amount;
state.log(this._purchaseLog(amount, state)); state.log(this._purchaseLog(amount, state));
for (const rkey of Object.keys(this._costMultiplier)) { for (const key in this._costMultiplier) {
this.cost[rkey] *= this._costMultiplier[rkey]; const rkey = <ResourceKey>key;
this.cost[rkey] =
(this.cost[rkey] ?? 0) * (this._costMultiplier[rkey] ?? 1);
} }
} }
} }

View File

@ -1,17 +1,18 @@
/// <reference path="./Infrastructure.ts" /> /// <reference path="./Infrastructure.ts" />
class Tent extends Infrastructure { class Tent extends Infrastructure {
constructor () { constructor (config: GameConfig) {
super('Tents', super('Tents',
'Provides room to house 2 followers.'); `Provides room to house ${config.formatNumber(config.cfgTentFollowerCapacity)} followers.`);
this.cost.money = 250; this.cost.money = config.cfgTentStartingCost;
this._costMultiplier.money = 1.05; this._costMultiplier.money = config.cfgTentCostMultiplier;
} }
public max: (state: GameState) => number = (state) => { public max: (state: GameState) => number = (state) => {
// ten extra tents per compound // ten extra tents per compound
let max = state.config.cfgStartingTentMax; let max = state.config.cfgTentStartingMax;
max += state.getResource('cmpnd').value * 10; max += (state.getResource(ResourceKey.compounds)?.value ?? 0)
* state.config.cfgCompoundTentCapacity;
return max; return max;
}; };
} }

View File

@ -5,7 +5,7 @@ class DebugRenderer implements IRenderer {
private _handleClick = true; private _handleClick = true;
public render (state: GameState): void { public render (state: GameState): void {
const rkeys: string[] = state.getResources(); const rkeys = state.getResources();
const container = document.getElementById('irreligious-game'); const container = document.getElementById('irreligious-game');
if (!this._initialized) { if (!this._initialized) {
if (container === null) { if (container === null) {
@ -42,7 +42,8 @@ class DebugRenderer implements IRenderer {
} }
// create containers for each resource // create containers for each resource
for (const rkey of rkeys) { for (const rkey of rkeys) {
const resource: IResource = state.getResource(rkey); const resource = state.getResource(rkey);
if (resource === null) continue;
const resContainer = document.getElementById( const resContainer = document.getElementById(
`resource-container-${resource.resourceType}`); `resource-container-${resource.resourceType}`);
if (resContainer === null) continue; if (resContainer === null) continue;
@ -70,8 +71,7 @@ class DebugRenderer implements IRenderer {
el.innerHTML = content; el.innerHTML = content;
resContainer.appendChild(el); resContainer.appendChild(el);
if (resource.clickAction !== null) { if (resource.clickAction !== null) {
const btn: Element = const btn = el.getElementsByClassName('resource-btn')[0];
el.getElementsByClassName('resource-btn')[0];
btn.addEventListener('click', (): void => { btn.addEventListener('click', (): void => {
state.performClick(rkey); state.performClick(rkey);
}); });
@ -92,38 +92,34 @@ class DebugRenderer implements IRenderer {
}); });
} }
for (const rkey of rkeys) { 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}`); const el = document.getElementById(`resource-details-${rkey}`);
if (el !== null && resource.isUnlocked(state)) { if (el !== null && resource.isUnlocked(state)) {
if (el.className !== 'resource') el.className = 'resource'; if (el.className !== 'resource') el.className = 'resource';
const elV: Element = const elV = el.getElementsByClassName('resource-value')[0];
el.getElementsByClassName('resource-value')[0]; const elT = el.getElementsByClassName('resource-max')[0];
const elT: Element = const value = resource.valueInWholeNumbers
el.getElementsByClassName('resource-max')[0];
const value: number = resource.valueInWholeNumbers
? Math.floor(resource.value) ? Math.floor(resource.value)
: resource.value; : resource.value;
elV.innerHTML = state.formatNumber(value); elV.innerHTML = state.config.formatNumber(value);
elT.innerHTML = resource.max !== null elT.innerHTML = resource.max !== null
? ` / ${state.formatNumber(resource.max(state))}` ? ` / ${state.config.formatNumber(resource.max(state))}`
: ''; : '';
const elB: HTMLCollectionOf<Element> = const elB = el.getElementsByClassName('resource-btn');
el.getElementsByClassName('resource-btn');
if (elB.length > 0) { 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)); && (resource.max === null || resource.value < resource.max(state));
if (enabled) elB[0].removeAttribute('disabled'); if (enabled) elB[0].removeAttribute('disabled');
else elB[0].setAttribute('disabled', 'disabled'); else elB[0].setAttribute('disabled', 'disabled');
} }
if (resource.inc !== null && resource.inc(state) > 0) { if (resource.inc !== null && resource.inc(state) > 0) {
const elI: Element = const elI = el.getElementsByClassName('resource-inc')[0];
el.getElementsByClassName('resource-inc')[0];
elI.innerHTML = elI.innerHTML =
` +${state.formatNumber(resource.inc(state))}/s`; ` +${state.config.formatNumber(resource.inc(state))}/s`;
} }
if (this._handleClick) { if (this._handleClick) {
const elC: HTMLCollectionOf<Element> = const elC = el.getElementsByClassName('resource-cost');
el.getElementsByClassName('resource-cost');
if (elC.length > 0) { if (elC.length > 0) {
elC[0].innerHTML = this._getCostStr(resource, state); elC[0].innerHTML = this._getCostStr(resource, state);
} }
@ -152,15 +148,14 @@ class DebugRenderer implements IRenderer {
private _getCostStr (resource: IResource, state: GameState): string { private _getCostStr (resource: IResource, state: GameState): string {
let cost = ''; let cost = '';
if (resource.cost !== null) {
for (const rkey of state.getResources()) { for (const rkey of state.getResources()) {
if (isNaN(resource.cost[rkey])) continue; if (resource.cost?.[rkey] !== undefined) {
if (cost !== '') cost += ', '; if (cost !== '') cost += ', ';
if (rkey === 'money') { if (rkey === ResourceKey.money) {
cost += `$${state.formatNumber(resource.cost[rkey])}`; cost += `$${state.config.formatNumber(resource.cost[rkey] ?? 0)}`;
} else { } else {
cost += `${state.formatNumber(resource.cost[rkey])} cost += `${state.config.formatNumber(resource.cost[rkey] ?? 0)}
${state.getResource(rkey).name}`; ${state.getResource(rkey)?.name ?? rkey}`;
} }
} }
} }