replaced hard-coded values with configs
This commit is contained in:
parent
caf5152551
commit
3f57d6ce91
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
source[1].addValue(-1, state);
|
||||
const curFollowers = this._followerSources[source[0]];
|
||||
this._followerSources[source[0]] = !isNaN(curFollowers)
|
||||
? curFollowers + 1
|
||||
: 1;
|
||||
if (source !== null) {
|
||||
source[1].addValue(-1, state);
|
||||
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);
|
||||
dest[1].addValue(1, state);
|
||||
const curFollowers = this._followerDests[dest[0]];
|
||||
this._followerDests[dest[0]] = !isNaN(curFollowers)
|
||||
? curFollowers + 1
|
||||
: 1;
|
||||
if (dest !== null) {
|
||||
dest[1].addValue(1, state);
|
||||
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];
|
||||
delete 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);
|
||||
msg +=
|
||||
`${state.formatNumber(this._followerSources[rkey])} from ${religion.name}`;
|
||||
total += this._followerSources[rkey];
|
||||
delete this._followerSources[rkey];
|
||||
const followers = this._followerSources[rkey];
|
||||
if (religion !== null && followers !== undefined) {
|
||||
if (msg !== '') msg += ', ';
|
||||
msg +=
|
||||
`${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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
for (const rkey of state.getResources()) {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Reference in New Issue