azeroth-shell 0.1.1 → 0.2.0

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.
Files changed (66) hide show
  1. package/README.md +326 -82
  2. package/bin/azsh.mjs +21 -19
  3. package/package.json +19 -1
  4. package/src/autopilot/plan.ts +8 -11
  5. package/src/autopilot/view.ts +12 -11
  6. package/src/cli/combatLogFormat.ts +22 -0
  7. package/src/cli/fileRepository.mjs +46 -1
  8. package/src/cli/gzLoader.mjs +47 -6
  9. package/src/cli/hdArt.mjs +81 -0
  10. package/src/cli/iconArt.mjs +135 -0
  11. package/src/cli/iconGrid.mjs +100 -0
  12. package/src/cli/modelArt.mjs +295 -0
  13. package/src/cli/paperdoll.mjs +76 -0
  14. package/src/cli/perfCheck.mjs +56 -0
  15. package/src/cli/repl.mjs +36 -7
  16. package/src/cli/rotationText.mjs +6 -6
  17. package/src/cli/sixel.mjs +307 -0
  18. package/src/cli/term.mjs +106 -12
  19. package/src/cli/text.mjs +5 -3
  20. package/src/cli/tui.mjs +252 -98
  21. package/src/cli/tuiAutopilot.mjs +347 -0
  22. package/src/cli/tuiBg.mjs +371 -42
  23. package/src/cli/tuiCheck.mjs +415 -28
  24. package/src/cli/tuiCity.mjs +118 -13
  25. package/src/cli/tuiCompanions.mjs +25 -15
  26. package/src/cli/tuiCreate.mjs +223 -0
  27. package/src/cli/tuiDungeon.mjs +1 -1
  28. package/src/cli/tuiFight.mjs +425 -88
  29. package/src/cli/tuiFightPanes.mjs +27 -8
  30. package/src/cli/tuiFloor.mjs +2 -2
  31. package/src/cli/tuiGear.mjs +10 -6
  32. package/src/cli/tuiLoot.mjs +1 -0
  33. package/src/cli/tuiPaperdoll.mjs +107 -0
  34. package/src/cli/tuiPartyStage.mjs +177 -0
  35. package/src/cli/tuiProfessions.mjs +4 -2
  36. package/src/cli/tuiRotationCond.mjs +390 -0
  37. package/src/cli/tuiSaves.mjs +22 -3
  38. package/src/cli/tuiSearch.mjs +75 -22
  39. package/src/cli/tuiSettings.mjs +15 -0
  40. package/src/cli/tuiSimLab.mjs +1 -1
  41. package/src/cli/tuiStage.mjs +304 -0
  42. package/src/cli/tuiTalentTree.mjs +124 -0
  43. package/src/cli/tuiViews.mjs +296 -115
  44. package/src/cli/tuiWorld.mjs +4 -5
  45. package/src/data/gear-art.bin.br +0 -0
  46. package/src/data/hd-art.json +114 -0
  47. package/src/data/icon-art.bin.gz +0 -0
  48. package/src/data/model-art.bin.gz +0 -0
  49. package/src/data/rotations/pve/paladin/retribution.json +2 -2
  50. package/src/domain/bg/match.ts +85 -24
  51. package/src/domain/bg/strategies.ts +144 -29
  52. package/src/domain/bg/types.ts +2 -0
  53. package/src/domain/bg/wsg.ts +35 -20
  54. package/src/domain/quests/doQuest.ts +24 -6
  55. package/src/domain/rotation/build.ts +3031 -3031
  56. package/src/domain/rotation/classes/paladin.ts +16 -11
  57. package/src/domain/rotation/evaluate.ts +3 -0
  58. package/src/engine/combat/applySpell.ts +20 -9
  59. package/src/engine/combat/applySpellLate.ts +16 -12
  60. package/src/engine/combat/areaAura.ts +284 -283
  61. package/src/engine/combat/fighterFactory.ts +1 -1
  62. package/src/engine/combat/pickAction.ts +39 -1
  63. package/src/engine/combat/procsDamage.ts +31 -1
  64. package/src/engine/combat/sessionChase.ts +7 -1
  65. package/src/engine/combat/sessionMotionKit.ts +40 -2
  66. package/src/quests/quests.ts +60 -0
package/README.md CHANGED
@@ -1,10 +1,28 @@
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. 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.
3
+ Azeroth shell. A WoW Classic 1.12 text RPG that runs in the terminal, built on CMaNGOS Classic
4
+ data. The engine comes from another project of mine that is not released; this is its terminal
5
+ front end. No graphics, no browser, no build step.
6
6
 
7
- ```
7
+ > Personal, non-commercial fan project. World of Warcraft names, models, textures, icons and data are
8
+ > trademarks and copyrights of Blizzard Entertainment or its licensors. Classic-DB is part of the
9
+ > CMaNGOS Project; neither it nor this project is affiliated with or endorsed by Blizzard.
10
+
11
+ ![A live fight: a level 1 mage against a kobold on the stage, the rotation casting, the combat log streaming below](media/fight.gif)
12
+
13
+ - [Install](#install)
14
+ - [Keys](#keys)
15
+ - [Playing](#playing): [character](#new-character) · [world and autopilot](#world-and-the-autopilot) ·
16
+ [fights](#fights) · [rotation](#rotation) · [gear](#gear-and-bags) · [vendors, trainers, talents](#vendors-trainers-talents) ·
17
+ [professions](#professions) · [companions](#companions) · [dungeons](#dungeons) · [city services](#city-services)
18
+ - [Art and terminals](#art-and-terminals)
19
+ - [Line mode and saves](#line-mode-and-saves)
20
+ - [Where it comes from](#where-it-comes-from)
21
+ - [Development](#development)
22
+
23
+ ## Install
24
+
25
+ ```sh
8
26
  npx azeroth-shell # run it straight from npm
9
27
  npm install -g azeroth-shell # or install it once, then:
10
28
  azeroth-shell # pick a character or roll one (azsh for short)
@@ -13,13 +31,14 @@ azeroth-shell # pick a character or roll one (azsh for short)
13
31
  From a checkout: `npm install -g .` or `node bin/azsh.mjs`.
14
32
 
15
33
  Needs Node 22.18 or newer (it strips TypeScript types natively). No dependencies. The tree is
16
- about 29 MB: 15 MB of gzipped game data and 13 MB of engine source.
34
+ about 41 MB: 15 MB of gzipped game data, 12 MB of model art (creatures, and every item on every
35
+ body) and 13 MB of engine source.
17
36
 
18
- ## Playing
37
+ ## Keys
19
38
 
20
- In a terminal `azsh` opens the windowed game: a header with your pools and gold, eleven tabs, a
21
- cursor-driven list with a detail pane beside it, and a scrolling log underneath. Nothing scrolls
22
- off the top; every screen redraws in place.
39
+ `azsh` opens the windowed game: a header with your pools and gold, eleven tabs, a cursor-driven
40
+ list with a detail pane beside it, and a scrolling log underneath. Nothing scrolls off the top;
41
+ every screen redraws in place.
23
42
 
24
43
  | Key | Does |
25
44
  | --- | --- |
@@ -33,107 +52,332 @@ off the top; every screen redraws in place.
33
52
  | `:` | the command line: every verb of the line game (`:rest 120`, `:auto 50 farm kobold`, `:help`) |
34
53
  | `?` | the key list |
35
54
 
55
+ Each screen below has its own keys; they are listed with it.
56
+
57
+ ![The tabs, one after another: Quests, Bags, Gear, Spells, Rotation, Travel, Professions, Companions, Character](media/tabs.gif)
58
+
59
+ ## Playing
60
+
61
+ ### New character
62
+
63
+ **New character** is one form: Race, Class, Gender, Name, then the body's own looks (skin, face,
64
+ hair style and colour, facial hair; horns and tusks where the race has them). The hero stands
65
+ beside the form in the starting gear as you change them, and under the form a card says what the
66
+ race and the class are and what a level 1 starts with: pools, attributes, racials, spells and gear.
67
+
68
+ | Key | Does |
69
+ | --- | --- |
70
+ | `Left` `Right` | change the row under the cursor |
71
+ | `Enter` on Race or Class | open the list with each choice drawn |
72
+ | `Enter` on Name | type a name |
73
+ | `n` | roll a name from the world's name pool |
74
+ | `r` | roll a look |
75
+ | `Esc` | back to the character list |
76
+
77
+ **Random character** rolls a world bot instead, at its own level, geared and named.
78
+
79
+ ![The create form: races and classes cycled with the hero redrawn each time, a name typed, a look rolled, Create](media/create.gif)
80
+
81
+ ### World and the autopilot
82
+
36
83
  **World** lists the quests on offer (`!` when you can take one, `?` when one is ready to hand in),
37
84
  the people here, the exits and every creature you can hunt, coloured by level like the client.
38
- `Enter` on a creature starts a live fight; `f` resolves it instantly; `a` starts the autopilot.
85
+
86
+ | Key | Does |
87
+ | --- | --- |
88
+ | `Enter` on a creature | start a live fight |
89
+ | `f` | resolve the fight instantly |
90
+ | `a` | start the autopilot |
91
+
39
92
  The autopilot accepts quests, picks targets, rests and hands in on its own, one planner step every
40
- third of a second, and every fight it picks plays out live on screen with the same frames and
41
- combat log as a fight you started yourself. Between fights it keeps its own panel (your frame and
42
- what it is doing) instead of flashing back to the tabs; `1`-`9` looks at a tab, `w` goes back to
43
- watching. `Esc` stops it; `Esc` during one of its fights runs away and stops it.
93
+ third of a second. Every fight it picks plays out live on screen with the same frames and combat
94
+ log as a fight you started yourself. Between fights it keeps its own panel (your frame and what it
95
+ is doing) instead of flashing back to the tabs.
96
+
97
+ | Key while the autopilot runs | Does |
98
+ | --- | --- |
99
+ | `1`-`9` | look at a tab |
100
+ | `w` | back to watching; in a fight, take the keys so `1`-`0` press the action bar (`w` or `Esc` hands them back) |
101
+ | `Esc` | stop it; during one of its fights, run away and stop it |
102
+
103
+ ![The autopilot: the pre-flight card, then the run panel saying what it is doing, with its fights played live inside it](media/autopilot.gif)
44
104
 
45
- **Fights** run on the engine's real-time session, ticking every 100 ms: unit frames with health,
46
- power, a fixed cast slot and auras that hold their place. Your rotation plays by default; press a
47
- number to fire that ability over it, `a` to take full control (the numbered ability bar shows in
48
- manual play), `Space` to pause, `+`/`-` to run the fight faster or slower, `Esc` to run away. The 1.12 combat log streams into the log pane.
105
+ ### Fights
106
+
107
+ Fights run on the engine's real-time session, ticking every 100 ms, on a stage: your sprite down
108
+ the left, the foe down the right (its size says its rank). Each has a plate under its feet (name,
109
+ level, class or rank, health, power), its cast or crowd control under that and its aura icons under
110
+ that. The foe stands at its real distance (the engine keeps positions in yards): a melee foe closes
111
+ in, a caster stays at range.
112
+
113
+ Hits rise from the chest, an auto-attack swing lunges the sprite, and a missile (a Fireball, an
114
+ arrow; never a strike) crosses the gap as a glyph. Your cast bar sits centred under the floor and
115
+ the keyed ability bar closes the scene; its slots never move, a cooldown or the cast in progress is
116
+ all they say, and the slot of a spell just used flashes. The 1.12 combat log streams into the log pane.
117
+
118
+ A party fight (companions along, a dungeon pull) is the same stage with every body small: the
119
+ party down the left (you in your gear, the companions in theirs, the pets a size under), the pack
120
+ down the right, and a narrow plate under each sprite with its role letter and name, its health as
121
+ a percent, its power, and under that its cast bar, crowd control or "dead". Each column stands by
122
+ its real distance to the other side, from the far edge to the lane at melee contact, and the
123
+ closing goes to whoever walked it: the healer hangs back, a mage walks into range, the tank runs
124
+ in, and a mob that runs onto the healer crosses the lane while the healer stays put. The sprites
125
+ take the widest box the terminal affords and shrink with the crowd; past the smallest the floor
126
+ counts who is out of frame. Terminals under 90 columns keep the list of unit frames.
127
+
128
+ Your rotation plays by default.
129
+
130
+ | Key | Does |
131
+ | --- | --- |
132
+ | `1`-`0` | fire that ability over the rotation |
133
+ | `a` | take full control |
134
+ | `Space` | pause |
135
+ | `+` `-` | run the fight faster or slower |
136
+ | `t` | (party fight) pause and give a companion an order: attack a target, cast an ability, hold ground |
137
+ | `p` | (dungeon) pull the next trash pack in too |
138
+ | `Esc` | run away |
139
+
140
+ ### Rotation
49
141
 
50
142
  **Rotation** is what auto-play casts for you: the same rule list the web editor edits, phase by
51
143
  phase, every rule as a sentence. Rules are read top to bottom each tick and the first one whose
52
- conditions hold fires. `Enter` on a rule opens its menu (turn on or off, change when it fires,
53
- move, delete); `Enter` on `+ Add a rule` picks a spell or combat item; the Settings rows at the
54
- bottom pick a spec or rebuild the recommended list. `K`/`J`, `a`, `c`, `d`, `s`, `r` still work as
55
- shortcuts.
144
+ conditions hold fires.
145
+
146
+ | Key | Does |
147
+ | --- | --- |
148
+ | `Enter` on a rule | its menu: turn on or off, change when it fires, move, delete |
149
+ | `Enter` on `+ Add a rule` | pick a spell or combat item |
150
+ | Settings rows (bottom) | pick a spec or rebuild the recommended list |
151
+ | `K` `J` | move the rule up or down |
152
+ | `a`, `c`, `d` | add a rule, edit the condition, delete |
153
+ | `s`, `r` | pick a spec, rebuild the recommended list |
154
+ | `w` | whose rotation: yours or a companion's |
155
+ | `p` | rotation profiles: select, add, rename, delete |
156
+ | `D` | target dummy: run the rotation against a dummy |
157
+ | `A` | autopilot settings |
158
+
159
+ ### Gear and Bags
160
+
161
+ **Gear** shows every slot with what you wear. `Enter` on a slot lists everything in your bags (and
162
+ in the bank, in a bank town) that fits it, with the stat, armor and damage difference in the detail
163
+ pane; `Enter` there puts it on. The engine's equipment helper scores your bags against what you
164
+ wear: upgrades are listed at the top of the tab and marked `▲` in Bags. From 160 columns the tab is
165
+ a character sheet: you in your gear in the middle, the worn pieces' icons down both sides, Shift+arrows
166
+ walking the slots.
167
+
168
+ ![The Gear tab as a character sheet: a level 60 shaman in raid gear, the slots' icons on both sides, the cursor walking them](media/gear.gif)
169
+
170
+ | Key | Does |
171
+ | --- | --- |
172
+ | `Enter` on a slot | list what fits, then `Enter` to equip |
173
+ | `u` (Gear or Bags) | put on every upgrade at once |
174
+ | `x` | take a piece off |
175
+ | `b` (Bags) | open the bank, in a bank town |
176
+ | `g` (Bags) | open Send goods (see [Professions](#professions)) |
177
+
178
+ ### Vendors, trainers, talents
179
+
180
+ **Vendors** open in a buy list with prices, stock and why a line is unavailable. A reagent your
181
+ party crafts with is flagged.
182
+
183
+ | Key | Does |
184
+ | --- | --- |
185
+ | `Tab` | flip to selling your bags |
186
+ | `j` | sell greys |
187
+ | `r` | repair |
188
+
189
+ **Trainers** show what you can learn now and what is coming up at which level.
190
+ **Talents** spend and refund points per tree. From 160 columns the tab draws the three trees side by
191
+ side as icon grids with their prerequisite arrows, Shift+arrows walking the tiers.
192
+
193
+ ![The Talents tab: Elemental, Enhancement and Restoration drawn as the client's trees, a talent's text in the pane beside them](media/talents.gif)
56
194
 
57
- **Vendors** open in a buy list with prices, stock and why a line is unavailable; `Tab` flips to
58
- selling your bags, `j` sells greys, `r` repairs; a reagent your party crafts with is flagged.
59
- **Trainers** show what you can learn now and what is coming up at which level. **Talents** spend
60
- and refund points per tree.
195
+ ### Professions
61
196
 
62
197
  **Professions** lists your trade skills and your companions', the profession trainers in the zone
63
198
  and any Plans or Patterns in your bags. `Enter` on a profession opens its recipe book, coloured by
64
- difficulty (orange always raises the skill, gray never): `Enter` makes one, `A` makes all you can,
65
- `N` asks how many, `h` shows only what you can make, `/` searches, and `c` hands the book to a
66
- companion who knows the craft (they craft from the whole party's bags and banks). Short vendor
67
- reagents such as thread, vials and flux are bought from your purse as you craft, the cost shown on
68
- the row. Enchanting opens a bench instead: formulas apply to your gear or a party member's, and
69
- green-or-better bag items can be disenchanted. Mining, Herbalism, Skinning and Fishing list the
70
- zone's nodes (also on the World tab) with `Enter` to gather. *Everything the party can make* is one
71
- book across every crafter. `g` here, in Bags or in Companions opens **Send goods**: trade goods go
72
- to the companions whose recipes use them and upgrades go on whoever gains, after you review it.
199
+ difficulty (orange always raises the skill, gray never). Short vendor reagents such as thread,
200
+ vials and flux are bought from your purse as you craft, the cost shown on the row.
201
+
202
+ | Key in a recipe book | Does |
203
+ | --- | --- |
204
+ | `Enter` | make one |
205
+ | `A` | make all you can |
206
+ | `N` | ask how many |
207
+ | `h` | show only what you can make |
208
+ | `/` | search |
209
+ | `c` | hand the book to a companion who knows the craft (they craft from the whole party's bags and banks) |
210
+
211
+ ![The Professions tab, then an Engineering recipe book: every recipe with its icon, coloured by difficulty, what is missing in red](media/professions.gif)
212
+
213
+ - **Enchanting** opens a bench instead: formulas apply to your gear or a party member's, and
214
+ green-or-better bag items can be disenchanted.
215
+ - **Mining, Herbalism, Skinning and Fishing** list the zone's nodes (also on the World tab) with
216
+ `Enter` to gather.
217
+ - **Everything the party can make** is one book across every crafter.
218
+ - **Send goods** (`g` here, in Bags or in Companions): trade goods go to the companions whose
219
+ recipes use them and upgrades go on whoever gains, after you review it.
220
+
221
+ ### Companions
73
222
 
74
223
  **Companions** shows your party (up to four) and bench. At an inn, *Hire a companion* lists the
75
- adventurers for hire with their gear and price. Each companion's page sets their role, seats or
76
- benches them, gears them (`Enter` on a slot picks from their bags and yours), gives and takes
77
- items, teaches them a profession and opens their recipe book or sends them on a gathering trip
78
- that pays out while you play. The *Fights* row (`m`, or `:squad solo|elites|party`) says who comes along:
79
- Solo, companions only against elites, or the whole party on every pull. That rule covers live fights, `f`,
80
- `:fight` and the autopilot; party frames show each companion's role, target and cast, and `t` in a fight
81
- pauses it to give one of them an order (attack a target, cast an ability, hold ground).
224
+ adventurers for hire with their gear and price.
225
+
226
+ Each companion's page sets their role, seats or benches them, gears them (`Enter` on a slot picks
227
+ from their bags and yours), gives and takes items, teaches them a profession and opens their recipe
228
+ book, or sends them on a gathering trip that pays out while you play.
229
+
230
+ The *Fights* row (`m`, or `:squad solo|elites|party`) says who comes along: Solo, companions only
231
+ against elites, or the whole party on every pull. That rule covers live fights, `f`, `:fight` and
232
+ the autopilot. Party frames show each companion's role, target and cast, and `t` in a fight pauses
233
+ it to give one of them an order.
234
+
235
+ ### Dungeons
82
236
 
83
237
  **Dungeons** sit under *Dungeons* on the World tab of the zone that holds their door (and on the
84
- Travel tab), with any held run or raid lockout on the row. `Enter` opens the card before the door: route
85
- (Bosses or Full), party or raid, who walks in (`f` fills from every companion you own to what the
86
- instance wants), the 10/20/40-man raid group, the paladins' Blessings, and food and water for everyone
87
- short of it. Inside, the run window lists the rooms in route order with their stops (`B` boss, `T` trash,
88
- `R` rare), the doors, levers and chests nearest each stop, and the NPCs with quests. `Enter` on an
89
- open stop pulls it live with the party (`p` in the fight pulls the next trash pack in too), on a gate
90
- uses it, on a chest opens it. Locked stops say what opens them. `z` rests and eats between pulls,
91
- `F` runs every remaining stop without watching and ends on a run report (damage, healing, loot, wipes),
92
- `P` opens the Blessings board, `L` leaves (a 5-man is held for 30 minutes) and `R` resets.
93
-
94
- **Gear** shows every slot with what you wear. `Enter` on a slot lists everything in your bags
95
- (and in the bank, in a bank town) that fits it, with the stat, armor and damage difference in
96
- the detail pane; `Enter` there puts it on. The engine's equipment helper scores your bags
97
- against what you wear: upgrades are listed at the top of the tab and marked `▲` in Bags, and
98
- `u` (on either tab) puts on every upgrade at once. `x` takes a piece off.
99
-
100
- **City services** appear under *Services* on the World tab, above the people: the **Bank**
101
- in the six capitals and the goblin banker towns (`b` in Bags opens it too; `Tab` flips between
102
- depositing and withdrawing, `n` moves part of a stack, `+` buys a bank bag slot, `B` slots a
103
- bag), the **Auction House** in the capitals (`Tab` cycles browse, sell and your own auctions;
104
- `Enter` buys out, `b` bids, `s` searches by name; selling asks quantity, bid, buyout and
105
- duration), and the **Mailbox** by every inn, where sales, refunds and overflow loot arrive
106
- (`Enter` takes a letter's money and items, `A` empties the box, `d` deletes). `:bank`, `:ah`
107
- and `:mail` open them from the command line.
238
+ Travel tab), with any held run or raid lockout on the row. `Enter` opens the card before the door:
239
+ route (Bosses or Full), party or raid, who walks in (`f` fills from every companion you own to what
240
+ the instance wants), the 10/20/40-man raid group, the paladins' Blessings, and food and water for
241
+ everyone short of it.
242
+
243
+ Inside, the run window lists the rooms in route order with their stops (`B` boss, `T` trash,
244
+ `R` rare), the doors, levers and chests nearest each stop, and the NPCs with quests. Locked stops
245
+ say what opens them.
246
+
247
+ | Key | Does |
248
+ | --- | --- |
249
+ | `Enter` on an open stop | pull it live with the party (`p` in the fight pulls the next trash pack in too) |
250
+ | `Enter` on a gate / chest | use it / open it |
251
+ | `z` | rest and eat between pulls |
252
+ | `F` | run every remaining stop without watching; ends on a run report (damage, healing, loot, wipes) |
253
+ | `P` | open the Blessings board |
254
+ | `L` | leave (a 5-man is held for 30 minutes) |
255
+ | `R` | reset |
256
+
257
+ ### City services
258
+
259
+ **City services** appear under *Services* on the World tab, above the people. `:bank`, `:ah` and
260
+ `:mail` open them from the command line.
261
+
262
+ **Bank**, in the six capitals and the goblin banker towns (`b` in Bags opens it too):
263
+
264
+ | Key | Does |
265
+ | --- | --- |
266
+ | `Tab` | flip between depositing and withdrawing |
267
+ | `n` | move part of a stack |
268
+ | `+` | buy a bank bag slot |
269
+ | `B` | slot a bag |
270
+
271
+ **Auction House**, in the capitals:
272
+
273
+ | Key | Does |
274
+ | --- | --- |
275
+ | `Tab` | cycle browse, sell and your own auctions |
276
+ | `Enter` | buy out |
277
+ | `b` | bid |
278
+ | `s` | search by name |
279
+
280
+ Selling asks quantity, bid, buyout and duration.
281
+
282
+ **Mailbox**, by every inn, where sales, refunds and overflow loot arrive:
283
+
284
+ | Key | Does |
285
+ | --- | --- |
286
+ | `Enter` | take a letter's money and items |
287
+ | `A` | empty the box |
288
+ | `d` | delete |
289
+
290
+ ## Art and terminals
291
+
292
+ The colours are plain ANSI, so your terminal's theme sets the palette. For a CRT look turn on the
293
+ terminal's own retro effect (Windows Terminal: `"experimental.retroTerminalEffect": true` on the
294
+ profile; cool-retro-term elsewhere).
295
+
296
+ At 90 columns or wider the creature you face is drawn beside the fight frames and under its detail
297
+ on the World tab. Your own character, in the gear you wear, stands on the Character tab, on the
298
+ character picker, at creation and (from 130 columns) on the other side of the fight, and a
299
+ companion's page shows them the same way. It is truecolour half-block art baked from the game's
300
+ models; a terminal without 24-bit colour shows it in the nearest colours it has.
301
+
302
+ A terminal that draws sixel images (Windows Terminal 1.22+, WezTerm, foot, iTerm2; VS Code with
303
+ `terminal.integrated.enableImages`) gets every creature, every character in their gear and every
304
+ icon in real pixels instead. azsh asks the terminal at start.
305
+
306
+ ![The same hero on the create screen: half-block art on the left, sixel on the right](media/art-compare.png)
307
+
308
+ | Variable | Does |
309
+ | --- | --- |
310
+ | `AZSH_SIXEL=0` | keep the half-block art |
311
+ | `AZSH_SIXEL=1` | turn sixel on for a terminal that does not say |
312
+ | `AZSH_HOME` | move the save folder (default `~/.azsh`) |
313
+
314
+ ## Line mode and saves
108
315
 
109
316
  Piped input, or `azsh --repl`, gives the line game instead: type `help` for its commands.
110
- `azsh --tui` forces the windowed game even without a terminal, which is how it is driven through pipes
111
- in tests. Keys are decoded from the raw byte stream, so a lone `Esc` acts at once.
317
+ `azsh --tui` forces the windowed game even without a terminal, which is how it is driven through
318
+ pipes in tests. Keys are decoded from the raw byte stream, so a lone `Esc` acts at once.
112
319
 
113
320
  Saves live in `~/.azsh/saves/`, one JSON file per character. `AZSH_HOME` moves that folder.
114
321
 
115
322
  ## Where it comes from
116
323
 
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.
324
+ The engine, rules, world, quests, autopilot and data are taken as-is from a larger, unreleased
325
+ project of mine that runs the same game with a graphical front end. The data is the CMaNGOS Classic
326
+ world database plus a DBC bake, every table gzipped on disk (`src/cli/gzLoader.mjs` serves the
327
+ static imports). Everything the player reads traces to a CMaNGOS row or a 1.12 client string.
328
+ Nothing here is invented.
125
329
 
126
- Everything the player reads traces to a CMaNGOS row or a 1.12 client string. Nothing here is
127
- invented.
330
+ What is new here is the terminal front end in `src/cli/`:
128
331
 
129
- ## Checks
332
+ | Module | Holds |
333
+ | --- | --- |
334
+ | `tui.mjs` | frame, input, dialogs |
335
+ | `tuiViews.mjs` | tabs |
336
+ | `tuiProfessions.mjs`, `tuiCompanions.mjs` | crafting, companions |
337
+ | `tuiFight.mjs` | live fight, autopilot |
338
+ | `repl.mjs` | line game and the shared verb table |
130
339
 
131
- ```
340
+ A few small UI modules (combat log wording, rotation sentences, dungeon floor scripts) are ported
341
+ from the same project.
342
+
343
+ The art is Blizzard's too. The icons are the 1.12 client's own icon textures; the creature sprites
344
+ and the geared character bodies are renders of the client's models. The packs in `src/data` are
345
+ small half-block reductions of them, and the full-size packs a sixel terminal downloads (the
346
+ `art-*` GitHub releases) are the same pictures at their native size. They are here for a
347
+ non-commercial fan project, nothing is sold or will be, and they come down on request.
348
+
349
+ What is not ported, and why, is the "Not porting" list in `PORT-PLAN.md`.
350
+
351
+ ## Development
352
+
353
+ Checks:
354
+
355
+ ```sh
132
356
  npm run check # boot the store, fight ten kobolds, save and reload
133
- npm run check:tui # drive the windowed game headlessly: tabs, quest, vendor, trainer, gear, bank, auction house, live fight, autopilot, crafting, hiring, send goods
357
+ npm run check:tui # drive the windowed game headlessly: tabs, quest, vendor, trainer, gear, bank,
358
+ # auction house, live fight, autopilot, crafting, hiring, send goods
134
359
  npm run check:pipe # the real terminal path through pipes: raw key bytes in, ANSI frames out, Esc, paging, quit
135
360
  npm run check:all # all three; add --frames to check:tui to print every screen
361
+ npm run check:perf # timings, not pass/fail: module import, boot stages, heap, a repaint on every tab, a fight tick by tick
362
+ ```
363
+
364
+ Data and art bakes:
365
+
366
+ ```sh
136
367
  npm run data:pack # gzip the bake after a data refresh; data:unpack reverses it
368
+ npm run art:bake # rebuild src/data/model-art.bin.gz from phonecraft's creature renders (needs ffmpeg)
369
+ npm run art:gear # rebuild src/data/gear-art.bin.gz from phonecraft's paperdoll bake (needs Python with Pillow)
370
+ npm run art:icons # rebuild src/data/icon-art.bin.gz from phonecraft's spell and item icons (needs ffmpeg)
371
+ npm run art:hd # rebuild the full-size art sixel draws: model-art-hd.bin.br, icon-art-hd.bin.br and
372
+ # gear-art-hd/<body>.bin.br (needs ffmpeg, and Python with Pillow for the gear)
137
373
  ```
138
374
 
139
- What is not ported, and why, is the "Not porting" list in `PORT-PLAN.md`.
375
+ The pictures in this file are played, not drawn: `scripts/demo-record.mjs` runs the game on pipes with
376
+ a scripted player and logs every byte it writes with the time, and `scripts/demo-render.py` replays
377
+ the log through a small terminal emulator (cells, colours, sixel) and hands the frames to ffmpeg.
378
+
379
+ ```sh
380
+ node scripts/demo-record.mjs new media/rec/new.jsonl --cols 160 --rows 48 # scenes: new, bot, art
381
+ python scripts/demo-render.py media/rec/new.jsonl --marks # the clips in a recording
382
+ python scripts/demo-render.py media/rec/new.jsonl --clip fight --gif media/fight.gif [--mp4 media/fight.mp4]
383
+ ```
package/bin/azsh.mjs CHANGED
@@ -1,19 +1,21 @@
1
- #!/usr/bin/env node
2
- // azsh: Azeroth shell. Node 22.18+ strips TypeScript types natively, so the engine loads as-is.
3
- // A terminal gets the windowed game; piped input (or --repl) gets the line game.
4
- const [major, minor] = process.versions.node.split('.').map(Number);
5
- if (major < 22 || (major === 22 && minor < 18)) {
6
- console.error(`azsh needs Node 22.18 or newer (found ${process.versions.node}).`);
7
- process.exit(1);
8
- }
9
- await import('../src/cli/register.mjs');
10
- const argv = process.argv.slice(2);
11
- const wantRepl = argv.includes('--repl') || (!argv.includes('--tui') && (!process.stdin.isTTY || !process.stdout.isTTY));
12
- if (wantRepl) {
13
- const { main } = await import('../src/cli/repl.mjs');
14
- await main(argv.filter((a) => a !== '--repl' && a !== '--tui'));
15
- } else {
16
- const { runTui } = await import('../src/cli/tui.mjs');
17
- await runTui({ argv });
18
- process.exit(0); // the save is written before the screen closes; a piped stdin would otherwise hold the loop open
19
- }
1
+ #!/usr/bin/env node
2
+ // azsh: Azeroth shell. Node 22.18+ strips TypeScript types natively, so the engine loads as-is.
3
+ // A terminal gets the windowed game; piped input (or --repl) gets the line game.
4
+ const [major, minor] = process.versions.node.split('.').map(Number);
5
+ if (major < 22 || (major === 22 && minor < 18)) {
6
+ console.error(`azsh needs Node 22.18 or newer (found ${process.versions.node}).`);
7
+ process.exit(1);
8
+ }
9
+ await import('../src/cli/register.mjs');
10
+ const argv = process.argv.slice(2);
11
+ const wantRepl = argv.includes('--repl') || (!argv.includes('--tui') && (!process.stdin.isTTY || !process.stdout.isTTY));
12
+ if (wantRepl) {
13
+ const { main } = await import('../src/cli/repl.mjs');
14
+ await main(argv.filter((a) => a !== '--repl' && a !== '--tui'));
15
+ } else {
16
+ // Loading the engine takes a moment (longer on the first run, which fills the stripped-JS cache); say so.
17
+ process.stdout.write('azsh: loading...\n');
18
+ const { runTui } = await import('../src/cli/tui.mjs');
19
+ await runTui({ argv });
20
+ process.exit(0); // the save is written before the screen closes; a piped stdin would otherwise hold the loop open
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azeroth-shell",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Azeroth shell. A WoW Classic 1.12 text RPG for the terminal, built on CMaNGOS data.",
6
6
  "keywords": [
@@ -12,6 +12,15 @@
12
12
  "tui",
13
13
  "cmangos"
14
14
  ],
15
+ "author": "Simen Lien",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/LienSimen/azsh.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/LienSimen/azsh/issues"
22
+ },
23
+ "homepage": "https://github.com/LienSimen/azsh#readme",
15
24
  "bin": {
16
25
  "azeroth-shell": "bin/azsh.mjs",
17
26
  "azsh": "bin/azsh.mjs"
@@ -19,19 +28,28 @@
19
28
  "files": [
20
29
  "bin/",
21
30
  "src/",
31
+ "!src/data/*-hd.bin.br",
32
+ "!src/data/gear-art-hd/",
22
33
  "docs/"
23
34
  ],
24
35
  "engines": {
25
36
  "node": ">=22.18"
26
37
  },
27
38
  "scripts": {
39
+ "test": "npm run check:all",
28
40
  "start": "node bin/azsh.mjs",
29
41
  "check": "node --import ./src/cli/register.mjs src/cli/bootCheck.mjs",
30
42
  "data:pack": "node scripts/pack-data.mjs",
31
43
  "data:unpack": "node scripts/pack-data.mjs --unpack",
44
+ "art:bake": "node scripts/bake-model-art.mjs",
45
+ "art:gear": "python scripts/bake-gear-art.py",
46
+ "art:icons": "node scripts/bake-icon-art.mjs",
47
+ "art:hd": "node scripts/bake-model-art.mjs --hd && node scripts/bake-icon-art.mjs --hd && python scripts/bake-gear-art.py --hd",
48
+ "art:publish": "node scripts/publish-hd-art.mjs",
32
49
  "build:exe": "node scripts/build-exe.mjs",
33
50
  "check:tui": "node --import ./src/cli/register.mjs src/cli/tuiCheck.mjs",
34
51
  "check:pipe": "node scripts/tui-pipe-check.mjs",
52
+ "check:perf": "node --import ./src/cli/register.mjs src/cli/perfCheck.mjs",
35
53
  "check:all": "npm run check && npm run check:tui && npm run check:pipe"
36
54
  }
37
55
  }
@@ -46,6 +46,8 @@ import {
46
46
  prereqMet,
47
47
  dropperNoiseFloorFor,
48
48
  compareQuestDroppers,
49
+ pickQuestDropper,
50
+ dropperPickSalt,
49
51
  } from '../quests/quests.ts';
50
52
  import { ownedInventory } from '../domain/items/keyring.ts';
51
53
  import { mailboxAsInventory } from '../domain/economy/mail.ts';
@@ -1502,17 +1504,12 @@ function planStepCore(
1502
1504
  });
1503
1505
  if (candidates.length > 0) {
1504
1506
  // Kill objectives first, then a normal dropper over an elite, then closer, then highest drop.
1505
- const target = candidates.reduce((a, b) => {
1506
- const aKill = killNeed.has(a.entry);
1507
- const bKill = killNeed.has(b.entry);
1508
- if (aKill !== bKill) return aKill ? a : b;
1509
- const cmp = compareQuestDroppers(
1510
- { level: a.level, chance: itemChance.get(a.entry) ?? 0, rank: a.rank ?? 0 },
1511
- { level: b.level, chance: itemChance.get(b.entry) ?? 0, rank: b.rank ?? 0 },
1512
- state.char.level,
1513
- );
1514
- return cmp <= 0 ? a : b;
1515
- });
1507
+ // One of the near-equal pool, rotated per fight — not the same template every pull.
1508
+ const kills = candidates.filter((h) => killNeed.has(h.entry));
1509
+ const ranked = (kills.length > 0 ? kills : candidates)
1510
+ .map((h) => ({ h, level: h.level, chance: itemChance.get(h.entry) ?? 0, rank: h.rank ?? 0 }))
1511
+ .sort((a, b) => compareQuestDroppers(a, b, state.char.level) || a.h.entry - b.h.entry);
1512
+ const target = pickQuestDropper(ranked, state.char.level, dropperPickSalt(state))!.h;
1516
1513
  questFight = { creatureEntry: target.entry, questEntry: local.entry };
1517
1514
  }
1518
1515
  }