added crypto currency resource

This commit is contained in:
Rudis Muiznieks 2021-08-21 19:02:57 -05:00
parent 70fa078579
commit 0758d8276b
13 changed files with 147 additions and 80 deletions

View File

@ -3,8 +3,20 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.resource { .resource {
min-width: 8em; min-width: 12em;
border: 2px solid black; border: 2px solid black;
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
margin: 0 0.5em 0.5em 0; 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;
}

View File

@ -12,8 +12,8 @@ function gameLoop (state: GameState, renderer: IRenderer): void {
const elapsedTime: number = globalStartTime > 0 const elapsedTime: number = globalStartTime > 0
? (new Date()).getTime() - globalStartTime : 0; ? (new Date()).getTime() - globalStartTime : 0;
renderer.render(state);
state.advance(elapsedTime); state.advance(elapsedTime);
renderer.render(state);
// run again in 1sec // run again in 1sec
globalStartTime = (new Date()).getTime(); globalStartTime = (new Date()).getTime();

View File

@ -1,5 +1,6 @@
/// <reference path="./GameState.ts" /> /// <reference path="./GameState.ts" />
/// <reference path="./resource/Credibility.ts" /> /// <reference path="./resource/Credibility.ts" />
/// <reference path="./resource/CryptoCurrency.ts" />
/// <reference path="./resource/Money.ts" /> /// <reference path="./resource/Money.ts" />
/// <reference path="./resource/PlayerOrg.ts" /> /// <reference path="./resource/PlayerOrg.ts" />
/// <reference path="./resource/Religion.ts" /> /// <reference path="./resource/Religion.ts" />
@ -57,10 +58,11 @@ class GameConfig {
this.relNoneShare * this.worldPopulation)); this.relNoneShare * this.worldPopulation));
// add hidden resources // add hidden resources
state.addResource('creds', new Credibility(2)); state.addResource('creds', new Credibility());
// add resources // add resources
state.addResource('money', new Money(100)); state.addResource('money', new Money(3.50));
state.addResource('crpto', new CryptoCurrency());
return state; return state;
} }

View File

@ -21,13 +21,16 @@ class GameState {
// advance each resource // advance each resource
for (const rkey of this._resourceKeys) { 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); this._resources[rkey].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 max: number = this._resources[rkey].max const max: number = this._resources[rkey].max
? this._resources[rkey].max(this) ? this._resources[rkey].max(this)
: null; : null;
@ -41,6 +44,9 @@ class GameState {
if (max !== null && this._resources[rkey].value > max) { if (max !== null && this._resources[rkey].value > max) {
this._resources[rkey].value = max; this._resources[rkey].value = max;
} }
if (this._resources[rkey].value < 0) {
this._resources[rkey].value = 0;
}
} }
} }

View File

@ -1,27 +1,20 @@
/// <reference path="./Hidden.ts" /> /// <reference path="./Passive.ts" />
class Credibility extends Hidden { class Credibility extends Passive {
private _lastValue: number; private _lastValue: number = 100;
constructor (public value: number) { constructor () {
super(value); super(
this._lastValue = value; '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 { public max (state: GameState): number {
return 2; return 100;
} }
public inc (state: GameState): number { public inc (state: GameState): number {
return 0.01; return 0.25;
}
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;
} }
} }

View File

@ -0,0 +1,11 @@
/// <reference path="./Purchasable.ts" />
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;
}
}

View File

@ -2,7 +2,7 @@ enum ResourceType {
Religion = 'religion', Religion = 'religion',
Consumable = 'consumable', Consumable = 'consumable',
Infrastructure = 'infrastructure', Infrastructure = 'infrastructure',
Hidden = 'hidden' Passive = 'passive'
} }
interface IResource { interface IResource {

View File

@ -3,6 +3,7 @@
class Money extends Purchasable { class Money extends Purchasable {
private _lastCollectionTime: number = 0; private _lastCollectionTime: number = 0;
public resourceType: ResourceType = ResourceType.Consumable;
public cost: { [key: string]: number } = { }; public cost: { [key: string]: number } = { };
constructor ( constructor (
@ -11,22 +12,28 @@ 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 = 10000;
} }
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
return true; 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'); const plorg: IResource = state.getResource('plorg');
if (plorg.value === 0) { if (plorg.value === 0) {
state.log('You have no followers to collect from!'); state.log('You have no followers to collect from!');
return 0; return 0;
} }
if (state.now - this._lastCollectionTime < 30000) { const diff: number = state.now - this._lastCollectionTime;
this.cost.creds = 0.05; if (diff < 30000) {
state.deductCost(this.cost); const lost: number = 30000 / diff / 3;
delete this.cost.creds; state.getResource('creds').value -= lost;
} }
// each follower gives you $10 // each follower gives you $10
const tithings: number = plorg.value * 10; const tithings: number = plorg.value * 10;

View File

@ -1,22 +1,28 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
abstract class Hidden implements IResource { abstract class Passive implements IResource {
public readonly resourceType: ResourceType = ResourceType.Hidden; public readonly resourceType: ResourceType = ResourceType.Passive;
public readonly clickText: null = null; public readonly clickText: null = null;
public readonly clickDescription: null = null; public readonly clickDescription: null = null;
public readonly cost: null = null; public readonly cost: null = null;
public readonly clickAction: 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 ( 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 { public inc (state: GameState): number | null {
return null; return this._baseInc;
} }
public max (state: GameState): number | null { public max (state: GameState): number | null {

View File

@ -6,7 +6,6 @@ class PlayerOrg implements IResource {
public readonly description: string = 'In you they trust.'; public readonly description: string = 'In you they trust.';
public cost: { [key: string]: number } = { }; public cost: { [key: string]: number } = { };
public readonly max: null = null;
public readonly inc: null = null; public readonly inc: null = null;
public value: number = 0; public value: number = 0;
@ -15,42 +14,57 @@ class PlayerOrg implements IResource {
public clickDescription: string = 'Gather new followers.'; public clickDescription: string = 'Gather new followers.';
private _lastLostTime: number = 0; private _lastLostTime: number = 0;
private _baseMax: number = 5;
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
return true; return true;
} }
public max (state: GameState): number {
return this._baseMax;
}
public clickAction (state: GameState): void { public clickAction (state: GameState): void {
const creds: number = // don't exceed max
Math.pow(Math.ceil(state.getResource('creds').value), 2); if (this.value >= this.max(state)) {
if (this.value >= creds) { 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.'); 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 { } else {
const source: [string, IResource] = this._getRandomReligion(state); state.log('Your recruiting efforts failed.');
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.');
}
} }
} }
public advanceAction (time: number, state: GameState): void { public advanceAction (time: number, state: GameState): void {
const creds: number = // chance to lose some followers every 10s if credibility < 100%
Math.pow(Math.ceil(state.getResource('creds').value), 2); if (state.now - this._lastLostTime > 10000) {
if (this.value > creds) { if (this.value > 0) {
if (state.now - this._lastLostTime > 10000) { const creds: IResource = state.getResource('creds');
const lost: number = const ratio: number = Math.ceil(creds.value) / creds.max(state);
Math.ceil((this.value - creds) / 10 * Math.random()); if (Math.random() > ratio) {
this.value -= lost; const lost: number = Math.ceil(this.value / 25 * (1 - ratio));
const dest: [string, IResource] = this._getRandomReligion(state); this.value -= lost;
dest[1].value += lost; const dest: [string, IResource] = this._getRandomReligion(state);
state.log(`You lost ${lost} followers to ${dest[1].name}.`); dest[1].value += lost;
this._lastLostTime = state.now; state.log(`You lost ${lost} followers to ${dest[1].name}.`);
}
} }
this._lastLostTime = state.now;
} }
} }

View File

@ -7,10 +7,11 @@ abstract class Purchasable implements IResource {
public clickText: string = 'Purchase'; public clickText: string = 'Purchase';
public clickDescription: 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 _baseMax: number | null = null;
protected _isUnlocked: boolean = false;
constructor ( constructor (
public readonly name: string, public readonly name: string,
@ -20,12 +21,9 @@ abstract class Purchasable implements IResource {
public clickAction (state: GameState): void { public clickAction (state: GameState): void {
if (this.max(state) !== null && this.value >= this.max(state)) return; if (this.max(state) !== null && this.value >= this.max(state)) return;
if (state.deductCost(this.cost)) { if (state.deductCost(this.cost)) {
this.value += this._incrementAmount(state); this.value += this._purchaseAmount(state);
if (this._costMultiplier !== null for (const rkey of Object.keys(this._costMultiplier)) {
&& Object.keys(this._costMultiplier !== null)) { this.cost[rkey] *= this._costMultiplier[rkey];
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 { 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; return 1;
} }
} }

View File

@ -22,8 +22,7 @@ class DebugRenderer implements IRenderer {
head.appendChild(style); head.appendChild(style);
// 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))) {
&& ResourceType[item] !== ResourceType.Hidden) {
const el: HTMLElement = document.createElement('div'); const el: HTMLElement = document.createElement('div');
el.id = `resource-container-${ResourceType[item]}`; el.id = `resource-container-${ResourceType[item]}`;
el.className = 'resource-type-container'; el.className = 'resource-type-container';
@ -34,7 +33,6 @@ class DebugRenderer implements IRenderer {
const rkeys: string[] = state.getResources(); const rkeys: string[] = state.getResources();
for (const rkey of rkeys) { for (const rkey of rkeys) {
const resource: IResource = state.getResource(rkey); const resource: IResource = state.getResource(rkey);
if (resource.resourceType === ResourceType.Hidden) continue;
const container: HTMLElement = document const container: HTMLElement = document
.getElementById(`resource-container-${resource.resourceType}`); .getElementById(`resource-container-${resource.resourceType}`);
if (resource.isUnlocked(state)) { if (resource.isUnlocked(state)) {
@ -45,8 +43,11 @@ class DebugRenderer implements IRenderer {
el.className = 'resource'; el.className = 'resource';
el.id = `resource-details-${rkey}`; el.id = `resource-details-${rkey}`;
let content: string = ` let content: string = `
<span class='resource-title' title='${resource.description}'> <span class='resource-title'
${resource.name}</span><br> title='${this._escape(resource.description)}'>
${this._escape(resource.name
? 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>
@ -54,8 +55,8 @@ class DebugRenderer implements IRenderer {
if (resource.clickText !== null) { if (resource.clickText !== null) {
content += `<br> content += `<br>
<button class='resource-btn' <button class='resource-btn'
title='${resource.clickDescription}'> title='${this._escape(resource.clickDescription)}'>
${resource.clickText}</button>`; ${this._escape(resource.clickText)}</button>`;
} }
if (resource.cost !== null if (resource.cost !== null
&& Object.keys(resource.cost).length !== 0) { && Object.keys(resource.cost).length !== 0) {
@ -89,7 +90,7 @@ class DebugRenderer implements IRenderer {
const elC: HTMLCollectionOf<Element> = const elC: HTMLCollectionOf<Element> =
el.getElementsByClassName('resource-cost'); el.getElementsByClassName('resource-cost');
if (elC.length > 0) { 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; this._handleClick = false;
} }
private getCostStr (resource: IResource, state: GameState): string { private _escape (text: string): string {
const escapes: { [key: string]: string } = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
}
const escaper: RegExp = /[&<>"'\/]/g;
return text.replace(escaper, (match: string): string =>
escapes[match]);
}
private _getCostStr (resource: IResource, state: GameState): string {
let cost: string = ''; let cost: string = '';
for (const rkey of state.getResources()) { for (const rkey of state.getResources()) {
if (resource.cost[rkey] !== undefined) { if (resource.cost[rkey] !== undefined) {

View File

@ -14,7 +14,7 @@
true, { true, {
"limit": 75, "limit": 75,
"check-strings": true, "check-strings": true,
"ignore-pattern": "\\s+state\\.log\\(" "ignore-pattern": "(^\\s+state\\.log\\(|^\\s+['\"`])"
} }
], ],
"whitespace": [ "whitespace": [