renderer plus working purchasable resource

This commit is contained in:
Rudis Muiznieks 2021-08-20 20:47:02 -05:00
parent 88bc020f1a
commit 65fe779d9a
17 changed files with 244 additions and 44 deletions

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
all: lint build
build:
tsc
lint:
tslint --project .
clean:
rm -f public/js/irreligious.js
run:
firefox public/index.html

10
public/css/debugger.css Normal file
View File

@ -0,0 +1,10 @@
#irreligious-game {
overflow: auto;
}
div.resource {
float: left;
border: 2px solid black;
margin-right: 5px;
margin-bottom: 5px;
padding: 5px 10px;
}

View File

@ -6,5 +6,6 @@
<script src='js/irreligious.js'></script>
</head>
<body>
<div id='irreligious-game'></div>
</body>
</html>

View File

@ -1,25 +1,38 @@
/// <reference path="./model/GameConfig.ts" />
/// <reference path="./model/GameState.ts" />
/// <reference path="./render/DebugRenderer.ts" />
/// <reference path="./render/IRenderer.ts" />
let globalStartTime = 0;
let globalTimeout: number = null;
function gameLoop (state: GameState): void {
function gameLoop (state: GameState, renderer: IRenderer): void {
// figure out how much actual time has passed
const elapsedTime = globalStartTime > 0
? (new Date()).getTime() - globalStartTime : 0;
renderer.render(state);
state.advance(elapsedTime);
// run again in 1sec
globalStartTime = (new Date()).getTime();
setTimeout(() => gameLoop(state), 1000);
globalTimeout = setTimeout(() => gameLoop(state, renderer), 1000);
}
// run with default config at startup
(() => {
const config = new GameConfig();
const renderer = new DebugRenderer();
const state = config.generateState();
if (document.readyState !== 'loading') gameLoop(state);
else document.addEventListener('DOMContentLoaded', () => gameLoop(state));
// re-run main loop immediately on user clicks
state.onResourceClick = () => {
if (globalTimeout !== null) {
clearTimeout(globalTimeout);
gameLoop(state, renderer);
}
}
if (document.readyState !== 'loading') gameLoop(state, renderer);
else document.addEventListener('DOMContentLoaded', () => gameLoop(state, renderer));
})();

View File

@ -13,10 +13,10 @@ class GameConfig {
public relBuddhismShare: number = 0.06;
public relSikhismShare: number = 0.04;
public relJudaismShare: number = 0.02;
public relOtherShare:number = 0.02;
public relOtherShare: number = 0.02;
public relNoneShare: number = 0.16;
public generateState(): GameState {
public generateState (): GameState {
const state = new GameState();
// create player organization

View File

@ -1,13 +1,51 @@
/// <reference path="./IResource.ts" />
/// <reference path="./resource/IResource.ts" />
class GameState {
private _resources: {[key: string]: IResource} = {};
private _resources: Record<string, IResource> = { };
private _resourceKeys: string[] = [];
public onResourceClick: () => void = null;
public addResource (key: string, resource: IResource): void {
this._resourceKeys.push(key);
this._resources[key] = resource;
}
public advance (time: number): void {
console.log(`Advancing state by ${time}ms...`);
for (const rkey of this._resourceKeys) {
if (this._resources[rkey].advanceAction !== null) {
this._resources[rkey].advanceAction(time, this);
}
}
}
public getResources (): string[] {
return this._resourceKeys;
}
public getResource (key: string): IResource {
return this._resources[key];
}
public performClick (resourceKey: string): void {
if (this._resources[resourceKey].clickAction !== null) {
this._resources[resourceKey].clickAction(this);
if (this.onResourceClick !== null) {
this.onResourceClick();
}
}
}
public deductCost (cost: { [rkey: string]: number }): boolean {
if (cost === null || Object.keys(cost) === null) return true;
for (const rkey of Object.keys(cost)) {
if (this._resources[rkey].value < cost[rkey]) {
return false;
}
}
for (const rkey of Object.keys(cost)) {
this._resources[rkey].value -= cost[rkey];
}
return true;
}
}

View File

@ -1,14 +0,0 @@
enum ResourceType {
Religion,
Consumable
}
interface IResource {
name: string;
description: string;
resourceType: ResourceType;
value: number;
max?: number;
unlocked: boolean;
}

View File

@ -1,14 +0,0 @@
/// <reference path="../IResource.ts" />
class Consumable implements IResource {
public readonly resourceType = ResourceType.Consumable;
constructor (
public readonly name: string,
public readonly description: string,
public value: number,
public unlocked: boolean,
public max?: number,
) {
}
}

View File

@ -0,0 +1,21 @@
enum ResourceType {
Religion,
Consumable,
Infrastructure
}
interface IResource {
name: string;
description: string;
resourceType: ResourceType;
value: number;
max?: number;
unlocked: boolean;
clickText: string;
clickDescription: string;
clickAction: (state: GameState) => void;
advanceAction: (time: number, state: GameState) => void;
}

View File

@ -1,11 +1,13 @@
/// <reference path="./Consumable.ts" />
/// <reference path="./Purchasable.ts" />
class Money extends Consumable {
class Money extends Purchasable {
constructor (
public value: number,
public max: number
) {
super('Money', 'Used to purchase goods and services.',
value, true, max);
super('Money', 'Used to purchase goods and services.', null);
this.clickText = 'Beg';
this.clickDescription = 'Alms for the poor.';
this.unlocked = true;
}
}

View File

@ -1,4 +1,4 @@
/// <reference path="../IResource.ts" />
/// <reference path="./IResource.ts" />
class PlayerOrganization implements IResource {
public readonly name = 'Player';
@ -7,5 +7,12 @@ class PlayerOrganization implements IResource {
public readonly max?: number = null;
public readonly unlocked = true;
public readonly clickText: string = null;
public readonly clickDescription: string = null;
public readonly clickAction: () => void = null;
public readonly advanceAction: (time: number) => void = null;
public value = 0;
}

View File

@ -0,0 +1,28 @@
/// <reference path="./IResource.ts" />
abstract class Purchasable implements IResource {
public readonly resourceType = ResourceType.Infrastructure;
public readonly max?: number = null;
public value: number = 0;
public unlocked: boolean = false;
public clickText: string = "Purchase";
public clickDescription: string = null;
constructor (
public readonly name: string,
public readonly description: string,
private _cost: { [key: string]: number }
) { }
public clickAction (state: GameState) {
if (this.max !== null && this.value >= this.max) return;
if (state.deductCost(this._cost)) {
this.value += 1;
}
}
public advanceAction (time: number, state: GameState) {
// do nothing
}
}

View File

@ -1,9 +1,13 @@
/// <reference path="../IResource.ts" />
/// <reference path="./IResource.ts" />
class Religion implements IResource {
public readonly resourceType = ResourceType.Religion;
public readonly max?: number = null;
public readonly unlocked = true;
public readonly clickText: string = null;
public readonly clickDescription: string = null;
public readonly clickAction: () => void = null;
public readonly advanceAction: (time: number) => void = null;
constructor (
public readonly name: string,

View File

@ -0,0 +1,67 @@
/// <reference path="../model/GameState.ts" />
/// <reference path="./IRenderer.ts" />
class DebugRenderer implements IRenderer {
private _initialized = false;
public render (state: GameState) {
const container = document.getElementById('irreligious-game');
if (container === null) {
console.log('Cannot find #irreligious-game container.'); // tslint:disable-line
} else {
if (!this._initialized) {
this._initialized = true;
const style = document.createElement('link');
style.setAttribute('rel', 'stylesheet');
style.setAttribute('href', 'css/debugger.css');
const head = document.getElementsByTagName('head')[0];
head.appendChild(style);
}
for (const rkey of state.getResources()) {
const resource = state.getResource(rkey);
if (resource.unlocked) {
let el = document.getElementById(`r_${rkey}`);
if (el === null) {
el = document.createElement('div');
el.className = 'resource';
el.id = `r_${rkey}`;
let content = `
<span class='resourceTitle' title='${resource.description}'>${resource.name}</span><br>
<span class='value'></span><span class='max'></span>
`;
if (resource.clickText !== null) {
content += `<br><button class='btn' title='${resource.clickDescription}'>${resource.clickText}</button>`;
}
el.innerHTML = content;
container.appendChild(el);
if (resource.clickAction !== null) {
const btn = el.getElementsByClassName('btn')[0];
btn.addEventListener('click', () => state.performClick(rkey));
}
}
const elV = el.getElementsByClassName('value')[0];
const elT = el.getElementsByClassName('max')[0];
elV.innerHTML = this.formatNumber(resource.value, 1);
elT.innerHTML = resource.max !== null ? ` / ${this.formatNumber(resource.max, 2)}` : '';
}
}
}
}
private formatNumber (num: number, digits: number): string {
const lookup = [
{ value: 1, symbol: "" },
{ value: 1e3, symbol: "k" },
{ value: 1e6, symbol: "M" },
{ value: 1e9, symbol: "G" },
{ value: 1e12, symbol: "T" },
{ value: 1e15, symbol: "P" },
{ value: 1e18, symbol: "E" }
];
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
const item = lookup.slice().reverse().find((i) => num >= i.value);
return item
? (num / item.value).toFixed(digits).replace(rx, "$1") + item.symbol
: "0";
}
}

5
src/render/IRenderer.ts Normal file
View File

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

View File

@ -6,7 +6,7 @@
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES5",
"target": "ES6",
"module": "none"
}
}

19
tslint.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "tslint:recommended",
"rules": {
"no-reference": false,
"space-before-function-paren": true,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-rest-spread",
"check-type",
"check-typecast",
"check-type-operator",
"check-preblock",
"check-postbrace"
]
}
}