azeroth-shell 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -13
- package/package.json +12 -3
- package/src/cli/term.mjs +33 -15
- package/src/cli/tui.mjs +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# azsh
|
|
2
2
|
|
|
3
|
-
Azeroth shell. A WoW Classic 1.12 text RPG that runs in the terminal, built on
|
|
4
|
-
CMaNGOS Classic data
|
|
5
|
-
no browser, no build step.
|
|
3
|
+
Azeroth shell. A WoW Classic 1.12 text RPG that runs in the terminal, built on
|
|
4
|
+
CMaNGOS Classic data. The engine comes from another project of mine that is not released; this is its
|
|
5
|
+
terminal front end. No graphics, no browser, no build step.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
npx azeroth-shell # run it straight from npm
|
|
9
9
|
npm install -g azeroth-shell # or install it once, then:
|
|
10
|
-
|
|
10
|
+
azeroth-shell # pick a character or roll one (azsh for short)
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
From a checkout: `npm install -g .` or `node bin/azsh.mjs`.
|
|
@@ -114,17 +114,17 @@ Saves live in `~/.azsh/saves/`, one JSON file per character. `AZSH_HOME` moves t
|
|
|
114
114
|
|
|
115
115
|
## Where it comes from
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
The engine, rules, world, quests, autopilot and data are taken as-is from a larger, unreleased project
|
|
118
|
+
of mine that runs the same game with a graphical front end. The data is the CMaNGOS Classic world
|
|
119
|
+
database plus a DBC bake, every table gzipped on disk (`src/cli/gzLoader.mjs` serves the static
|
|
120
|
+
imports). What is new here is the terminal front end in `src/cli/`: `tui.mjs` (frame, input, dialogs),
|
|
121
|
+
`tuiViews.mjs` (tabs), `tuiProfessions.mjs` and `tuiCompanions.mjs` (crafting, companions),
|
|
122
|
+
`tuiFight.mjs` (live fight, autopilot) and `repl.mjs` (line game and the shared verb table). A few
|
|
123
|
+
small UI modules (combat log wording, rotation sentences, dungeon floor scripts) are ported from the
|
|
124
|
+
same project.
|
|
125
125
|
|
|
126
126
|
Everything the player reads traces to a CMaNGOS row or a 1.12 client string. Nothing here is
|
|
127
|
-
invented
|
|
127
|
+
invented.
|
|
128
128
|
|
|
129
129
|
## Checks
|
|
130
130
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azeroth-shell",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Azeroth shell. A WoW Classic 1.12 text RPG for the terminal, built on CMaNGOS data.
|
|
6
|
-
"keywords": [
|
|
5
|
+
"description": "Azeroth shell. A WoW Classic 1.12 text RPG for the terminal, built on CMaNGOS data.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"wow",
|
|
8
|
+
"classic",
|
|
9
|
+
"rpg",
|
|
10
|
+
"terminal",
|
|
11
|
+
"game",
|
|
12
|
+
"tui",
|
|
13
|
+
"cmangos"
|
|
14
|
+
],
|
|
7
15
|
"bin": {
|
|
16
|
+
"azeroth-shell": "bin/azsh.mjs",
|
|
8
17
|
"azsh": "bin/azsh.mjs"
|
|
9
18
|
},
|
|
10
19
|
"files": [
|
package/src/cli/term.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { stdin, stdout } from 'node:process';
|
|
|
9
9
|
import { fit } from './text.mjs';
|
|
10
10
|
|
|
11
11
|
const ESC = '\x1b[';
|
|
12
|
+
const MIN_COLS = 60, MIN_ROWS = 20;
|
|
12
13
|
|
|
13
14
|
const CSI_FINAL = { A: 'up', B: 'down', C: 'right', D: 'left', H: 'home', F: 'end', Z: 'shift-tab', P: 'f1', Q: 'f2', R: 'f3', S: 'f4' };
|
|
14
15
|
const CSI_TILDE = { 1: 'home', 2: 'insert', 3: 'delete', 4: 'end', 5: 'pageup', 6: 'pagedown', 7: 'home', 8: 'end', 11: 'f1', 12: 'f2', 13: 'f3', 14: 'f4', 15: 'f5', 17: 'f6', 18: 'f7', 19: 'f8', 20: 'f9', 21: 'f10', 23: 'f11', 24: 'f12' };
|
|
@@ -64,40 +65,57 @@ export class RealTerm {
|
|
|
64
65
|
this.isTTY = stdout.isTTY === true && stdin.isTTY === true;
|
|
65
66
|
this.last = [];
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
get
|
|
68
|
+
/** Layout size: never below the minimum the screens are laid out for (see `tooSmall`). */
|
|
69
|
+
get cols() { return Math.max(MIN_COLS, stdout.columns || 100); }
|
|
70
|
+
get rows() { return Math.max(MIN_ROWS, stdout.rows || 32); }
|
|
71
|
+
get tooSmall() { return (stdout.columns || 100) < MIN_COLS || (stdout.rows || 32) < MIN_ROWS; }
|
|
69
72
|
open() {
|
|
70
73
|
if (stdin.isTTY) stdin.setRawMode(true);
|
|
71
74
|
stdin.setEncoding('utf8');
|
|
72
75
|
stdin.resume();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
// Autowrap off: a line a terminal measures wider than we do (an ambiguous-width glyph) is clipped
|
|
77
|
+
// instead of wrapping and scrolling the whole frame out of step with `last`.
|
|
78
|
+
stdout.write(`${ESC}?1049h${ESC}?25l${ESC}?7l${ESC}H${ESC}2J`);
|
|
79
|
+
// Ctrl-L forgets what is on screen and repaints everything, to recover from a terminal that moved it.
|
|
80
|
+
this._data = (chunk) => { for (const k of decodeKeys(chunk)) { if (k === 'ctrl-l') this.refresh(); else this.onKey(k); } };
|
|
81
|
+
this._resize = () => this.refresh();
|
|
76
82
|
stdin.on('data', this._data);
|
|
77
83
|
stdout.on('resize', this._resize);
|
|
78
84
|
}
|
|
79
85
|
close() {
|
|
80
86
|
stdin.off('data', this._data);
|
|
81
87
|
stdout.off('resize', this._resize);
|
|
82
|
-
stdout.write(`${ESC}?25h${ESC}?1049l`);
|
|
88
|
+
stdout.write(`${ESC}0m${ESC}?7h${ESC}?25h${ESC}?1049l`);
|
|
83
89
|
if (stdin.isTTY) stdin.setRawMode(false);
|
|
84
90
|
stdin.pause();
|
|
85
91
|
}
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
refresh() { this.last = []; this.onResize(); }
|
|
93
|
+
/** Paint a whole frame. Lines are fitted to the width; unchanged lines are skipped, unless most of the frame
|
|
94
|
+
* changed (a new screen), which repaints it all so a stale row cannot outlive a screen change. The last
|
|
95
|
+
* column stays blank so a terminal without deferred wrap never scrolls when the bottom row is painted. */
|
|
88
96
|
draw(lines) {
|
|
97
|
+
if (this.tooSmall) { this.drawTooSmall(); return; }
|
|
89
98
|
const w = this.cols - 1, h = this.rows;
|
|
90
|
-
let out = '';
|
|
91
99
|
const next = [];
|
|
92
|
-
for (let i = 0; i < h; i++)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
100
|
+
for (let i = 0; i < h; i++) next.push(fit(lines[i] ?? '', w));
|
|
101
|
+
const changed = next.filter((line, i) => this.last[i] !== line).length;
|
|
102
|
+
const full = !this.last.length || changed * 2 > h;
|
|
103
|
+
// Every row is padded to the width, so only a frame with nothing known on screen needs a clear first.
|
|
104
|
+
let out = this.last.length ? '' : `${ESC}0m${ESC}H${ESC}2J`;
|
|
105
|
+
next.forEach((line, i) => { if (full || this.last[i] !== line) out += `${ESC}${i + 1};1H${ESC}0m${line}`; });
|
|
98
106
|
this.last = next;
|
|
99
107
|
if (out) stdout.write(out);
|
|
100
108
|
}
|
|
109
|
+
/** The screens need MIN_COLS x MIN_ROWS; a smaller terminal gets a notice instead of a frame drawn off its edge. */
|
|
110
|
+
drawTooSmall() {
|
|
111
|
+
const c = stdout.columns || 100, r = stdout.rows || 32;
|
|
112
|
+
const msg = [`Terminal is ${c}x${r}.`, `azsh needs at least ${MIN_COLS}x${MIN_ROWS}.`, 'Make the window larger.'];
|
|
113
|
+
const top = Math.max(0, Math.floor((r - msg.length) / 2));
|
|
114
|
+
let out = `${ESC}0m${ESC}H${ESC}2J`;
|
|
115
|
+
msg.forEach((m, i) => { if (top + i < r) out += `${ESC}${top + i + 1};${Math.max(1, Math.floor((c - m.length) / 2) + 1)}H${m.slice(0, c)}`; });
|
|
116
|
+
this.last = [];
|
|
117
|
+
stdout.write(out);
|
|
118
|
+
}
|
|
101
119
|
/** Delay helper for real-time loops. */
|
|
102
120
|
setInterval(fn, ms) { return setInterval(fn, ms); }
|
|
103
121
|
clearInterval(h) { clearInterval(h); }
|
package/src/cli/tui.mjs
CHANGED
|
@@ -46,7 +46,7 @@ const HELP_LINES = [
|
|
|
46
46
|
' s (World tab) find a thing in the world: who sells an item and where, what drops it, where an',
|
|
47
47
|
' ore or herb grows, a dungeon entrance, an NPC. Enter travels there or opens it.',
|
|
48
48
|
' z sit and rest 30 s ? this help',
|
|
49
|
-
' Ctrl-C / :quit save and leave',
|
|
49
|
+
' Ctrl-C / :quit save and leave Ctrl-L repaint a garbled screen',
|
|
50
50
|
'',
|
|
51
51
|
'In the world',
|
|
52
52
|
' Enter on a creature starts a live fight; f fights it instantly. Enter on a quest, vendor, trainer or',
|