Merge commit 'ac510b651e7e6936df287b3509c86f680b9be776'

master
Valerie 2021-07-27 17:49:48 -04:00
commit 31eba8c1f6
55 changed files with 6103 additions and 2129 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ node_modules
out
data
*.log
bin

72
.vscode/tasks.json vendored
View File

@ -2,9 +2,9 @@
"version": "2.0.0",
"tasks": [
{
"label": "build",
"label": "tsc",
"type": "npm",
"script": "compile:watch",
"script": "tsc:watch",
"isBackground": true,
"group": {
"kind": "build",
@ -17,12 +17,76 @@
"reveal": "never",
"echo": false,
"focus": false,
"panel": "dedicated"
"panel": "shared",
"group": "main"
},
"problemMatcher": {
"base": "$tsc-watch",
"applyTo": "allDocuments"
}
},
"dependsOn": [
"install deps"
]
},
{
"label": "dev",
"type": "npm",
"script": "dev",
"isBackground": true,
"group": "build",
"runOptions": {
"runOn": "folderOpen"
},
"presentation": {
"reveal": "never",
"echo": false,
"focus": false,
"panel": "shared",
"group": "main"
},
"problemMatcher": [],
"dependsOn": [
"install deps"
]
},
{
"label": "install deps",
"type": "npm",
"script": "install",
"isBackground": true,
"group": "build",
"runOptions": {
"runOn": "folderOpen"
},
"presentation": {
"reveal": "never",
"echo": false,
"focus": false,
"panel": "shared",
"group": "main"
},
"problemMatcher": []
},
{
"label": "rollup",
"type": "npm",
"script": "rollup:watch",
"isBackground": true,
"group": "build",
"runOptions": {
"runOn": "folderOpen"
},
"presentation": {
"reveal": "never",
"echo": false,
"focus": false,
"panel": "shared",
"group": "main"
},
"problemMatcher": [],
"dependsOn": [
"install deps"
]
}
]
}

10
babel.config.json 100644
View File

@ -0,0 +1,10 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry"
}
]
]
}

View File

@ -0,0 +1,3 @@
import './core/items/CoreItems.js';
import './core/tasks/CoreTasks.js';
import './core/themes/standard.js';

View File

@ -1,32 +0,0 @@
import { registerAction } from '@actions';
import { Game } from '@game';
import { ItemState } from '@items';
import { TaskState } from '@tasks';
import { SelectItem } from '../../../src/ui/SelectItem.js';
import { FLINT_NORMAL } from '../items/CoreItems.js';
import { GATHER_FLINT, MAKE_ARROWHEAD } from '../tasks/CoreTasks.js';
// registerAction('Gather Slate', (qty) => {
// Game.current.board.addTask({
// taskId: 'core:gather-slate',
// options: {}
// })
// });
registerAction('Gather Flint', () => {
const taskState = new TaskState(GATHER_FLINT);
Game.current.board.addTask(taskState);
});
registerAction('Create Arrowhead', async () => {
// const rock = new ItemState(FLINT_NORMAL, 1, null);
const item = await SelectItem.show((itemState) => {
return true;
});
const task = new TaskState(MAKE_ARROWHEAD, {
baseMaterial: rock
});
Game.current.board.addTask(task);
});

View File

@ -30,6 +30,7 @@ export const LOG = new Item()
export const STICK = new Item()
.setName("Stick")
.plural('Sticks')
.setId('core:resources/stick')
export const PLANT_FIBRES = new Item()
@ -56,7 +57,7 @@ export const SANDSTONE_PEBBLE = new Item()
.setName("Sandstone Pebble")
.setId('core:sandstone-pebble')
export const SLATE_NORMAL = new Item()
export const SLATE = new Item()
.setName("Slate")
.setId('core:slate')

View File

@ -1,32 +1,45 @@
import { Task } from "@tasks";
import { Game } from '@game';
import { ARROWHEAD, FLINT_NORMAL, SLATE_NORMAL } from '../items/CoreItems.js';
import { ItemState } from "@items";
import { Popup } from "../../../src/ui/Popup.js";
import { inspect } from 'util';
import { Item, ItemState } from "@items";
import { Place, ResourceNode } from "@world";
// import { STICK } from "../items/CoreItems.js";
export const GATHER_FLINT = new Task('core:gather-flint')
.setName('Gather Flint')
.setStatus('SCAVENGING')
.setWork(1000)
.setTasklistVisibility(true)
.setCategory("work")
.setCompletionEvent(() => {
const qty = Math.floor(Math.random() * 5) + 1;
Game.current.inv.add(new ItemState(FLINT_NORMAL, 1, null));
});
export const MAKE_ARROWHEAD = new Task<{
baseMaterial: ItemState<any>
}>('core:gather-slate')
.setName('Craft Arrowhead')
.setStatus('CRAFTING')
.setWork(1000)
.setTasklistVisibility(true)
.setCategory("craft")
.setCompletionEvent((data) => {
const itemState = new ItemState(ARROWHEAD, 1, {
baseMaterial: data.baseMaterial
});
Game.current.inv.add(itemState);
});
// export const GATHER_FLINT = new Task('core:gather-flint')
// .setName('Gather Flint')
// .setStatus('SCAVENGING')
// .setWork(1000)
// .setTasklistVisibility(true)
// .setCategory("work")
// .setCompletionEvent(() => {
// const qty = Math.floor(Math.random() * 5) + 1;
// Game.current.inv.add(new ItemState(FLINT_NORMAL, 1, null));
// });
// export const MAKE_ARROWHEAD = new Task<{
// baseMaterial: ItemState<any>
// }>('core:gather-slate')
// .setName('Craft Arrowhead')
// .setStatus('CRAFTING')
// .setWork(1000)
// .setTasklistVisibility(true)
// .setCategory("craft")
// .setCompletionEvent((data) => {
// const itemState = new ItemState(ARROWHEAD, 1, {
// baseMaterial: data.baseMaterial
// });
// Game.current.inv.add(itemState);
// });
export const STICK = new Item()
.setName("Stick")
.plural('Sticks')
.setId('core:resources/stick')
export const Forest = new Place()
.setName('Forest')
.setId('core:forest')
.setFrequency(1)
.setHabitable(true)
.populateResources(() => [
new ResourceNode(new ItemState(STICK, 10_000))
])

View File

@ -1,38 +0,0 @@
import path from 'path';
const moduleAliases = {
"@themes": "./out/src/registries/Themes.js",
"@actions": "./out/src/registries/Actions.js",
"@tasks": "./out/src/registries/Tasks.js",
"@items": "./out/src/registries/Items.js",
"@ui": "./out/src/ui/UI.js",
"@game": "./out/src/Game.js"
};
const getAliases = () => {
const base = process.cwd();
const aliases = moduleAliases || {};
const absoluteAliases = Object.keys(aliases).reduce((acc, key) =>
aliases[key][0] === '/'
? acc
: { ...acc, [key]: 'file:///' + path.join(base, aliases[key]) },
aliases)
return absoluteAliases;
}
const isAliasInSpecifier = (path, alias) => {
return path.indexOf(alias) === 0
&& (path.length === alias.length || path[alias.length] === '/')
}
const aliases = getAliases();
export const resolve = (specifier, parentModuleURL, defaultResolve) => {
const alias = Object.keys(aliases).find((key) => isAliasInSpecifier(specifier, key));
const newSpecifier = alias === undefined
? specifier
: path.join(aliases[alias], specifier.substr(alias.length));
return defaultResolve(newSpecifier, parentModuleURL);
}

View File

@ -1,21 +1,32 @@
{
"name": "df-idle",
"name": "hadean",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"type": "module",
"dependencies": {
"@babel/core": "^7.14.8",
"@babel/preset-env": "^7.14.8",
"@nodegui/nodegui": "^0.34.0",
"@rollup/plugin-alias": "^3.1.4",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-json": "^4.1.0",
"@types/blessed": "^0.1.17",
"@types/bonjour": "^3.5.8",
"@types/chai": "^4.2.19",
"@types/chokidar": "^2.1.3",
"@types/faker": "^5.5.6",
"@types/fs-extra": "^9.0.11",
"@types/mocha": "^8.2.2",
"@types/node-ipc": "^9.1.5",
"@types/uuid": "^8.3.0",
"@types/ws": "^7.4.6",
"@types/watch": "^1.0.2",
"@types/ws": "^7.4.5",
"@web/dev-server": "^0.1.18",
"bonjour": "^3.5.0",
"chai": "^4.3.4",
"chalk": "^4.1.1",
"chokidar": "^3.5.2",
"deepmerge": "^4.2.2",
"faker": "^5.5.3",
"frigid": "^1.3.13",
@ -23,25 +34,36 @@
"get-port": "^5.1.1",
"mocha": "^9.0.1",
"module-alias": "^2.2.2",
"multiview": "^3.0.1",
"neo-blessed": "^0.2.0",
"node-dev": "^7.0.0",
"node-ipc": "^10.0.2",
"nodemon": "^2.0.7",
"printable-characters": "^1.0.42",
"rollup": "^2.53.3",
"sisteransi": "^1.0.5",
"supervisor": "^0.12.0",
"typescript": "^4.3.2",
"uuid": "^8.3.2",
"walk-sync": "^3.0.0",
"watch": "^1.0.2",
"ws": "^7.4.6",
"yarn": "^1.22.10"
},
"scripts": {
"compile:watch": "tsc --watch",
"start": "node --no-warnings --loader ./lib/aliases.mjs --enable-source-maps out/src/index.js",
"dev": "nodemon -I --watch out --exec yarn start",
"compile": "yarn tsc & yarn rollup",
"start": "yarn qode bin/app.bundle.cjs",
"dev": "yarn x bin/ipc-tower.bundle.cjs",
"prod": "git fetch && git pull && yarn && tsc && yarn start",
"test": "mocha",
"lint": "eslint src/**/*.ts",
"profile": "node --inspect --no-warnings --loader ./lib/aliases.mjs --enable-source-maps out/src/index.js"
"web": "web-dev-server --open index.html --node-resolve --watch",
"profile": "yarn qode --inspect-brk bin/app.bundle.cjs",
"qode": "qode",
"x": "node",
"tsc": "tsc",
"tsc:watch": "yarn tsc --watch",
"rollup": "rollup --config rollup.config.js",
"rollup:watch": "yarn rollup --watch"
}
}

15
perf.js
View File

@ -1,15 +0,0 @@
console.clear();
const n = 10 ** 8;
// const map = new Map();
console.log('sequential insertion');
for(let run = 0; run < 10; run ++) {
const arr = [];
console.time('array');
for(let i = 0; i < n; i ++) {
arr.push(133769420)
}
console.timeEnd('array')
}

57
rollup.config.js 100644
View File

@ -0,0 +1,57 @@
import alias from '@rollup/plugin-alias';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import tsconfig from './tsconfig.json';
const __dirname = dirname(fileURLToPath(import.meta.url));
const aliases = Object.entries(tsconfig.compilerOptions.paths).map(([name, [path]]) => {
return {
find: name,
replacement: resolve(__dirname, 'out', path) + '.js'
};
});
const shared = {
plugins: [
alias({
entries: [
...aliases,
{
find: 'frigid',
replacement: resolve(__dirname, 'node_modules/frigid/out/index.js')
},
{
find: 'node-ipc',
replacement: resolve(__dirname, 'node_modules/node-ipc/node-ipc.js')
},
{
find: 'event-pubsub',
replacement: resolve(__dirname, 'node_modules/event-pubsub/index.js')
},
]
}),
],
watch: {
include: 'out/**/*'
}
}
export default [
{
...shared,
input: './out/src/index.js',
output: {
file: 'bin/app.bundle.cjs',
format: 'cjs'
}
},
{
...shared,
input: './out/src/hot-index.js',
output: {
file: 'bin/ipc-tower.bundle.cjs',
format: 'cjs'
}
}
];

10
src/Constants.ts 100644
View File

@ -0,0 +1,10 @@
export const IPC_PATH = '/tmp/dfi.dev';
export const IPC_CLIENT_APPSAPCE = 'dfi.';
export const IPC_CLIENT_CONNECT_NAME = 'dev';
export const IPC_QUIT_EVENT = 'app.quit';
export const IPC_RESTART_EVENT = 'app.restart';
export const IPC_REQUEST_RESTART = 'app.request-restart';
export const MDNS_TYPE = 'hdn';
export const APPLICATION_NAME = 'Hadean'

View File

@ -1,26 +1,23 @@
import { Frigid, Serializable } from 'frigid';
import { DEBUG } from 'frigid/out/Serializable.js';
import { Frigid } from 'frigid';
import { Pawn } from './Pawn.js';
import { TaskList } from './TaskList.js';
import { Inventory } from './Inventory.js';
import { Menu } from './ui/Menu.js';
import Time, { Tickable } from './Time.js';
import { render, Renderable, setTitle, start } from './ui/UI.js';
import { setTitle, start, update } from '@ui';
import { ready } from './multiplayer/mDNS.js';
import faker from 'faker';
import { World } from '@world';
let game: Game = null;
export class Game extends Frigid implements Tickable, Renderable {
export class Game extends Frigid implements Tickable {
pawns: Pawn[] = [];
selected: Pawn;
inventory: Inventory;
board: TaskList;
menu: Menu;
clock: Time;
name: string;
[DEBUG] = true;
world: World;
static get current(): Game {
if (!game) throw new Error('Somehow called a game before it existed?');
@ -31,7 +28,7 @@ export class Game extends Frigid implements Tickable, Renderable {
for(const pawn of this.pawns) {
pawn.tick();
}
render();
update();
}
get inv() { return this.inventory; }
@ -57,25 +54,22 @@ export class Game extends Frigid implements Tickable, Renderable {
start();
this.name ??= faker.address.city();
setTitle(this.name);
this.world ??= new World();
this.pawns ??= [];
this.selected ??= this.pawns[0] || null;
this.menu = new Menu();
this.board ??= new TaskList();
this.inventory ??= new Inventory();
this.inventory.validate();
this.clock ??= new Time();
this.clock.thing = this;
this.clock.start();
this.clock.start(this);
this.pawns = [];
if(this.pawns.length === 0) {
for(let i = 0; i < 3; i ++) this.pawns.push(new Pawn());
}
ready(this.name);
render(this);
}
static serializationDependencies() {
return [ Pawn, Inventory, TaskList, Time ];
}
render() {
this.menu.render();
this.board.render();
return [ Pawn, Inventory, TaskList, Time, World ];
}
}

View File

@ -1,10 +1,9 @@
import { Serializable } from 'frigid';
import { Game } from './Game.js';
import { Item, ItemState } from './registries/Items.js';
import { Popup } from './ui/Popup.js';
import { Renderable } from './ui/UI.js';
import { Popup } from '@ui';
export class Inventory extends Serializable implements Renderable {
export class Inventory extends Serializable {
items: ItemState<any>[];
ctor() {

17
src/Memories.ts 100644
View File

@ -0,0 +1,17 @@
import { Game } from "@game";
import { Pawn } from "./Pawn.js";
export function injectTravelMemory(target: Pawn) {
return {
type: "travel",
time: {
age: target.age,
locale: Game.current.clock.toString()
},
location: Game.current.name
}
}
function injectMemory(pawn: Pawn, memory: any) {
pawn.memories.push(memory);
}

View File

@ -20,6 +20,10 @@ export type BirthMemory = ProtoMemory & {
location: string,
}
export type AnyMemory =
TravelMemory
| BirthMemory;
export function stringify(memory: Memory): string {
switch(memory.type) {
case "birth": {

View File

@ -1,19 +1,12 @@
import { Serializable } from 'frigid';
import faker from 'faker';
import { TaskState } from './registries/Tasks.js';
import { Task, TaskState } from './registries/Tasks.js';
import Time, { Tickable } from './Time.js';
import { Game } from './Game.js';
import { render } from './ui/UI.js';
// import { render, Renderable, RenderMode } from '@ui';
import { Memory } from './Memory.js';
import { getTheme } from '@themes';
// const STATUS = {
// IDLE: Symbol('IDLE')
// }
const energyScale = 0.1;
const MAX_ENERGY = 100;
// TODO add stats getter to return % of all stats
export class Pawn extends Serializable implements Tickable {
@ -21,54 +14,18 @@ export class Pawn extends Serializable implements Tickable {
first: string,
last: string
};
job: TaskState<any>;
awake: boolean;
sex: number;
energy: number;
fun: number;
age: number;
memories: Memory[];
job: TaskState<unknown, unknown>;
async tick() {
this.age ++;
this.energy -= energyScale;
if(this.awake === false) {
this.energy += energyScale * 4;
if(this.energy >= MAX_ENERGY) {
this.awake = true;
}
} else {
if(this.job) {
this.job.doWork(1, this);
this.energy -= energyScale;
if(this.job?.completed) {
this.stopWorking();
}
} else {
const inactive = Game.current.board.tasks.filter(task => {
return task.worker === null;
});
if(inactive.length > 0) {
const task = inactive[0];
// const task = inactive[Math.floor(Math.random() * inactive.length)];
this.assignJob(task);
}
}
if(this.energy <= 0) {
this.stopWorking();
this.awake = false;
}
}
}
get idle() {
return !this.job && this.awake;
return !this.job;
}
ctor() {
@ -80,8 +37,6 @@ export class Pawn extends Serializable implements Tickable {
this.sex = Math.round(Math.random());
this.name.first = faker.name.firstName(this.sex);
}
this.awake ??= true;
this.energy ??= MAX_ENERGY;
this.memories ??= [];
if(!this.age) {
this.age = Math.floor(525600 * (16 + Math.random() * 9));
@ -96,41 +51,54 @@ export class Pawn extends Serializable implements Tickable {
}
if(this.job?.completed) {
this.stopWorking();
// if(this.job?.completed) {
// this.stopWorking();
// }
}
taskCompleted() {
this.job = null
}
stopWorking() {
if(this.job) {
this.job.stopJob();
// this.job.unclaim(this);
this.job = null;
}
}
assignJob(task: TaskState<any>) {
this.job?.stopJob()
this.job = task;
this.job.claim(this);
}
get status() {
if(this.job) {
return this.job.task.status;
} else {
return this.awake ? getTheme().status.idle('IDLE') : getTheme().status.self('RESTING')
}
return 'SEMETHING';
// if(this.job) {
// return this.job.task.status;
// } else {
// return this.awake ? getTheme().status.idle('IDLE') : getTheme().status.self('RESTING')
// }
}
static serializationDependencies() {
return [TaskState]
}
toString() {
if(this.name) {
return this.name.first + ' ' + this.name.last;
} else {
return '[Object Pawn]';
}
}
// toString() {
// return this.render(RenderMode.ONELINE);
// }
// render(mode: RenderMode): string {
// if(mode === RenderMode.ONELINE) {
// if(this.name) {
// return this.name.first + ' ' + this.name.last;
// } else {
// return '[Object Pawn]';
// }
// } else if (mode === RenderMode.DETAILS) {
// return `${
// this.toString()
// }{|}${
// this.status
// }\nDETAILS\nDETAILS`
// }
// }
}
// const task =

View File

@ -0,0 +1,67 @@
import chalk from 'chalk';
import EventEmitter from 'events';
import ipc from 'node-ipc';
import {
IPC_CLIENT_CONNECT_NAME,
IPC_CLIENT_APPSAPCE,
IPC_QUIT_EVENT,
IPC_RESTART_EVENT,
IPC_REQUEST_RESTART
} from './Constants.js';
let connected = false;
const oldConsoleLog = console.log;
const patchLog = () => console.log = console.log.bind(console, chalk.cyan('[CLIENT]'));
const restoreLog = () => console.log = oldConsoleLog;
// const log = (...args: any[]) => console.log(chalk.cyan('[CLIENT]'), ...args);
class ProcessManagerClass extends EventEmitter {
quit() {
this.emit('shutdown');
process.exit(0);
}
restart() {
this.emit('shutdown');
if (connected) {
ipc.of[name].emit(IPC_RESTART_EVENT);
}
setTimeout(() => {
process.exit(0);
})
}
get connected() {
return connected;
}
}
export const ProcessManager = new ProcessManagerClass();
const name = IPC_CLIENT_CONNECT_NAME;
ipc.config.appspace = IPC_CLIENT_APPSAPCE;
ipc.config.silent = true;
ipc.connectTo(name, () => {
ipc.of[name].on('connect', () => {
connected = true;
patchLog();
});
ipc.of[name].on('disconnect', () => {
connected = false
restoreLog();
});
ipc.of[name].on(IPC_REQUEST_RESTART, () => {
console.log('received restart request');
// ProcessManager.restart();
ProcessManager.emit('reload');
})
});
process.on('SIGKILL', () => ProcessManager.quit());
process.on('SIGTERM', () => ProcessManager.quit());
process.on('SIGINT', () => ProcessManager.quit());
///

View File

@ -1,11 +1,9 @@
import { getTheme } from '@themes';
import { Serializable } from 'frigid';
import { Task, TaskState } from './registries/Tasks.js';
import { render, Renderable, panels } from './ui/UI.js';
const taskTypes = {};
export class TaskList extends Serializable implements Renderable {
tasks: TaskState<any>[] = [];
export class TaskList extends Serializable {
tasks: TaskState<unknown, unknown>[] = [];
clear() {
for(const task of this.tasks) {
@ -13,27 +11,33 @@ export class TaskList extends Serializable implements Renderable {
}
}
static serializationDependencies() {
return [TaskState]
static serializationDependencies(): any[] {
return [];
}
addTask(task: TaskState<any>) {
// TODO assign task dependant on pawn skills
getUnclaimedTask(): TaskState<unknown, unknown> | null {
// const availableTasks = this.tasks.filter(task => !task.claimed);
// if(availableTasks.length > 0) {
// return availableTasks[0]
// } else return null;
return null;
}
addTask(task: TaskState<unknown, unknown>) {
this.tasks = [...this.tasks, task];
}
removeTask(task: TaskState<any>) {
removeTask(task: TaskState<unknown, unknown>) {
this.tasks = this.tasks.filter(v => v !== task);
}
render() {
// const width = tasksPanel.width;
panels.left.setContent(`${this.tasks.map(task => {
return task.toString();
}).join('\n')}`);
// return this.tasks.map(task => task.toString()).join('\n');
// panels.left.setContent(`${this.tasks.map(task => `${
// getTheme().normal(task.toString())
// } ${
// getTheme().dimmed(task.worker?.toString() ?? '')
// }`).join('\n')}`);
// return '';
}
}
// export function registerTask(name, clazz) {
// taskTypes[name] = clazz;
// }

View File

@ -1,7 +1,6 @@
import chalk from "chalk";
import { Serializable } from "frigid";
import { getTheme } from "@themes";
import { Renderable } from "./ui/UI.js";
type AbbreviatedMonthName = string;
@ -19,8 +18,9 @@ const months: AbbreviatedMonthName[] = [
'Oct', 'Nov', 'Dec'
]
export default class Time extends Serializable implements Renderable {
rate: number;
// TODO split ticker util and calendar util...
export default class Time extends Serializable {
targetTPS: number;
paused = true;
thing: Tickable;
@ -31,6 +31,12 @@ export default class Time extends Serializable implements Renderable {
hour: number;
minute: number;
ticksInSecond: number;
lastTPSCheckpoint: number;
tps: number;
_boundTick: Function;
constructor(timestamp: number = 0) {
super();
this.minute = timestamp;
@ -68,16 +74,34 @@ export default class Time extends Serializable implements Renderable {
}
toString() {
return `${this.hour}:${Math.floor(this.minute).toString().padStart(2, '0')} ${months[this.month]} ${this.day + 1}, ${this.normalizedYear}`
return `${
this.hour
}:${
Math.floor(this.minute).toString().padStart(2, '0')
} ${
months[this.month]
} ${
this.day + 1
}, ${
this.normalizedYear
} [${
this.tps
} / ${
this.targetTPS
}]`;
}
ctor() {
this.rate = 60;
this.targetTPS = 2000;
this.minute ??= 0;
this.hour ??= 0;
this.day ??= 0;
this.month ??= 0;
this.year ??= 0;
this.tps ??= 0;
this.lastTPSCheckpoint = ms4();
this.ticksInSecond = 0;
this._boundTick = this.doTick.bind(this);
}
get second() {
@ -113,7 +137,13 @@ export default class Time extends Serializable implements Renderable {
this.paused = true;
}
start() {
resume() {
this.paused = false;
setTimeout(this.doTick.bind(this), 0);
}
start(tickable: Tickable) {
this.thing = tickable;
this.paused = false;
setTimeout(this.doTick.bind(this), 0);
}
@ -131,8 +161,6 @@ export default class Time extends Serializable implements Renderable {
}
}
normalize() {
// while(t)
while(this.minute >= 60) {
@ -174,20 +202,46 @@ export default class Time extends Serializable implements Renderable {
}
async doTick() {
this.advanceTime(1);
const timeout = 1000 / this.rate;
const start = new Date().getTime();
const timeout = 1000 / this.targetTPS;
// const start = ms4()
const start = ms4();
if(this.thing) {
await this.thing.tick();
}
const elapsed = new Date().getTime() - start;
const wait = Math.max(timeout - elapsed, 0);
if(this.paused) return;
setTimeout(this.doTick.bind(this), wait)
}
const end = ms4()
const elapsed = end - start;
const wait = timeout - elapsed;
const normalizedWait = Math.floor(Math.max(wait, 0));
// process.stdout.write(`tick took ${elapsed} waiting ${normalizedWait}\n`);
if(wait < 0) {
const ticksOver = (-wait / timeout) + 1;
console.log(chalk.yellow('Can\'t keep up! Tick took ' + ticksOver.toFixed(2) + ' ticks (' + (timeout - wait).toFixed(4) + 'ms)'));
}
this.ticksInSecond ++;
if(end > this.lastTPSCheckpoint + 1000) {
this.lastTPSCheckpoint = end;
this.tps = this.ticksInSecond;
this.ticksInSecond = 0;
}
if(this.paused) return;
setTimeout(this._boundTick, normalizedWait)
}
}
export interface Tickable {
tick: () => Promise<void>
}
function ms4() {
const a = process.hrtime()
return a[0]*10e2 + a[1]/1000000;
}

33
src/Util.ts 100644
View File

@ -0,0 +1,33 @@
import { lstatSync } from 'fs';
import { parse, resolve } from 'path';
import walkSync from 'walk-sync';
import { fileURLToPath } from 'url';
import { APPLICATION_NAME } from './Constants.js';
import chalk from 'chalk';
export function osrsNumber(x: number): string {
if(x < 10_000) return '' + x;
else if (x < 10_000_000) return Math.floor(x / 1000) + 'K';
else return Math.floor(x / 1_000_000) + 'M';
}
export async function loadExtensions() {
console.log('Loading extensions');
const extensionsPath = resolve(parse(fileURLToPath(import.meta.url)).dir, '../content');
const extensions = walkSync(extensionsPath)
.map(path => [path, resolve(extensionsPath, path)])
.filter(path => lstatSync(path[1]).isFile())
.filter(path => parse(path[1]).ext === '.js');
console.log('found', extensions.length, 'extensions');
for (const path of extensions) {
console.log('=== [', path[0], '] ===');
await import(path[1]);
console.log();
}
console.log('Setup Complete.');
}

View File

@ -1,14 +1,145 @@
import { ItemState } from "@items";
import { Game } from "@game";
import { ItemFilter, ItemState } from "@items";
import { Task, TaskState } from "@tasks";
import { Serializable } from "frigid";
import { RESOURCE_COLLECTION_TASK } from "./world/ResourceCollectionTask.js";
const explorationConstant = 0.001; // km ish
export class World extends Serializable {
places: PlaceState[] = [];
distanceExplored: number; // km ish
home: PlaceState = null;
static serializationDependencies() {
return [PlaceState];
}
class WorldItemState {
itemState: ItemState<any>
constructor() {
explore() {
for(const [id, place] of places) {
const threshold = (explorationConstant * place.frequency);
if(Math.random() <= threshold) {
const angle = Math.random() * Math.PI * 2
const x = Math.sin(angle) * this.distanceExplored;
const y = Math.cos(angle) * this.distanceExplored;
const newPlaceState = new PlaceState(
place,
Math.round(x * 1000),
Math.round(y * 1000)
);
if(this.home === null) {
this.home = newPlaceState;
}
this.places.push(newPlaceState);
}
}
this.distanceExplored += explorationConstant;
}
ctor() {
this.distanceExplored ??= 0;
this.home ??= null;
this.places ??= [];
if(this.home === null) {
let hasHabitablePlacesLoaded = false;
for(const [id, place] of places) {
if(place.habitable) hasHabitablePlacesLoaded = true;
break;
}
if(hasHabitablePlacesLoaded) {
while(this.home === null) {
this.explore();
}
} else {
throw new Error('No habitable places loaded\n'
+ 'unable to create home!')
}
}
}
}
const places: Map<string, Place> = new Map();
export class PlaceState extends Serializable {
resources: ResourceNode[];
placeId: string;
x: number;
y: number;
constructor(place: Place, x: number, y: number) {
super();
this.placeId = place.id;
this.resources = this.place.populate();
for(const node of this.resources) node.setPlace(this);
this.x = x;
this.y = y;
}
get place() {
return places.get(this.placeId);
}
static serializationDependencies() {
return [ResourceNode];
}
}
export class Place {
name: string;
id: string;
frequency: number;
habitable: boolean;
populate: () => ResourceNode[];
setName(name: string) {
this.name = name;
return this;
}
setId(id: string) {
this.id = id;
places.set(this.id, this);
return this;
}
populateResources(fn: () => ResourceNode[]) {
this.populate = fn;
return this;
}
setFrequency(frequency: number) {
this.frequency = frequency;
return this;
}
setHabitable(habitable: boolean) {
this.habitable = habitable;
return this;
}
}
export class ResourceNode extends Serializable {
resources: ItemState<unknown>;
place: PlaceState;
constructor(resources: ItemState<unknown>) {
// collectionRequirements: ItemFilter[] = []
super();
this.resources = resources;
}
setPlace(place: PlaceState) {
this.place = place;
}
request(qty: number) {
Game.current.board.addTask(
new TaskState(RESOURCE_COLLECTION_TASK, {
qty,
node: this
})
);
}
}

90
src/hot-index.ts 100644
View File

@ -0,0 +1,90 @@
import ipc from 'node-ipc';
import {
IPC_PATH,
IPC_QUIT_EVENT,
IPC_RESTART_EVENT,
IPC_REQUEST_RESTART
} from './Constants.js';
import { spawn, ChildProcess } from 'child_process';
import chokidar from 'chokidar';
import chalk from 'chalk';
ipc.config.silent = true;
// should be obtained from process spawn args, but whatever!
const exec = 'qode' +
(process.platform === "win32" ? '.cmd' : '');
const args = [
'bin/app.bundle.cjs'
];
const log = console.log.bind(console, chalk.green('[TOWER]'));
// varying state data
let connected = 0;
let proc: ChildProcess = null;
let restartTimer: NodeJS.Timeout = null;
function ensureAlive() {
if(proc) {
return;
}
proc = spawn(exec, args, {
stdio: 'inherit'
});
proc.once('exit', () => {
proc = null;
});
log(`[${
proc.pid
}] ${
chalk.grey(`${
exec
} ${
args.join(' ')
}`)
}`);
}
async function ensureDead() {
if(!proc) {
return;
}
const killedPromise =
new Promise(res => proc.once('exit', res));
proc.kill(9);
await killedPromise;
proc = null;
}
async function restart() {
await ensureDead();
ensureAlive();
}
function fileChange() {
if(restartTimer) clearTimeout(restartTimer)
restartTimer = setTimeout(() => {
ensureAlive();
ipc.server.broadcast(IPC_REQUEST_RESTART);
restartTimer = null;
}, 100);
}
// start the server, connect events
ipc.serve(IPC_PATH, () => {
ipc.server.on(IPC_QUIT_EVENT, ensureDead);
ipc.server.on(IPC_RESTART_EVENT, restart);
ipc.server.on('connect', () => connected ++);
ipc.server.on('disconnect', () => connected --);
});
ipc.server.start();
// open the process
ensureAlive();
//begin watching for files, ignore changes on boot.
chokidar.watch('./out', {
ignoreInitial: true
}).on('all', fileChange);

View File

@ -1,54 +1,59 @@
import { render } from './ui/UI.js';
import { ensureDirSync } from 'fs-extra';
import { lstatSync } from 'fs';
import { parse, resolve } from 'path';
import walkSync from 'walk-sync';
import { fileURLToPath } from 'url';
import { parse } from 'path';
import { Game } from '@game';
import { isStarted, stop } from './ui/UI.js';
import { writeFileSync } from 'fs';
import {
isStarted,
stop,
update,
GameView,
setView,
start
} from '@ui';
// @ts-ignore
import ansi from 'sisteransi';
// HACK static extension loading
import './../content/content.js';
import { loadExtensions } from './Util.js';
import { APPLICATION_NAME } from './Constants.js';
import chalk from 'chalk';
import { ProcessManager } from './ProcessManager.js';
// console.clear();
ProcessManager.on('shutdown', gracefulShutdown);
function gracefulShutdown() {
if(isStarted()) {
stop();
}
console.log('shutting down gracefully...');
// if (isStarted()) {
// stop();
// }
if (Game.current) {
console.log('saving world...');
console.log(chalk.cyan('Saving world...'));
Game.current.sync();
console.log(chalk.cyan('World Saved!'));
}
console.log('exitting');
process.exit(0);
}
process.on('exit', gracefulShutdown);
// process.on('exit', gracefulShutdown);
const saveFile = process.argv[2] || 'data/world01.json';
ensureDirSync(parse(saveFile).dir);
// loadExtensions();
// TODO extract extension loading into separate file
console.log('df-idle: Loading extensions');
const extensionsPath = resolve(parse(fileURLToPath(import.meta.url)).dir, '../content');
const extensions = walkSync(extensionsPath)
.map(path => resolve(extensionsPath, path))
.filter(path => lstatSync(path).isFile())
.filter(path => parse(path).ext === '.js');
// .map(path => import(path));
console.log('found', extensions.length, 'extensions');
for(const path of extensions) {
console.log('=== [', path, '] ===');
await import(path);
console.log();
}
// TODO replace with splash screen
// for (let seconds = 0; seconds > 0; seconds --) {
// process.stdout.write('Starting ' + APPLICATION_NAME + ' in ' + seconds + '\r');
// }
// process.stdout.write('Starting ' + APPLICATION_NAME + ' in ' + 0 + '\n');
// console.clear();
// TODO move render logic into game, so that the ui doesnt exist until the game does...
// maybe, i mean, an argument could be made for not that, because the game
// isnt necessarily the entire thing, its just one instance of a save file.
// But probably the initial menu screens will be their own thing entirely.
const game = Game.create(saveFile);
start();
const gameView = new GameView(game);
setView(gameView);

View File

@ -5,22 +5,24 @@ import * as uuid from 'uuid';
import faker from 'faker';
import chalk from 'chalk';
import { Item } from '../registries/Items.js';
import WebSocket from 'ws';
import { Popup } from '../ui/Popup.js';
import WebSocket, { EventEmitter } from 'ws';
import { Popup } from '@ui';
import { inspect } from 'util'
import { Pawn } from '../Pawn.js';
import { Game } from '../Game.js';
import { Player } from './Player.js';
import { injectTravelMemory } from '../Memories.js';
import { MDNS_TYPE } from '../Constants.js';
const mdns = bonjour();
const ID = uuid.v4();
let devices: Player[] = [];
const network = {
const network = new (class Network extends EventEmitter {
get players() {
return devices;
}
}
})();
export type GiftMessage = {
pawns: string[],
@ -32,7 +34,7 @@ export default network;
export async function ready(name: string) {
const port = await getPort({port: getPort.makeRange(52300, 52399)});
mdns.publish({
type: 'dfi',
type: MDNS_TYPE,
name,
port: port
});
@ -43,15 +45,8 @@ export async function ready(name: string) {
const pawns = [];
for(const pawnJson of pawnJsons) {
const pawn: Pawn = Pawn.fromJson(pawnJson);
pawn.memories.push({
type: "travel",
time: {
age: pawn.age,
locale: Game.current.clock.toString()
},
location: Game.current.name
})
pawns.push(pawn);
injectTravelMemory(pawn);
}
Popup.show(`${(() => {
if(pawns.length === 0) return `A care package has arrived from ${from}.`;
@ -64,13 +59,15 @@ export async function ready(name: string) {
}
mdns.find({
type: 'dfi'
type: MDNS_TYPE
}, (service) => {
network.emit('change');
const p = new Player();
p.name = service.name;
p.host = service.host;
p.port = service.port;
devices.push(p);
}).on("down", (service) => {
network.emit('change');
// TODO remove player from MP
})

250
src/qt/GameView.ts 100644
View File

@ -0,0 +1,250 @@
import { Game } from '@game';
import { ItemState } from '@items';
import {
QLabel,
QTabWidget,
QWidget,
QIcon,
QGridLayout,
FocusPolicy,
AlignmentFlag,
QBoxLayout,
Direction,
QScrollArea,
} from '@nodegui/nodegui';
import network from '../multiplayer/mDNS.js';
import { Player } from '../multiplayer/Player.js';
import { Pawn } from '../Pawn.js';
import { View } from './View.js';
export class GameView extends View {
game: Game;
title: QLabel;
timeLabel: QLabel;
left: QWidget;
right: QTabWidget;
addComponents(): void {
this.addLayout();
this.title = new QLabel();
this.left = new QWidget();
this.right = new QTabWidget();
this.timeLabel = new TimeWidget();
this.title.setText(this.game.name);
this.layout.addWidget(this.title, 0, 0);
this.layout.addWidget(this.timeLabel, 0, 1);
this.layout.addWidget(this.left, 1, 0);
this.layout.addWidget(this.right, 1, 1);
this.layout.setRowStretch(0, 0);
this.layout.setRowStretch(1, 1);
this.layout.setColumnStretch(0, 3);
this.layout.setColumnStretch(1, 2);
this.right.addTab(new PawnPageWidget(), new QIcon(), 'Pawns');
this.right.addTab(new InventoryPageWidget(), new QIcon(), 'Inventory');
this.right.addTab(new MultiplayerPageWidget(), new QIcon(), 'Multiplayer');
}
constructor(game: Game) {
super();
this.game = game;
}
}
class GridItem extends QWidget {
rootLayout: QGridLayout;
get layout(): QGridLayout {
return this.rootLayout;
}
constructor() {
super();
this.rootLayout = new QGridLayout()
this.setLayout(this.rootLayout);
this.setInlineStyle(`
width: \'100%\';
background: coral;
margin: 0px;
padding: 0px;
`);
this.setFocusPolicy(FocusPolicy.ClickFocus);
}
}
function addSplitText(layout: QGridLayout, left: string, right: string, row: number) {
layout.setSpacing(0);
const nameLabel = new QLabel();
nameLabel.setText(left);
nameLabel.setAlignment(AlignmentFlag.AlignLeft | AlignmentFlag.AlignTop);
const activityLabel = new QLabel();
activityLabel.setText(right);
activityLabel.setAlignment(AlignmentFlag.AlignRight | AlignmentFlag.AlignTop);
layout.addWidget(nameLabel, row, 0, 1, 1);
layout.addWidget(activityLabel, row, 1, 1, 1);
layout.setRowStretch(row, 1);
// in theory this is redundant, calling this
// function on the same layout multiple times...
layout.setColumnStretch(0, 1);
layout.setColumnStretch(1, 1);
}
class PawnWidget extends GridItem {
constructor(pawn: Pawn) {
super();
addSplitText(
this.layout,
pawn.name.first + ' ' + pawn.name.last,
pawn.status,
0
);
}
}
class ItemWidget extends GridItem {
constructor(itemState: ItemState<unknown>) {
super();
addSplitText(
this.layout,
itemState.name,
'' + (itemState.qty),
0
);
}
}
abstract class ScrollPanel extends QScrollArea {
centralWidget: QWidget;
vLayout: QBoxLayout;
constructor() {
super();
this.setInlineStyle(`
background: rgba(0, 0, 0, 0);
border: none;
`)
this.centralWidget = new QWidget();
this.centralWidget.setInlineStyle(`
background: rgba(0, 0, 0, 0);
`)
// this.setVerticalScrollBarPolicy(ScrollBarPolicy.ScrollBarAlwaysOn);
this.setWidgetResizable(true);
this.setWidget(this.centralWidget);
this.vLayout = new QBoxLayout(Direction.TopToBottom);
this.centralWidget.setLayout(this.vLayout);
this.fill();
this.vLayout.addStretch(1);
// for(let i = 0; i < 100; i ++) {
// const button = new QPushButton();
// button.setText('' + i);
// this.vLayout.addWidget(button);
// }
}
refill() {
for(const a of this.nodeChildren) {
console.log(a);
}
this.fill();
}
addWidget(widget: QWidget) {
this.vLayout.addWidget(widget);
}
abstract fill(): void;
}
class PawnPageWidget extends ScrollPanel {
fill() {
for(const pawn of Game.current.pawns) {
this.addWidget(new PawnWidget(pawn));
}
}
}
// class PawnPageWidget extends QListWidget {
// constructor() {
// super();
// for (const pawn of Game.current.pawns) {
// const pawnWidget = new PawnWidget(pawn);
// const item = new QListWidgetItem();
// this.addItem(item);
// this.setItemWidget(item, pawnWidget);
// }
// }
// }
// class PawnPageWidget extends QWidget {
// constructor() {
// super();
// this.setLayout(new QBoxLayout(Direction.TopToBottom));
// // this.setLayout(new FlexLayout());
// for(const pawn of Game.current.pawns) {
// this.layout.addWidget(new PawnWidget(pawn));
// }
// }
// }
class TimeWidget extends QLabel {
constructor() {
super();
this.setAlignment(AlignmentFlag.AlignRight | AlignmentFlag.AlignVCenter);
setInterval(() => {
this.setText(Game.current.clock.toString());
}, 100)
}
}
class InventoryPageWidget extends ScrollPanel {
fill() {
for(const itemState of Game.current.inv.items) {
this.addWidget(new ItemWidget(itemState))
}
}
}
class MultiplayerPlayerWidget extends GridItem {
constructor(player: Player) {
super();
addSplitText(
this.layout,
player.name,
player.host + ':' + player.port,
0
)
}
}
class MultiplayerPageWidget extends ScrollPanel {
constructor() {
super();
network.on('change', () => {
this.refill();
})
}
fill(): void {
for(const player of network.players) {
this.addWidget(new MultiplayerPlayerWidget(player))
}
}
}

View File

@ -0,0 +1,18 @@
import { QGridLayout, QLabel, QWidget } from "@nodegui/nodegui";
import { View } from "./View.js";
export class LoadingView extends View {
label: QLabel;
addComponents(): void {
this.addLayout();
this.label = new QLabel();
this.label.setText('Loading World...');
this.layout.addWidget(this.label);
}
destory() {
}
}

5
src/qt/Popup.ts 100644
View File

@ -0,0 +1,5 @@
export class Popup {
static show(content: string) {
}
}

View File

@ -0,0 +1,31 @@
import { FocusReason, QGridLayout, QLabel, QMainWindow, QPushButton, QWidget } from "@nodegui/nodegui";
import { ProcessManager } from "../ProcessManager";
export class RequestReloadPopup {
static exists = false;
static show() {
if(this.exists) return;
this.exists = true;
const window = new QMainWindow();
window.setFixedSize(200, 100);
const root = new QWidget();
window.setCentralWidget(root);
const layout = new QGridLayout();
root.setLayout(layout);
const label = new QLabel();
label.setText('A reload has been requested');
layout.addWidget(label, 0, 0, 4, 3);
const reloadButton = new QPushButton();
reloadButton.setText('Reload');
layout.addWidget(reloadButton, 4, 2, 1, 1);
window.show();
window.setWindowTitle('Reload?');
reloadButton.addEventListener('clicked', () => {
ProcessManager.restart();
})
}
}
//////

19
src/qt/View.ts 100644
View File

@ -0,0 +1,19 @@
import { QGridLayout, QMainWindow, QWidget, WindowState } from "@nodegui/nodegui";
import { QEvent } from "@nodegui/nodegui/dist/lib/QtGui/QEvent/QEvent";
import { win } from "@ui";
export abstract class View {
root: QWidget;
layout: QGridLayout;
addLayout() {
this.root = new QWidget();
this.root.setObjectName("root");
win.setCentralWidget(this.root);
this.layout = new QGridLayout();
this.root.setLayout(this.layout);
}
abstract addComponents(): void;
}

70
src/qt/index.ts 100644
View File

@ -0,0 +1,70 @@
import {
QMainWindow, WidgetEventTypes
} from '@nodegui/nodegui';
import { APPLICATION_NAME } from '../Constants.js';
import { ProcessManager } from '../ProcessManager.js';
import { LoadingView } from './LoadingView.js';
import { RequestReloadPopup } from './RequestReloadPopup.js';
import { View } from './View.js';
export { GameView } from './GameView.js';
export { Popup } from './Popup.js';
export const win = new QMainWindow();
win.setFixedSize(800, 600);
win.setWindowTitle(APPLICATION_NAME);
// win.setStyleSheet(`
// QTabWidget::tab {
// bottom: 2px;
// }
// `);
win.show();
(global as any).win = win;
win.addEventListener(WidgetEventTypes.Paint, _ => _);
win.addEventListener('customContextMenuRequested', console.log);
win.addEventListener('objectNameChanged', console.log);
win.addEventListener('windowIconChanged', console.log);
win.addEventListener('windowTitleChanged', console.log);
win.addEventListener(
WidgetEventTypes.Close,
() => ProcessManager.quit()
);
setView(new LoadingView());
export function start() {
}
export function stop() {
win.close();
}
export function setView(view: View) {
view.addComponents();
}
export function setTitle(title: string) {
}
export function update() {
}
export function isStarted() {
return true;
return win.isVisible();
}
ProcessManager.on('reload', () => {
RequestReloadPopup.show();
});
// HACK this is bullshit, :)
function f() {
win.repaint();
win.update();
setTimeout(f, 0);
}
f();

View File

@ -1,20 +0,0 @@
import { getTheme } from "@themes";
import { Renderable } from "../ui/UI.js";
export const actions: Action[] = [];
export function registerAction(name: string, invoke: () => void) {
console.log('Registered action', name);
actions.push(new Action(name, invoke))
}
export class Action {
name: string;
qty: number;
invoke: (qty: number) => void;
constructor(name: string, done: (qty: number) => void) {
this.name = name;
this.invoke = done;
}
}

View File

@ -1,24 +1,33 @@
import { Serializable } from 'frigid';
import { getTheme } from '@themes';
import { Renderable } from '../ui/UI.js';
import { osrsNumber } from '../Util.js';
export type ItemID = string;
const items = new Map<ItemID, Item<any>>();
// ITEMS SHALL BE SINGULAR
export class Item<Data> extends Serializable {
export class Item<Data = any> {
name = '';
name = {
singular: '',
plural: ''
}
id: ItemID = '';
props: Map<string, any> = new Map();
setName(name: string) {
this.name = name;
this.name.singular = name;
this.name.plural = name;
this.register(false);
return this;
}
plural(name: string) {
this.name.plural = name;
return this;
}
setId(id: ItemID) {
this.id = id;
this.register(false);
@ -27,7 +36,7 @@ export class Item<Data> extends Serializable {
register(force = true) {
if((!this.id || !this.name) && !force) return;
console.log('Added item', (this.name ?? "[No Name]").padStart(20, ' '), `| (${this.id})`)
// console.log('Added item', (this.name.singular ?? "[No Name]").padStart(20, ' '), `| (${this.id})`)
items.set(this.id, this);
return this;
}
@ -43,7 +52,7 @@ export class Item<Data> extends Serializable {
}
}
export class ItemState<Data> extends Serializable implements Renderable {
export class ItemState<Data> extends Serializable {
qty: number;
itemId: ItemID;
data: Data;
@ -54,13 +63,17 @@ export class ItemState<Data> extends Serializable implements Renderable {
return new ItemState<Data>(this.item, qty, this.data);
}
get name() {
return this.qty === 1 ? this.item.name.singular : this.item.name.plural;
}
get item() {
if(!items.has(this.itemId))
throw new Error('unknown item: ' + this.itemId);
return items.get(this.itemId);
}
constructor(item: Item<Data>, amount: number, data: Data) {
constructor(item: Item<Data>, amount: number, data: Data = null) {
super();
this.qty = amount;
this.itemId = item.id;
@ -68,7 +81,10 @@ export class ItemState<Data> extends Serializable implements Renderable {
}
render() {
return getTheme().normal(` ${this.item.name}{|}${this.qty} `);
return getTheme().normal(` ${osrsNumber(this.qty)} ${(() => {
if(this.qty === 1) return this.item.name.singular;
else return this.item.name.plural;
})()} `);
}
}

View File

@ -3,122 +3,96 @@ import EventEmitter from 'events';
import chalk from 'chalk';
import { Pawn } from '../Pawn.js';
import { Game } from '../Game.js';
import { panels } from '../ui/UI.js';
import { progressbar, ProgressbarStyle } from '../Progressbar.js';
export const tasks: Map<string, Task<any>> = new Map();
export type TaskCategory = "self" | "work" | "craft" | "idle";
const tasks: Map<string, Task<unknown, unknown>> = new Map();
type WorkFunction<Data, State> = (taskState: TaskState<Data, State>, dtime: number) => void;
type InitFunction<Data, State> = (data: Data) => State;
export class Task<Data> {
export class Task<Data, State> {
id: string;
work: number;
name: string | ((data: Data) => string);
status: string;
tasklistVisibility: boolean;
category: TaskCategory;
completionEvent: (data: Data) => void;
fn: WorkFunction<Data, State>;
init: InitFunction<Data, State>;
name: string;
toString: (data: Data, state: State) => string;
setTasklistVisibility(b: boolean) {
this.tasklistVisibility = b;
constructor(id: string) {
this.id = id;
tasks.set(id, this);
}
setInitiate(init: InitFunction<Data, State>) {
this.init = init;
return this;
}
setName(name: string | ((data: Data) => string)) {
setFunction(fn: WorkFunction<Data, State>) {
this.fn = fn;
return this;
}
setName(name: string) {
this.name = name;
return this;
}
setStatus(s: string) {
this.status = s;
setToString(fn: (data: Data, state: State) => string) {
this.toString = fn;
return this;
}
setCategory(c: TaskCategory) {
this.category = c;
return this;
}
setWork(n: number) {
this.work = n;
return this;
}
setCompletionEvent(fn: (data: Data) => void) {
this.completionEvent = fn;
return this;
}
constructor(id: string) {
this.id = id;
this.tasklistVisibility = true;
this.category = "work";
tasks.set(this.id, this);
}
}
export class TaskState<T> extends Serializable {
export class TaskState<Data, State> extends Serializable {
taskId: string;
progress: number;
workFn: WorkFunction<Data, State>;
x: number;
y: number;
data: Data;
completed: boolean = false;
worker: Pawn;
data: T;
state: State;
ctor() {
// retest completion when loaded, JUST IN CASE
this.testCompletion();
}
constructor(task: Task<T>, data: T = {} as T) {
constructor(task: Task<Data, State>, data: Data) {
super();
this.taskId = task.id;
this.progress = 0;
this.worker = null;
// preset the data to nothing JIC
this.data = data;
this.taskId = task.id;
this.workFn = this.task.fn.bind(this);
}
stopJob() {
this.worker = null;
setLocation(x: number, y: number) {
this.x = x;
this.y = y;
}
doWork(work = 1, pawn: Pawn) {
this.worker = pawn;
this.progress += work;
this.testCompletion();
get task(): Task<Data, State> {
// casting because the id this is associated with
// should have the datatype of the task it was
// created with, but it stored as unknown
return tasks.get(this.taskId) as Task<Data, State>;
}
testCompletion() {
if (this.taskId && this.completed) {
this.task.completionEvent(this.data);
Game.current.board.removeTask(this);
}
get name() {
return this.task.name;
}
free() {
this.worker = null;
work(dtime: number) {
this.workFn(this, dtime);
}
claim(pawn: Pawn) {
this.worker = pawn;
}
get completion() {
return Math.min(1, this.progress / this.task.work);
}
get task() {
return tasks.get(this.taskId);
}
get completed() {
return this.completion >= 1;
unclaim() {
this.worker = null;
}
toString() {
// HACK magic number 2 here, is the border
// of the panel
const width = panels.left.width - 2;
const left = ' ' + this.task.name + ' ' + (this.worker?.toString() || chalk.bold.black('Queued'));
const bar = width - 2;
return `${left}\n ${progressbar(this.completion, bar, ProgressbarStyle.progress)}\n`;
return this.task.toString(this.data, this.state)
}
}
// export interface TaskProvider {
// hasTask(): boolean;
// getTask(): TaskState<unknown, unknown>;
// }

View File

@ -54,7 +54,7 @@ export const backupTheme: Theme = {
selected: chalk.ansi256(117).inverse
},
border: {
focused: '#ffffff',
focused: '#00ff00',
normal: '#888888'
},
progressBar: {
@ -80,7 +80,7 @@ let currentTheme = backupTheme;
const themes: Map<ThemeName, Theme> = new Map();
export function registerTheme(name: ThemeName, theme: Partial<Theme>) {
console.log('Registered theme', name);
// console.log('Registered theme', name);
themes.set(name, merge(backupTheme, theme));
}

View File

@ -0,0 +1,79 @@
import { Game } from "@game";
import { boxStyle, getTheme } from "@themes";
import { panels } from "./UI.js";
import blessed from 'neo-blessed';
import { ProcessManager } from "../ProcessManager.js";
// TODO convert all these popup-y things to be View based
// make them be boxes that have a view
export class EscapeMenu {
options = [
'RESUME',
'RELOAD',
'QUIT'
];
selected = 0;
box;
static show() {
new EscapeMenu();
}
protected constructor() {
this.box = blessed.box({
top: 3,
left: 'center',
width: 20,
height: 'shrink',
content: '',
tags: true,
...boxStyle(),
});
this.box.on('keypress', (evt: {}, key: {full: string}) => {
if(key.full === 'up') {
this.selected --;
if(this.selected === -1) this.selected = this.options.length - 1;
} else if (key.full === 'down') {
this.selected ++;
if(this.selected === this.options.length) this.selected = 0;
} else if (key.full === 'enter') {
switch(this.selected) {
case 0: {
Game.current.clock.resume();
panels.screen.remove(this.box);
break;
}
case 1: {
ProcessManager.restart();
break;
}
case 2: {
Game.current.sync();
ProcessManager.quit();
}
}
} else if(key.full === 'escape') {
Game.current.clock.resume();
panels.screen.remove(this.box);
}
this.render();
});
panels.screen.append(this.box);
this.box.focus();
Game.current.clock.pause();
this.render();
}
render() {
const space = ' '.repeat(this.box.width / 2 - 4);
this.box.setContent(space + 'Paused\n\n' + this.options.map((v, i) => {
if(i === this.selected) {
return ` ${getTheme().bright(v)} `;
} else {
return ` ${getTheme().normal(v)} `;
}
}).join('\n'));
panels.screen.render();
}
}

View File

@ -29,7 +29,7 @@ export class GiftPopup {
if(key.full === 'enter') {
this.send();
} if(key.full === 'escape' || key.full === 'enter') {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
} else if (key.full === 'up') {
this.selected --;

View File

@ -10,8 +10,8 @@ import PawnsView from './view/PawnsView.js';
import InventoryView from './view/InventoryView.js';
import MultiplayerView from './view/MultiplayerView.js';
import { View } from './View.js';
import { ActionsView } from './view/ActionsView.js';
import { tasks } from '@tasks';
import WorldResourcesView from './view/WorldView.js';
import { EscapeMenu } from './EscapeMenu.js';
const clamp = (min: number, max: number, value: number) => Math.min(Math.max(value, min), max);
@ -28,7 +28,7 @@ export class Menu implements Renderable {
new PawnsView(),
new InventoryView(),
new MultiplayerView(),
new ActionsView()
new WorldResourcesView()
]
get view() {
@ -53,14 +53,12 @@ export class Menu implements Renderable {
} else if (key.full === 'right') {
this.advanceView();
} else if (key.full === 'escape') {
this.advanceView();
EscapeMenu.show();
// debugging hotkeys
} else if (key.full === '1') {
Popup.show(inspect(stats));
} else if (key.full === '2') {
Popup.show(inspect(tasks));
} else if (key.full === '3') {
Popup.show(inspect(stats));
} else if (key.full === 'z') {
Game.current.pawns.push(new Pawn());
@ -114,5 +112,6 @@ export class Menu implements Renderable {
this.renderView(),
].join('\n');
panels.right.setContent(content);
return '';
}
}

View File

@ -23,7 +23,7 @@ export class PawnDetails {
});
this.box.on('keypress', (evt: {}, key: {full: string}) => {
if(key.full === 'escape' || key.full === 'enter') {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
} else if (key.full === 'up') {
// this.selected --;

View File

@ -11,7 +11,7 @@ export class Popup {
new Popup(content)
}
private constructor(content: string) {
protected constructor(content: string) {
this.box = blessed.box({
top: 'center',
left: 'center',
@ -23,7 +23,7 @@ export class Popup {
});
this.box.on('keypress', (evt: {}, key: {full: string}) => {
if(key.full === 'escape' || key.full === 'enter') {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
}
});

View File

@ -1,7 +1,7 @@
import { Game } from "@game";
import { ItemState } from "@items";
import { boxStyle, getTheme } from "@themes";
import { panels } from "@ui";
import { panels } from "./UI";
import EventEmitter from "events";
import blessed from 'neo-blessed';
@ -33,7 +33,7 @@ export class SelectItem {
}
private close() {
Game.current.clock.start();
Game.current.clock.resume();
panels.screen.remove(this.box);
}

View File

@ -0,0 +1,49 @@
import { Renderable, RenderMode } from './UI';
import { KeypressAcceptor } from './Menu.js';
import { getTheme } from '@themes';
export class SelectionBox implements Renderable, KeypressAcceptor {
selectedIdx = 0;
stuff: Renderable[] = [];
height: number;
offset: number = 0;
getData: () => Renderable[];
// buffer =
constructor(height: number) {
this.height = height;
}
setGetData(fn: () => Renderable[]) {
this.getData = fn;
}
render() {
return this.stuff.map((renderable, idx) => {
if(idx === this.selectedIdx) {
return ` ${getTheme().bright(` ${renderable.render(RenderMode.DETAILS)}`)} `;
} else {
return ` ${getTheme().normal(` ${renderable.render(RenderMode.DETAILS)}`)} `;
}
}).join('\n')
}
up() {
this.selectedIdx --;
// if(this.selectedIdx === -1) {
// }
}
down() {
this.selectedIdx ++;
}
keypress(key: { full: string; }) {
if(key.full === 'up') {
}
};
}

View File

@ -3,9 +3,10 @@ import blessed from 'neo-blessed';
import ansi from 'sisteransi';
import { boxStyle, getTheme } from '@themes';
export { Popup } from './Popup.js';
export { Menu } from './Menu.js'
export interface Renderable {
render(): void
render(mode?: RenderMode): string
}
let leftPanel: any;
@ -29,15 +30,15 @@ export function isStarted() {
export const panels = {
get left() {
assertStarted()
assertStarted();
return leftPanel;
},
get right() {
assertStarted()
assertStarted();
return rightPanel;
},
get screen() {
assertStarted()
assertStarted();
return screen;
}
}
@ -58,7 +59,7 @@ export function render(thing?: Renderable) {
export function start() {
assertNotStarted();
process.stdout.write('\x1b[?1049h');
// process.stdout.write('\x1b[?1049h');
screen = blessed.screen({
smartCSR: true,
@ -99,12 +100,12 @@ export function start() {
process.stdout.write(ansi.cursor.hide);
// todo make a real menu
screen.key(['C-c'], function() {
process.stdout.write(ansi.cursor.show);
setTimeout(_ => {
process.exit(0);
})
});
// screen.key(['C-c'], function() {
// process.stdout.write(ansi.cursor.show);
// setTimeout(_ => {
// process.exit(0);
// })
// });
screen.key('f2', () => {
rightPanel.focus();
@ -120,7 +121,8 @@ export function start() {
export function stop() {
screen.destroy();
process.stdout.write('\x1b[?1049l');
// process.stdout.write('\x1b[?1049l');
process.stdout.write(ansi.cursor.show);
}
// move to some debugging shit, idk
@ -145,3 +147,9 @@ ansiTestCard += '{/center}';
export {
ansiTestCard
};
export enum RenderMode {
ONELINE,
DETAILS,
DYNAMIC
}

View File

@ -1,8 +1,8 @@
import { Renderable } from './UI.js';
import { Renderable } from './UI';
import { KeypressAcceptor } from './Menu.js';
export abstract class View implements Renderable, KeypressAcceptor {
abstract render(): void;
abstract render(): string;
abstract keypress(key: { full: string; }): void;
name: string;

View File

@ -2,7 +2,7 @@ import { getTheme } from "@themes";
import { Game } from "../../Game.js";
import { progressbar } from "../../Progressbar.js";
import { PawnDetails } from "../PawnDetails.js";
import { panels } from "../UI.js";
import { panels } from "../UI";
import { View } from "../View.js";
export default class PawnsView extends View {
@ -30,7 +30,7 @@ export default class PawnsView extends View {
let str = '';
if(selected) {
str += ` ${getTheme().bright(` ${pawn.toString()}`)}{|}${pawn.status} \n`;
str += ` ${getTheme().normal('Energy')}{|}${progressbar(pawn.energy / 100, (panels.right.width - 4) / 2)} \n`;
// str += ` ${getTheme().normal('Energy')}{|}${progressbar(pawn.energy / 100, (panels.right.width - 4) / 2)} \n`;
} else {
str += ` ${getTheme().normal(pawn.toString())}{|}${pawn.status} `;
}

View File

@ -0,0 +1,27 @@
import { Game } from "@game";
import { View } from "../View.js";
export default class WorldResourcesView extends View {
constructor() {
super();
this.name = 'World'
}
render(): string {
return `Explored: ${
Game.current.world.distanceExplored.toFixed(3)
} km\n${
Game.current.world.places?.map(place => ` ${
place.placeId
} (${place.x}, ${place.y})\n${
place.resources.map(resourceNode => ` ${
resourceNode.resources.render()
}`)
}`)
}`
}
keypress(key: { full: string; }): void {
if(key.full === 'enter') {
Game.current.world.home.resources[0].request(10);
}
}
}

View File

@ -1,33 +0,0 @@
import { getTheme } from "@themes";
import { Game } from "../../Game.js";
import { Renderable } from "../UI.js";
import { View } from "../View.js";
import { actions } from "@actions";
export class ActionsView extends View {
actionIdx: number = 0;
constructor() {
super();
this.name = 'Actions';
}
keypress(key: {full: string}) {
if(key.full === 'up') {
this.actionIdx --;
} else if (key.full === 'down') {
this.actionIdx ++;
} else if (key.full === 'enter') {
actions[this.actionIdx].invoke(1);
}
}
render() {
return actions.map((action, idx) => `${(() => {
if(this.actionIdx === idx) {
return getTheme().bright(' ' + action.name);
} else {
return getTheme().normal(' ' + action.name);
}
})()}`).join('\n');
}
}

View File

View File

@ -0,0 +1,32 @@
import { Task } from "@tasks"
import { ResourceNode } from "@world";
type Data = {
qty: number,
node: ResourceNode
};
type State = {
workCounter: number
}
export const RESOURCE_COLLECTION_TASK =
new Task<Data, State>('core:resource-collection-task')
.setName('Collect Resources')
.setFunction(((taskState, dTime) => {
}))
.setToString((data, state) => {
return 'Collect ' +
data.qty +
' ' +
(data.qty === 1 ? data.node.resources.item.name.singular :
data.node.resources.item.name.plural) +
' from ' +
data.node.place.place.name;
})
.setInitiate((data: Data) => {
return {
workCounter: 0
}
});

View File

@ -12,13 +12,15 @@
"@actions": ["./src/registries/Actions"],
"@tasks": ["./src/registries/Tasks"],
"@items": ["./src/registries/Items"],
"@ui": ["./src/ui/UI"],
"@world": ["./src/World"],
"@ui": ["./src/qt/index"],
"@game": ["./src/Game"]
},
"noImplicitAny": true
},
"include": [
"src/**/*.ts",
"content/**/*.ts"
"content/**/*.ts",
"ui-engine/**/*.ts"
]
}

55
ui-engine/Node.ts 100644
View File

@ -0,0 +1,55 @@
export abstract class Node {
x: number;
y: number;
w: number;
h: number;
children: Node[]
constructor() {
this.children = [];
}
layout(w: number, h: number) {
this.w = w;
this.h = h;
this.onResize(w, h);
this.children.forEach(node => node.layout(w, h));
}
append(child: Node) {
this.children.push(child);
this.layout(this.w, this.h);
}
abstract render(x: number, y: number): [number, string];
abstract onResize(w: number, h: number): void
}
export class TextNode extends Node {
_content: string;
constructor(content: string) {
super();
this.content = content;
}
set content(val: string) {
this._content = val
this.layout(this.w, this.h);
}
onResize(w: number, h: number): void {
return;
}
render(x: number, y: number): [number, string] {
if(y !== this.y) return null;
if(x < this.x) return null;
if(x > this.x + this.content.length) return null;
return [0, this.content[x - this.x]];
}
}

62
ui-engine/index.ts 100644
View File

@ -0,0 +1,62 @@
import ansi from 'sisteransi';
import { Node, TextNode } from './Node.js';
const TERMINAL_HIGH_BUFFER = '\x1b[?1049h';
const TERMINAL_LOW_BUFFER = '\x1b[?1049l'
class Screen extends Node {
buffer: ArrayBuffer;
bufferView: Uint8Array;
paletteBuffer: ArrayBuffer;
paletteBufferView: Uint8Array;
constructor() {
super();
process.stdout.write(TERMINAL_HIGH_BUFFER);
process.stdout.write(ansi.cursor.hide);
this.layout(process.stdout.columns, process.stdout.rows);
}
onResize(w: number, h: number) {
this.buffer = new ArrayBuffer(w * h);
this.bufferView = new Uint8Array(this.buffer);
this.paletteBuffer = new ArrayBuffer(w * h);
this.paletteBufferView = new Uint8Array(this.paletteBuffer);
}
// paletteAt(x: number, y: number) {
// }
offset(x: number, y: number) {
return y * this.h + x
}
updateTerminal() {
process.stdout.write(ansi.cursor.to(0, 0))
for(let y = 0; y < this.h; y ++) {
for(let x = 0; x < this.w; x ++) {
}
process.stdout.write('\r\n')
}
}
render(x: number, y: number): [number, string] {
throw new Error('Method not implemented.');
}
destroy() {
process.stdout.write(TERMINAL_LOW_BUFFER);
process.stdout.write(ansi.cursor.show);
}
}
// class Palette {
// }
const screen = new Screen();
screen.append(new TextNode('Test'))

3092
yarn.lock

File diff suppressed because it is too large Load Diff