renderer plus working purchasable resource
This commit is contained in:
parent
88bc020f1a
commit
65fe779d9a
|
@ -0,0 +1,13 @@
|
||||||
|
all: lint build
|
||||||
|
|
||||||
|
build:
|
||||||
|
tsc
|
||||||
|
|
||||||
|
lint:
|
||||||
|
tslint --project .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f public/js/irreligious.js
|
||||||
|
|
||||||
|
run:
|
||||||
|
firefox public/index.html
|
|
@ -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;
|
||||||
|
}
|
|
@ -6,5 +6,6 @@
|
||||||
<script src='js/irreligious.js'></script>
|
<script src='js/irreligious.js'></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id='irreligious-game'></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
21
src/main.ts
21
src/main.ts
|
@ -1,25 +1,38 @@
|
||||||
/// <reference path="./model/GameConfig.ts" />
|
/// <reference path="./model/GameConfig.ts" />
|
||||||
/// <reference path="./model/GameState.ts" />
|
/// <reference path="./model/GameState.ts" />
|
||||||
|
/// <reference path="./render/DebugRenderer.ts" />
|
||||||
|
/// <reference path="./render/IRenderer.ts" />
|
||||||
|
|
||||||
let globalStartTime = 0;
|
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
|
// figure out how much actual time has passed
|
||||||
const elapsedTime = globalStartTime > 0
|
const elapsedTime = globalStartTime > 0
|
||||||
? (new Date()).getTime() - globalStartTime : 0;
|
? (new Date()).getTime() - globalStartTime : 0;
|
||||||
|
|
||||||
|
renderer.render(state);
|
||||||
state.advance(elapsedTime);
|
state.advance(elapsedTime);
|
||||||
|
|
||||||
// run again in 1sec
|
// run again in 1sec
|
||||||
globalStartTime = (new Date()).getTime();
|
globalStartTime = (new Date()).getTime();
|
||||||
setTimeout(() => gameLoop(state), 1000);
|
globalTimeout = setTimeout(() => gameLoop(state, renderer), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// run with default config at startup
|
// run with default config at startup
|
||||||
(() => {
|
(() => {
|
||||||
const config = new GameConfig();
|
const config = new GameConfig();
|
||||||
|
const renderer = new DebugRenderer();
|
||||||
const state = config.generateState();
|
const state = config.generateState();
|
||||||
|
|
||||||
if (document.readyState !== 'loading') gameLoop(state);
|
// re-run main loop immediately on user clicks
|
||||||
else document.addEventListener('DOMContentLoaded', () => gameLoop(state));
|
state.onResourceClick = () => {
|
||||||
|
if (globalTimeout !== null) {
|
||||||
|
clearTimeout(globalTimeout);
|
||||||
|
gameLoop(state, renderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState !== 'loading') gameLoop(state, renderer);
|
||||||
|
else document.addEventListener('DOMContentLoaded', () => gameLoop(state, renderer));
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,13 +1,51 @@
|
||||||
/// <reference path="./IResource.ts" />
|
/// <reference path="./resource/IResource.ts" />
|
||||||
|
|
||||||
class GameState {
|
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 {
|
public addResource (key: string, resource: IResource): void {
|
||||||
|
this._resourceKeys.push(key);
|
||||||
this._resources[key] = resource;
|
this._resources[key] = resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public advance (time: number): void {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
enum ResourceType {
|
|
||||||
Religion,
|
|
||||||
Consumable
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IResource {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
resourceType: ResourceType;
|
|
||||||
value: number;
|
|
||||||
max?: number;
|
|
||||||
unlocked: boolean;
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
/// <reference path="./Consumable.ts" />
|
/// <reference path="./Purchasable.ts" />
|
||||||
|
|
||||||
class Money extends Consumable {
|
class Money extends Purchasable {
|
||||||
constructor (
|
constructor (
|
||||||
public value: number,
|
public value: number,
|
||||||
public max: number
|
public max: number
|
||||||
) {
|
) {
|
||||||
super('Money', 'Used to purchase goods and services.',
|
super('Money', 'Used to purchase goods and services.', null);
|
||||||
value, true, max);
|
this.clickText = 'Beg';
|
||||||
|
this.clickDescription = 'Alms for the poor.';
|
||||||
|
this.unlocked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/// <reference path="../IResource.ts" />
|
/// <reference path="./IResource.ts" />
|
||||||
|
|
||||||
class PlayerOrganization implements IResource {
|
class PlayerOrganization implements IResource {
|
||||||
public readonly name = 'Player';
|
public readonly name = 'Player';
|
||||||
|
@ -7,5 +7,12 @@ class PlayerOrganization implements IResource {
|
||||||
public readonly max?: number = null;
|
public readonly max?: number = null;
|
||||||
public readonly unlocked = true;
|
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;
|
public value = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,13 @@
|
||||||
/// <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.Religion;
|
||||||
public readonly max?: number = null;
|
public readonly max?: number = null;
|
||||||
public readonly unlocked = true;
|
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 (
|
constructor (
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference path="../model/GameState.ts" />
|
||||||
|
|
||||||
|
interface IRenderer {
|
||||||
|
render (state: GameState);
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"target": "ES5",
|
"target": "ES6",
|
||||||
"module": "none"
|
"module": "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue