diff --git a/public/css/debugger.css b/public/css/debugger.css index af26d36..dccd315 100644 --- a/public/css/debugger.css +++ b/public/css/debugger.css @@ -3,8 +3,20 @@ flex-wrap: wrap; } .resource { - min-width: 8em; + min-width: 12em; border: 2px solid black; padding: 0.25em 0.5em; margin: 0 0.5em 0.5em 0; } +#resource-container-religion .resource { + background-color: #ccf; +} +#resource-container-consumable .resource { + background-color: #cfc; +} +#resource-container-infrastructure .resource { + background-color: #fcc; +} +#resource-container-passive .resource { + background-color: #ffc; +} diff --git a/src/main.ts b/src/main.ts index d1d2a2b..1f2aa6c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,8 +12,8 @@ function gameLoop (state: GameState, renderer: IRenderer): void { const elapsedTime: number = globalStartTime > 0 ? (new Date()).getTime() - globalStartTime : 0; - renderer.render(state); state.advance(elapsedTime); + renderer.render(state); // run again in 1sec globalStartTime = (new Date()).getTime(); diff --git a/src/model/GameConfig.ts b/src/model/GameConfig.ts index f5d74a6..a60cab2 100644 --- a/src/model/GameConfig.ts +++ b/src/model/GameConfig.ts @@ -1,5 +1,6 @@ /// /// +/// /// /// /// @@ -57,10 +58,11 @@ class GameConfig { this.relNoneShare * this.worldPopulation)); // add hidden resources - state.addResource('creds', new Credibility(2)); + state.addResource('creds', new Credibility()); // add resources - state.addResource('money', new Money(100)); + state.addResource('money', new Money(3.50)); + state.addResource('crpto', new CryptoCurrency()); return state; } diff --git a/src/model/GameState.ts b/src/model/GameState.ts index bdeea2c..4d882bf 100644 --- a/src/model/GameState.ts +++ b/src/model/GameState.ts @@ -21,13 +21,16 @@ class GameState { // advance each resource for (const rkey of this._resourceKeys) { - if (this._resources[rkey].advanceAction !== null) { + if (this._resources[rkey].isUnlocked(this) + && this._resources[rkey].advanceAction !== null) { this._resources[rkey].advanceAction(time, this); } } // perform auto increments for (const rkey of this._resourceKeys) { + if (!this._resources[rkey].isUnlocked(this)) continue; + const max: number = this._resources[rkey].max ? this._resources[rkey].max(this) : null; @@ -41,6 +44,9 @@ class GameState { if (max !== null && this._resources[rkey].value > max) { this._resources[rkey].value = max; } + if (this._resources[rkey].value < 0) { + this._resources[rkey].value = 0; + } } } diff --git a/src/model/resource/Credibility.ts b/src/model/resource/Credibility.ts index 1ebb3b4..f424437 100644 --- a/src/model/resource/Credibility.ts +++ b/src/model/resource/Credibility.ts @@ -1,27 +1,20 @@ -/// +/// -class Credibility extends Hidden { - private _lastValue: number; +class Credibility extends Passive { + private _lastValue: number = 100; - constructor (public value: number) { - super(value); - this._lastValue = value; + constructor () { + super( + 'Credibility', + 'How trustworthy you are perceived to be. Affects your ability to recruit and retain followers.', + 100, 100, 0.25); } public max (state: GameState): number { - return 2; + return 100; } public inc (state: GameState): number { - return 0.01; - } - - public advanceAction (time: number, state: GameState): void { - if (Math.ceil(this._lastValue) < Math.ceil(this.value)) { - state.log('Your credibility has gone up.'); - } else if (Math.ceil(this._lastValue) > Math.ceil(this.value)) { - state.log('Your credibility has gone down.'); - } - this._lastValue = this.value; + return 0.25; } } diff --git a/src/model/resource/CryptoCurrency.ts b/src/model/resource/CryptoCurrency.ts new file mode 100644 index 0000000..31f7a0d --- /dev/null +++ b/src/model/resource/CryptoCurrency.ts @@ -0,0 +1,11 @@ +/// + +class CryptoCurrency extends Purchasable { + constructor () { + super('CryptoCurrency', + "Can't be spent directly, but provides a steady stream of passive income."); + this.cost.money = 100; + this._costMultiplier.money = 1.1; + this._baseMax = 1000; + } +} diff --git a/src/model/resource/IResource.ts b/src/model/resource/IResource.ts index 46ef5db..8c58c44 100644 --- a/src/model/resource/IResource.ts +++ b/src/model/resource/IResource.ts @@ -2,7 +2,7 @@ enum ResourceType { Religion = 'religion', Consumable = 'consumable', Infrastructure = 'infrastructure', - Hidden = 'hidden' + Passive = 'passive' } interface IResource { diff --git a/src/model/resource/Money.ts b/src/model/resource/Money.ts index 11160f5..ae6ff6d 100644 --- a/src/model/resource/Money.ts +++ b/src/model/resource/Money.ts @@ -3,6 +3,7 @@ class Money extends Purchasable { private _lastCollectionTime: number = 0; + public resourceType: ResourceType = ResourceType.Consumable; public cost: { [key: string]: number } = { }; constructor ( @@ -11,22 +12,28 @@ class Money extends Purchasable { super('Money', 'Used to purchase goods and services.'); this.clickText = 'Collect Tithes'; this.clickDescription = 'Voluntary contributions from followers.'; + this._baseMax = 10000; } public isUnlocked (state: GameState): boolean { return true; } - protected _incrementAmount (state: GameState): number { + public inc (state: GameState): number { + // crypto currency + return state.getResource('crpto').value * 0.5; + } + + protected _purchaseAmount (state: GameState): number { const plorg: IResource = state.getResource('plorg'); if (plorg.value === 0) { state.log('You have no followers to collect from!'); return 0; } - if (state.now - this._lastCollectionTime < 30000) { - this.cost.creds = 0.05; - state.deductCost(this.cost); - delete this.cost.creds; + const diff: number = state.now - this._lastCollectionTime; + if (diff < 30000) { + const lost: number = 30000 / diff / 3; + state.getResource('creds').value -= lost; } // each follower gives you $10 const tithings: number = plorg.value * 10; diff --git a/src/model/resource/Hidden.ts b/src/model/resource/Passive.ts similarity index 56% rename from src/model/resource/Hidden.ts rename to src/model/resource/Passive.ts index 21da49d..fe5a4e9 100644 --- a/src/model/resource/Hidden.ts +++ b/src/model/resource/Passive.ts @@ -1,22 +1,28 @@ /// -abstract class Hidden implements IResource { - public readonly resourceType: ResourceType = ResourceType.Hidden; +abstract class Passive implements IResource { + public readonly resourceType: ResourceType = ResourceType.Passive; public readonly clickText: null = null; public readonly clickDescription: null = null; public readonly cost: null = null; public readonly clickAction: null = null; - public readonly name: null = null; - public readonly description: null = null; - protected _baseMax: number | null = null; + protected _baseMax: number | null; + protected _baseInc: number | null; constructor ( - public value: number - ) { } + public name: string, + public description: string, + public value: number, + max: number | null, + inc: number | null + ) { + this._baseMax = max; + this._baseInc = inc; + } public inc (state: GameState): number | null { - return null; + return this._baseInc; } public max (state: GameState): number | null { diff --git a/src/model/resource/PlayerOrg.ts b/src/model/resource/PlayerOrg.ts index 9f41471..03a53c7 100644 --- a/src/model/resource/PlayerOrg.ts +++ b/src/model/resource/PlayerOrg.ts @@ -6,7 +6,6 @@ class PlayerOrg implements IResource { public readonly description: string = 'In you they trust.'; public cost: { [key: string]: number } = { }; - public readonly max: null = null; public readonly inc: null = null; public value: number = 0; @@ -15,42 +14,57 @@ class PlayerOrg implements IResource { public clickDescription: string = 'Gather new followers.'; private _lastLostTime: number = 0; + private _baseMax: number = 5; public isUnlocked (state: GameState): boolean { return true; } + public max (state: GameState): number { + return this._baseMax; + } + public clickAction (state: GameState): void { - const creds: number = - Math.pow(Math.ceil(state.getResource('creds').value), 2); - if (this.value >= creds) { + // don't exceed max + if (this.value >= this.max(state)) { + state.log('You have no room for more followers.'); + return; + } + + // chance to fail increases as credibility decreases + const creds: IResource = state.getResource('creds'); + const ratio: number = Math.ceil(creds.value) / creds.max(state); + if (Math.random() > ratio) { state.log('Your recruiting efforts failed.'); + return; + } + + const source: [string, IResource] = this._getRandomReligion(state); + this.cost[source[0]] = 1; + if (state.deductCost(this.cost)) { + this.value++; + delete this.cost[source[0]]; + state.log(`You converted one new follower from ${source[1].name}!`); } else { - const source: [string, IResource] = this._getRandomReligion(state); - this.cost[source[0]] = 1; - if (state.deductCost(this.cost)) { - this.value++; - delete this.cost[source[0]]; - state.log(`You converted one new follower from ${source[1].name}!`); - } else { - state.log('Your recruiting efforts failed.'); - } + state.log('Your recruiting efforts failed.'); } } public advanceAction (time: number, state: GameState): void { - const creds: number = - Math.pow(Math.ceil(state.getResource('creds').value), 2); - if (this.value > creds) { - if (state.now - this._lastLostTime > 10000) { - const lost: number = - Math.ceil((this.value - creds) / 10 * Math.random()); - this.value -= lost; - const dest: [string, IResource] = this._getRandomReligion(state); - dest[1].value += lost; - state.log(`You lost ${lost} followers to ${dest[1].name}.`); - this._lastLostTime = state.now; + // chance to lose some followers every 10s if credibility < 100% + if (state.now - this._lastLostTime > 10000) { + if (this.value > 0) { + const creds: IResource = state.getResource('creds'); + const ratio: number = Math.ceil(creds.value) / creds.max(state); + if (Math.random() > ratio) { + const lost: number = Math.ceil(this.value / 25 * (1 - ratio)); + this.value -= lost; + const dest: [string, IResource] = this._getRandomReligion(state); + dest[1].value += lost; + state.log(`You lost ${lost} followers to ${dest[1].name}.`); + } } + this._lastLostTime = state.now; } } diff --git a/src/model/resource/Purchasable.ts b/src/model/resource/Purchasable.ts index 2cc9b06..2e0b0d3 100644 --- a/src/model/resource/Purchasable.ts +++ b/src/model/resource/Purchasable.ts @@ -7,10 +7,11 @@ abstract class Purchasable implements IResource { public clickText: string = 'Purchase'; public clickDescription: string = 'Purchase'; - public cost: { [key: string]: number } | null = null; + public cost: { [key: string]: number } = { }; - protected _costMultiplier: { [key: string]: number } | null = null; + protected _costMultiplier: { [key: string]: number } = { }; protected _baseMax: number | null = null; + protected _isUnlocked: boolean = false; constructor ( public readonly name: string, @@ -20,12 +21,9 @@ abstract class Purchasable implements IResource { public clickAction (state: GameState): void { if (this.max(state) !== null && this.value >= this.max(state)) return; if (state.deductCost(this.cost)) { - this.value += this._incrementAmount(state); - if (this._costMultiplier !== null - && Object.keys(this._costMultiplier !== null)) { - for (const rkey of Object.keys(this._costMultiplier)) { - this.cost[rkey] *= this._costMultiplier[rkey]; - } + this.value += this._purchaseAmount(state); + for (const rkey of Object.keys(this._costMultiplier)) { + this.cost[rkey] *= this._costMultiplier[rkey]; } } } @@ -43,10 +41,13 @@ abstract class Purchasable implements IResource { } public isUnlocked (state: GameState): boolean { - return false; + if (!this._isUnlocked && state.isPurchasable(this.cost)) { + this._isUnlocked = true; + } + return this._isUnlocked; } - protected _incrementAmount (state: GameState): number { + protected _purchaseAmount (state: GameState): number { return 1; } } diff --git a/src/render/DebugRenderer.ts b/src/render/DebugRenderer.ts index 6f94eef..075b0be 100644 --- a/src/render/DebugRenderer.ts +++ b/src/render/DebugRenderer.ts @@ -22,8 +22,7 @@ class DebugRenderer implements IRenderer { head.appendChild(style); // create containers for each resource type for (const item in ResourceType) { - if (isNaN(Number(item)) - && ResourceType[item] !== ResourceType.Hidden) { + if (isNaN(Number(item))) { const el: HTMLElement = document.createElement('div'); el.id = `resource-container-${ResourceType[item]}`; el.className = 'resource-type-container'; @@ -34,7 +33,6 @@ class DebugRenderer implements IRenderer { const rkeys: string[] = state.getResources(); for (const rkey of rkeys) { const resource: IResource = state.getResource(rkey); - if (resource.resourceType === ResourceType.Hidden) continue; const container: HTMLElement = document .getElementById(`resource-container-${resource.resourceType}`); if (resource.isUnlocked(state)) { @@ -45,8 +43,11 @@ class DebugRenderer implements IRenderer { el.className = 'resource'; el.id = `resource-details-${rkey}`; let content: string = ` - - ${resource.name}
+ + ${this._escape(resource.name + ? resource.name + : rkey)}
@@ -54,8 +55,8 @@ class DebugRenderer implements IRenderer { if (resource.clickText !== null) { content += `
`; + title='${this._escape(resource.clickDescription)}'> + ${this._escape(resource.clickText)}`; } if (resource.cost !== null && Object.keys(resource.cost).length !== 0) { @@ -89,7 +90,7 @@ class DebugRenderer implements IRenderer { const elC: HTMLCollectionOf = el.getElementsByClassName('resource-cost'); if (elC.length > 0) { - elC[0].innerHTML = this.getCostStr(resource, state); + elC[0].innerHTML = this._getCostStr(resource, state); } } } @@ -97,7 +98,21 @@ class DebugRenderer implements IRenderer { this._handleClick = false; } - private getCostStr (resource: IResource, state: GameState): string { + private _escape (text: string): string { + const escapes: { [key: string]: string } = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + } + const escaper: RegExp = /[&<>"'\/]/g; + return text.replace(escaper, (match: string): string => + escapes[match]); + } + + private _getCostStr (resource: IResource, state: GameState): string { let cost: string = ''; for (const rkey of state.getResources()) { if (resource.cost[rkey] !== undefined) { diff --git a/tslint.json b/tslint.json index 0699b91..53bd174 100644 --- a/tslint.json +++ b/tslint.json @@ -14,7 +14,7 @@ true, { "limit": 75, "check-strings": true, - "ignore-pattern": "\\s+state\\.log\\(" + "ignore-pattern": "(^\\s+state\\.log\\(|^\\s+['\"`])" } ], "whitespace": [