changed cost into function; changed inc into ResourcNumber

This commit is contained in:
Rudis Muiznieks 2021-09-12 22:27:07 -05:00
parent a0daffbddf
commit 0b6e263cc7
19 changed files with 162 additions and 131 deletions

14
TODO.md
View File

@ -1,7 +1,5 @@
## In Progress
- [ ] change `inc` to a `ResourceNumber` instead of a `number`
- [ ] change `cost` into a function that computes based on `value`
## Initial Game Progression Plan
@ -13,17 +11,16 @@
- [x] houses (+follower cap)
- [x] churches (+pastor cap)
- [x] hire pastors to recruit and gather tithes (+money, +followers)
- [ ] hire compound managers to auto-build (+tents, +houses, +churches)
- [ ] fluctuating crypto market value passive determines crypto value
- [x] hire compound managers to auto-build (+tents, +houses, +churches)
- [x] fluctuating crypto market value passive determines crypto value
- [ ] prosperity gospel policy increases tithes, decreases recruitment rate
- main resource problems:
- [x] lose money on purchases
- [x] lose money for salaries
- [ ] lose money for compound maintenance
- [x] gain money from tithes
- [x] low credibility loses followers
- [ ] low and decreasing money loses jobs
- [ ] gain or lose money on crypto based on market passive
- [x] low and decreasing money loses jobs
- [x] gain or lose money on crypto based on market passive
### Phase 2: 1K-100K Followers
@ -84,10 +81,7 @@
## Short-Term Todos
- [ ] add `disabledHint` to `userActions` to generate hint about how to enable an action
- [x] add an action to sell purchasables and regain some portion of their cost
- [ ] add a `policy` resource type that can be toggled on or off
- [x] remove recruitment effects of credibility (that will be for notoriety instead)
- [x] change `value` to a getter that does `Math.floor` if it's a whole number resource
## Long-Term Ideas

View File

@ -89,12 +89,12 @@ class GameConfig {
public cfgCryptoMarketAdjustAmount = 0.1;
public cfgCryptoMarketAdjustPeriod = 30000;
public cfgCryptoMarketGrowthBias = 0.1;
public cfgDefaultSellMultiplier = 0.5;
public cfgFollowerGainLossLogTimer = 10000;
public cfgNoMoneyQuitRate = 0.2;
public cfgNoMoneyQuitTime = 10000;
public cfgPassiveMax = 100;
public cfgPastorRecruitRate = 0.01;
public cfgSellCostBackMultiplier = 0.5;
public cfgTimeBetweenTithes = 10000;
public cfgTitheAmount = 10000;
public cfgTitheCredibilityHitFactor = 3;
@ -202,7 +202,7 @@ class GameConfig {
// add resources
state.addResource(new Money(3.5));
state.addResource(new CryptoCurrency(this));
state.addResource(new CryptoCurrency());
state.addResource(new Tent(this));
state.addResource(new House(this));
state.addResource(new Church(this));

View File

@ -58,7 +58,10 @@ class GameState {
resource.inc !== undefined &&
(resource.max === undefined || resource.value < resource.max(this))
) {
resource.addValue((resource.inc(this) * time) / 1000, this);
resource.addValue(
(resourceNumberSum(resource.inc(this)) * time) / 1000,
this
);
}
if (resource.max !== undefined && resource.value > resource.max(this)) {
@ -138,7 +141,6 @@ class GameState {
if (resource === undefined) continue;
const resSav: ResourceConfig = {
value: resource.value,
cost: resource.cost,
};
if (resource.emitConfig !== undefined) {
resSav.config = resource.emitConfig();

View File

@ -1,3 +1,5 @@
/// <reference path="./resource/SharedTypes.ts" />
const numberFormatDigits = 1;
function formatNumber(num: number): string {
@ -26,3 +28,12 @@ function formatNumber(num: number): string {
return `${sign}${number}`;
}
function resourceNumberSum(res: ResourceNumber): number {
let sum = 0;
for (const key in res) {
const rkey = <ResourceKey>key;
sum += res[rkey] ?? 0;
}
return sum;
}

View File

@ -10,7 +10,7 @@ class BuildingPermit extends Research {
'building permits',
'Unlocks several new buildings you can build outside of your compounds.'
);
this.cost.money = config.cfgInitialCost.buildingPermit;
this._baseCost.money = config.cfgInitialCost.buildingPermit;
}
public isUnlocked = (state: GameState): boolean => {

View File

@ -15,7 +15,7 @@ class Church extends Infrastructure {
undefined,
undefined
);
this.cost.money = config.cfgInitialCost.churches;
this._baseCost.money = config.cfgInitialCost.churches;
this._costMultiplier.money = config.cfgCostMultiplier.churches;
}
@ -23,12 +23,15 @@ class Church extends Infrastructure {
(state.resource.compounds?.value ?? 0) *
(state.config.cfgCapacity.compounds?.churches ?? 0);
public inc = (state: GameState): number => {
// compound managers
return (
public inc = (state: GameState): ResourceNumber => {
const inc: ResourceNumber = {};
const compoundManagers =
(state.resource.compoundManagers?.value ?? 0) *
(state.config.cfgBuySpeed.compoundManagers?.churches ?? 0)
);
(state.config.cfgBuySpeed.compoundManagers?.churches ?? 0);
if (compoundManagers > 0) {
inc.compoundManagers = compoundManagers;
}
return inc;
};
public isUnlocked = (state: GameState): boolean => {

View File

@ -11,7 +11,7 @@ class Compound extends Infrastructure {
'Provides space for tents, houses, and churches and a place to hide more money.',
true
);
this.cost.money = config.cfgInitialCost.compounds;
this._baseCost.money = config.cfgInitialCost.compounds;
this._costMultiplier.money = config.cfgCostMultiplier.compounds;
}

View File

@ -26,6 +26,9 @@ class Credibility extends Passive {
public max = (state: GameState): number => state.config.cfgPassiveMax;
public inc = (state: GameState): number =>
state.config.cfgCredibilityRestoreRate;
public inc = (state: GameState): ResourceNumber => {
const inc: ResourceNumber = {};
inc.credibility = state.config.cfgCredibilityRestoreRate;
return inc;
};
}

View File

@ -3,7 +3,7 @@
class CryptoCurrency extends Purchasable {
public readonly resourceKey = ResourceKey.cryptoCurrency;
constructor(config: GameConfig) {
constructor() {
super(
'FaithCoin',
'faithcoin',
@ -11,11 +11,21 @@ class CryptoCurrency extends Purchasable {
"A crypto coin that can't be spent directly, but provides a steady stream of passive income.",
true
);
this.cost.money = config.cfgInitialCost.cryptoCurrency;
this.valueInWholeNumbers = false;
}
public isUnlocked = (_state: GameState): boolean => true;
public cost = (state: GameState): ResourceNumber => {
const cost: ResourceNumber = {};
const market = state.resource.cryptoMarket;
if (market !== undefined) {
cost.money = market.value;
} else {
cost.money = state.config.cfgInitialCost.cryptoCurrency;
}
return cost;
};
public isUnlocked = (): boolean => true;
public max = (state: GameState): number =>
state.config.cfgInitialMax.cryptoCurrency ?? 0;

View File

@ -40,17 +40,13 @@ class CryptoMarket extends Hidden {
) {
adjustment = state.config.cfgCryptoCurrencyMinimumValue - this.value;
}
//if (Math.abs(adjustment) > 0) {
if (Math.abs(adjustment) > 0) {
this.addValue(adjustment, state);
state.log(
`FaithCoin just ${
adjustment > 0 ? 'increased' : 'decreased'
} in value by $${formatNumber(Math.abs(adjustment))}.`
);
//}
if (crypto?.cost !== undefined) {
crypto.cost.money = this.value;
state.autoAction(); // cause redraw
}
}
};

View File

@ -39,12 +39,14 @@ class Follower extends Resource {
return max;
};
public inc = (state: GameState): number => {
let inc = 0;
public inc = (state: GameState): ResourceNumber => {
const inc: ResourceNumber = {};
// pastor recruiting
const pastors = state.resource.pastors?.value ?? 0;
inc += pastors * state.config.cfgPastorRecruitRate;
const pastors =
(state.resource.pastors?.value ?? 0) * state.config.cfgPastorRecruitRate;
if (pastors > 0) inc.pastors = pastors;
// credibility adjustment
// this should be based on notoriety instead

View File

@ -13,7 +13,7 @@ class House extends Infrastructure {
)} followers.`,
true
);
this.cost.money = config.cfgInitialCost.houses;
this._baseCost.money = config.cfgInitialCost.houses;
this._costMultiplier.money = config.cfgCostMultiplier.houses;
}
@ -21,12 +21,15 @@ class House extends Infrastructure {
(state.resource.compounds?.value ?? 0) *
(state.config.cfgCapacity.compounds?.houses ?? 0);
public inc = (state: GameState): number => {
// compound managers
return (
public inc = (state: GameState): ResourceNumber => {
const inc: ResourceNumber = {};
const compoundManagers =
(state.resource.compoundManagers?.value ?? 0) *
(state.config.cfgBuySpeed.compoundManagers?.houses ?? 0)
);
(state.config.cfgBuySpeed.compoundManagers?.houses ?? 0);
if (compoundManagers > 0) {
inc.compoundManagers = compoundManagers;
}
return inc;
};
public isUnlocked = (state: GameState): boolean => {

View File

@ -4,10 +4,9 @@ abstract class Job extends Resource {
public readonly resourceType = ResourceType.job;
public readonly valueInWholeNumbers = true;
public readonly cost: ResourceNumber = {};
public max?: (state: GameState) => number = undefined;
public inc?: (state: GameState) => number = undefined;
public inc?: (state: GameState) => ResourceNumber = undefined;
public userActions: ResourceAction[] = [
{
@ -31,7 +30,6 @@ abstract class Job extends Resource {
},
];
protected _costMultiplier: { [key in ResourceKey]?: number } = {};
protected _isUnlocked = false;
constructor(
@ -102,18 +100,9 @@ abstract class Job extends Resource {
private _promoteFollower(state: GameState): void {
if (Job.availableJobs(state) <= 0) return;
if (
this.max !== undefined &&
this.value < this.max(state) &&
state.deductCost(this.cost)
) {
if (this.max !== undefined && this.value < this.max(state)) {
this.addValue(1, state);
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);
}
}
}
@ -121,10 +110,5 @@ abstract class Job extends Resource {
if (this.value <= 0) return;
this.addValue(-1, state);
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

@ -13,7 +13,7 @@ class Megachurch extends Infrastructure {
)} pastors`,
true
);
this.cost.money = config.cfgInitialCost.megaChurches;
this._baseCost.money = config.cfgInitialCost.megaChurches;
this._costMultiplier.money = config.cfgCostMultiplier.megaChurches;
}

View File

@ -40,11 +40,11 @@ class Money extends Resource {
return max;
};
public inc = (state: GameState): number => {
let inc = 0;
public inc = (state: GameState): ResourceNumber => {
const inc: ResourceNumber = {};
// tithings
inc +=
const tithings =
((state.resource.pastors?.value ?? 0) *
(state.resource.followers?.value ?? 0) *
(state.config.cfgTitheAmount ?? 0) *
@ -52,10 +52,13 @@ class Money extends Resource {
state.config.cfgTimeBetweenTithes;
// salaries
inc -=
const compoundManagers =
(state.resource.compoundManagers?.value ?? 0) *
(state.config.cfgSalary.compoundManagers ?? 0);
if (tithings > 0) inc.pastors = tithings;
if (compoundManagers > 0) inc.compoundManagers = compoundManagers * -1;
return inc;
};

View File

@ -4,10 +4,9 @@ abstract class Purchasable extends Resource {
public readonly resourceType: ResourceType = ResourceType.purchasable;
public valueInWholeNumbers = true;
public cost: ResourceNumber = {};
public inc?: (state: GameState) => number = undefined;
public max?: (state: GameState) => number = undefined;
public inc?: (state: GameState) => ResourceNumber = undefined;
public userActions: ResourceAction[] = [
{
@ -15,19 +14,17 @@ abstract class Purchasable extends Resource {
description: this._purchaseDescription,
isEnabled: (state: GameState): boolean =>
(this.max === undefined || this.value < this.max(state)) &&
state.isPurchasable(this.cost),
state.isPurchasable(this.cost(state)),
performAction: (state: GameState): void => {
this._purchase(state);
},
},
];
protected _costMultiplier: ResourceNumber = {};
protected _sellMultiplier?: number | ResourceNumber;
protected readonly _baseCost: ResourceNumber = {};
protected readonly _costMultiplier: ResourceNumber = {};
protected _isUnlocked = false;
private _lastWholeNumberValue = 0;
constructor(
public readonly label: string,
public readonly singularName: string,
@ -52,8 +49,21 @@ abstract class Purchasable extends Resource {
}
}
public cost = (_: GameState): ResourceNumber => {
if (this.value <= 0) return this._baseCost;
const actualCost: ResourceNumber = {};
for (const key in this._baseCost) {
const rkey = <ResourceKey>key;
const baseCost = this._baseCost[rkey] ?? 0;
const multiplier = this._costMultiplier[rkey] ?? 1;
actualCost[rkey] = baseCost * Math.pow(multiplier, this.value);
}
return actualCost;
};
public isUnlocked = (state: GameState): boolean => {
if (!this._isUnlocked && state.isPurchasable(this.cost)) {
if (!this._isUnlocked && state.isPurchasable(this.cost(state))) {
this._isUnlocked = true;
}
return this._isUnlocked;
@ -73,21 +83,6 @@ abstract class Purchasable extends Resource {
}
};
public addValue = (amount: number, _: GameState): void => {
this.rawValue += amount;
const wholeNumberChange = this.value - this._lastWholeNumberValue;
if (wholeNumberChange > 0) {
for (const key in this._costMultiplier) {
const rkey = <ResourceKey>key;
this.cost[rkey] =
(this.cost[rkey] ?? 0) *
(this._costMultiplier[rkey] ?? 1) *
wholeNumberChange;
}
this._lastWholeNumberValue = this.value;
}
};
protected _purchaseLog(amount: number, _state: GameState): string {
let verb = 'purchased';
if (amount < 0) {
@ -101,7 +96,7 @@ abstract class Purchasable extends Resource {
private _purchase(state: GameState): void {
if (this.max !== undefined && this.value >= this.max(state)) return;
if (state.deductCost(this.cost)) {
if (state.deductCost(this.cost(state))) {
this.addValue(1, state);
state.log(this._purchaseLog(1, state));
}
@ -109,25 +104,17 @@ abstract class Purchasable extends Resource {
private _sell(state: GameState): void {
if (this.value <= 0) return;
const costBack: ResourceNumber = {};
for (const key in this.cost) {
const rkey = <ResourceKey>key;
let cost = this.cost[rkey];
if (cost === undefined) continue;
// revert cost multiplier
cost /= this._costMultiplier[rkey] ?? 1;
this.cost[rkey] = cost;
const multiplier =
this._sellMultiplier === undefined
? state.config.cfgDefaultSellMultiplier
: typeof this._sellMultiplier === 'number'
? this._sellMultiplier
: this._sellMultiplier[rkey] ?? state.config.cfgDefaultSellMultiplier;
// penalize return on used item
costBack[rkey] = cost * -1 * multiplier;
state.deductCost(costBack);
}
this.addValue(-1, state);
state.log(this._purchaseLog(-1, state));
const costBack: ResourceNumber = {};
for (const key in this.cost(state)) {
const rkey = <ResourceKey>key;
const cost = this.cost(state)[rkey];
if (cost === undefined) continue;
costBack[rkey] = cost * state.config.cfgSellCostBackMultiplier * -1;
}
state.deductCost(costBack);
}
}

View File

@ -1,6 +1,10 @@
/// <reference path="./IResource.ts" />
abstract class Resource implements IResource {
public inc?: (state: GameState) => ResourceNumber = undefined;
public cost?: (state: GameState) => ResourceNumber = undefined;
public max?: (state: GameState) => number = undefined;
protected rawValue = 0;
public abstract readonly resourceType: ResourceType;

View File

@ -13,7 +13,7 @@ class Tent extends Infrastructure {
)} followers.`,
true
);
this.cost.money = config.cfgInitialCost.tents;
this._baseCost.money = config.cfgInitialCost.tents;
this._costMultiplier.money = config.cfgCostMultiplier.tents;
}
@ -26,8 +26,14 @@ class Tent extends Infrastructure {
return max;
};
public inc = (state: GameState): number =>
// compound managers
public inc = (state: GameState): ResourceNumber => {
const inc: ResourceNumber = {};
const compoundManagers =
(state.resource.compoundManagers?.value ?? 0) *
(state.config.cfgBuySpeed.compoundManagers?.tents ?? 0);
if (compoundManagers > 0) {
inc.compoundManagers = compoundManagers;
}
return inc;
};
}

View File

@ -73,7 +73,7 @@ class DebugRenderer implements IRenderer {
}
if (
resource.cost !== undefined &&
Object.keys(resource.cost).length !== 0
Object.keys(resource.cost(state)).length !== 0
) {
content += "<br>Cost: <span class='resource-cost'></span>";
}
@ -81,7 +81,6 @@ class DebugRenderer implements IRenderer {
resContainer.appendChild(el);
if (resource.userActions !== undefined) {
for (let i = 0; i < resource.userActions.length; i++) {
const action = resource.userActions[i];
const btn = document.getElementById(`resource-btn-${rkey}-${i}`);
btn?.addEventListener('click', (): void => {
state.performAction(rkey, i);
@ -135,13 +134,17 @@ class DebugRenderer implements IRenderer {
}
}
}
const inc =
resource.inc !== undefined ? resource.inc(state) : undefined;
if (resource.inc !== undefined) {
const resInc = resource.inc(state);
const inc = resourceNumberSum(resInc);
const elI = el.getElementsByClassName('resource-inc')[0];
if (inc !== undefined && inc !== 0) {
if (inc !== 0) {
elI.innerHTML = ` ${inc > 0 ? '+' : ''}${formatNumber(inc)}/s`;
elI.setAttribute('title', this._getIncDetails(resource, state));
} else if (elI.innerHTML !== '') {
elI.innerHTML = '';
elI.removeAttribute('title');
}
}
if (this._handleClick) {
const elC = el.getElementsByClassName('resource-cost');
@ -177,17 +180,37 @@ class DebugRenderer implements IRenderer {
private _getCostStr(resource: IResource, state: GameState): string {
let cost = '';
if (resource.cost === undefined) return cost;
for (const rkey of state.resources) {
if (resource.cost?.[rkey] !== undefined) {
const rcost = resource.cost(state)[rkey];
if (rcost !== undefined && rcost > 0) {
if (cost !== '') cost += ', ';
if (rkey === ResourceKey.money) {
cost += `$${formatNumber(resource.cost[rkey] ?? 0)}`;
cost += `$${formatNumber(rcost)}`;
} else {
cost += `${formatNumber(resource.cost[rkey] ?? 0)}
${state.resource[rkey]?.pluralName ?? rkey}`;
cost += `${formatNumber(rcost)} ${
(rcost > 1
? state.resource[rkey]?.pluralName
: state.resource[rkey]?.singularName) ?? rkey
}`;
}
}
}
return cost;
}
private _getIncDetails(resource: IResource, state: GameState): string {
let inc = '';
if (resource.inc === undefined) return inc;
for (const rkey of state.resources) {
const incRes = state.resource[rkey];
if (incRes === undefined || incRes.label === undefined) continue;
const rinc = resource.inc(state)[rkey];
if (rinc !== undefined && rinc !== 0) {
if (inc !== '') inc += '\n';
inc += `${incRes.label}: ${rinc > 0 ? '+' : ''}${formatNumber(rinc)}/s`;
}
}
return inc;
}
}