finally fixed colors in blessed!

ui-refactor
Bronwen 2021-06-19 12:40:01 -04:00
parent 3c6986acfe
commit 8e38189560
10 changed files with 391 additions and 352 deletions

View File

@ -1,6 +1,5 @@
import { Serializable } from 'frigid'; import { Serializable } from 'frigid';
import faker from 'faker'; import faker from 'faker';
import chalk from 'chalk';
import log from './log.js'; import log from './log.js';
import { Task } from './tasks/Task.js'; import { Task } from './tasks/Task.js';
import Time, { Tickable } from './Time.js'; import Time, { Tickable } from './Time.js';
@ -10,13 +9,13 @@ import { render } from './ui/UI.js';
import { Memory } from './Memory.js'; import { Memory } from './Memory.js';
const LABORS = { const LABORS = {
CUT_TREE: Symbol('CUT_TREE'), CUT_TREE: Symbol('CUT_TREE'),
MINING: Symbol('CUT_TREE'), MINING: Symbol('CUT_TREE'),
} }
const SKILLS = { const SKILLS = {
PICKAXE: Symbol('PICKAXE'), PICKAXE: Symbol('PICKAXE'),
HATCHET: Symbol('HATCHET') HATCHET: Symbol('HATCHET')
} }
// const STATUS = { // const STATUS = {
@ -26,121 +25,121 @@ const SKILLS = {
const energyScale = 0.1; const energyScale = 0.1;
export class Pawn extends Serializable implements Tickable { export class Pawn extends Serializable implements Tickable {
name: { name: {
first: string, first: string,
last: string last: string
}; };
job: Task; job: Task;
awake: boolean; awake: boolean;
sex: number; sex: number;
energy: number; energy: number;
fun: number; fun: number;
age: number; age: number;
memories: Memory[]; memories: Memory[];
async tick() { async tick() {
this.age ++; this.age ++;
this.energy -= energyScale; this.energy -= energyScale;
if(this.awake === false) { if(this.awake === false) {
this.energy += energyScale * 4; this.energy += energyScale * 4;
if(this.energy >= 100) { if(this.energy >= 100) {
this.awake = true; this.awake = true;
} }
} else { } else {
if(this.job) { if(this.job) {
this.job.doWork(1, this); this.job.doWork(1, this);
this.energy -= energyScale; this.energy -= energyScale;
if(this.job?.completed) { if(this.job?.completed) {
this.stopWorking(); this.stopWorking();
} }
} else { } else {
const inactive = Game.current.board.tasks.filter(task => { const inactive = Game.current.board.tasks.filter(task => {
return task.worker === null; return task.worker === null;
}); });
if(inactive.length > 0) { if(inactive.length > 0) {
const task = inactive[0]; const task = inactive[0];
// const task = inactive[Math.floor(Math.random() * inactive.length)]; // const task = inactive[Math.floor(Math.random() * inactive.length)];
this.assignJob(task); this.assignJob(task);
} }
} }
if(this.energy <= 0) { if(this.energy <= 0) {
this.stopWorking(); this.stopWorking();
this.awake = false; this.awake = false;
} }
} }
} }
get idle() { get idle() {
return !this.job && this.awake; return !this.job && this.awake;
} }
ctor() { ctor() {
log.info('Pawn::ctor') log.info('Pawn::ctor')
this.name ??= { this.name ??= {
first: faker.name.firstName(), first: faker.name.firstName(),
last: faker.name.lastName() last: faker.name.lastName()
}; };
if(!this.sex) { if(!this.sex) {
this.sex = Math.round(Math.random()); this.sex = Math.round(Math.random());
this.name.first = faker.name.firstName(this.sex); this.name.first = faker.name.firstName(this.sex);
} }
this.awake ??= true; this.awake ??= true;
this.energy ??= 100; this.energy ??= 100;
this.memories ??= []; this.memories ??= [];
if(!this.age) { if(!this.age) {
this.age = Math.floor(525600 * (16 + Math.random() * 9)); this.age = Math.floor(525600 * (16 + Math.random() * 9));
this.memories.push({ this.memories.push({
type: "birth", type: "birth",
location: Game.current.name, location: Game.current.name,
time: { time: {
age: 0, age: 0,
locale: new Time(Game.current.clock.stamp - this.age).toString() locale: new Time(Game.current.clock.stamp - this.age).toString()
} }
}) })
} }
if(this.job?.completed) { if(this.job?.completed) {
this.stopWorking(); this.stopWorking();
} }
} }
stopWorking() { stopWorking() {
if(this.job) { if(this.job) {
this.job.stopJob(); this.job.stopJob();
this.job = null; this.job = null;
} }
} }
assignJob(task: Task) { assignJob(task: Task) {
this.job?.stopJob() this.job?.stopJob()
this.job = task; this.job = task;
this.job.claim(this); this.job.claim(this);
} }
get status() { get status() {
if(this.job) { if(this.job) {
return this.job.status; return this.job.status;
} else { } else {
return this.awake ? chalk.bold.black('IDLE') : chalk.blue('RESTING') return this.awake ? chalk.bold.black('IDLE') : chalk.blue('RESTING')
} }
} }
static serializationDependencies() { static serializationDependencies() {
return [Task, ChopTreeTask] return [Task, ChopTreeTask]
} }
toString() { toString() {
if(this.name) { if(this.name) {
return this.name.first + ' ' + this.name.last; return this.name.first + ' ' + this.name.last;
} else { } else {
return '[Object Pawn]'; return '[Object Pawn]';
} }
} }
} }

View File

@ -1,197 +1,198 @@
import chalk from "chalk"; import chalk from "chalk";
import { Serializable } from "frigid"; import { Serializable } from "frigid";
import { isThisTypeNode } from "typescript";
import log from "./log.js";
import { getTheme } from "./ui/Theme.js"; import { getTheme } from "./ui/Theme.js";
import { Renderable } from "./ui/UI.js"; import { Renderable } from "./ui/UI.js";
type AbbreviatedMonthName = string;
const daysInMonth = [ const daysInMonth = [
31, 28, 31, 31, 28, 31,
30, 31, 30, 30, 31, 30,
31, 31, 30, 31, 31, 30,
31, 30, 31 31, 30, 31
]; ];
const months = [ const months: AbbreviatedMonthName[] = [
'Jan', 'Feb', 'Mar', 'Jan', 'Feb', 'Mar',
'Apr', 'May', 'Jun', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec' 'Oct', 'Nov', 'Dec'
] ]
export default class Time extends Serializable implements Renderable { export default class Time extends Serializable implements Renderable {
rate: number; rate: number;
paused = true; paused = true;
thing: Tickable; thing: Tickable;
year: number; year: number;
month: number; month: number;
day: number; day: number;
hour: number; hour: number;
minute: number; minute: number;
constructor(timestamp: number = 0) { constructor(timestamp: number = 0) {
super(); super();
this.minute = timestamp; this.minute = timestamp;
this.normalize(); this.normalize();
} }
asAge() { asAge() {
if(this.year > 1) { if(this.year > 1) {
return this.year + ' years old'; return this.year + ' years old';
} else { } else {
if(this.month > 2) { if(this.month > 2) {
return this.month + ' months old'; return this.month + ' months old';
} else { } else {
if(this.day > 1) { if(this.day > 1) {
return this.day + ' days old'; return this.day + ' days old';
} else if(this.day === 1) { } else if(this.day === 1) {
return '1 day old'; return '1 day old';
} else { } else {
return 'newborn'; return 'newborn';
} }
} }
} }
} }
render() { render() {
const sym = (this.hour >= 6 && this.hour < 20) ? const sym = (this.hour >= 6 && this.hour < 20) ?
chalk.ansi256(226).bgAnsi256(27)(' ') : chalk.ansi256(226).bgAnsi256(27)(' ') :
chalk.ansi256(254).bgAnsi256(17)(' ☾ ') chalk.ansi256(254).bgAnsi256(17)(' ☾ ')
return `${sym} ${ return `${sym} ${
getTheme().normal(`${ getTheme().normal(`${
this.hour.toString().padStart(2, ' ') this.hour.toString().padStart(2, ' ')
}:${ }:${
this.minute.toString().padStart(2, '0') this.minute.toString().padStart(2, '0')
} ${ } ${
months[this.month] months[this.month]
} ${ } ${
this.day + 1 this.day + 1
}, ${ }, ${
this.normalizedYear this.normalizedYear
}`) }`)
}`; }`;
// return '☾' || '☼'; // return '☾' || '☼';
} }
toString() { toString() {
return `${this.hour}:${this.minute.toString().padStart(2, '0')} ${months[this.month]} ${this.day + 1}, ${this.normalizedYear}` return `${this.hour}:${this.minute.toString().padStart(2, '0')} ${months[this.month]} ${this.day + 1}, ${this.normalizedYear}`
} }
ctor() { ctor() {
this.rate = 60; this.rate = 60;
this.minute ??= 0; this.minute ??= 0;
this.hour ??= 0; this.hour ??= 0;
this.day ??= 0; this.day ??= 0;
this.month ??= 0; this.month ??= 0;
this.year ??= 0; this.year ??= 0;
} }
get stamp() { get stamp() {
let minute = this.minute; let minute = this.minute;
let hour = this.hour; let hour = this.hour;
let day = this.day; let day = this.day;
let month = this.month; let month = this.month;
let year = this.year; let year = this.year;
const daysInYear = daysInMonth.reduce((a, b) => a+b, 0); const daysInYear = daysInMonth.reduce((a, b) => a+b, 0);
day += daysInYear * year; day += daysInYear * year;
year = 0; year = 0;
while(month > 0) { while(month > 0) {
day += daysInMonth[month]; day += daysInMonth[month];
month --; month --;
} }
hour += day * 24; hour += day * 24;
day = 0; day = 0;
minute += hour * 60; minute += hour * 60;
hour = 0; hour = 0;
return minute; return minute;
} }
pause() { pause() {
this.paused = true; this.paused = true;
} }
start() { start() {
this.paused = false; this.paused = false;
setTimeout(this.doTick.bind(this), 0); setTimeout(this.doTick.bind(this), 0);
} }
advanceTime(minutes) { advanceTime(minutes) {
this.minute ++; this.minute ++;
this.normalize() this.normalize()
} }
get normalizedYear() { get normalizedYear() {
if(this.year >= 0) { if(this.year >= 0) {
return (this.year + 1).toString().padStart(4, '0') + ' CE'; return (this.year + 1).toString().padStart(4, '0') + ' CE';
} else { } else {
return Math.abs(this.year).toString().padStart(4, '0') + ' BCE'; return Math.abs(this.year).toString().padStart(4, '0') + ' BCE';
} }
} }
normalize() { normalize() {
while(this.minute >= 60) { while(this.minute >= 60) {
this.minute -= 60; this.minute -= 60;
this.hour ++; this.hour ++;
} }
while(this.minute < 0) { while(this.minute < 0) {
this.minute += 60; this.minute += 60;
this.hour --; this.hour --;
} }
while(this.hour >= 24) { while(this.hour >= 24) {
this.hour -= 24; this.hour -= 24;
this.day ++; this.day ++;
} }
while(this.hour < 0) { while(this.hour < 0) {
this.hour += 24; this.hour += 24;
this.day --; this.day --;
} }
while(this.day < 0) { while(this.day < 0) {
this.day += daysInMonth[ this.day += daysInMonth[
((this.month % months.length) + months.length) % months.length ((this.month % months.length) + months.length) % months.length
]; ];
this.month --; this.month --;
} }
while(this.day >= daysInMonth[this.month % months.length]) { while(this.day >= daysInMonth[this.month % months.length]) {
this.day -= daysInMonth[this.month % months.length]; this.day -= daysInMonth[this.month % months.length];
this.month ++; this.month ++;
} }
while(this.month >= 12) { while(this.month >= 12) {
this.month -= 12; this.month -= 12;
this.year ++; this.year ++;
} }
while(this.month < 0) { while(this.month < 0) {
this.month += 12; this.month += 12;
this.year --; this.year --;
} }
} }
async doTick() { async doTick() {
this.advanceTime(1); this.advanceTime(1);
const timeout = 1000 / this.rate; const timeout = 1000 / this.rate;
const start = new Date().getTime(); const start = new Date().getTime();
if(this.thing) { if(this.thing) {
await this.thing.tick(); await this.thing.tick();
} }
const elapsed = new Date().getTime() - start; const elapsed = new Date().getTime() - start;
const wait = Math.max(timeout - elapsed, 0); const wait = Math.max(timeout - elapsed, 0);
if(this.paused) return; if(this.paused) return;
setTimeout(this.doTick.bind(this), wait) setTimeout(this.doTick.bind(this), wait)
} }
} }
export interface Tickable { export interface Tickable {
tick: () => Promise<void> tick: () => Promise<void>
} }

View File

@ -2,29 +2,17 @@ import { Pawn } from '../Pawn.js';
import log from '../log.js'; import log from '../log.js';
import { menuPanel, Renderable } from './UI.js'; import { menuPanel, Renderable } from './UI.js';
import { Game } from '../Game.js'; import { Game } from '../Game.js';
import { ChopTreeTask } from '../tasks/ChopTreeTask.js'; import { progressbar, stats } from '../Progressbar.js';
import { progressbar, stats, barCache } from '../Progressbar.js';
import { Popup } from './Popup.js'; import { Popup } from './Popup.js';
import mdns from '../multiplayer/mDNS.js'; import mdns from '../multiplayer/mDNS.js';
import { GiftPopup } from './GiftPopup.js'; import { getTheme } from './Theme.js';
import { PawnDetails } from './PawnDetails.js';
import { defaultTheme, getTheme } from './Theme.js';
import { inspect } from 'util'; import { inspect } from 'util';
import PawnsView from './view/PawnsView.js'; import PawnsView from './view/PawnsView.js';
import InventoryView from './view/InventoryView.js'; import InventoryView from './view/InventoryView.js';
import MultiplayerView from './view/MultiplayerView.js'; import MultiplayerView from './view/MultiplayerView.js';
import { View } from './View.js';
// TODO extract View const clamp = (min, max, value) => Math.min(Math.max(value, min), max);
export abstract class View implements Renderable, KeypressAcceptor {
abstract render(): void;
abstract keypress(key: {full: string}): void;
static PAWNS: View = new PawnsView();
static INVENTORY: View = new InventoryView();
static MULTIPLAYER: View = new MultiplayerView();
name: string
}
// TODO move KeypressAcceptor to ui something idk // TODO move KeypressAcceptor to ui something idk
export interface KeypressAcceptor { export interface KeypressAcceptor {
@ -34,16 +22,35 @@ export interface KeypressAcceptor {
export class Menu implements Renderable { export class Menu implements Renderable {
trees: number = 10; trees: number = 10;
view: View = View.PAWNS; viewIndex: number = 0;
views: View[] = [
new PawnsView(),
new InventoryView(),
new MultiplayerView()
]
get view() {
return this.views[this.viewIndex];
}
advanceView() {
this.viewIndex ++;
this.viewIndex = clamp(0, this.views.length - 1, this.viewIndex);
}
regressView() {
this.viewIndex --;
this.viewIndex = clamp(0, this.views.length - 1, this.viewIndex);
}
constructor() { constructor() {
menuPanel.on('keypress', (evt, key) => { menuPanel.on('keypress', (evt, key) => {
log.info('keypress', key); log.info('keypress', key);
if (key.full === 'left') { if (key.full === 'left') {
this.view = View[Object.keys(View)[Math.min(Math.max(Object.values(View).indexOf(this.view) - 1, 0), Object.keys(View).length - 1)]] this.regressView();
} else if (key.full === 'right') { } else if (key.full === 'right') {
this.view = View[Object.keys(View)[Math.min(Math.max(Object.values(View).indexOf(this.view) + 1, 0), Object.keys(View).length - 1)]] this.advanceView();
// debugging hotkeys // debugging hotkeys
} else if (key.full === '1') { } else if (key.full === '1') {
@ -116,7 +123,7 @@ export class Menu implements Renderable {
} }
}).join(''); }).join('');
})()}{/center}\n\n${(() => { })()}{/center}\n\n${(() => {
this.view.view.render(); this.view.render();
})()}` })()}`
} }

View File

@ -15,12 +15,15 @@ export class Popup {
this.box = blessed.box({ this.box = blessed.box({
top: 'center', top: 'center',
left: 'center', left: 'center',
width: 'shrink', width: '100%',
height: 'shrink', height: 'shrink',
content: getTheme().normal(content) + `\n\n{|}` + getTheme().hotkey('enter') + getTheme().normal(`: Okay `), // content: getTheme().normal(content) + `\n\n{|}` + getTheme().hotkey('enter') + getTheme().normal(`: Okay `),
tags: true, tags: true,
...boxStyle(), ...boxStyle(),
}); });
let stuff = '';
for(let i = 16; i < 232; i ++) stuff += chalk.bgAnsi256(i).black(` ${i.toString().padStart(3, ' ')} ${(i-15)%18===0?'\n':''}`)
this.box.setContent(stuff)
this.box.on('keypress', (evt, key) => { this.box.on('keypress', (evt, key) => {
if(key.full === 'escape' || key.full === 'enter') { if(key.full === 'escape' || key.full === 'enter') {
Game.current.clock.start(); Game.current.clock.start();

View File

@ -1,77 +1,89 @@
import chalk from "chalk"; // blessed doesnt know QUITE how to deal with 16m color modes
// it will always downsample them to 256. which is fine, but
// blessed's algorithm sucks, and comes out with incorrect
// mappings for certain colors. Instead of dealing with that,
// here, we simply tell chalk to always output ansi256 codes
// instead of upsampling them to 16m codes.
import chalk from 'chalk';
chalk.level = 2;
type StyleFunction = (text: string) => string; type StyleFunction = (text: string) => string;
export type Theme = { export type Theme = {
header: StyleFunction, header: StyleFunction,
subheader: StyleFunction, subheader: StyleFunction,
normal: StyleFunction, normal: StyleFunction,
selected: StyleFunction, selected: StyleFunction,
hotkey: StyleFunction, hotkey: StyleFunction,
tab: { tab: {
normal: StyleFunction, normal: StyleFunction,
selected: StyleFunction selected: StyleFunction
}, },
border: { border: {
focused: string, focused: string,
normal: string normal: string
}, },
progressBar: { progressBar: {
indicator: { indicator: {
critical: StyleFunction, critical: StyleFunction,
warning: StyleFunction, warning: StyleFunction,
normal: StyleFunction, normal: StyleFunction,
excellent: StyleFunction, excellent: StyleFunction,
buckets: [number, number, number] buckets: [number, number, number]
}, },
normal: StyleFunction normal: StyleFunction
} },
status: {
idle: StyleFunction,
self: StyleFunction,
}
} }
export const defaultTheme: Theme = { export const defaultTheme: Theme = {
header: chalk.ansi256(255).bold, header: chalk.ansi256(255).bold,
subheader: chalk.ansi256(243).bold, subheader: chalk.ansi256(243).bold,
normal: chalk.ansi256(243), normal: chalk.ansi256(243),
selected: chalk.ansi256(250), selected: chalk.ansi256(250),
hotkey: chalk.ansi256(40), hotkey: chalk.ansi256(40),
tab: { tab: {
normal: chalk.ansi256(117), normal: chalk.ansi256(117),
selected: chalk.ansi256(117).inverse selected: chalk.ansi256(117).inverse
}, },
border: { border: {
focused: '#ffffff', focused: '#ffffff',
normal: '#222222' normal: '#222222'
}, },
progressBar: { progressBar: {
indicator: { indicator: {
critical: chalk.bgAnsi256(235).ansi256(88), critical: chalk.bgAnsi256(235).ansi256(88),
warning: chalk.bgAnsi256(235).ansi256(202), warning: chalk.bgAnsi256(235).ansi256(202),
normal: chalk.bgAnsi256(235).ansi256(70), normal: chalk.bgAnsi256(235).ansi256(70),
excellent: chalk.bgAnsi256(235).ansi256(87), excellent: chalk.bgAnsi256(235).ansi256(87),
buckets: [.1, .25, .95] buckets: [.1, .25, .95]
}, },
normal: chalk.bgAnsi256(235).ansi256(243) normal: chalk.bgAnsi256(235).ansi256(243)
} }
} }
const debugStyle = chalk.ansi256(213); const debugStyle = chalk.ansi256(213);
export const debugTheme: Theme = { export const debugTheme: Theme = {
header: debugStyle.inverse, header: debugStyle.inverse,
subheader: debugStyle, subheader: debugStyle,
normal: debugStyle, normal: debugStyle,
selected: debugStyle.inverse, selected: debugStyle.inverse,
hotkey: debugStyle, hotkey: debugStyle,
tab: { tab: {
normal: debugStyle, normal: debugStyle,
selected: debugStyle.inverse, selected: debugStyle.inverse,
}, },
border: { border: {
focused: '#ff88ff', focused: '#ff88ff',
normal: '#ff00ff' normal: '#ff00ff'
}, },
progressBar: defaultTheme.progressBar progressBar: defaultTheme.progressBar
} }
export function getTheme(): Theme { export function getTheme(): Theme {
return defaultTheme; return defaultTheme;
} }

9
src/ui/View.ts 100644
View File

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

View File

@ -1,6 +1,10 @@
import { View } from "../Menu"; import { View } from "../View.js";
export default class InventoryView extends View { export default class InventoryView extends View {
constructor() {
super();
this.name = 'Inventory';
}
keypress: (key: { full: string; }) => void; keypress: (key: { full: string; }) => void;
render() { void 0 }; render() { void 0 };
} }

View File

@ -1,7 +1,10 @@
import { KeypressAcceptor, View } from "../Menu"; import { View } from "../View.js";
import { Renderable } from "../UI";
export default class MultiplayerView extends View { export default class MultiplayerView extends View {
constructor() {
super();
this.name = 'Multiplayer';
}
keypress: (key: { full: string; }) => void; keypress: (key: { full: string; }) => void;
render() { void 0 }; render() { void 0 };
} }

View File

@ -1,4 +1,4 @@
import { View } from "../Menu.js"; import { View } from "../View.js";
export default class PawnsView extends View { export default class PawnsView extends View {
constructor() { constructor() {

View File

@ -210,6 +210,7 @@ multicast-dns@^6.0.1:
neo-blessed@^0.2.0: neo-blessed@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/neo-blessed/-/neo-blessed-0.2.0.tgz#30f9495fdd104494402b62c6273a9c9b82de4f2b" resolved "https://registry.yarnpkg.com/neo-blessed/-/neo-blessed-0.2.0.tgz#30f9495fdd104494402b62c6273a9c9b82de4f2b"
integrity sha512-C2kC4K+G2QnNQFXUIxTQvqmrdSIzGTX1ZRKeDW6ChmvPRw8rTkTEJzbEQHiHy06d36PCl/yMOCjquCRV8SpSQw==
object-is@^1.0.1: object-is@^1.0.1:
version "1.1.5" version "1.1.5"