diff --git a/content/core/actions/CoreActions.ts b/content/core/actions/CoreActions.ts index 020d6c0..b590016 100644 --- a/content/core/actions/CoreActions.ts +++ b/content/core/actions/CoreActions.ts @@ -2,16 +2,10 @@ 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 Flint', (qty) => { -// Game.current.board.addTask({ -// taskId: 'core:gather-flint', -// options: {} -// }) -// }); - // registerAction('Gather Slate', (qty) => { // Game.current.board.addTask({ // taskId: 'core:gather-slate', @@ -24,8 +18,11 @@ registerAction('Gather Flint', () => { Game.current.board.addTask(taskState); }); -registerAction('Create Arrowhead', (qty) => { - const rock = new ItemState(FLINT_NORMAL, 1, null); +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 }); diff --git a/content/core/items/CoreItems.ts b/content/core/items/CoreItems.ts index 2dfb41a..483d277 100644 --- a/content/core/items/CoreItems.ts +++ b/content/core/items/CoreItems.ts @@ -1,8 +1,23 @@ import { Item, ItemFilter, ItemProperty, ItemState } from '@items' +class Material { + name: string; + hardness: number; + + setName(name: string) { + this.name = name; + return this; + } + + setHardness(n: number) { + this.hardness = n; + } +} + // #region properties! export const ROCK = new ItemProperty('core:rock') -export const ROCK_HARDNESS = new ItemProperty('core:mohs-hardness') +export const MATERIAL = new ItemProperty('core:material') +export const ROCK_SIZE = new ItemProperty('core:rock-size') export const SEDIMENTARY = new ItemProperty('core:sedimentary') export const IGNEOUS = new ItemProperty('core:igneous') export const METAMORPHIC = new ItemProperty('core:metamorphic') @@ -26,7 +41,12 @@ export const PLANT_FIBRES = new Item() export const FLINT_NORMAL = new Item() .setName("Flint") .setId('core:flint') - .setProperty(ROCK_HARDNESS, 7) + .setProperty(MATERIAL, new Material() + .setName('Flint') + .setHardness(7) + ) + .setProperty(ROCK, true) + .setProperty(IGNEOUS, true) export const SANDSTONE_NORMAL = new Item() .setName("Sandstone") @@ -104,9 +124,13 @@ export const OBSIDIAN_SPEAR = new Item() // #endregion -// export function FILTER_CRAFTABLE_ROCK(item: ItemState) { -// return -// } +export function FILTER_CRAFTABLE_ROCK(itemState: ItemState) { + if(!itemState.item.getProperty(MATERIAL)) return false; + const mat: Material = itemState.item.getProperty(MATERIAL) as Material; + return itemState.item.getProperty(ROCK) + && mat.hardness >= 6 + && mat.hardness < 10 +} // tools: plant fibres = rope, flint hatchet // shale - igneous. metamorphasis => slate \ No newline at end of file diff --git a/content/core/themes/standard.ts b/content/core/themes/standard.ts index 16524ad..b5e7844 100644 --- a/content/core/themes/standard.ts +++ b/content/core/themes/standard.ts @@ -4,5 +4,5 @@ import chalk from 'chalk' registerTheme("default", {}); registerTheme("high contrast", { - selected: chalk.ansi256(250).inverse + bright: chalk.ansi256(250).inverse }); \ No newline at end of file diff --git a/lib/aliases.mjs b/lib/aliases.mjs index 1c7f461..1a1ad90 100644 --- a/lib/aliases.mjs +++ b/lib/aliases.mjs @@ -5,6 +5,7 @@ const moduleAliases = { "@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" }; diff --git a/package.json b/package.json index f17d23f..b848641 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "@types/bonjour": "^3.5.8", "@types/chai": "^4.2.19", "@types/faker": "^5.5.6", + "@types/fs-extra": "^9.0.11", "@types/mocha": "^8.2.2", "@types/uuid": "^8.3.0", + "@types/ws": "^7.4.6", "bonjour": "^3.5.0", "chai": "^4.3.4", "chalk": "^4.1.1", diff --git a/perf.js b/perf.js new file mode 100644 index 0000000..d468025 --- /dev/null +++ b/perf.js @@ -0,0 +1,15 @@ +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') +} \ No newline at end of file diff --git a/src/Game.ts b/src/Game.ts index 25c4472..9772cfd 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -9,7 +9,7 @@ import { render, Renderable, setTitle, start } from './ui/UI.js'; import { ready } from './multiplayer/mDNS.js'; import faker from 'faker'; -let game = null; +let game: Game = null; export class Game extends Frigid implements Tickable, Renderable { pawns: Pawn[] = []; @@ -47,7 +47,7 @@ export class Game extends Frigid implements Tickable, Renderable { }); } - advanceSelection(number) { + advanceSelection(number: number) { let index = this.pawns.indexOf(this.selected); this.selected = this.pawns[Math.min(Math.max(index + number, 0), this.pawns.length - 1)]; } diff --git a/src/Inventory.ts b/src/Inventory.ts index c488101..9c505b3 100644 --- a/src/Inventory.ts +++ b/src/Inventory.ts @@ -42,29 +42,23 @@ export class Inventory extends Serializable implements Renderable { } private reduceInv() { - // TODO deduplicate itemstates... - // use a reduce to reconstruct the array. - // REMEMBER TO MAINTAIN THE OBJECTs! - // dont do immutability to it, as the objects - // may have crossreferences! (maybe) - } + this.items = this.items.reduce((items, itemState) => { - // add(item: Item, qty: number = 1) { - // const id = item.id; - // const existingArr = this.items.filter(itemState => { - // return itemState.itemId === id; - // }); - // let existing: ItemState = null; - // if(existingArr.length === 1) { - // existing = existingArr[0]; - // } - // if(existing) { - // existing.qty += qty; - // } else { - // this.items.push(new ItemState(item, qty, {})); - // } - // Game.current.sync(); - // } + // TODO at some point, be able to merge data items? + + const existing = items.find(testItemState => { + return itemState.itemId === testItemState.itemId + && itemState.data === testItemState.data; + }); + + if(existing) { + existing.qty += itemState.qty; + } else { + items.push(itemState); + } + return items + }, [] as ItemState[]) + } render() { return this.items.map(item => item.render()).join('\n'); diff --git a/src/TaskList.ts b/src/TaskList.ts index a080313..9c4625e 100644 --- a/src/TaskList.ts +++ b/src/TaskList.ts @@ -21,7 +21,7 @@ export class TaskList extends Serializable implements Renderable { this.tasks = [...this.tasks, task]; } - removeTask(task) { + removeTask(task: TaskState) { this.tasks = this.tasks.filter(v => v !== task); } diff --git a/src/Time.ts b/src/Time.ts index f3c9ee2..13c7d88 100644 --- a/src/Time.ts +++ b/src/Time.ts @@ -118,7 +118,7 @@ export default class Time extends Serializable implements Renderable { setTimeout(this.doTick.bind(this), 0); } - advanceTime(seconds) { + advanceTime(seconds: number) { this.minute += seconds / 60; this.normalize() } diff --git a/src/World.ts b/src/World.ts index 0eb8a61..b4c1941 100644 --- a/src/World.ts +++ b/src/World.ts @@ -1,5 +1,14 @@ +import { ItemState } from "@items"; import { Serializable } from "frigid"; export class World extends Serializable { - + +} + +class WorldItemState { + itemState: ItemState + + constructor() { + + } } \ No newline at end of file diff --git a/src/multiplayer/mDNS.ts b/src/multiplayer/mDNS.ts index 26e9bad..2aeeed6 100644 --- a/src/multiplayer/mDNS.ts +++ b/src/multiplayer/mDNS.ts @@ -29,7 +29,7 @@ export type GiftMessage = { export default network; -export async function ready(name) { +export async function ready(name: string) { const port = await getPort({port: getPort.makeRange(52300, 52399)}); mdns.publish({ type: 'dfi', @@ -39,7 +39,7 @@ export async function ready(name) { const wss = new WebSocket.Server({ port }); wss.on('connection', function connection(ws) { ws.on('message', function incoming(message) { - const {pawns: pawnJsons, from} = JSON.parse(message); + const {pawns: pawnJsons, from} = JSON.parse(message.toString()); const pawns = []; for(const pawnJson of pawnJsons) { const pawn: Pawn = Pawn.fromJson(pawnJson); diff --git a/src/registries/Actions.ts b/src/registries/Actions.ts index 7f69640..1707d26 100644 --- a/src/registries/Actions.ts +++ b/src/registries/Actions.ts @@ -3,7 +3,7 @@ import { Renderable } from "../ui/UI.js"; export const actions: Action[] = []; -export function registerAction(name: string, invoke: (qty: number) => void) { +export function registerAction(name: string, invoke: () => void) { console.log('Registered action', name); actions.push(new Action(name, invoke)) } diff --git a/src/registries/Items.ts b/src/registries/Items.ts index 03b3591..d4c8a3a 100644 --- a/src/registries/Items.ts +++ b/src/registries/Items.ts @@ -6,16 +6,14 @@ export type ItemID = string; const items = new Map>(); -export type PropertyValue = number | boolean; - // ITEMS SHALL BE SINGULAR export class Item extends Serializable { name = ''; id: ItemID = ''; - props: Map = new Map(); + props: Map = new Map(); - setName(name) { + setName(name: string) { this.name = name; this.register(false); return this; @@ -35,7 +33,7 @@ export class Item extends Serializable { } setProperty(prop: ItemProperty, value: any) { - this.props[prop.name] = value; + this.props.set(prop.name, value); return this; } @@ -50,6 +48,12 @@ export class ItemState extends Serializable implements Renderable { itemId: ItemID; data: Data; + take(qty: number) { + if(this.qty < qty) throw new Error('cant split more than stack from stack...'); + this.qty -= qty; + return new ItemState(this.item, qty, this.data); + } + get item() { if(!items.has(this.itemId)) throw new Error('unknown item: ' + this.itemId); diff --git a/src/registries/Themes.ts b/src/registries/Themes.ts index a1b17d7..2b08525 100644 --- a/src/registries/Themes.ts +++ b/src/registries/Themes.ts @@ -13,8 +13,9 @@ type StyleFunction = (text: string) => string; export type Theme = { header: StyleFunction, subheader: StyleFunction, + bright: StyleFunction, normal: StyleFunction, - selected: StyleFunction, + dimmed: StyleFunction, hotkey: StyleFunction, tab: { normal: StyleFunction, @@ -42,10 +43,11 @@ export type Theme = { } export const backupTheme: Theme = { - header: chalk.ansi256(255).bold, - subheader: chalk.ansi256(243).bold, - normal: chalk.ansi256(243), - selected: chalk.ansi256(250), + header: chalk.ansi256(255), + subheader: chalk.ansi256(250), + bright: chalk.ansi256(255), + normal: chalk.ansi256(250), + dimmed: chalk.ansi256(245), hotkey: chalk.ansi256(40), tab: { normal: chalk.ansi256(117), @@ -53,7 +55,7 @@ export const backupTheme: Theme = { }, border: { focused: '#ffffff', - normal: '#222222' + normal: '#888888' }, progressBar: { indicator: { diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..b037197 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1 @@ +declare module "neo-blessed"; \ No newline at end of file diff --git a/src/ui/GiftPopup.ts b/src/ui/GiftPopup.ts index 749a64f..f8cf5d6 100644 --- a/src/ui/GiftPopup.ts +++ b/src/ui/GiftPopup.ts @@ -25,7 +25,7 @@ export class GiftPopup { tags: true, ...boxStyle(), }); - this.box.on('keypress', (evt, key) => { + this.box.on('keypress', (evt: {}, key: {full: string}) => { if(key.full === 'enter') { this.send(); } if(key.full === 'escape' || key.full === 'enter') { @@ -69,7 +69,7 @@ export class GiftPopup { this.box.setContent(`${(() => { let pawns = []; for (const [pawn, qty] of this.pawns.entries()) { - const style = i === this.selected ? getTheme().selected : getTheme().normal; + const style = i === this.selected ? getTheme().bright : getTheme().normal; if(qty > 0) { pawns.push(style(`{|}${pawn.toString()} `)) } else { diff --git a/src/ui/Menu.ts b/src/ui/Menu.ts index 41aea90..4290f2c 100644 --- a/src/ui/Menu.ts +++ b/src/ui/Menu.ts @@ -13,7 +13,7 @@ import { View } from './View.js'; import { ActionsView } from './view/ActionsView.js'; import { tasks } from '@tasks'; -const clamp = (min, max, value) => Math.min(Math.max(value, min), max); +const clamp = (min: number, max: number, value: number) => Math.min(Math.max(value, min), max); // TODO move KeypressAcceptor to ui something idk export interface KeypressAcceptor { @@ -46,7 +46,7 @@ export class Menu implements Renderable { } constructor() { - panels.right.on('keypress', (evt, key) => { + panels.right.on('keypress', (evt: {}, key: {full: string}) => { if (key.full === 'left') { this.regressView(); diff --git a/src/ui/PawnDetails.ts b/src/ui/PawnDetails.ts index 2699717..971db0a 100644 --- a/src/ui/PawnDetails.ts +++ b/src/ui/PawnDetails.ts @@ -21,7 +21,7 @@ export class PawnDetails { tags: true, ...boxStyle(), }); - this.box.on('keypress', (evt, key) => { + this.box.on('keypress', (evt: {}, key: {full: string}) => { if(key.full === 'escape' || key.full === 'enter') { Game.current.clock.start(); panels.screen.remove(this.box); diff --git a/src/ui/Popup.ts b/src/ui/Popup.ts index e01ab72..e64534e 100644 --- a/src/ui/Popup.ts +++ b/src/ui/Popup.ts @@ -7,11 +7,11 @@ import { panels } from './UI.js'; export class Popup { box; - static show(content) { + static show(content: string) { new Popup(content) } - private constructor(content) { + private constructor(content: string) { this.box = blessed.box({ top: 'center', left: 'center', @@ -21,7 +21,7 @@ export class Popup { tags: true, ...boxStyle(), }); - this.box.on('keypress', (evt, key) => { + this.box.on('keypress', (evt: {}, key: {full: string}) => { if(key.full === 'escape' || key.full === 'enter') { Game.current.clock.start(); panels.screen.remove(this.box); diff --git a/src/ui/SelectItem.ts b/src/ui/SelectItem.ts new file mode 100644 index 0000000..97493e3 --- /dev/null +++ b/src/ui/SelectItem.ts @@ -0,0 +1,77 @@ +import { Game } from "@game"; +import { ItemState } from "@items"; +import { boxStyle, getTheme } from "@themes"; +import { panels } from "@ui"; +import EventEmitter from "events"; +import blessed from 'neo-blessed'; + +type ItemFilterFunction = (itemState: ItemState) => boolean; + +export class SelectItem { + box: any; + emitter: EventEmitter; + qty: number; + items: ItemState[]; + selectedIdx: number; + + static show(filter: ItemFilterFunction, qty: number = 1): Promise> { + const si = new SelectItem(filter, qty); + return new Promise(res => { + si.emitter.on('selected', (itemState: ItemState) => { + res(itemState); + }); + si.emitter.on('cancel', () => { + res(null); + }) + }); + } + + private open() { + panels.screen.append(this.box); + this.box.focus(); + Game.current.clock.pause(); + } + + private close() { + Game.current.clock.start(); + panels.screen.remove(this.box); + } + + get selectedItem(): ItemState { + return null; + } + + private constructor(filter: ItemFilterFunction, qty: number) { + this.emitter = new EventEmitter(); + this.qty = qty; + this.box = blessed.box({ + top: 'center', + left: 'center', + width: 'shrink', + height: 'shrink', + tags: true, + ...boxStyle(), + }); + this.box.on('keypress', (evt: {}, key: {full: string}) => { + if(key.full === 'escape') { + this.emitter.emit('cancel'); + this.close(); + } else if(key.full === 'enter') { + this.emitter.emit('selected', this.selectedItem.take(this.qty)); + this.close(); + } else if(key.full === 'down') { + + } + }); + this.items = Game.current.inv.items.filter(filter); + this.update(); + this.open(); + } + + + + update() { + this.box.setContent('test'); + panels.screen.render(); + } +} \ No newline at end of file diff --git a/src/ui/UI.ts b/src/ui/UI.ts index 32a028c..d248fd2 100644 --- a/src/ui/UI.ts +++ b/src/ui/UI.ts @@ -1,4 +1,3 @@ - import chalk from 'chalk'; import blessed from 'neo-blessed'; import ansi from 'sisteransi'; @@ -99,7 +98,8 @@ export function start() { process.stdout.write(ansi.cursor.hide); - screen.key(['C-c'], function(ch, key) { + // todo make a real menu + screen.key(['C-c'], function() { process.stdout.write(ansi.cursor.show); setTimeout(_ => { process.exit(0); diff --git a/src/ui/view/ActionsView.ts b/src/ui/view/ActionsView.ts index b53385d..fb0c868 100644 --- a/src/ui/view/ActionsView.ts +++ b/src/ui/view/ActionsView.ts @@ -12,7 +12,7 @@ export class ActionsView extends View { this.name = 'Actions'; } - keypress(key) { + keypress(key: {full: string}) { if(key.full === 'up') { this.actionIdx --; } else if (key.full === 'down') { @@ -24,7 +24,7 @@ export class ActionsView extends View { render() { return actions.map((action, idx) => `${(() => { if(this.actionIdx === idx) { - return getTheme().selected(' ❯ ' + action.name); + return getTheme().bright(' ❯ ' + action.name); } else { return getTheme().normal(' ' + action.name); } diff --git a/src/ui/view/MultiplayerView.ts b/src/ui/view/MultiplayerView.ts index 2bce850..bf62cd6 100644 --- a/src/ui/view/MultiplayerView.ts +++ b/src/ui/view/MultiplayerView.ts @@ -25,7 +25,7 @@ export default class MultiplayerView extends View { render() { if(mdns.players.length === 0) return `{center}${getTheme().normal('No friends online')}{/center}`; return mdns.players.map((player, i) => { - if(i === this.selected) return ' ' + getTheme().selected(' ❯ ' + player.toString()); + if(i === this.selected) return ' ' + getTheme().bright(' ❯ ' + player.toString()); else return ' ' + getTheme().normal(player.toString()); }).join('\n'); }; diff --git a/src/ui/view/PawnsView.ts b/src/ui/view/PawnsView.ts index c9b7c9b..9d8574c 100644 --- a/src/ui/view/PawnsView.ts +++ b/src/ui/view/PawnsView.ts @@ -6,36 +6,36 @@ import { panels } from "../UI.js"; import { View } from "../View.js"; export default class PawnsView extends View { - constructor() { - super(); - this.name = 'Pawns'; - } + constructor() { + super(); + this.name = 'Pawns'; + } - keypress(key: { full: string; }) { - if (key.full === 'delete') { - Game.current.removePawn(Game.current.selected); - } else if (key.full === 'up') { - Game.current.advanceSelection(-1); - } else if (key.full === 'down') { - Game.current.advanceSelection(1); - } else if (key.full === 'enter') { - new PawnDetails(Game.current.selected); - } - } + keypress(key: { full: string; }) { + if (key.full === 'delete') { + Game.current.removePawn(Game.current.selected); + } else if (key.full === 'up') { + Game.current.advanceSelection(-1); + } else if (key.full === 'down') { + Game.current.advanceSelection(1); + } else if (key.full === 'enter') { + new PawnDetails(Game.current.selected); + } + } - render() { - return `${ - Game.current.pawns.map(pawn => `${(function() { - const selected = pawn === Game.current.selected; - let str = ''; - if(selected) { - str += ` ${getTheme().selected(` ❯ ${pawn.toString()}`)}{|}${pawn.status} \n`; - str += ` ${getTheme().normal('Energy')}{|}${progressbar(pawn.energy / 100, (panels.right.width - 4) / 2)} \n`; - } else { - str += ` ${getTheme().normal(pawn.toString())}{|}${pawn.status} `; - } - return str; - })()}`).join('\n') - }`; - } + render() { + return `${ + Game.current.pawns.map(pawn => `${(function() { + const selected = pawn === Game.current.selected; + 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`; + } else { + str += ` ${getTheme().normal(pawn.toString())}{|}${pawn.status} `; + } + return str; + })()}`).join('\n') + }`; + } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 4977fa7..e907747 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,10 @@ "@actions": ["./src/registries/Actions"], "@tasks": ["./src/registries/Tasks"], "@items": ["./src/registries/Items"], + "@ui": ["./src/ui/UI"], "@game": ["./src/Game"] - } + }, + "noImplicitAny": true }, "include": [ "src/**/*.ts", diff --git a/yarn.lock b/yarn.lock index b45ded0..17e473b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,13 @@ version "5.5.6" resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.5.6.tgz#039b700a9d8ad9150ecc842bf5e717e2027b6f75" +"@types/fs-extra@^9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.11.tgz#8cc99e103499eab9f347dbc6ca4e99fb8d2c2b87" + integrity sha512-mZsifGG4QeQ7hlkhO56u7zt/ycBgGxSVsFI/6lGTU34VtwkiqrrSDgw0+ygs8kFGWcXnFQWMrzF2h7TtDFNixA== + dependencies: + "@types/node" "*" + "@types/minimatch@^3.0.3", "@types/minimatch@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" @@ -53,6 +60,13 @@ version "8.3.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" +"@types/ws@^7.4.6": + version "7.4.6" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.6.tgz#c4320845e43d45a7129bb32905e28781c71c1fff" + integrity sha512-ijZ1vzRawI7QoWnTNL8KpHixd2b2XVb9I9HAqI3triPsh1EC0xH0Eg6w2O3TKbDCgiNNlJqfrof6j4T2I+l9vw== + dependencies: + "@types/node" "*" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"