give resources control over their own increments

This commit is contained in:
Rudis Muiznieks 2021-08-21 10:44:53 -05:00
parent 405f512e7b
commit 8cab5cdb29
13 changed files with 138 additions and 77 deletions

7
cmd
View File

@ -2,11 +2,14 @@
cmd=$1 cmd=$1
if [ "$cmd" = "build" ]; then if [ "$cmd" = "lint" ]; then
tslint --project .
elif [ "$cmd" = "build" ]; then
tslint --project . && tsc tslint --project . && tsc
elif [ "$cmd" = "run" ]; then elif [ "$cmd" = "run" ]; then
firefox public/index.html & firefox public/index.html &
else else
echo "Usage: ./cmd build - lint and compile" echo "Usage: ./cmd lint - lint"
echo " ./cmd build - lint and compile"
echo " ./cmd run - run in firefox" echo " ./cmd run - run in firefox"
fi fi

View File

@ -1,11 +1,9 @@
#irreligious-game, .resource-type-container { .resource-type-container {
clear: all; display: flex;
overflow: auto;
} }
div.resource { .resource {
float: left;
border: 2px solid black; border: 2px solid black;
margin-right: 5px;
margin-bottom: 5px;
padding: 5px 10px; padding: 5px 10px;
margin: 0 5px 5px 0;
flex-shrink: 0;
} }

View File

@ -3,12 +3,13 @@
/// <reference path="./render/DebugRenderer.ts" /> /// <reference path="./render/DebugRenderer.ts" />
/// <reference path="./render/IRenderer.ts" /> /// <reference path="./render/IRenderer.ts" />
let globalStartTime = 0; let globalStartTime: number = 0;
let globalTimeout: number = null; let globalTimeout: number = null;
let cycleLength: number = 250;
function gameLoop (state: GameState, renderer: IRenderer): void { function gameLoop (state: GameState, renderer: IRenderer): void {
// figure out how much actual time has passed // figure out how much actual time has passed
const elapsedTime = globalStartTime > 0 const elapsedTime: number = globalStartTime > 0
? (new Date()).getTime() - globalStartTime : 0; ? (new Date()).getTime() - globalStartTime : 0;
renderer.render(state); renderer.render(state);
@ -16,17 +17,18 @@ 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(() => gameLoop(state, renderer), 1000); globalTimeout = setTimeout((): void =>
gameLoop(state, renderer), cycleLength);
} }
// run with default config at startup // run with default config at startup
(() => { ((): void => {
const config = new GameConfig(); const config: GameConfig = new GameConfig();
const renderer = new DebugRenderer(); const renderer: IRenderer = new DebugRenderer();
const state = config.generateState(); const state: GameState = config.generateState();
// re-run main loop immediately on user clicks // re-run main loop immediately on user clicks
state.onResourceClick.push(() => { state.onResourceClick.push((): void => {
if (globalTimeout !== null) { if (globalTimeout !== null) {
clearTimeout(globalTimeout); clearTimeout(globalTimeout);
gameLoop(state, renderer); gameLoop(state, renderer);
@ -34,5 +36,6 @@ function gameLoop (state: GameState, renderer: IRenderer): void {
}); });
if (document.readyState !== 'loading') gameLoop(state, renderer); if (document.readyState !== 'loading') gameLoop(state, renderer);
else document.addEventListener('DOMContentLoaded', () => gameLoop(state, renderer)); else document.addEventListener('DOMContentLoaded', (): void =>
gameLoop(state, renderer));
})(); })();

View File

@ -17,7 +17,7 @@ class GameConfig {
public relNoneShare: number = 0.16; public relNoneShare: number = 0.16;
public generateState (): GameState { public generateState (): GameState {
const state = new GameState(); const state: GameState = new GameState();
// create player organization // create player organization
state.addResource('plorg', new Religion( state.addResource('plorg', new Religion(
@ -57,7 +57,7 @@ class GameConfig {
this.relNoneShare * this.worldPopulation)); this.relNoneShare * this.worldPopulation));
// add purchasable resources // add purchasable resources
state.addResource('money', new Money(0, 1000)); state.addResource('money', new Money(0));
state.addResource('bonds', new Savings(0)); state.addResource('bonds', new Savings(0));
return state; return state;

View File

@ -16,8 +16,14 @@ class GameState {
if (this._resources[rkey].advanceAction !== null) { if (this._resources[rkey].advanceAction !== null) {
this._resources[rkey].advanceAction(time, this); this._resources[rkey].advanceAction(time, this);
} }
if (this._resources[rkey].inc > 0) { const max: number | null = this._resources[rkey].max(this);
this._resources[rkey].value += this._resources[rkey].inc * time / 1000; if (this._resources[rkey].inc(this) > 0
&& (max === null || this._resources[rkey].value < max)) {
this._resources[rkey].value +=
this._resources[rkey].inc(this) * time / 1000;
}
if (max !== null && this._resources[rkey].value > max) {
this._resources[rkey].value = max;
} }
} }
} }

View File

@ -10,8 +10,8 @@ interface IResource {
resourceType: ResourceType; resourceType: ResourceType;
value: number; value: number;
inc: number; max: (state: GameState) => number | null;
max?: number; inc: (state: GameState) => number | null;
cost: { [key: string]: number }; cost: { [key: string]: number };
isUnlocked: (state: GameState) => boolean; isUnlocked: (state: GameState) => boolean;

View File

@ -3,14 +3,21 @@
class Money extends Purchasable { class Money extends Purchasable {
constructor ( constructor (
public value: number, public value: number,
public max: number
) { ) {
super('Money', 'Used to purchase goods and services.'); super('Money', 'Used to purchase goods and services.');
this.clickText = 'Beg'; this.clickText = 'Beg';
this.clickDescription = 'Alms for the poor.'; this.clickDescription = 'Alms for the poor.';
this._baseMax = 1000;
} }
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
return true; return true;
} }
public inc (state: GameState): number {
let baseInc: number = 0;
// bonds give $1/s
baseInc += state.getResource('bonds').value;
return baseInc;
}
} }

View File

@ -1,27 +1,26 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
abstract class Purchasable implements IResource { abstract class Purchasable implements IResource {
public readonly resourceType = ResourceType.Infrastructure; public readonly resourceType: ResourceType = ResourceType.Infrastructure;
public readonly max?: number = null;
public value: number = 0; public value: number = 0;
public inc: number = 0;
public clickText: string = 'Purchase'; public clickText: string = 'Purchase';
public clickDescription: string = 'Purchase'; public clickDescription: string = 'Purchase';
public cost: { [key: string]: number } = null; public cost: { [key: string]: number } = null;
protected _costMultiplier: { [key: string]: number } = null; protected _costMultiplier: { [key: string]: number } = null;
protected _baseMax: number | null = null;
constructor ( constructor (
public readonly name: string, public readonly name: string,
public readonly description: string public readonly description: string
) { } ) { }
public clickAction (state: GameState) { public clickAction (state: GameState): void {
if (this.max !== null && this.value >= this.max) return; if (this.max(state) !== null && this.value >= this.max(state)) return;
if (state.deductCost(this.cost)) { if (state.deductCost(this.cost)) {
this.value += 1; this.value += 1;
this.purchaseEffect(state);
if (this._costMultiplier !== null if (this._costMultiplier !== null
&& Object.keys(this._costMultiplier !== null)) { && Object.keys(this._costMultiplier !== null)) {
for (const rkey of Object.keys(this._costMultiplier)) { for (const rkey of Object.keys(this._costMultiplier)) {
@ -31,6 +30,14 @@ abstract class Purchasable implements IResource {
} }
} }
public inc (state: GameState): number | null {
return null;
}
public max (state: GameState): number | null {
return this._baseMax;
}
public advanceAction (time: number, state: GameState): void { public advanceAction (time: number, state: GameState): void {
return; return;
} }
@ -38,8 +45,4 @@ abstract class Purchasable implements IResource {
public isUnlocked (state: GameState): boolean { public isUnlocked (state: GameState): boolean {
return false; return false;
} }
protected purchaseEffect (state: GameState) {
return;
}
} }

View File

@ -1,14 +1,15 @@
/// <reference path="./IResource.ts" /> /// <reference path="./IResource.ts" />
class Religion implements IResource { class Religion implements IResource {
public readonly resourceType = ResourceType.Religion; public readonly resourceType: ResourceType = ResourceType.Religion;
public readonly max?: number = null;
public readonly clickText: string = null; public readonly clickText: string = null;
public readonly clickDescription: string = null; public readonly clickDescription: string = null;
public readonly clickAction: () => void = null;
public readonly advanceAction: (time: number) => void = null; public readonly advanceAction: (time: number) => void = null;
public readonly cost: { [key: string]: number } = null; public readonly cost: { [key: string]: number } = null;
public readonly inc: number = 0;
public readonly max: () => null = (): null => null;
public readonly inc: () => null = (): null => null;
public readonly clickAction: () => void = null;
constructor ( constructor (
public readonly name: string, public readonly name: string,

View File

@ -1,8 +1,7 @@
/// <reference path="./Purchasable.ts" /> /// <reference path="./Purchasable.ts" />
class Savings extends Purchasable { class Savings extends Purchasable {
public max?: number = null; private _isUnlocked: boolean = false;
private _isUnlocked = false;
constructor ( constructor (
public value: number, public value: number,
@ -21,7 +20,7 @@ class Savings extends Purchasable {
return false; return false;
} }
protected purchaseEffect (state: GameState) { protected purchaseEffect (state: GameState): void {
state.getResource('money').inc += 1; return;
} }
} }

View File

@ -2,67 +2,87 @@
/// <reference path="./IRenderer.ts" /> /// <reference path="./IRenderer.ts" />
class DebugRenderer implements IRenderer { class DebugRenderer implements IRenderer {
private _initialized = false; private _initialized: boolean = false;
private _handleClick = true; private _handleClick: boolean = true;
public render (state: GameState) { public render (state: GameState): void {
if (!this._initialized) { if (!this._initialized) {
const container = document.getElementById('irreligious-game'); const container: HTMLElement =
document.getElementById('irreligious-game');
this._initialized = true; this._initialized = true;
state.onResourceClick.push(() => this._handleClick = true); state.onResourceClick.push((): void => {
const style = document.createElement('link'); this._handleClick = true;
});
const style: HTMLElement = 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 = document.getElementsByTagName('head')[0]; const head: HTMLElement = document.getElementsByTagName('head')[0];
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))) {
const el = 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';
container.appendChild(el); container.appendChild(el);
} }
} }
} }
const rkeys = state.getResources(); const rkeys: string[] = state.getResources();
for (const rkey of rkeys) { for (const rkey of rkeys) {
const resource = state.getResource(rkey); const resource: IResource = state.getResource(rkey);
console.log(`getting container resource-container-${resource.resourceType}`); // tslint:disable-line const container: HTMLElement = document
const container = document.getElementById(`resource-container-${resource.resourceType}`); .getElementById(`resource-container-${resource.resourceType}`);
if (resource.isUnlocked(state)) { if (resource.isUnlocked(state)) {
let el = document.getElementById(`resource-details-${rkey}`); let el: HTMLElement = document
.getElementById(`resource-details-${rkey}`);
if (el === null) { if (el === null) {
el = document.createElement('div'); el = document.createElement('div');
el.className = 'resource'; el.className = 'resource';
el.id = `resource-details-${rkey}`; el.id = `resource-details-${rkey}`;
let content = ` let content: string = `
<span class='resource-title' title='${resource.description}'>${resource.name}</span><br> <span class='resource-title' title='${resource.description}'>
<span class='resource-value'></span><span class='resource-max'></span><span class='resource-inc'></span> ${resource.name}</span><br>
<span class='resource-value'></span>
<span class='resource-max'></span>
<span class='resource-inc'></span>
`; `;
if (resource.clickText !== null) { if (resource.clickText !== null) {
content += `<br><button class='resource-btn' title='${resource.clickDescription}'>${resource.clickText}</button>`; content += `<br>
<button class='resource-btn'
title='${resource.clickDescription}'>
${resource.clickText}</button>`;
} }
if (resource.cost !== null && Object.keys(resource.cost) !== null) { if (resource.cost !== null
&& Object.keys(resource.cost) !== null) {
content += `<br>Cost: <span class='resource-cost'></span>`; content += `<br>Cost: <span class='resource-cost'></span>`;
} }
el.innerHTML = content; el.innerHTML = content;
container.appendChild(el); container.appendChild(el);
if (resource.clickAction !== null) { if (resource.clickAction !== null) {
const btn = el.getElementsByClassName('resource-btn')[0]; const btn: Element =
btn.addEventListener('click', () => state.performClick(rkey)); el.getElementsByClassName('resource-btn')[0];
btn.addEventListener('click', (): void =>
state.performClick(rkey));
} }
} }
const elV = el.getElementsByClassName('resource-value')[0]; const elV: Element =
const elT = el.getElementsByClassName('resource-max')[0]; el.getElementsByClassName('resource-value')[0];
const elT: Element =
el.getElementsByClassName('resource-max')[0];
elV.innerHTML = this.formatNumber(resource.value, 1); elV.innerHTML = this.formatNumber(resource.value, 1);
elT.innerHTML = resource.max !== null ? ` / ${this.formatNumber(resource.max, 1)}` : ''; elT.innerHTML = resource.max(state) !== null
? ` / ${this.formatNumber(resource.max(state), 1)}`
: '';
if (this._handleClick) { if (this._handleClick) {
if (resource.inc > 0) { if (resource.inc(state) > 0) {
const elI = el.getElementsByClassName('resource-inc')[0]; const elI: Element =
elI.innerHTML = ` +${this.formatNumber(resource.inc, 1)}/s`; el.getElementsByClassName('resource-inc')[0];
elI.innerHTML =
` +${this.formatNumber(resource.inc(state), 1)}/s`;
} }
const elC = el.getElementsByClassName('resource-cost'); const elC: HTMLCollectionOf<Element> =
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);
} }
@ -72,15 +92,16 @@ class DebugRenderer implements IRenderer {
this._handleClick = false; this._handleClick = false;
} }
private getCostStr (resource: IResource, state: GameState) { private getCostStr (resource: IResource, state: GameState): string {
let cost = ''; 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) {
if (cost !== '') cost += ', '; if (cost !== '') cost += ', ';
if (rkey === 'money') { if (rkey === 'money') {
cost += `$${this.formatNumber(resource.cost[rkey], 1)}`; cost += `$${this.formatNumber(resource.cost[rkey], 1)}`;
} else { } else {
cost += `${this.formatNumber(resource.cost[rkey], 1)} ${state.getResource(rkey).name}`; cost += `${this.formatNumber(resource.cost[rkey], 1)}
${state.getResource(rkey).name}`;
} }
} }
} }
@ -88,7 +109,8 @@ class DebugRenderer implements IRenderer {
} }
private formatNumber (num: number, digits: number): string { private formatNumber (num: number, digits: number): string {
const lookup = [ type vlookup = { value: number, symbol: string };
const lookup: vlookup[] = [
{ value: 1, symbol: "" }, { value: 1, symbol: "" },
{ value: 1e3, symbol: "K" }, { value: 1e3, symbol: "K" },
{ value: 1e6, symbol: "M" }, { value: 1e6, symbol: "M" },
@ -97,8 +119,10 @@ class DebugRenderer implements IRenderer {
{ 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: RegExp = /\.0+$|(\.[0-9]*[1-9])0+$/;
const item = lookup.slice().reverse().find((i) => num >= i.value); const item: vlookup =
lookup.slice().reverse()
.find((i: vlookup): boolean => num >= i.value);
return item return item
? (num / item.value).toFixed(digits).replace(rx, "$1") + item.symbol ? (num / item.value).toFixed(digits).replace(rx, "$1") + item.symbol
: num.toFixed(digits).replace(rx, "$1"); : num.toFixed(digits).replace(rx, "$1");

View File

@ -1,5 +1,5 @@
/// <reference path="../model/GameState.ts" /> /// <reference path="../model/GameState.ts" />
interface IRenderer { interface IRenderer {
render (state: GameState); render (state: GameState): void;
} }

View File

@ -3,6 +3,13 @@
"rules": { "rules": {
"no-reference": false, "no-reference": false,
"space-before-function-paren": true, "space-before-function-paren": true,
"triple-equals": true,
"max-line-length": [
true, {
"limit": 75,
"check-strings": true
}
],
"whitespace": [ "whitespace": [
true, true,
"check-branch", "check-branch",
@ -14,6 +21,16 @@
"check-type-operator", "check-type-operator",
"check-preblock", "check-preblock",
"check-postbrace" "check-postbrace"
],
"typedef": [
true,
"call-signature",
"arrow-call-signature",
"parameter",
"arrow-parameter",
"property-declaration",
"variable-declaration",
"member-variable-declaration"
] ]
} }
} }