fixes for linter

This commit is contained in:
Rudis Muiznieks 2021-09-05 14:45:37 -05:00
parent da6aac7003
commit 3e767b396b
25 changed files with 245 additions and 241 deletions

View File

@ -9,6 +9,10 @@
"parserOptions": {"project": "tsconfig.json"}, "parserOptions": {"project": "tsconfig.json"},
"plugins": ["@typescript-eslint"], "plugins": ["@typescript-eslint"],
"rules": { "rules": {
"max-len": ["warn", {
"ignoreStrings": true,
"ignoreTemplateLiterals": true,
"ignoreRegExpLiterals": true}],
"@typescript-eslint/triple-slash-reference": "off", "@typescript-eslint/triple-slash-reference": "off",
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/array-type": ["warn", {"default": "array-simple"}], "@typescript-eslint/array-type": ["warn", {"default": "array-simple"}],
@ -29,7 +33,6 @@
"@typescript-eslint/no-base-to-string": "error", "@typescript-eslint/no-base-to-string": "error",
"@typescript-eslint/no-confusing-non-null-assertion": "error", "@typescript-eslint/no-confusing-non-null-assertion": "error",
"@typescript-eslint/no-confusing-void-expression": "error", "@typescript-eslint/no-confusing-void-expression": "error",
"@typescript-eslint/no-dynamic-delete": "warn",
"@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",
@ -61,12 +64,6 @@
"allowNullableObject": false}], "allowNullableObject": false}],
"@typescript-eslint/switch-exhaustiveness-check": "warn", "@typescript-eslint/switch-exhaustiveness-check": "warn",
"@typescript-eslint/type-annotation-spacing": "warn", "@typescript-eslint/type-annotation-spacing": "warn",
"@typescript-eslint/typedef": ["error", {
"arrowParameter": true,
"memberVariableDeclaration": true,
"parameter": true,
"propertyDeclaration": true,
"variableDeclaration": true}],
"@typescript-eslint/unified-signatures": "warn", "@typescript-eslint/unified-signatures": "warn",
"@typescript-eslint/brace-style": "warn", "@typescript-eslint/brace-style": "warn",
"@typescript-eslint/comma-dangle": ["warn", "always-multiline"], "@typescript-eslint/comma-dangle": ["warn", "always-multiline"],
@ -74,7 +71,17 @@
"@typescript-eslint/default-param-last": "error", "@typescript-eslint/default-param-last": "error",
"@typescript-eslint/dot-notation": "error", "@typescript-eslint/dot-notation": "error",
"@typescript-eslint/func-call-spacing": ["warn", "never"], "@typescript-eslint/func-call-spacing": ["warn", "never"],
"@typescript-eslint/indent": ["warn", 2], "@typescript-eslint/indent": ["warn", 2, {
"SwitchCase": 1,
"VariableDeclarator": 1,
"outerIIFEBody": 1,
"FunctionDeclaration": {"parameters": 1, "body": 1},
"CallExpression": {"arguments": 1},
"ArrayExpression": 1,
"ObjectExpression": 1,
"MemberExpression": 2,
"flatTernaryExpressions": false,
"ignoreComments": false}],
"@typescript-eslint/keyword-spacing": "error", "@typescript-eslint/keyword-spacing": "error",
"@typescript-eslint/lines-between-class-members": ["warn", "always", { "@typescript-eslint/lines-between-class-members": ["warn", "always", {
"exceptAfterSingleLine": true}], "exceptAfterSingleLine": true}],

View File

@ -2,7 +2,7 @@
/// <reference path="./render/DebugRenderer.ts" /> /// <reference path="./render/DebugRenderer.ts" />
let globalStartTime = 0; let globalStartTime = 0;
let globalTimeout: number = null; let globalTimeout: number | null = null;
const cycleLength = 250; const cycleLength = 250;
function gameLoop (state: GameState, renderer: IRenderer): void { function gameLoop (state: GameState, renderer: IRenderer): void {
@ -15,8 +15,9 @@ function gameLoop (state: GameState, renderer: IRenderer): void {
// run again in 1sec // run again in 1sec
globalStartTime = new Date().getTime(); globalStartTime = new Date().getTime();
globalTimeout = setTimeout((): void => globalTimeout = setTimeout((): void => {
gameLoop(state, renderer), cycleLength); gameLoop(state, renderer);
}, cycleLength);
} }
function startGame (state: GameState, renderer: IRenderer): void { function startGame (state: GameState, renderer: IRenderer): void {
@ -47,6 +48,7 @@ function startGame (state: GameState, renderer: IRenderer): void {
}); });
if (document.readyState !== 'loading') startGame(state, renderer); if (document.readyState !== 'loading') startGame(state, renderer);
else document.addEventListener('DOMContentLoaded', (): void => else document.addEventListener('DOMContentLoaded', (): void => {
startGame(state, renderer)); startGame(state, renderer);
});
})(); })();

View File

@ -25,15 +25,20 @@ class GameConfig {
public relOtherShare = 0.02; public relOtherShare = 0.02;
public relNoneShare = 0.16; public relNoneShare = 0.16;
public cfgStartingPlayerMax = 5;
public cfgStartingMoneyMax = 500000;
public cfgStartingTentMax = 5;
public cfgStartingCryptoMax = 1000;
public cfgStartingMegaChurchMax = 2;
public cfgTitheAmount = 10; public cfgTitheAmount = 10;
public cfgTimeBetweenTithes = 30000; public cfgTimeBetweenTithes = 30000;
public cfgCryptoReturnAmount = 1; public cfgCryptoReturnAmount = 1;
public cfgCredibilityRestoreRate = 0.25; public cfgCredibilityRestoreRate = 0.25;
public cfgPastorRecruitRate= 0.01; public cfgPastorRecruitRate = 0.01;
public generateState (): GameState { public generateState (): GameState {
const state: GameState = new GameState(); const state: GameState = new GameState(this);
state.config = this;
// create player organization // create player organization
state.addResource('plorg', new PlayerOrg()); state.addResource('plorg', new PlayerOrg());

View File

@ -1,20 +1,25 @@
class GameState { class GameState {
public readonly config: GameConfig;
public onResourceClick: Array<() => void> = [];
public logger: ILogger | null = null;
public numberFormatDigits = 1;
public now = 0;
private readonly _versionMaj: number = 0; private readonly _versionMaj: number = 0;
private readonly _versionMin: number = 1; private readonly _versionMin: number = 1;
public config: GameConfig;
private _timeSinceSave = 0; private _timeSinceSave = 0;
private readonly _timeBetweenSaves: number = 10000; private readonly _timeBetweenSaves: number = 10000;
private _resources: {[key: string]: IResource} = { }; private _resources: { [key: string]: IResource } = { };
private _resourceKeys: string[] = []; private readonly _resourceKeys: string[] = [];
public onResourceClick: Array<() => void> = [];
public logger: ILogger = null;
public numberFormatDigits = 1;
public now = 0; constructor (config: GameConfig) {
this.config = config;
}
public addResource (key: string, resource: IResource): void { public addResource (key: string, resource: IResource): void {
this._resourceKeys.push(key); this._resourceKeys.push(key);
@ -32,32 +37,29 @@ class GameState {
// advance each resource // advance each resource
for (const rkey of this._resourceKeys) { for (const rkey of this._resourceKeys) {
if (this._resources[rkey].isUnlocked(this) const resource = this._resources[rkey];
&& this._resources[rkey].advanceAction !== null) { if (this._resources[rkey].isUnlocked(this)) {
this._resources[rkey].advanceAction(time, this); if (resource.advanceAction !== null)
resource.advanceAction(time, this);
} }
} }
// perform auto increments // perform auto increments
for (const rkey of this._resourceKeys) { for (const rkey of this._resourceKeys) {
if (!this._resources[rkey].isUnlocked(this)) continue; const resource = this._resources[rkey];
if (!resource.isUnlocked(this)) continue;
const max: number = this._resources[rkey].max if (resource.inc !== null && (resource.max === null
? this._resources[rkey].max(this) || this._resources[rkey].value < resource.max(this))) {
: null; this._resources[rkey].addValue(resource.inc(this) * time / 1000, this);
const inc: number = this._resources[rkey].inc
? this._resources[rkey].inc(this)
: 0;
if (inc > 0 && (max === null
|| this._resources[rkey].value < max)) {
this._resources[rkey].addValue(inc * time / 1000, this);
} }
const val: number = this._resources[rkey].value;
if (max !== null && val > max) { if (resource.max !== null && resource.value > resource.max(this)) {
this._resources[rkey].addValue((val - max) * -1, this); this._resources[rkey].addValue(
(resource.value - resource.max(this)) * -1, this);
} }
if (val < 0) { if (resource.value < 0) {
this._resources[rkey].addValue(val * -1, this); this._resources[rkey].addValue(resource.value * -1, this);
} }
} }
} }
@ -71,18 +73,19 @@ class GameState {
} }
public performClick (resourceKey: string): void { public performClick (resourceKey: string): void {
if (!this._resources[resourceKey].isUnlocked(this)) return; const resource = this._resources[resourceKey];
if (!resource.isUnlocked(this)) return;
if (this._resources[resourceKey].clickAction !== null) { if (resource.clickAction !== null) {
this._resources[resourceKey].clickAction(this); resource.clickAction(this);
for (const callback of this.onResourceClick) { for (const callback of this.onResourceClick) {
callback(); callback();
} }
} }
} }
public deductCost (cost: { [rkey: string]: number }): boolean { public deductCost (cost: { [rkey: string]: number } | null): boolean {
if (cost === null || Object.keys(cost) === null) return true; if (cost === null) return true;
if (!this.isPurchasable(cost)) return false; if (!this.isPurchasable(cost)) return false;
for (const rkey of Object.keys(cost)) { for (const rkey of Object.keys(cost)) {
this._resources[rkey].addValue(cost[rkey] * -1, this); this._resources[rkey].addValue(cost[rkey] * -1, this);
@ -90,8 +93,8 @@ class GameState {
return true; return true;
} }
public isPurchasable (cost: { [rkey: string]: number }): boolean { public isPurchasable (cost: { [rkey: string]: number } | null): boolean {
if (cost === null || Object.keys(cost) === null) return true; if (cost === null) return true;
for (const rkey of Object.keys(cost)) { for (const rkey of Object.keys(cost)) {
if (this._resources[rkey].value < cost[rkey]) { if (this._resources[rkey].value < cost[rkey]) {
return false; return false;
@ -101,24 +104,24 @@ class GameState {
} }
public formatNumber (num: number): string { public formatNumber (num: number): string {
type vlookup = { value: number, symbol: string }; type UnitLookup = { value: number, symbol: string };
const lookup: vlookup[] = [ const lookup: UnitLookup[] = [
{ value: 1, symbol: '' }, { value: 1, symbol: '' },
{ value: 1e3, symbol: 'K' }, { value: 1e3, symbol: 'K' },
{ value: 1e6, symbol: 'M' }, { value: 1e6, symbol: 'M' },
{ value: 1e9, symbol: 'G' }, { value: 1e9, symbol: 'G' },
{ value: 1e12, symbol: 'T' }, { value: 1e12, symbol: 'T' },
{ value: 1e15, symbol: 'P' }, { value: 1e15, symbol: 'P' },
{ value: 1e18, symbol: 'E' } { value: 1e18, symbol: 'E' },
]; ];
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
let item: vlookup; let item: UnitLookup | undefined;
for (item of lookup.slice().reverse()) { for (item of lookup.slice().reverse()) {
if (num >= item.value) break; if (num >= item.value) break;
} }
return item return item !== undefined
? (num / item.value).toFixed(this.numberFormatDigits) ? (num / item.value).toFixed(
.replace(rx, '$1') + item.symbol this.numberFormatDigits).replace(rx, '$1') + item.symbol
: num.toFixed(this.numberFormatDigits).replace(rx, '$1'); : num.toFixed(this.numberFormatDigits).replace(rx, '$1');
} }
@ -129,15 +132,15 @@ class GameState {
} }
public save (): void { public save (): void {
const saveObj: any = { }; const saveObj: SaveData = {};
saveObj.version = { saveObj.version = {
maj: this._versionMaj, maj: this._versionMaj,
min: this._versionMin min: this._versionMin,
}; };
for (const rkey of this._resourceKeys) { for (const rkey of this._resourceKeys) {
saveObj[rkey] = { saveObj[rkey] = {
value: this._resources[rkey].value, value: this._resources[rkey].value,
cost: this._resources[rkey].cost cost: this._resources[rkey].cost,
}; };
} }
const saveStr: string = btoa(JSON.stringify(saveObj)); const saveStr: string = btoa(JSON.stringify(saveObj));
@ -145,33 +148,32 @@ class GameState {
} }
public load (): void { public load (): void {
const saveStr: string = localStorage.getItem('savegame'); const saveStr: string | null = localStorage.getItem('savegame');
if (saveStr !== null) { if (saveStr !== null) {
try { try {
const saveObj: { [key: string]: any } = const saveObj: SaveData = <SaveData>JSON.parse(atob(saveStr));
JSON.parse(atob(saveStr)); if (this._versionMaj === saveObj.version?.maj) {
if (this._versionMaj === saveObj.version.maj) {
for (const rkey of this._resourceKeys) { for (const rkey of this._resourceKeys) {
if (saveObj[rkey] !== undefined const saveRes = <{
&& saveObj[rkey].value !== undefined value: number;
&& saveObj[rkey].cost !== undefined) { cost: { [key: string]: number } | null;
// @ts-ignore } | undefined> saveObj[rkey];
this._resources[rkey].value = saveObj[rkey].value; if (saveRes !== undefined) {
// @ts-ignore // @ts-expect-error writing read-only value from save data
this._resources[rkey].cost = saveObj[rkey].cost; this._resources[rkey].value = saveRes.value;
// @ts-expect-error writing read-only cost from save data
this._resources[rkey].cost = saveRes.cost ?? null;
} }
} }
} else { } else {
// tslint:disable-next-line // tslint:disable-next-line
console.log('The saved game is too old to load.'); console.log('The saved game is too old to load.');
} }
} catch (e) { } catch (e: unknown) {
// tslint:disable-next-line
console.log('There was an error loading the saved game.'); console.log('There was an error loading the saved game.');
console.log(e); // tslint:disable-line console.log(e);
} }
} else { } else {
// tslint:disable-next-line
console.log('No save game was found.'); console.log('No save game was found.');
} }
} }
@ -183,3 +185,11 @@ class GameState {
this.log('Reset all game resources.'); this.log('Reset all game resources.');
} }
} }
type SaveData = {
[key: string]: {
value: number;
cost: { [key: string]: number } | null;
} | { maj: number, min: number } | undefined;
version?: { maj: number, min: number };
};

View File

@ -1,5 +1,5 @@
class DebugLogger implements ILogger { class DebugLogger implements ILogger {
private _container: HTMLElement; private readonly _container: HTMLElement;
constructor (container: HTMLElement) { constructor (container: HTMLElement) {
this._container = container; this._container = container;
@ -9,7 +9,9 @@ class DebugLogger implements ILogger {
const p: HTMLElement = document.createElement('p'); const p: HTMLElement = document.createElement('p');
p.innerText = text; p.innerText = text;
this._container.appendChild(p); this._container.appendChild(p);
this._container.parentElement.scrollTop = if (this._container.parentElement !== null) {
this._container.parentElement.scrollHeight; this._container.parentElement.scrollTop =
this._container.parentElement.scrollHeight;
}
} }
} }

View File

@ -1,3 +1,3 @@
interface ILogger { interface ILogger {
msg (text: string): void; msg: (text: string) => void;
} }

View File

@ -8,10 +8,8 @@ class Church extends Infrastructure {
this._costMultiplier.money = 1.01; this._costMultiplier.money = 1.01;
} }
public max (state: GameState): number { public max: (state: GameState) => number = (state) =>
// one church per compound state.getResource('cmpnd').value;
return state.getResource('cmpnd').value;
}
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;

View File

@ -5,15 +5,10 @@ class Credibility extends Passive {
super( super(
'Credibility', 'Credibility',
'Affects your ability to recruit and retain followers.'); 'Affects your ability to recruit and retain followers.');
this._baseMax = 100;
this.value = 100; this.value = 100;
} }
public max (): number { public max: (state: GameState) => number = (_state) => 100;
return 100; public inc: (state: GameState) => number = (state) =>
} state.config.cfgCredibilityRestoreRate;
public inc (state: GameState): number {
return state.config.cfgCredibilityRestoreRate;
}
} }

View File

@ -6,7 +6,9 @@ class CryptoCurrency extends Purchasable {
"A crypto coin that can't be spent directly, but provides a steady stream of passive income."); "A crypto coin that can't be spent directly, but provides a steady stream of passive income.");
this.cost.money = 100; this.cost.money = 100;
this._costMultiplier.money = 1.1; this._costMultiplier.money = 1.1;
this._baseMax = 1000;
this.valueInWholeNumbers = false; this.valueInWholeNumbers = false;
} }
public max: (state: GameState) => number = (state) =>
state.config.cfgStartingCryptoMax;
} }

View File

@ -8,10 +8,9 @@ class House extends Infrastructure {
this._costMultiplier.money = 1.01; this._costMultiplier.money = 1.01;
} }
public max (state: GameState): number { // two houses per compound
// two houses per compound public max: (state: GameState) => number = (state) =>
return state.getResource('cmpnd').value * 2; state.getResource('cmpnd').value * 2;
}
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;

View File

@ -1,29 +1,29 @@
enum ResourceType { enum ResourceType {
Religion = 'religion', religion = 'religion',
Job = 'job', job = 'job',
Consumable = 'consumable', consumable = 'consumable',
Infrastructure = 'infrastructure', infrastructure = 'infrastructure',
Research = 'research', research = 'research',
Passive = 'passive' passive = 'passive',
} }
interface IResource { interface IResource {
readonly resourceType: ResourceType; readonly resourceType: ResourceType;
readonly name: string | null; readonly name: string;
readonly description: string | null; readonly description: string;
readonly valueInWholeNumbers: boolean; readonly valueInWholeNumbers: boolean;
readonly clickText: string; readonly clickText: string | null;
readonly clickDescription: string; readonly clickDescription: string | null;
// readonly altClickText?: string; // readonly altClickText?: string;
// readonly altClickDescription?: string; // readonly altClickDescription?: string;
readonly value: number; readonly value: number;
readonly cost: { [key: string]: number }; readonly cost: { [key: string]: number } | null;
max (state: GameState): number | null; max: ((state: GameState) => number) | null;
inc (state: GameState): number | null; inc: ((state: GameState) => number) | null;
clickAction(state: GameState): void; clickAction: ((state: GameState) => void) | null;
// altClickAction (state: GameState): void; // altClickAction (state: GameState): void;
addValue (amount: number, state: GameState): void; addValue: (amount: number, state: GameState) => void;
isUnlocked (state: GameState): boolean; isUnlocked: (state: GameState) => boolean;
advanceAction (time: number, state: GameState): void; advanceAction: ((time: number, state: GameState) => void) | null;
} }

View File

@ -1,5 +1,5 @@
/// <reference path="./Purchasable.ts" /> /// <reference path="./Purchasable.ts" />
abstract class Infrastructure extends Purchasable { abstract class Infrastructure extends Purchasable {
public readonly resourceType: ResourceType = ResourceType.Infrastructure; public readonly resourceType: ResourceType = ResourceType.infrastructure;
} }

View File

@ -1,14 +1,16 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
abstract class Job implements IResource { abstract class Job implements IResource {
public readonly resourceType: ResourceType = ResourceType.Job; public readonly resourceType: ResourceType = ResourceType.job;
public readonly valueInWholeNumbers: boolean = true; public readonly valueInWholeNumbers: boolean = true;
public readonly clickText: string = 'Hire'; public readonly clickText: string = 'Hire';
public readonly clickDescription: string = public readonly clickDescription: string = 'Promote one of your followers.';
'Promote one of your followers.';
public value = 0; public value = 0;
public readonly cost: { [key: string]: number } = { }; public readonly cost: { [key: string]: number } = { };
public max: ((state: GameState) => number) | null = null;
public inc: ((state: GameState) => number) | null = null;
protected _costMultiplier: { [key: string]: number } = { }; protected _costMultiplier: { [key: string]: number } = { };
protected _isUnlocked = false; protected _isUnlocked = false;
@ -17,20 +19,14 @@ abstract class Job implements IResource {
public readonly description: string public readonly description: string
) { } ) { }
public max (state: GameState): number | null {
return null;
}
public inc (): number | null {
return null;
}
public clickAction (state: GameState): void { public clickAction (state: GameState): void {
if (this._availableJobs(state) <= 0) { if (this._availableJobs(state) <= 0) {
state.log('You have no unemployed followers to promote.'); state.log('You have no unemployed followers to promote.');
return; return;
} }
if (this.value < this.max(state) && state.deductCost(this.cost)) { if (this.max !== null && this.value < this.max(state)
&& state.deductCost(this.cost)) {
this.addValue(1); this.addValue(1);
state.log(this._hireLog(1, state)); state.log(this._hireLog(1, state));
for (const rkey of Object.keys(this._costMultiplier)) { for (const rkey of Object.keys(this._costMultiplier)) {
@ -43,21 +39,21 @@ abstract class Job implements IResource {
this.value += amount; this.value += amount;
} }
public isUnlocked (state: GameState): boolean { public isUnlocked (_state: GameState): boolean {
return this._isUnlocked; return this._isUnlocked;
} }
public advanceAction (time: number, state: GameState): void { public advanceAction (_time: number, _state: GameState): void {
return; return;
} }
protected _availableJobs (state: GameState): number { protected _availableJobs (state: GameState): number {
// number of followers minus the number of filled jobs // number of followers minus the number of filled jobs
const followers: number = state.getResource('plorg').value; const followers: number = state.getResource('plorg').value;
const hired: number = state.getResources() const hired: number = state.getResources().reduce(
.reduce((tot: number, rkey: string): number => { (tot: number, rkey: string): number => {
const res: IResource = state.getResource(rkey); const res: IResource = state.getResource(rkey);
return res.resourceType === ResourceType.Job return res.resourceType === ResourceType.job
? tot + res.value ? tot + res.value
: tot; : tot;
}, 0); }, 0);
@ -66,7 +62,7 @@ abstract class Job implements IResource {
return max; return max;
} }
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.name}.`;
} }
} }

View File

@ -6,9 +6,11 @@ class MegaChurch extends Infrastructure {
'Room for 5 pastors'); 'Room for 5 pastors');
this.cost.money = 7500000; this.cost.money = 7500000;
this._costMultiplier.money = 1.01; this._costMultiplier.money = 1.01;
this._baseMax = 2;
} }
public max: (state: GameState) => number = (state) =>
state.config.cfgStartingMegaChurchMax;
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;
const permit: IResource = state.getResource('blpmt'); const permit: IResource = state.getResource('blpmt');

View File

@ -1,7 +1,7 @@
/// <reference path="./Purchasable.ts" /> /// <reference path="./Purchasable.ts" />
class Money extends Purchasable { class Money extends Purchasable {
public readonly resourceType: ResourceType = ResourceType.Consumable; public readonly resourceType: ResourceType = ResourceType.consumable;
private _lastCollectionTime = 0; private _lastCollectionTime = 0;
@ -11,18 +11,17 @@ class Money extends Purchasable {
super('Money', 'Used to purchase goods and services.'); super('Money', 'Used to purchase goods and services.');
this.clickText = 'Collect Tithes'; this.clickText = 'Collect Tithes';
this.clickDescription = 'Voluntary contributions from followers.'; this.clickDescription = 'Voluntary contributions from followers.';
this._baseMax = 500000;
this.valueInWholeNumbers = false; this.valueInWholeNumbers = false;
this._isUnlocked = true; this._isUnlocked = true;
} }
public max (state: GameState): number | null { public max: (state: GameState) => number = (state: GameState) => {
let max: number = this._baseMax; let max: number = state.config.cfgStartingMoneyMax;
max += state.getResource('cmpnd').value * 500000; max += state.getResource('cmpnd').value * 500000;
return max; return max;
} };
public inc (state: GameState): number { public inc: (state: GameState) => number = (state) => {
let inc = 0; let inc = 0;
// crypto currency // crypto currency
@ -30,7 +29,7 @@ class Money extends Purchasable {
* state.config.cfgCryptoReturnAmount; * state.config.cfgCryptoReturnAmount;
return inc; return inc;
} };
protected _purchaseAmount (state: GameState): number { protected _purchaseAmount (state: GameState): number {
const plorg: IResource = state.getResource('plorg'); const plorg: IResource = state.getResource('plorg');

View File

@ -1,7 +1,7 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
abstract class Passive implements IResource { abstract class Passive implements IResource {
public readonly resourceType: ResourceType = ResourceType.Passive; public readonly resourceType: ResourceType = ResourceType.passive;
public readonly valueInWholeNumbers: boolean = false; public readonly valueInWholeNumbers: boolean = false;
public readonly clickText: null = null; public readonly clickText: null = null;
public readonly clickDescription: null = null; public readonly clickDescription: null = null;
@ -10,31 +10,22 @@ abstract class Passive implements IResource {
public readonly clickAction: null = null; public readonly clickAction: null = null;
protected _baseMax: number | null; public max: ((state: GameState) => number) | null = null;
protected _baseInc: number | null; public inc: ((state: GameState) => number) | null = null;
public advanceAction: ((time: number, state: GameState) => void) | null = null;
constructor ( constructor (
public readonly name: string, public readonly name: string,
public readonly description: string public readonly description: string
) { } ) { }
public max (state: GameState): number | null {
return this._baseMax;
}
public inc (state: GameState): number | null { public addValue (amount: number, _state: GameState): void {
return this._baseInc;
}
public addValue (amount: number, state: GameState): void {
this.value += amount; this.value += amount;
} }
public isUnlocked (state: GameState): boolean { public isUnlocked (_state: GameState): boolean {
return true; return true;
} }
public advanceAction (time: number, state: GameState): void {
return;
}
} }

View File

@ -8,11 +8,11 @@ class Pastor extends Job {
'Collect tithings for you and recruit new members from other faiths automatically.'); 'Collect tithings for you and recruit new members from other faiths automatically.');
} }
public max (state: GameState): number { public max: (state: GameState) => number = (state) => {
let max: number = state.getResource('chrch').value * 2; let max: number = state.getResource('chrch').value * 2;
max += state.getResource('mchch').value * 5; max += state.getResource('mchch').value * 5;
return max; return max;
} };
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
if (this._isUnlocked) return true; if (this._isUnlocked) return true;
@ -30,7 +30,7 @@ class Pastor extends Job {
if (Math.floor(plorg.value) < tithed) if (Math.floor(plorg.value) < tithed)
tithed = Math.floor(plorg.value); tithed = Math.floor(plorg.value);
let collected: number = tithed * state.config.cfgTitheAmount; let collected: number = tithed * state.config.cfgTitheAmount;
if (collected > money.max(state) - money.value) if (money.max !== null && collected > money.max(state) - money.value)
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);

View File

@ -1,7 +1,7 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
class PlayerOrg implements IResource { class PlayerOrg implements IResource {
public readonly resourceType: ResourceType = ResourceType.Religion; public readonly resourceType: ResourceType = ResourceType.religion;
public readonly name: string = 'Player'; public readonly name: string = 'Player';
public readonly description: string = 'In you they trust.'; public readonly description: string = 'In you they trust.';
public readonly valueInWholeNumbers: boolean = true; public readonly valueInWholeNumbers: boolean = true;
@ -11,13 +11,12 @@ class PlayerOrg implements IResource {
public readonly cost: null = null; public readonly cost: null = null;
private _timeSinceLastLost = 0; private _timeSinceLastLost = 0;
private _baseMax = 5;
private _lastRecruitmentLog = 0; private _lastRecruitmentLog = 0;
private _followerSources: { [key: string]: number } = { }; private _followerSources: { [key: string]: number } = { };
private _followerDests: { [key: string]: number } = { }; private _followerDests: { [key: string]: number } = { };
public max (state: GameState): number { public max (state: GameState): number {
let max: number = this._baseMax; let max: number = state.config.cfgStartingPlayerMax;
max += state.getResource('tents').value * 2; max += state.getResource('tents').value * 2;
max += state.getResource('house').value * 10; max += state.getResource('house').value * 10;
return max; return max;
@ -27,12 +26,12 @@ class PlayerOrg implements IResource {
let inc = 0; let inc = 0;
// pastor recruiting // pastor recruiting
const pastors: number = state.getResource('pstor').value; const pastors = state.getResource('pstor').value;
inc += pastors * state.config.cfgPastorRecruitRate; inc += pastors * state.config.cfgPastorRecruitRate;
// credibility adjustment // credibility adjustment
const creds: IResource = state.getResource('creds'); const creds = state.getResource('creds');
inc *= creds.value / creds.max(state); if (creds.max !== null) inc *= creds.value / creds.max(state);
return inc; return inc;
} }
@ -46,10 +45,12 @@ class PlayerOrg implements IResource {
// chance to fail increases as credibility decreases // chance to fail increases as credibility decreases
const creds: IResource = state.getResource('creds'); const creds: IResource = state.getResource('creds');
const ratio: number = Math.ceil(creds.value) / creds.max(state); if (creds.max !== null) {
if (Math.random() > ratio) { const ratio: number = Math.ceil(creds.value) / creds.max(state);
state.log('Your recruitment efforts failed.'); if (Math.random() > ratio) {
return; state.log('Your recruitment efforts failed.');
return;
}
} }
this._lastRecruitmentLog = 0; // always log on click this._lastRecruitmentLog = 0; // always log on click
@ -57,17 +58,17 @@ class PlayerOrg implements IResource {
} }
public addValue (amount: number, state: GameState): void { public addValue (amount: number, state: GameState): void {
const oldValue: number = this.value; const oldValue = this.value;
this.value += amount; this.value += amount;
const diff: number = Math.floor(this.value) - Math.floor(oldValue); const diff = Math.floor(this.value) - Math.floor(oldValue);
if (diff > 0) { if (diff > 0) {
// gained followers must come from other faiths // gained followers must come from other faiths
for (let i = 0; i < diff; i++) { for (let i = 0; i < diff; i++) {
const source: [string, IResource] = this._getRandomReligion(state); const source = this._getRandomReligion(state);
source[1].addValue(-1, state); source[1].addValue(-1, state);
const curFollowers: number = this._followerSources[source[0]]; const curFollowers = this._followerSources[source[0]];
this._followerSources[source[0]] = curFollowers this._followerSources[source[0]] = !isNaN(curFollowers)
? curFollowers + 1 ? curFollowers + 1
: 1; : 1;
} }
@ -77,14 +78,14 @@ class PlayerOrg implements IResource {
const dest: [string, IResource] = this._getRandomReligion(state); const dest: [string, IResource] = this._getRandomReligion(state);
dest[1].addValue(1, state); dest[1].addValue(1, state);
const curFollowers: number = this._followerDests[dest[0]]; const curFollowers: number = this._followerDests[dest[0]];
this._followerDests[dest[0]] = curFollowers this._followerDests[dest[0]] = !isNaN(curFollowers)
? curFollowers + 1 ? curFollowers + 1
: 1; : 1;
} }
} }
} }
public isUnlocked (state: GameState): boolean { public isUnlocked (_state: GameState): boolean {
return true; return true;
} }
@ -93,11 +94,13 @@ class PlayerOrg implements IResource {
this._timeSinceLastLost += time; this._timeSinceLastLost += time;
if (this._timeSinceLastLost > 10000) { if (this._timeSinceLastLost > 10000) {
if (this.value > 0) { if (this.value > 0) {
const creds: IResource = state.getResource('creds'); const creds = state.getResource('creds');
const ratio: number = Math.ceil(creds.value) / creds.max(state); if (creds.max !== null) {
if (Math.random() > ratio) { const ratio: number = Math.ceil(creds.value) / creds.max(state);
const lost: number = Math.ceil(this.value / 25 * (1 - ratio)); if (Math.random() > ratio) {
this.addValue(lost * -1, state); const lost: number = Math.ceil(this.value / 25 * (1 - ratio));
this.addValue(lost * -1, state);
}
} }
} }
this._timeSinceLastLost = 0; this._timeSinceLastLost = 0;
@ -113,8 +116,7 @@ class PlayerOrg implements IResource {
for (const rkey of Object.keys(this._followerDests)) { for (const rkey of Object.keys(this._followerDests)) {
if (msg !== '') msg += ', '; if (msg !== '') msg += ', ';
const religion: IResource = state.getResource(rkey); const religion: IResource = state.getResource(rkey);
msg += msg += `${state.formatNumber(this._followerDests[rkey])} to ${religion.name}`;
`${state.formatNumber(this._followerDests[rkey])} to ${religion.name}`;
total += this._followerDests[rkey]; total += this._followerDests[rkey];
delete this._followerDests[rkey]; delete this._followerDests[rkey];
} }

View File

@ -1,15 +1,17 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
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 clickText = 'Purchase';
public clickDescription = 'Purchase'; public clickDescription = 'Purchase';
public value = 0; public value = 0;
public readonly cost: { [key: string]: number } = { }; public readonly cost: { [key: string]: number } = { };
public inc: ((state: GameState) => number) | null = null;
public max: ((_state: GameState) => number) | null = null;
protected _costMultiplier: { [key: string]: number } = { }; protected _costMultiplier: { [key: string]: number } = { };
protected _baseMax: number | null = null;
protected _isUnlocked = false; protected _isUnlocked = false;
constructor ( constructor (
@ -17,16 +19,9 @@ abstract class Purchasable implements IResource {
public readonly description: string public readonly description: string
) { } ) { }
public max (state: GameState): number | null {
return this._baseMax;
}
public inc (state: GameState): number | null {
return null;
}
public clickAction (state: GameState): void { public clickAction (state: GameState): void {
if (this.max(state) !== null && this.value >= this.max(state)) return; if (this.max !== null && this.value >= this.max(state)) return;
if (state.deductCost(this.cost)) { if (state.deductCost(this.cost)) {
const amount: number = this._purchaseAmount(state); const amount: number = this._purchaseAmount(state);
if (amount > 0) { if (amount > 0) {
@ -39,7 +34,7 @@ abstract class Purchasable implements IResource {
} }
} }
public addValue (amount: number, state: GameState): void { public addValue (amount: number, _state: GameState): void {
this.value += amount; this.value += amount;
} }

View File

@ -1,7 +1,7 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
class Religion implements IResource { class Religion implements IResource {
public readonly resourceType: ResourceType = ResourceType.Religion; public readonly resourceType: ResourceType = ResourceType.religion;
public readonly valueInWholeNumbers: boolean = true; public readonly valueInWholeNumbers: boolean = true;
public readonly clickText: null = null; public readonly clickText: null = null;
public readonly clickDescription: null = null; public readonly clickDescription: null = null;
@ -18,11 +18,11 @@ class Religion implements IResource {
public value: number, public value: number,
) { } ) { }
public addValue (amount: number, state: GameState): void { public addValue (amount: number, _state: GameState): void {
this.value += amount; this.value += amount;
} }
public isUnlocked (state: GameState): boolean { public isUnlocked (_state: GameState): boolean {
return true; return true;
} }
} }

View File

@ -1,7 +1,7 @@
/// <reference path="./Purchasable.ts" /> /// <reference path="./Purchasable.ts" />
abstract class Research extends Purchasable { abstract class Research extends Purchasable {
public readonly resourceType: ResourceType = ResourceType.Research; public readonly resourceType: ResourceType = ResourceType.research;
constructor ( constructor (
public readonly name: string, public readonly name: string,
@ -9,8 +9,9 @@ abstract class Research extends Purchasable {
) { ) {
super(name, description); super(name, description);
this.value = 0; this.value = 0;
this._baseMax = 1;
this.clickText = 'Learn'; this.clickText = 'Learn';
this.clickDescription = 'Complete this research.' this.clickDescription = 'Complete this research.';
} }
public max: (_state: GameState) => number = (_state) => 1;
} }

View File

@ -6,13 +6,12 @@ class Tent extends Infrastructure {
'Provides room to house 2 followers.'); 'Provides room to house 2 followers.');
this.cost.money = 250; this.cost.money = 250;
this._costMultiplier.money = 1.05; this._costMultiplier.money = 1.05;
this._baseMax = 5;
} }
public max (state: GameState): number { public max: (state: GameState) => number = (state) => {
// ten extra tents per compound // ten extra tents per compound
let max: number = this._baseMax; let max: number = state.config.cfgStartingTentMax;
max += state.getResource('cmpnd').value * 10; max += state.getResource('cmpnd').value * 10;
return max; return max;
} };
} }

View File

@ -1,38 +1,41 @@
/// <reference path="../model/logging/DebugLogger.ts" /> /// <reference path="../model/logging/DebugLogger.ts" />
class DebugRenderer implements IRenderer { // eslint-disable-line @typescript-eslint/no-unused-vars class DebugRenderer implements IRenderer {
private _initialized = false; private _initialized = false;
private _handleClick = true; private _handleClick = true;
public render (state: GameState): void { public render (state: GameState): void {
const rkeys: string[] = state.getResources(); const rkeys: string[] = state.getResources();
const container = document.getElementById('irreligious-game');
if (!this._initialized) { if (!this._initialized) {
const container: HTMLElement = if (container === null) {
document.getElementById('irreligious-game'); console.error('could not find game container');
return;
}
this._initialized = true; this._initialized = true;
state.onResourceClick.push((): void => { state.onResourceClick.push((): void => {
this._handleClick = true; this._handleClick = true;
}); });
const style: HTMLElement = document.createElement('link'); const style = document.createElement('link');
style.setAttribute('rel', 'stylesheet'); style.setAttribute('rel', 'stylesheet');
style.setAttribute('href', 'css/debugger.css'); style.setAttribute('href', 'css/debugger.css');
const head: HTMLElement = document.getElementsByTagName('head')[0]; const head = document.getElementsByTagName('head')[0];
head.appendChild(style); head.appendChild(style);
// create resource area and logging area // create resource area and logging area
const resDiv: HTMLElement = document.createElement('div'); const resDiv = document.createElement('div');
resDiv.id = 'resource-section'; resDiv.id = 'resource-section';
container.appendChild(resDiv); container.appendChild(resDiv);
const logDiv: HTMLElement = document.createElement('div'); const logDiv = document.createElement('div');
logDiv.id = 'logging-section'; logDiv.id = 'logging-section';
container.appendChild(logDiv); container.appendChild(logDiv);
const logContent: HTMLElement = document.createElement('div'); const logContent = document.createElement('div');
logDiv.appendChild(logContent); logDiv.appendChild(logContent);
state.logger = new DebugLogger(logContent); state.logger = new DebugLogger(logContent);
// create containers for each resource type // create containers for each resource type
for (const item in ResourceType) { for (const item in ResourceType) {
if (isNaN(Number(item))) { if (isNaN(Number(item))) {
const el: HTMLElement = document.createElement('div'); const el = document.createElement('div');
el.id = `resource-container-${<string>ResourceType[item]}`; el.id = `resource-container-${item.toString()}`;
el.className = 'resource-type-container'; el.className = 'resource-type-container';
resDiv.appendChild(el); resDiv.appendChild(el);
} }
@ -40,23 +43,21 @@ class DebugRenderer implements IRenderer { // eslint-disable-line @typescript-es
// create containers for each resource // create containers for each resource
for (const rkey of rkeys) { for (const rkey of rkeys) {
const resource: IResource = state.getResource(rkey); const resource: IResource = state.getResource(rkey);
const resContainer: HTMLElement = const resContainer = document.getElementById(
document.getElementById( `resource-container-${resource.resourceType}`);
`resource-container-${resource.resourceType}`); if (resContainer === null) continue;
const el: HTMLElement = document.createElement('div'); const el = document.createElement('div');
el.className = 'resource locked'; el.className = 'resource locked';
el.id = `resource-details-${rkey}`; el.id = `resource-details-${rkey}`;
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 ${this._escape(resource.name)}</span><br>
? resource.name
: rkey)}</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 !== null) { if (resource.clickText !== null && resource.clickDescription !== null) {
content += `<br> content += `<br>
<button class='resource-btn' <button class='resource-btn'
title='${this._escape(resource.clickDescription)}'> title='${this._escape(resource.clickDescription)}'>
@ -71,31 +72,29 @@ class DebugRenderer implements IRenderer { // eslint-disable-line @typescript-es
if (resource.clickAction !== null) { if (resource.clickAction !== null) {
const btn: Element = const btn: Element =
el.getElementsByClassName('resource-btn')[0]; el.getElementsByClassName('resource-btn')[0];
btn.addEventListener('click', (): void => btn.addEventListener('click', (): void => {
state.performClick(rkey)); state.performClick(rkey);
});
} }
} }
// create tools footer // create tools footer
const footer: HTMLElement = document.createElement('div'); const footer = document.createElement('div');
footer.className = 'footer'; footer.className = 'footer';
footer.innerHTML = ` footer.innerHTML = `
<button id='dbg-btn-reset'>Reset Game</button> <button id='dbg-btn-reset'>Reset Game</button>
`; `;
resDiv.appendChild(footer); resDiv.appendChild(footer);
document.getElementById('dbg-btn-reset') document.getElementById('dbg-btn-reset')?.addEventListener('click',
.addEventListener('click', (): void => { (): void => {
state.reset(); state.reset();
document.getElementById('irreligious-game').innerHTML = ''; container.innerHTML = '';
this._initialized = false; this._initialized = false;
}); });
} }
for (const rkey of rkeys) { for (const rkey of rkeys) {
const resource: IResource = state.getResource(rkey); const resource: IResource = state.getResource(rkey);
const container: HTMLElement = document const el = document.getElementById(`resource-details-${rkey}`);
.getElementById(`resource-container-${resource.resourceType}`); if (el !== null && resource.isUnlocked(state)) {
const el: HTMLElement = document
.getElementById(`resource-details-${rkey}`);
if (resource.isUnlocked(state)) {
if (el.className !== 'resource') el.className = 'resource'; if (el.className !== 'resource') el.className = 'resource';
const elV: Element = const elV: Element =
el.getElementsByClassName('resource-value')[0]; el.getElementsByClassName('resource-value')[0];
@ -106,15 +105,13 @@ class DebugRenderer implements IRenderer { // eslint-disable-line @typescript-es
: resource.value; : resource.value;
elV.innerHTML = state.formatNumber(value); elV.innerHTML = state.formatNumber(value);
elT.innerHTML = resource.max !== null elT.innerHTML = resource.max !== null
&& resource.max(state) !== null
? ` / ${state.formatNumber(resource.max(state))}` ? ` / ${state.formatNumber(resource.max(state))}`
: ''; : '';
const elB: HTMLCollectionOf<Element> = const elB: HTMLCollectionOf<Element> =
el.getElementsByClassName('resource-btn'); el.getElementsByClassName('resource-btn');
if (elB.length > 0) { if (elB.length > 0) {
const enabled: boolean = state.isPurchasable(resource.cost) const enabled: boolean = state.isPurchasable(resource.cost)
&& (resource.max(state) === null && (resource.max === null || resource.value < resource.max(state));
|| resource.value < resource.max(state));
if (enabled) elB[0].removeAttribute('disabled'); if (enabled) elB[0].removeAttribute('disabled');
else elB[0].setAttribute('disabled', 'disabled'); else elB[0].setAttribute('disabled', 'disabled');
} }
@ -132,7 +129,7 @@ class DebugRenderer implements IRenderer { // eslint-disable-line @typescript-es
} }
} }
} else { } else {
if (el.className !== 'resource locked') if (el !== null && el.className !== 'resource locked')
el.className = 'resource locked'; el.className = 'resource locked';
} }
} }
@ -146,8 +143,8 @@ class DebugRenderer implements IRenderer { // eslint-disable-line @typescript-es
'>': '&gt;', '>': '&gt;',
'"': '&quot;', '"': '&quot;',
"'": '&#x27;', "'": '&#x27;',
'/': '&#x2F;' '/': '&#x2F;',
} };
const escaper = /[&<>"'/]/g; const escaper = /[&<>"'/]/g;
return text.replace(escaper, (match: string): string => return text.replace(escaper, (match: string): string =>
escapes[match]); escapes[match]);
@ -155,8 +152,9 @@ class DebugRenderer implements IRenderer { // eslint-disable-line @typescript-es
private _getCostStr (resource: IResource, state: GameState): string { private _getCostStr (resource: IResource, state: GameState): string {
let cost = ''; let cost = '';
for (const rkey of state.getResources()) { if (resource.cost !== null) {
if (resource.cost[rkey] !== undefined) { for (const rkey of state.getResources()) {
if (isNaN(resource.cost[rkey])) continue;
if (cost !== '') cost += ', '; if (cost !== '') cost += ', ';
if (rkey === 'money') { if (rkey === 'money') {
cost += `$${state.formatNumber(resource.cost[rkey])}`; cost += `$${state.formatNumber(resource.cost[rkey])}`;

View File

@ -1,3 +1,3 @@
interface IRenderer { // eslint-disable-line @typescript-eslint/no-unused-vars interface IRenderer {
render (state: GameState): void; render: (state: GameState) => void;
} }

View File

@ -1,12 +1,13 @@
{ {
"compilerOptions": { "compilerOptions": {
"noImplicitAny": false,
"rootDir": "./src/", "rootDir": "./src/",
"outFile": "./public/js/irreligious.js", "outFile": "./public/js/irreligious.js",
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"strictNullChecks": true, "strictNullChecks": true,
"noImplicitAny": true,
"strictPropertyInitialization": true,
"target": "ES5", "target": "ES5",
"module": "none", "module": "none",
"plugins": [{"name": "typescript-eslint-language-service"}] "plugins": [{"name": "typescript-eslint-language-service"}]