ability to define mulitple user actions per resource

This commit is contained in:
Rudis Muiznieks 2021-09-06 00:12:05 -05:00
parent ce3ae26b4b
commit 1bc4206a32
23 changed files with 302 additions and 222 deletions

View File

@ -37,8 +37,6 @@
"@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-implicit-any-catch": "error", "@typescript-eslint/no-implicit-any-catch": "error",
"@typescript-eslint/no-invalid-void-type": "error", "@typescript-eslint/no-invalid-void-type": "error",
"@typescript-eslint/no-parameter-properties": ["error", {
"allows": ["public", "readonly", "public readonly"]}],
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn", "@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn",
"@typescript-eslint/no-unnecessary-condition": "warn", "@typescript-eslint/no-unnecessary-condition": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "warn", "@typescript-eslint/no-unnecessary-qualifier": "warn",

View File

@ -4,27 +4,28 @@
/// <reference path="./resource/Compound.ts" /> /// <reference path="./resource/Compound.ts" />
/// <reference path="./resource/Credibility.ts" /> /// <reference path="./resource/Credibility.ts" />
/// <reference path="./resource/CryptoCurrency.ts" /> /// <reference path="./resource/CryptoCurrency.ts" />
/// <reference path="./resource/Follower.ts" />
/// <reference path="./resource/House.ts" /> /// <reference path="./resource/House.ts" />
/// <reference path="./resource/MegaChurch.ts" /> /// <reference path="./resource/Megachurch.ts" />
/// <reference path="./resource/Money.ts" /> /// <reference path="./resource/Money.ts" />
/// <reference path="./resource/Pastor.ts" /> /// <reference path="./resource/Pastor.ts" />
/// <reference path="./resource/PlayerOrg.ts" />
/// <reference path="./resource/Religion.ts" /> /// <reference path="./resource/Religion.ts" />
/// <reference path="./resource/Tent.ts" /> /// <reference path="./resource/Tent.ts" />
class GameConfig { class GameConfig {
public worldPopulation = 790000000; public worldPopulation = 790000000;
public numberFormatDigits = 1;
// religion configs // religion configs
public relChristianitySharer = 0.325; public cfgReligion: ResourceNumber = {
public relIslamShare = 0.215; christianity: 0.325,
public relHinduismShare = 0.16; islam: 0.215,
public relBuddhismShare = 0.06; hinduism: 0.16,
public relSikhismShare = 0.04; buddhism: 0.06,
public relJudaismShare = 0.02; sikhism: 0.04,
public relOtherShare = 0.02; judaism: 0.02,
public relNoneShare = 0.16; other: 0.02,
atheism: 0.16,
};
// general configs // general configs
public cfgInitialMax: ResourceNumber = { public cfgInitialMax: ResourceNumber = {
@ -81,40 +82,40 @@ class GameConfig {
const state = new GameState(this); const state = new GameState(this);
// create player organization // create player organization
state.addResource(ResourceKey.playerOrg, new PlayerOrg()); state.addResource(ResourceKey.playerOrg, new Follower());
// create world religions // create world religions
state.addResource(ResourceKey.christianity, new Religion( state.addResource(ResourceKey.christianity, new Religion(
'Christianity', 'God, Jesus, Bible, churches.', 'christian', 'christians', 'God, Jesus, Bible, churches.',
this.relChristianitySharer * this.worldPopulation)); (this.cfgReligion.christianity ?? 0) * this.worldPopulation));
state.addResource(ResourceKey.islam, new Religion( state.addResource(ResourceKey.islam, new Religion(
'Islam', 'God, Muhammad, Quran, mosques.', 'muslim', 'muslims', 'God, Muhammad, Quran, mosques.',
this.relIslamShare * this.worldPopulation)); (this.cfgReligion.islam ?? 0) * this.worldPopulation));
state.addResource(ResourceKey.hinduism, new Religion( state.addResource(ResourceKey.hinduism, new Religion(
'Hinduism', 'Dogma-free spiritualism.', 'hindu', 'hindus', 'Dogma-free spiritualism.',
this.relHinduismShare * this.worldPopulation)); (this.cfgReligion.hinduism ?? 0) * this.worldPopulation));
state.addResource(ResourceKey.buddhism, new Religion( state.addResource(ResourceKey.buddhism, new Religion(
'Buddhism', 'The minimization of suffering.', 'buddhist', 'buddhists', 'The minimization of suffering.',
this.relBuddhismShare * this.worldPopulation)); (this.cfgReligion.buddhism ?? 0) * this.worldPopulation));
state.addResource(ResourceKey.sikhism, new Religion( state.addResource(ResourceKey.sikhism, new Religion(
'Sikhism', 'Meditation and ten Gurus', 'sikh', 'sikhs', 'Meditation and ten Gurus',
this.relSikhismShare * this.worldPopulation)); (this.cfgReligion.sikhism ?? 0) * this.worldPopulation));
state.addResource(ResourceKey.judaism, new Religion( state.addResource(ResourceKey.judaism, new Religion(
'Judaism', 'God, Abraham, Torah, synagogues.', 'jew', 'jews', 'God, Abraham, Torah, synagogues.',
this.relJudaismShare * this.worldPopulation)); (this.cfgReligion.judaism ?? 0) * this.worldPopulation));
state.addResource(ResourceKey.other, new Religion( state.addResource(ResourceKey.other, new Religion(
'Other', 'A variety of belief systems.', 'other', 'others', 'A variety of belief systems.',
this.relOtherShare * this.worldPopulation)); (this.cfgReligion.other ?? 0) * this.worldPopulation));
state.addResource(ResourceKey.atheism, new Religion( state.addResource(ResourceKey.atheism, new Religion(
'Non-Religious', 'Atheists and agnostics.', 'atheist', 'atheists', 'Atheists and agnostics.',
this.relNoneShare * this.worldPopulation)); (this.cfgReligion.atheism ?? 0) * this.worldPopulation));
// add jobs // add jobs
state.addResource(ResourceKey.pastors, new Pastor()); state.addResource(ResourceKey.pastors, new Pastor());
@ -126,7 +127,7 @@ class GameConfig {
state.addResource(ResourceKey.houses, new House(this)); state.addResource(ResourceKey.houses, new House(this));
state.addResource(ResourceKey.churches, new Church(this)); state.addResource(ResourceKey.churches, new Church(this));
state.addResource(ResourceKey.compounds, new Compound(this)); state.addResource(ResourceKey.compounds, new Compound(this));
state.addResource(ResourceKey.megaChurches, new MegaChurch(this)); state.addResource(ResourceKey.megaChurches, new Megachurch(this));
// add research // add research
state.addResource(ResourceKey.buildingPermit, new BuildingPermit(this)); state.addResource(ResourceKey.buildingPermit, new BuildingPermit(this));
@ -136,26 +137,4 @@ class GameConfig {
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

@ -1,3 +1,5 @@
/// <reference path="./Utils.ts" />
class GameState { class GameState {
public readonly config: GameConfig; public readonly config: GameConfig;
@ -69,15 +71,17 @@ class GameState {
} }
} }
public performClick (resourceKey: ResourceKey): void { public performAction (resourceKey: ResourceKey, actionIndex: number): void {
const resource = this._resources[resourceKey]; const resource = this._resources[resourceKey];
if (resource === undefined || !resource.isUnlocked(this)) return; if (resource === undefined || resource.userActions === undefined
|| actionIndex > resource.userActions.length
|| !resource.isUnlocked(this)) return;
if (resource.clickAction !== undefined) { const action = resource.userActions[actionIndex];
resource.clickAction(this);
for (const callback of this.onResourceClick) { action.performAction(this);
callback(); for (const callback of this.onResourceClick) {
} callback();
} }
} }

23
src/model/Utils.ts Normal file
View File

@ -0,0 +1,23 @@
const numberFormatDigits = 1;
function 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(
numberFormatDigits).replace(rx, '$1') + item.symbol
: num.toFixed(numberFormatDigits).replace(rx, '$1');
}

View File

@ -2,7 +2,9 @@
class BuildingPermit extends Research { class BuildingPermit extends Research {
constructor (config: GameConfig) { constructor (config: GameConfig) {
super('Building Permit', super(
'building permit',
'building permits',
'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 = config.cfgInitialMax.buildingPermit; this.cost.money = config.cfgInitialMax.buildingPermit;
} }

View File

@ -2,8 +2,10 @@
class Church extends Infrastructure { class Church extends Infrastructure {
constructor (config: GameConfig) { constructor (config: GameConfig) {
super('Churches', super(
`Preaching grounds for ${config.formatNumber(config.cfgCapacity.churches?.pastors ?? 0)} pastors.`); 'church',
'churches',
`Preaching grounds for ${formatNumber(config.cfgCapacity.churches?.pastors ?? 0)} pastors.`);
this.cost.money = config.cfgInitialCost.churches; this.cost.money = config.cfgInitialCost.churches;
this._costMultiplier.money = config.cfgCostMultiplier.churches; this._costMultiplier.money = config.cfgCostMultiplier.churches;
} }

View File

@ -2,7 +2,9 @@
class Compound extends Infrastructure { class Compound extends Infrastructure {
constructor (config: GameConfig) { constructor (config: GameConfig) {
super('Compounds', super(
'compound',
'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 = config.cfgInitialCost.compounds; this.cost.money = config.cfgInitialCost.compounds;
this._costMultiplier.money = config.cfgCostMultiplier.compounds; this._costMultiplier.money = config.cfgCostMultiplier.compounds;

View File

@ -3,7 +3,8 @@
class Credibility extends Passive { class Credibility extends Passive {
constructor (config: GameConfig) { constructor (config: GameConfig) {
super( super(
'Credibility', 'credibility',
'credibilities',
'Affects your ability to recruit and retain followers.'); 'Affects your ability to recruit and retain followers.');
this.value = config.cfgPassiveMax; this.value = config.cfgPassiveMax;
} }

View File

@ -2,7 +2,9 @@
class CryptoCurrency extends Purchasable { class CryptoCurrency extends Purchasable {
constructor (config: GameConfig) { constructor (config: GameConfig) {
super('Faithcoin', super(
'FaithCoin',
'FaithCoins',
"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 = config.cfgInitialCost.cryptoCurrency; this.cost.money = config.cfgInitialCost.cryptoCurrency;
this._costMultiplier.money = config.cfgCostMultiplier.cryptoCurrency; this._costMultiplier.money = config.cfgCostMultiplier.cryptoCurrency;

View File

@ -1,14 +1,24 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
class PlayerOrg implements IResource { class Follower implements IResource {
public readonly resourceType = ResourceType.religion; public readonly resourceType = ResourceType.religion;
public readonly name = 'Player'; public readonly singularName = 'follower';
public readonly pluralName = 'followers';
public readonly description = 'In you they trust.'; public readonly description = 'In you they trust.';
public readonly valueInWholeNumbers = true; public readonly valueInWholeNumbers = true;
public readonly clickText = 'Recruit';
public readonly clickDescription = 'Gather new followers.';
public value = 0; public value = 0;
public userActions: ResourceAction[] = [
{
name: 'Recruit',
description: 'Gather new followers.',
isEnabled: (state: GameState): boolean => this.value < this.max(state),
performAction: (state: GameState): void => {
this._recruitFollower(state);
},
},
];
private _timeSinceLastLost = 0; private _timeSinceLastLost = 0;
private _lastRecruitmentLog = 0; private _lastRecruitmentLog = 0;
private _followerSources: ResourceNumber = { }; private _followerSources: ResourceNumber = { };
@ -37,27 +47,6 @@ class PlayerOrg implements IResource {
return inc; return inc;
} }
public clickAction (state: GameState): void {
// don't exceed max
if (this.value >= this.max(state)) {
state.log('You have no room for more followers.');
return;
}
// chance to fail increases as credibility decreases
const creds = state.resource.credibility;
if (creds?.max !== undefined) {
const ratio = Math.ceil(creds.value) / creds.max(state);
if (Math.random() > ratio) {
state.log('Your recruitment efforts failed.');
return;
}
}
this._lastRecruitmentLog = 0; // always log on click
this.addValue(1, state);
}
public addValue (amount: number, state: GameState): void { public addValue (amount: number, state: GameState): void {
const oldValue = this.value; const oldValue = this.value;
this.value += amount; this.value += amount;
@ -124,12 +113,12 @@ class PlayerOrg implements IResource {
const followers = this._followerDests[rkey]; const followers = this._followerDests[rkey];
if (religion !== undefined && followers !== undefined) { if (religion !== undefined && followers !== undefined) {
if (msg !== '') msg += ', '; if (msg !== '') msg += ', ';
msg += `${state.config.formatNumber(followers)} to ${religion.name}`; msg += `${formatNumber(followers)} to ${religion.pluralName}`;
total += followers; total += followers;
delete this._followerDests[rkey]; delete this._followerDests[rkey];
} }
} }
state.log(`You lost ${state.config.formatNumber(total)} followers: ${msg}`); state.log(`You lost ${formatNumber(total)} followers: ${msg}`);
} }
if (Object.keys(this._followerSources).length > 0) { if (Object.keys(this._followerSources).length > 0) {
let msg = ''; let msg = '';
@ -141,17 +130,38 @@ class PlayerOrg implements IResource {
if (religion !== undefined && followers !== undefined) { if (religion !== undefined && followers !== undefined) {
if (msg !== '') msg += ', '; if (msg !== '') msg += ', ';
msg += msg +=
`${state.config.formatNumber(followers)} from ${religion.name}`; `${formatNumber(followers)} from ${religion.pluralName}`;
total += followers; total += followers;
delete this._followerSources[rkey]; delete this._followerSources[rkey];
} }
} }
state.log(`You gained ${state.config.formatNumber(total)} followers: ${msg}`); state.log(`You gained ${formatNumber(total)} followers: ${msg}`);
} }
this._lastRecruitmentLog = state.now; this._lastRecruitmentLog = state.now;
} }
} }
private _recruitFollower (state: GameState): void {
// don't exceed max
if (this.value >= this.max(state)) {
state.log('You have no room for more followers.');
return;
}
// chance to fail increases as credibility decreases
const creds = state.resource.credibility;
if (creds?.max !== undefined) {
const ratio = Math.ceil(creds.value) / creds.max(state);
if (Math.random() > ratio) {
state.log('Your recruitment efforts failed.');
return;
}
}
this._lastRecruitmentLog = 0; // always log on click
this.addValue(1, state);
}
private _getRandomReligion ( private _getRandomReligion (
state: GameState): [ResourceKey, IResource] | null { state: GameState): [ResourceKey, IResource] | null {
const religs = [ResourceKey.christianity, ResourceKey.islam, const religs = [ResourceKey.christianity, ResourceKey.islam,

View File

@ -2,8 +2,10 @@
class House extends Infrastructure { class House extends Infrastructure {
constructor (config: GameConfig) { constructor (config: GameConfig) {
super('Houses', super(
`Provides room to house ${config.formatNumber(config.cfgCapacity.houses?.playerOrg ?? 0)} followers.`); 'house',
'houses',
`Provides room to house ${formatNumber(config.cfgCapacity.houses?.playerOrg ?? 0)} followers.`);
this.cost.money = config.cfgInitialCost.houses; this.cost.money = config.cfgInitialCost.houses;
this._costMultiplier.money = config.cfgCostMultiplier.houses; this._costMultiplier.money = config.cfgCostMultiplier.houses;
} }

View File

@ -1,55 +1,19 @@
enum ResourceType { /// <reference path="./SharedTypes.ts" />
religion = 'religion',
job = 'job',
consumable = 'consumable',
infrastructure = 'infrastructure',
research = 'research',
passive = 'passive',
}
enum ResourceKey {
playerOrg = 'playerOrg',
christianity = 'christianity',
islam = 'islam',
hinduism = 'hinduism',
buddhism = 'buddhism',
sikhism = 'sikhism',
judaism = 'judaism',
other = 'other',
atheism = 'atheism',
pastors = 'pastors',
money = 'money',
cryptoCurrency = 'cryptoCurrency',
tents = 'tents',
houses = 'houses',
churches = 'churches',
compounds = 'compounds',
buildingPermit = 'buildingPermit',
megaChurches = 'megaChurches',
credibility = 'credibility',
}
type ResourceNumber = { [key in ResourceKey]?: number };
interface IResource { interface IResource {
readonly resourceType: ResourceType; readonly resourceType: ResourceType;
readonly name: string; readonly singularName: string;
readonly pluralName: string;
readonly description: string; readonly description: string;
readonly valueInWholeNumbers: boolean; readonly valueInWholeNumbers: boolean;
readonly value: number; readonly value: number;
readonly cost?: ResourceNumber; readonly cost?: ResourceNumber;
readonly clickText?: string;
readonly clickDescription?: string;
// readonly altClickText?: string;
// readonly altClickDescription?: string;
max?: (state: GameState) => number; max?: (state: GameState) => number;
inc?: (state: GameState) => number; inc?: (state: GameState) => number;
clickAction?: (state: GameState) => void;
// altClickAction (state: GameState): void;
advanceAction?: (time: number, state: GameState) => void; advanceAction?: (time: number, state: GameState) => void;
userActions?: ResourceAction[];
addValue: (amount: number, state: GameState) => void; addValue: (amount: number, state: GameState) => void;
isUnlocked: (state: GameState) => boolean; isUnlocked: (state: GameState) => boolean;

View File

@ -3,39 +3,34 @@
abstract class Job implements IResource { abstract class Job implements IResource {
public readonly resourceType = ResourceType.job; public readonly resourceType = ResourceType.job;
public readonly valueInWholeNumbers = true; public readonly valueInWholeNumbers = true;
public readonly clickText = 'Hire';
public readonly clickDescription = 'Promote one of your followers.';
public value = 0; public value = 0;
public readonly cost: ResourceNumber = { }; public readonly cost: ResourceNumber = { };
public max?: (state: GameState) => number = undefined; public max?: (state: GameState) => number = undefined;
public inc?: (state: GameState) => number = undefined; public inc?: (state: GameState) => number = undefined;
public userActions: ResourceAction[] = [
{
name: 'Hire',
description: 'Promote one of your followers.',
isEnabled: (state: GameState): boolean =>
(this.max === undefined || this.value < this.max(state))
&& this.value < this._availableJobs(state),
performAction: (state: GameState): void => {
this._promoteFollower(state);
},
},
];
protected _costMultiplier: { [key in ResourceKey]?: number } = { }; protected _costMultiplier: { [key in ResourceKey]?: number } = { };
protected _isUnlocked = false; protected _isUnlocked = false;
constructor ( constructor (
public readonly name: string, public readonly singularName: string,
public readonly pluralName: string,
public readonly description: string public readonly description: string
) { } ) { }
public clickAction (state: GameState): void {
if (this._availableJobs(state) <= 0) {
state.log('You have no unemployed followers to promote.');
return;
}
if (this.max !== undefined && this.value < this.max(state)
&& state.deductCost(this.cost)) {
this.addValue(1);
state.log(this._hireLog(1, state));
for (const key in this._costMultiplier) {
const rkey = <ResourceKey>key;
this.cost[rkey] =
(this.cost[rkey] ?? 0) * (this._costMultiplier[rkey] ?? 1);
}
}
}
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;
@ -81,6 +76,23 @@ abstract class Job implements IResource {
} }
protected _hireLog (amount: number, _state: GameState): string { protected _hireLog (amount: number, _state: GameState): string {
return `You hired ${amount} x ${this.name}.`; return `You hired ${amount} x ${this.pluralName}.`;
}
private _promoteFollower (state: GameState): void {
if (this._availableJobs(state) <= 0) {
state.log('You have no unemployed followers to promote.');
return;
}
if (this.max !== undefined && this.value < this.max(state)
&& state.deductCost(this.cost)) {
this.addValue(1);
state.log(this._hireLog(1, state));
for (const key in this._costMultiplier) {
const rkey = <ResourceKey>key;
this.cost[rkey] =
(this.cost[rkey] ?? 0) * (this._costMultiplier[rkey] ?? 1);
}
}
} }
} }

View File

@ -1,9 +1,11 @@
/// <reference path="./Infrastructure.ts" /> /// <reference path="./Infrastructure.ts" />
class MegaChurch extends Infrastructure { class Megachurch extends Infrastructure {
constructor (config: GameConfig) { constructor (config: GameConfig) {
super('MegaChurches', super(
`Room for ${config.formatNumber(config.cfgCapacity.megaChurches?.pastors ?? 0)} pastors`); 'megachurch',
'megachurches',
`Room for ${formatNumber(config.cfgCapacity.megaChurches?.pastors ?? 0)} pastors`);
this.cost.money = config.cfgInitialCost.megaChurches; this.cost.money = config.cfgInitialCost.megaChurches;
this._costMultiplier.money = config.cfgCostMultiplier.megaChurches; this._costMultiplier.money = config.cfgCostMultiplier.megaChurches;
} }

View File

@ -8,9 +8,12 @@ class Money extends Purchasable {
constructor ( constructor (
public value: number public value: number
) { ) {
super('Money', 'Used to purchase goods and services.'); super(
this.clickText = 'Collect Tithes'; 'money',
this.clickDescription = 'Voluntary contributions from followers.'; 'moneys',
'Used to purchase goods and services.',
'Collect Tithes',
'Voluntary contributions from followers.');
this.valueInWholeNumbers = false; this.valueInWholeNumbers = false;
this._isUnlocked = true; this._isUnlocked = true;
} }
@ -54,6 +57,6 @@ class Money extends Purchasable {
protected _purchaseLog (amount: number, state: GameState): string { protected _purchaseLog (amount: number, state: GameState): string {
const followers = state.resource.playerOrg?.value ?? 0; const followers = state.resource.playerOrg?.value ?? 0;
return `You collected $${state.config.formatNumber(amount)} from ${state.config.formatNumber(followers)} followers.`; return `You collected $${formatNumber(amount)} from ${formatNumber(followers)} followers.`;
} }
} }

View File

@ -8,7 +8,8 @@ abstract class Passive implements IResource {
public advanceAction?: (time: number, state: GameState) => void = undefined; public advanceAction?: (time: number, state: GameState) => void = undefined;
constructor ( constructor (
public readonly name: string, public readonly singularName: string,
public readonly pluralName: string,
public readonly description: string public readonly description: string
) { } ) { }

View File

@ -4,7 +4,9 @@ class Pastor extends Job {
private _timeSinceLastTithe = 0; private _timeSinceLastTithe = 0;
constructor () { constructor () {
super('Pastors', super(
'pastor',
'pastors',
'Collect tithings for you and recruit new members from other faiths automatically.'); 'Collect tithings for you and recruit new members from other faiths automatically.');
} }
@ -38,7 +40,7 @@ class Pastor extends Job {
collected = money.max(state) - money.value; collected = money.max(state) - money.value;
if (collected > 0) { if (collected > 0) {
money?.addValue(collected, state); money?.addValue(collected, state);
state.log(`Your pastors collected $${state.config.formatNumber(collected)} in tithings from ${state.config.formatNumber(tithed)} followers.`); state.log(`Your pastors collected $${formatNumber(collected)} in tithings from ${formatNumber(tithed)} followers.`);
} }
this._timeSinceLastTithe = 0; this._timeSinceLastTithe = 0;
} }

View File

@ -3,39 +3,36 @@
abstract class Purchasable implements IResource { abstract class Purchasable implements IResource {
public readonly resourceType: ResourceType = ResourceType.consumable; public readonly resourceType: ResourceType = ResourceType.consumable;
public valueInWholeNumbers = true; public valueInWholeNumbers = true;
public clickText = 'Purchase';
public clickDescription = 'Purchase';
public value = 0; public value = 0;
public readonly cost: ResourceNumber = { }; public readonly cost: ResourceNumber = { };
public inc?: (state: GameState) => number = undefined; public inc?: (state: GameState) => number = undefined;
public max?: (_state: GameState) => number = undefined; public max?: (_state: GameState) => number = undefined;
public userActions: ResourceAction[] = [
{
name: this._purchaseButtonText,
description: this._purchaseDescription,
isEnabled: (state: GameState): boolean =>
(this.max === undefined || this.value < this.max(state))
&& state.isPurchasable(this.cost),
performAction: (state: GameState): void => {
this._purchase(state);
},
},
];
protected _costMultiplier: ResourceNumber = { }; protected _costMultiplier: ResourceNumber = { };
protected _isUnlocked = false; protected _isUnlocked = false;
constructor ( constructor (
public readonly name: string, public readonly singularName: string,
public readonly description: string public readonly pluralName: string,
public readonly description: string,
private readonly _purchaseButtonText: string = 'Purchase',
private readonly _purchaseDescription: string = `Buy a ${singularName}.`,
) { } ) { }
public clickAction (state: GameState): void {
if (this.max !== undefined && this.value >= this.max(state)) return;
if (state.deductCost(this.cost)) {
const amount = this._purchaseAmount(state);
if (amount > 0) {
this.value += amount;
state.log(this._purchaseLog(amount, state));
for (const key in this._costMultiplier) {
const rkey = <ResourceKey>key;
this.cost[rkey] =
(this.cost[rkey] ?? 0) * (this._costMultiplier[rkey] ?? 1);
}
}
}
}
public addValue (amount: number, _state: GameState): void { public addValue (amount: number, _state: GameState): void {
this.value += amount; this.value += amount;
} }
@ -56,6 +53,22 @@ abstract class Purchasable implements IResource {
} }
protected _purchaseLog (amount: number, _state: GameState): string { protected _purchaseLog (amount: number, _state: GameState): string {
return `You purchased ${amount} x ${this.name}.`; return `You purchased ${amount} x ${this.pluralName}.`;
}
private _purchase (state: GameState): void {
if (this.max !== undefined && this.value >= this.max(state)) return;
if (state.deductCost(this.cost)) {
const amount = this._purchaseAmount(state);
if (amount > 0) {
this.value += amount;
state.log(this._purchaseLog(amount, state));
for (const key in this._costMultiplier) {
const rkey = <ResourceKey>key;
this.cost[rkey] =
(this.cost[rkey] ?? 0) * (this._costMultiplier[rkey] ?? 1);
}
}
}
} }
} }

View File

@ -5,7 +5,8 @@ class Religion implements IResource {
public readonly valueInWholeNumbers = true; public readonly valueInWholeNumbers = true;
constructor ( constructor (
public readonly name: string, public readonly singularName: string,
public readonly pluralName: string,
public readonly description: string, public readonly description: string,
public value: number, public value: number,
) { } ) { }

View File

@ -5,13 +5,17 @@ abstract class Research extends Purchasable {
public inc = undefined; public inc = undefined;
constructor ( constructor (
public readonly name: string, public readonly singularName: string,
public readonly pluralName: string,
public readonly description: string public readonly description: string
) { ) {
super(name, description); super(
singularName,
pluralName,
description,
'Learn',
'Complete this research.');
this.value = 0; this.value = 0;
this.clickText = 'Learn';
this.clickDescription = 'Complete this research.';
} }
public max: (state: GameState) => number = (_state) => 1; public max: (state: GameState) => number = (_state) => 1;

View File

@ -0,0 +1,41 @@
/// <reference path="../GameState.ts" />
enum ResourceType {
religion = 'religion',
job = 'job',
consumable = 'consumable',
infrastructure = 'infrastructure',
research = 'research',
passive = 'passive',
}
enum ResourceKey {
playerOrg = 'playerOrg',
christianity = 'christianity',
islam = 'islam',
hinduism = 'hinduism',
buddhism = 'buddhism',
sikhism = 'sikhism',
judaism = 'judaism',
other = 'other',
atheism = 'atheism',
pastors = 'pastors',
money = 'money',
cryptoCurrency = 'cryptoCurrency',
tents = 'tents',
houses = 'houses',
churches = 'churches',
compounds = 'compounds',
buildingPermit = 'buildingPermit',
megaChurches = 'megaChurches',
credibility = 'credibility',
}
type ResourceNumber = { [key in ResourceKey]?: number };
type ResourceAction = {
name: string;
description: string;
isEnabled: (state: GameState) => boolean;
performAction: (state: GameState) => void;
};

View File

@ -2,8 +2,10 @@
class Tent extends Infrastructure { class Tent extends Infrastructure {
constructor (config: GameConfig) { constructor (config: GameConfig) {
super('Tents', super(
`Provides room to house ${config.formatNumber(config.cfgCapacity.tents?.playerOrg ?? 0)} followers.`); 'tent',
'tents',
`Provides room to house ${formatNumber(config.cfgCapacity.tents?.playerOrg ?? 0)} followers.`);
this.cost.money = config.cfgInitialCost.tents; this.cost.money = config.cfgInitialCost.tents;
this._costMultiplier.money = config.cfgCostMultiplier.tents; this._costMultiplier.money = config.cfgCostMultiplier.tents;
} }

View File

@ -53,17 +53,21 @@ class DebugRenderer implements IRenderer {
let content = ` let content = `
<span class='resource-title' <span class='resource-title'
title='${this._escape(resource.description)}'> title='${this._escape(resource.description)}'>
${this._escape(resource.name)}</span><br> ${this._escape(resource.pluralName)}</span><br>
<span class='resource-value'></span> <span class='resource-value'></span>
<span class='resource-max'></span> <span class='resource-max'></span>
<span class='resource-inc'></span> <span class='resource-inc'></span>
`; `;
if (resource.clickText !== undefined if (resource.userActions !== undefined) {
&& resource.clickDescription !== undefined) { content += '<br>';
content += `<br> for (let i = 0; i < resource.userActions.length; i++) {
<button class='resource-btn' const action = resource.userActions[i];
title='${this._escape(resource.clickDescription)}'> content += `<button
${this._escape(resource.clickText)}</button>`; id='resource-btn-${rkey}-${i}'
class='resource-btn'
title='${this._escape(action.description)}'>
${this._escape(action.name)}</button>`;
}
} }
if (resource.cost !== undefined if (resource.cost !== undefined
&& Object.keys(resource.cost).length !== 0) { && Object.keys(resource.cost).length !== 0) {
@ -71,11 +75,14 @@ class DebugRenderer implements IRenderer {
} }
el.innerHTML = content; el.innerHTML = content;
resContainer.appendChild(el); resContainer.appendChild(el);
if (resource.clickAction !== undefined) { if (resource.userActions !== undefined) {
const btn = el.getElementsByClassName('resource-btn')[0]; for (let i = 0; i < resource.userActions.length; i++) {
btn.addEventListener('click', (): void => { const action = resource.userActions[i];
state.performClick(rkey); const btn = document.getElementById(`resource-btn-${rkey}-${i}`);
}); btn?.addEventListener('click', (): void => {
state.performAction(rkey, i);
});
}
} }
} }
// create tools footer // create tools footer
@ -103,22 +110,25 @@ class DebugRenderer implements IRenderer {
const value = resource.valueInWholeNumbers const value = resource.valueInWholeNumbers
? Math.floor(resource.value) ? Math.floor(resource.value)
: resource.value; : resource.value;
elV.innerHTML = state.config.formatNumber(value); elV.innerHTML = formatNumber(value);
elT.innerHTML = resource.max !== undefined elT.innerHTML = resource.max !== undefined
? ` / ${state.config.formatNumber(resource.max(state))}` ? ` / ${formatNumber(resource.max(state))}`
: ''; : '';
const elB = el.getElementsByClassName('resource-btn'); if (resource.userActions !== undefined) {
if (elB.length > 0) { for (let i = 0; i < resource.userActions.length; i++) {
const enabled = state.isPurchasable(resource.cost) const elB = document.getElementById(`resource-btn-${rkey}-${i}`);
&& (resource.max === undefined if (elB === null) continue;
|| resource.value < resource.max(state)); if (resource.userActions[i].isEnabled(state)) {
if (enabled) elB[0].removeAttribute('disabled'); elB.removeAttribute('disabled');
else elB[0].setAttribute('disabled', 'disabled'); } else {
elB.setAttribute('disabled', 'disabled');
}
}
} }
if (resource.inc !== undefined && resource.inc(state) > 0) { if (resource.inc !== undefined && resource.inc(state) > 0) {
const elI = el.getElementsByClassName('resource-inc')[0]; const elI = el.getElementsByClassName('resource-inc')[0];
elI.innerHTML = elI.innerHTML =
` +${state.config.formatNumber(resource.inc(state))}/s`; ` +${formatNumber(resource.inc(state))}/s`;
} }
if (this._handleClick) { if (this._handleClick) {
const elC = el.getElementsByClassName('resource-cost'); const elC = el.getElementsByClassName('resource-cost');
@ -154,10 +164,10 @@ class DebugRenderer implements IRenderer {
if (resource.cost?.[rkey] !== undefined) { if (resource.cost?.[rkey] !== undefined) {
if (cost !== '') cost += ', '; if (cost !== '') cost += ', ';
if (rkey === ResourceKey.money) { if (rkey === ResourceKey.money) {
cost += `$${state.config.formatNumber(resource.cost[rkey] ?? 0)}`; cost += `$${formatNumber(resource.cost[rkey] ?? 0)}`;
} else { } else {
cost += `${state.config.formatNumber(resource.cost[rkey] ?? 0)} cost += `${formatNumber(resource.cost[rkey] ?? 0)}
${state.resource[rkey]?.name ?? rkey}`; ${state.resource[rkey]?.pluralName ?? rkey}`;
} }
} }
} }