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 {
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');
}
}

View File

@ -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 = <ResourceKey>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 = <ResourceKey>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 = <ResourceKey>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 = <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 = <ResourceKey>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;

View File

@ -1,16 +1,16 @@
/// <reference path="./Research.ts" />
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;

View File

@ -1,20 +1,20 @@
/// <reference path="./Infrastructure.ts" />
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;

View File

@ -1,17 +1,17 @@
/// <reference path="./Infrastructure.ts" />
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;

View File

@ -1,14 +1,16 @@
/// <reference path="./Passive.ts" />
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;
}

View File

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

View File

@ -1,21 +1,21 @@
/// <reference path="./Infrastructure.ts" />
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;

View File

@ -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;

View File

@ -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 = <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 {
// 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);

View File

@ -1,20 +1,20 @@
/// <reference path="./Infrastructure.ts" />
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;

View File

@ -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.`;
}
}

View File

@ -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;
}

View File

@ -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);
if (source !== null) {
source[1].addValue(-1, state);
const curFollowers = this._followerSources[source[0]];
this._followerSources[source[0]] = !isNaN(curFollowers)
? curFollowers + 1
: 1;
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);
if (dest !== null) {
dest[1].addValue(1, state);
const curFollowers = this._followerDests[dest[0]];
this._followerDests[dest[0]] = !isNaN(curFollowers)
? curFollowers + 1
: 1;
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 = <ResourceKey>key;
const religion = state.getResource(rkey);
msg += `${state.formatNumber(this._followerDests[rkey])} to ${religion.name}`;
total += 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 = <ResourceKey>key;
const religion = state.getResource(rkey);
const followers = this._followerSources[rkey];
if (religion !== null && followers !== undefined) {
if (msg !== '') msg += ', ';
msg +=
`${state.formatNumber(this._followerSources[rkey])} from ${religion.name}`;
total += this._followerSources[rkey];
`${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;
}
}

View File

@ -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 = <ResourceKey>key;
this.cost[rkey] =
(this.cost[rkey] ?? 0) * (this._costMultiplier[rkey] ?? 1);
}
}
}

View File

@ -1,17 +1,18 @@
/// <reference path="./Infrastructure.ts" />
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;
};
}

View File

@ -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<Element> =
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<Element> =
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;
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}`;
}
}
}