azeroth-shell 0.1.2 → 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.
- package/README.md +326 -82
- package/bin/azsh.mjs +21 -19
- package/package.json +9 -1
- package/src/autopilot/plan.ts +8 -11
- package/src/autopilot/view.ts +12 -11
- package/src/cli/combatLogFormat.ts +22 -0
- package/src/cli/gzLoader.mjs +47 -6
- package/src/cli/hdArt.mjs +81 -0
- package/src/cli/iconArt.mjs +135 -0
- package/src/cli/iconGrid.mjs +100 -0
- package/src/cli/modelArt.mjs +295 -0
- package/src/cli/paperdoll.mjs +76 -0
- package/src/cli/perfCheck.mjs +56 -0
- package/src/cli/repl.mjs +32 -7
- package/src/cli/rotationText.mjs +6 -6
- package/src/cli/sixel.mjs +307 -0
- package/src/cli/term.mjs +106 -12
- package/src/cli/text.mjs +5 -3
- package/src/cli/tui.mjs +231 -95
- package/src/cli/tuiAutopilot.mjs +347 -0
- package/src/cli/tuiBg.mjs +370 -41
- package/src/cli/tuiCheck.mjs +362 -27
- package/src/cli/tuiCity.mjs +118 -13
- package/src/cli/tuiCompanions.mjs +25 -15
- package/src/cli/tuiCreate.mjs +223 -0
- package/src/cli/tuiDungeon.mjs +1 -1
- package/src/cli/tuiFight.mjs +423 -86
- package/src/cli/tuiFightPanes.mjs +27 -8
- package/src/cli/tuiFloor.mjs +1 -1
- package/src/cli/tuiGear.mjs +10 -6
- package/src/cli/tuiLoot.mjs +1 -0
- package/src/cli/tuiPaperdoll.mjs +107 -0
- package/src/cli/tuiPartyStage.mjs +177 -0
- package/src/cli/tuiProfessions.mjs +4 -2
- package/src/cli/tuiRotationCond.mjs +390 -0
- package/src/cli/tuiSaves.mjs +10 -1
- package/src/cli/tuiSearch.mjs +75 -22
- package/src/cli/tuiStage.mjs +304 -0
- package/src/cli/tuiTalentTree.mjs +124 -0
- package/src/cli/tuiViews.mjs +284 -110
- package/src/cli/tuiWorld.mjs +2 -2
- package/src/data/gear-art.bin.br +0 -0
- package/src/data/hd-art.json +114 -0
- package/src/data/icon-art.bin.gz +0 -0
- package/src/data/model-art.bin.gz +0 -0
- package/src/data/rotations/pve/paladin/retribution.json +2 -2
- package/src/domain/bg/match.ts +85 -24
- package/src/domain/bg/strategies.ts +144 -29
- package/src/domain/bg/types.ts +2 -0
- package/src/domain/bg/wsg.ts +35 -20
- package/src/domain/quests/doQuest.ts +24 -6
- package/src/domain/rotation/build.ts +3031 -3031
- package/src/domain/rotation/classes/paladin.ts +16 -11
- package/src/domain/rotation/evaluate.ts +3 -0
- package/src/engine/combat/applySpell.ts +20 -9
- package/src/engine/combat/applySpellLate.ts +16 -12
- package/src/engine/combat/areaAura.ts +284 -283
- package/src/engine/combat/fighterFactory.ts +1 -1
- package/src/engine/combat/pickAction.ts +39 -1
- package/src/engine/combat/procsDamage.ts +31 -1
- package/src/engine/combat/sessionChase.ts +7 -1
- package/src/engine/combat/sessionMotionKit.ts +40 -2
- package/src/quests/quests.ts +60 -0
package/src/cli/tuiBg.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// Warsong Gulch and Arathi Basin as a text match, from the Honor window. The map, rules, roster and strategy layer
|
|
2
2
|
// are phonecraft's own (domain/bg: BgWorld, installWsg / installAb, bgRoster, BgMatch); what is not ported is
|
|
3
3
|
// bgScreen.ts's whole-match combat session. Here the world resolves its own engagements (BgMatch's attrition
|
|
4
|
-
// resolver, flat damage and healing per body), so a
|
|
5
|
-
//
|
|
6
|
-
// the
|
|
4
|
+
// resolver, flat damage and healing per body), so a fight is watched, but the field is yours to play: a strip map
|
|
5
|
+
// of the gulch above the scoreboard, orders for your own seat by place (our flag room, mid, their graveyard, a
|
|
6
|
+
// buff, the flag itself) or an autopilot job the strategy layer plays, and team calls to your side through
|
|
7
|
+
// BgMatch's playerCommanded hook. The clock drops back to 1x when a flag changes hands, so you get a moment to
|
|
8
|
+
// react. Enemies are drawn from your team's BgView, the same fair-vision rule the bots are held to. Honor and
|
|
9
|
+
// Marks of Honor settle as bgScreen.ts does.
|
|
7
10
|
import { bold, dim, yellow, green, red, cyan, fit, bar } from './text.mjs';
|
|
8
11
|
import { BATTLEGROUNDS } from '../domain/run/battlegroundRun.ts';
|
|
9
12
|
import { BgWorld } from '../domain/bg/bgWorld.ts';
|
|
10
13
|
import { BgMatch, attritionResolver } from '../domain/bg/match.ts';
|
|
11
|
-
import { installWsg, wsgGraveyard, WSG_GEOMETRY } from '../domain/bg/wsg.ts';
|
|
14
|
+
import { installWsg, wsgGraveyard, WSG_GEOMETRY, flagObjectiveId } from '../domain/bg/wsg.ts';
|
|
12
15
|
import { installAb, abGraveyard, AB_GEOMETRY, AB_NODES } from '../domain/bg/ab.ts';
|
|
13
16
|
import { buildFriendlySeats, buildEnemySeats, seatLevel } from '../domain/bg/bgRoster.ts';
|
|
14
17
|
import { bgTier } from '../domain/bg/bgTier.ts';
|
|
@@ -24,7 +27,9 @@ const H = (text) => ({ text, header: true });
|
|
|
24
27
|
const TEAM = { alliance: 'Alliance', horde: 'Horde' };
|
|
25
28
|
const paintTeam = (t, s) => (t === 'horde' ? red(s) : cyan(s));
|
|
26
29
|
const COUNTDOWN_MS = 10_000;
|
|
27
|
-
|
|
30
|
+
// Vanilla 1.12 had no clock and a double turtle could run for hours; patch 2.3 put 25 minutes on the
|
|
31
|
+
// gulch, which is what an unattended match here needs to reach a result. A tied clock goes to kills.
|
|
32
|
+
const MATCH_MAX_MS = 25 * 60_000;
|
|
28
33
|
// The job boards phonecraft deals seats from (bgScreen.ts:140-151).
|
|
29
34
|
const WSG_ROLES = ['flag-carrier', 'fc-escort', 'offense', 'defense', 'defense', 'mid', 'offense', 'fc-escort', 'mid', 'flex'];
|
|
30
35
|
const AB_ROLES = ['node-assault', 'node-assault', 'node-defense', 'node-assault', 'node-defense'];
|
|
@@ -34,6 +39,17 @@ const ROLE_TEXT = {
|
|
|
34
39
|
'node-assault': 'assault their bases', 'node-defense': 'hold our bases',
|
|
35
40
|
};
|
|
36
41
|
const STEPS_PER_TICK = 5; // world ticks (100 ms each) per screen tick at 1x
|
|
42
|
+
/** How long a team call holds your side before the strategy layer takes its jobs back. */
|
|
43
|
+
const CALL_MS = 45_000;
|
|
44
|
+
const ASCII = !!process.env.AZSH_ASCII;
|
|
45
|
+
const GLYPH = ASCII ? { flag: 'F', fight: 'x', dash: '-', mid: ':' } : { flag: '⚑', fight: '⚔', dash: '─', mid: '·' };
|
|
46
|
+
const MAP_ROWS = 7;
|
|
47
|
+
// The gulch is a long, narrow lane: Warsong (Horde) holds the west end and Silverwing (Alliance) the east, so the
|
|
48
|
+
// strip draws Horde on the left and Alliance on the right, at about 10 yards a column and 34 a row.
|
|
49
|
+
const WSG_FIELD = { x0: 895, x1: 1565, y0: 1335, y1: 1575 };
|
|
50
|
+
const POWERUP_CHAR = { speed: 's', regen: 'r', zerk: 'z' };
|
|
51
|
+
const xy = (p) => ({ x: p.x, y: p.y });
|
|
52
|
+
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
37
53
|
|
|
38
54
|
/** The Honor window's Warsong / Arathi rows. */
|
|
39
55
|
export function objectiveBgRows(app) {
|
|
@@ -66,33 +82,53 @@ export function startBattleground(app, bgId) {
|
|
|
66
82
|
const start = isAb ? AB_GEOMETRY.start : WSG_GEOMETRY.start;
|
|
67
83
|
const roles = isAb ? AB_ROLES : WSG_ROLES;
|
|
68
84
|
const addTeam = (side, seats) => {
|
|
69
|
-
seats.
|
|
85
|
+
const bodies = seats.map((seat, i) => {
|
|
70
86
|
const isPlayer = seat.kind === 'player';
|
|
71
87
|
const lvl = seatLevel(seat, state.char.level);
|
|
72
|
-
|
|
88
|
+
return { seat, i, isPlayer, lvl, hp: maxHealth(isPlayer ? state.char : companionCharacter(seat.comp, lvl, ds), ds), heals: isPlayer ? state.squadRole === 'healer' : seat.comp.role === 'healer' };
|
|
89
|
+
});
|
|
90
|
+
// The flag runs on the sturdiest body that does not heal, as a real team's does; dealt by seat, the
|
|
91
|
+
// run went to seat 0, a 560-hp healer who died 100 yd out of the enemy flag room every time.
|
|
92
|
+
const runner = bodies.filter((b) => !b.heals).sort((a, b) => b.hp - a.hp)[0] ?? bodies[0];
|
|
93
|
+
const rest = roles.filter((r) => r !== 'flag-carrier');
|
|
94
|
+
let k = 0;
|
|
95
|
+
for (const b of bodies) {
|
|
96
|
+
const { seat, i, isPlayer, lvl, hp } = b;
|
|
97
|
+
const role = isAb ? roles[i % roles.length] : b === runner ? 'flag-carrier' : rest[(k++) % rest.length];
|
|
73
98
|
world.addUnit({
|
|
74
99
|
id: `${side[0]}${i}`, team: side, name: isPlayer ? state.char.name : seat.comp.name, cls: isPlayer ? state.char.cls : seat.comp.cls, level: lvl,
|
|
75
100
|
pos: { x: start[side].x + (i % 5) - 2, y: start[side].y + Math.floor(i / 5) * 3 - 1 }, z: start[side].z ?? 0,
|
|
76
|
-
hp, maxHp: hp, role
|
|
77
|
-
//
|
|
101
|
+
hp, maxHp: hp, role, order: { kind: 'hold' }, speedYd: BASE_RUN_SPEED,
|
|
102
|
+
// The resolver heals with the seats the roster made healers (three a side), not with every
|
|
103
|
+
// paladin, priest, shaman and druid: by class, six of ten bodies a side healed and nobody died.
|
|
104
|
+
heals: b.heals,
|
|
105
|
+
// Autopilot to begin with: the strategy layer plays your seat to the job you pick. Your first order by
|
|
106
|
+
// place flips this to true and the layer lets go of you (match.ts runStrategy).
|
|
78
107
|
human: false,
|
|
79
108
|
});
|
|
80
|
-
}
|
|
109
|
+
}
|
|
81
110
|
world.setVision(side, FAIR_VISION);
|
|
82
111
|
};
|
|
83
112
|
addTeam(team, friendly);
|
|
84
113
|
addTeam(foe, enemies);
|
|
85
|
-
const match = new BgMatch({
|
|
86
|
-
world, seed: run.seed, strategyMs: 1000, strategyMsByTeam: { [foe]: tier.enemyStrategyMs },
|
|
87
|
-
resolveShard: attritionResolver(), graveyard: isAb ? (t) => abGraveyard(world, t) : wsgGraveyard, respawnMs: 30_000,
|
|
88
|
-
});
|
|
89
114
|
const bg = {
|
|
90
|
-
def, team, foe, isAb, world, match, wsgEvents, abEvents, selfId: run.selfId ?? `${team[0]}0`,
|
|
115
|
+
def, team, foe, isAb, world, match: null, wsgEvents, abEvents, selfId: run.selfId ?? `${team[0]}0`,
|
|
91
116
|
wsgAt: 0, abAt: 0, honor: 0, kills: 0, caps: 0, marks: 0, speed: 1, paused: false, timer: null, settled: false,
|
|
117
|
+
// Both sides' kills, from the field's deaths: the kill line under the map counts only yours.
|
|
118
|
+
teamKills: { alliance: 0, horde: 0 }, dead: new Set(),
|
|
92
119
|
bracket: bgBracketId(state.char.level, 10, 60),
|
|
120
|
+
// Your standing order ({ label, make }) while you drive your own seat, and the label of what it resolved to.
|
|
121
|
+
my: null, myLabel: null,
|
|
122
|
+
// Team calls: unit id → match clock the call expires on. The strategy layer skips these until then.
|
|
123
|
+
commanded: new Map(), slowLatch: null, zones: null,
|
|
93
124
|
};
|
|
125
|
+
bg.match = new BgMatch({
|
|
126
|
+
world, seed: run.seed, strategyMs: 1000, strategyMsByTeam: { [foe]: tier.enemyStrategyMs },
|
|
127
|
+
resolveShard: attritionResolver(), graveyard: isAb ? (t) => abGraveyard(world, t) : wsgGraveyard, respawnMs: 30_000,
|
|
128
|
+
playerCommanded: (id) => (bg.commanded.get(id) ?? -Infinity) > world.t,
|
|
129
|
+
});
|
|
94
130
|
app.bg = bg;
|
|
95
|
-
app.say(yellow(`You enter ${def.name} for the ${TEAM[team]}. ${run.teamSize}v${run.teamSize}.`) + dim(' The gates open in 10 seconds.'));
|
|
131
|
+
app.say(yellow(`You enter ${def.name} for the ${TEAM[team]}. ${run.teamSize}v${run.teamSize}.`) + dim(' The gates open in 10 seconds. o: your orders, t: a team call.'));
|
|
96
132
|
const view = bgView(app, bg);
|
|
97
133
|
app.push(view);
|
|
98
134
|
bg.timer = app.term.setInterval(() => { if (!bg.paused) { stepBg(app, bg, STEPS_PER_TICK * bg.speed); app.renderSoon(); } }, 100);
|
|
@@ -115,13 +151,34 @@ function bankRosterBonus(app, bg, side, cp) {
|
|
|
115
151
|
for (const u of bg.world.units.values()) if (u.team === side && u.id !== bg.selfId) app.g.bankWeeklyHonor(u.name, cp, 0);
|
|
116
152
|
}
|
|
117
153
|
|
|
154
|
+
/** Drop the clock back to 1x at a decision point, once. */
|
|
155
|
+
function slowDown(app, bg, why) {
|
|
156
|
+
if (bg.speed <= 1) return;
|
|
157
|
+
bg.speed = 1;
|
|
158
|
+
app.say(dim(`Back to 1x: ${why}.`));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The enemy carrying OUR flag, if any. */
|
|
162
|
+
function theirCarrier(bg) {
|
|
163
|
+
const f = bg.world.objectives.get(flagObjectiveId(bg.team));
|
|
164
|
+
const c = f?.state === 'carried' ? bg.world.units.get(f.carrier) : null;
|
|
165
|
+
return c && bg.world.alive(c) ? c : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
118
168
|
/** Run `n` world ticks and turn what happened into log lines and honor. */
|
|
119
169
|
export function stepBg(app, bg, n) {
|
|
120
170
|
const { world, match } = bg;
|
|
121
171
|
for (let i = 0; i < n && !world.ended; i++) match.tick();
|
|
172
|
+
// Deaths this step, by the side that scored them. Credits skip an honorless victim, so count bodies.
|
|
173
|
+
for (const u of world.units.values()) {
|
|
174
|
+
if (u.deadUntilMs != null) { if (!bg.dead.has(u.id)) { bg.dead.add(u.id); bg.teamKills[OTHER_TEAM[u.team]]++; } }
|
|
175
|
+
else bg.dead.delete(u.id);
|
|
176
|
+
}
|
|
122
177
|
for (const c of world.takeKillCredit()) {
|
|
123
178
|
const killer = world.units.get(c.killer), victim = world.units.get(c.victim);
|
|
124
|
-
if (!killer || !victim || killer.team === victim.team
|
|
179
|
+
if (!killer || !victim || killer.team === victim.team) continue;
|
|
180
|
+
if (killer.id !== bg.selfId) app.say(dim(`${paintTeam(killer.team, killer.name)} kills ${paintTeam(victim.team, victim.name)}.`));
|
|
181
|
+
if (world.isHonorless(victim)) continue;
|
|
125
182
|
if (killer.id === bg.selfId) {
|
|
126
183
|
// Player.cpp:6645 RewardHonor: the rank-and-level formula with the per-victim falloff.
|
|
127
184
|
const res = recordHonorableKill(app.g.state.char.honor ?? newHonorState(), { level: app.ch.level }, { id: victim.name, level: victim.level, rankPoints: 0 }, new Date().toISOString().slice(0, 10));
|
|
@@ -133,11 +190,12 @@ export function stepBg(app, bg, n) {
|
|
|
133
190
|
}
|
|
134
191
|
for (; bg.wsgAt < bg.wsgEvents.length; bg.wsgAt++) {
|
|
135
192
|
const e = bg.wsgEvents[bg.wsgAt];
|
|
136
|
-
if (e.kind === 'pickup') app.say(`${unitName(bg, e.unit)} picks up the ${TEAM[OTHER_TEAM[e.team]]} flag!`);
|
|
137
|
-
else if (e.kind === 'drop') app.say(dim(`${unitName(bg, e.unit)} drops the flag.`));
|
|
193
|
+
if (e.kind === 'pickup') { app.say(`${unitName(bg, e.unit)} picks up the ${TEAM[OTHER_TEAM[e.team]]} flag!`); slowDown(app, bg, 'a flag is up'); }
|
|
194
|
+
else if (e.kind === 'drop') { app.say(dim(`${unitName(bg, e.unit)} drops the flag.`)); slowDown(app, bg, 'a flag is on the ground'); }
|
|
138
195
|
else if (e.kind === 'return') app.say(dim(`The ${TEAM[e.team]} flag returns to its base.`));
|
|
139
196
|
else if (e.kind === 'capture') {
|
|
140
197
|
app.say(bold(paintTeam(e.team, `${unitName(bg, e.unit)} captures the flag for the ${TEAM[e.team]}!`)));
|
|
198
|
+
slowDown(app, bg, 'a capture');
|
|
141
199
|
const cp = WSG_FLAG_CAPTURED_HONOR[bg.bracket] ?? 0;
|
|
142
200
|
if (e.team === bg.team) { bg.caps++; awardCp(app, bg, cp); }
|
|
143
201
|
bankRosterBonus(app, bg, e.team, cp);
|
|
@@ -145,21 +203,57 @@ export function stepBg(app, bg, n) {
|
|
|
145
203
|
}
|
|
146
204
|
for (; bg.abAt < bg.abEvents.length; bg.abAt++) {
|
|
147
205
|
const e = bg.abEvents[bg.abAt];
|
|
148
|
-
if (e.kind === 'assault') { app.say(`${unitName(bg, e.unit)} assaults the ${nodeName(e.node)}.`); if (e.team === bg.team && e.unit === bg.selfId) bg.caps++; }
|
|
206
|
+
if (e.kind === 'assault') { app.say(`${unitName(bg, e.unit)} assaults the ${nodeName(e.node)}.`); if (e.team === bg.team && e.unit === bg.selfId) bg.caps++; slowDown(app, bg, `the ${nodeName(e.node)} is under assault`); }
|
|
149
207
|
else if (e.kind === 'capture') app.say(bold(paintTeam(e.team, `The ${TEAM[e.team]} claim the ${nodeName(e.node)}!`)));
|
|
150
208
|
else if (e.kind === 'defend') { app.say(`${unitName(bg, e.unit)} defends the ${nodeName(e.node)}.`); if (e.team === bg.team && e.unit === bg.selfId) bg.caps++; }
|
|
151
|
-
else if (e.kind === 'near-victory') app.say(bold(`The ${TEAM[e.team]} are close to victory!`));
|
|
209
|
+
else if (e.kind === 'near-victory') { app.say(bold(`The ${TEAM[e.team]} are close to victory!`)); slowDown(app, bg, 'the end is near'); }
|
|
152
210
|
else if (e.kind === 'honor') { const cp = hkHonorAtLevel(app.ch.level, e.amount ?? 1); if (e.team === bg.team) awardCp(app, bg, cp); bankRosterBonus(app, bg, e.team, cp); }
|
|
153
211
|
}
|
|
212
|
+
if (!bg.isAb) {
|
|
213
|
+
// Their carrier closing on a score while their flag sits at home: the moment to react.
|
|
214
|
+
const tc = theirCarrier(bg);
|
|
215
|
+
if (tc && world.objectives.get(flagObjectiveId(bg.foe))?.state === 'base' && dist(tc.pos, WSG_GEOMETRY.flagBase[bg.foe]) < 80) {
|
|
216
|
+
if (bg.slowLatch !== tc.id) { bg.slowLatch = tc.id; slowDown(app, bg, `${tc.name} is about to score`); }
|
|
217
|
+
} else bg.slowLatch = null;
|
|
218
|
+
}
|
|
219
|
+
expireCalls(bg);
|
|
220
|
+
driveSelf(app, bg);
|
|
154
221
|
if (world.ended && !bg.settled) settle(app, bg);
|
|
155
222
|
}
|
|
156
223
|
|
|
224
|
+
// A call lapses when its clock runs out, when the body died (kill() reset it to hold and the strategy layer must
|
|
225
|
+
// have it back on respawn), when it picked up the flag (runFlagHome is the layer's job), or when the unit it
|
|
226
|
+
// was following is gone.
|
|
227
|
+
function expireCalls(bg) {
|
|
228
|
+
const { world } = bg;
|
|
229
|
+
for (const [id, until] of bg.commanded) {
|
|
230
|
+
const u = world.units.get(id);
|
|
231
|
+
const tgt = u?.order.kind === 'follow' ? world.units.get(u.order.target) : null;
|
|
232
|
+
if (!u || world.t >= until || !world.alive(u) || u.carryingFlag || u.order.kind === 'hold' || (u.order.kind === 'follow' && !(tgt && world.alive(tgt)))) bg.commanded.delete(id);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Your standing order, re-resolved every screen tick: a respawn walks you back to your place, "take their flag"
|
|
237
|
+
// follows the flag wherever it went, and an order with nothing left to do lets go of you.
|
|
238
|
+
function driveSelf(app, bg) {
|
|
239
|
+
const me = bg.world.units.get(bg.selfId);
|
|
240
|
+
if (!me || !me.human || !bg.my || !bg.world.alive(me)) return;
|
|
241
|
+
const r = bg.my.make();
|
|
242
|
+
if (!r) { app.say(dim(`${bg.myLabel ?? bg.my.label}: nothing left to do. You hold here.`)); bg.my = null; bg.myLabel = 'holding here'; return; }
|
|
243
|
+
bg.world.issue(me.id, r.order, bg.team);
|
|
244
|
+
bg.myLabel = r.label ?? bg.my.label;
|
|
245
|
+
}
|
|
246
|
+
|
|
157
247
|
/** Marks, the win honor, the weekly pass, then out of the battleground (bgScreen.ts:1277-1330). */
|
|
158
248
|
function settle(app, bg) {
|
|
159
249
|
bg.settled = true;
|
|
160
250
|
if (bg.timer) app.term.clearInterval(bg.timer);
|
|
161
251
|
bg.timer = null;
|
|
162
252
|
const { world } = bg;
|
|
253
|
+
if (world.winner == null && bg.teamKills.alliance !== bg.teamKills.horde) {
|
|
254
|
+
world.winner = bg.teamKills.alliance > bg.teamKills.horde ? 'alliance' : 'horde';
|
|
255
|
+
app.say(yellow(`Time. The ${TEAM[world.winner]} take it on kills, ${bg.teamKills[world.winner]} to ${bg.teamKills[OTHER_TEAM[world.winner]]}.`));
|
|
256
|
+
}
|
|
163
257
|
const won = world.winner === bg.team, drawn = world.winner == null;
|
|
164
258
|
const marks = bg.isAb ? BG_MARK_SPELL.ab : BG_MARK_SPELL.wsg;
|
|
165
259
|
const spell = marks ? app.ds.spell(won ? marks.winner : marks.loser) : null;
|
|
@@ -191,12 +285,246 @@ function leave(app, bg) {
|
|
|
191
285
|
app.bg = null;
|
|
192
286
|
}
|
|
193
287
|
|
|
288
|
+
// ---- places ------------------------------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
/** The places you can be sent, named from your side of the field. */
|
|
291
|
+
function zonesOf(bg) {
|
|
292
|
+
if (bg.zones) return bg.zones;
|
|
293
|
+
const t = bg.team, f = bg.foe;
|
|
294
|
+
const z = [];
|
|
295
|
+
if (bg.isAb) {
|
|
296
|
+
z.push({ id: 'our-base', label: 'our base', pos: xy(AB_GEOMETRY.baseGraveyard[t]) });
|
|
297
|
+
for (const n of AB_NODES) z.push({ id: n.id, label: `the ${n.name}`, pos: { x: n.x, y: n.y } });
|
|
298
|
+
z.push({ id: 'their-base', label: 'their base', pos: xy(AB_GEOMETRY.baseGraveyard[f]) });
|
|
299
|
+
} else {
|
|
300
|
+
const g = WSG_GEOMETRY;
|
|
301
|
+
const buff = (kind, side) => xy(g.powerups.find((p) => p.id === `${kind}-${side[0]}`));
|
|
302
|
+
z.push(
|
|
303
|
+
{ id: 'our-base', label: 'our flag room', pos: xy(g.flagBase[t]) },
|
|
304
|
+
{ id: 'our-gy', label: 'our graveyard', pos: xy(g.graveyard[t]) },
|
|
305
|
+
{ id: 'our-speed', label: 'our speed buff', pos: buff('speed', t) },
|
|
306
|
+
{ id: 'our-regen', label: 'our restoration buff', pos: buff('regen', t) },
|
|
307
|
+
{ id: 'our-zerk', label: 'our berserking buff', pos: buff('zerk', t) },
|
|
308
|
+
{ id: 'mid', label: 'mid', pos: xy(g.mid) },
|
|
309
|
+
{ id: 'their-zerk', label: 'their berserking buff', pos: buff('zerk', f) },
|
|
310
|
+
{ id: 'their-regen', label: 'their restoration buff', pos: buff('regen', f) },
|
|
311
|
+
{ id: 'their-speed', label: 'their speed buff', pos: buff('speed', f) },
|
|
312
|
+
{ id: 'their-gy', label: 'their graveyard', pos: xy(g.graveyard[f]) },
|
|
313
|
+
{ id: 'their-base', label: 'their flag room', pos: xy(g.flagBase[f]) },
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
bg.zones = z;
|
|
317
|
+
return z;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Where on the field a point is, in words: the nearest named place, else which half. */
|
|
321
|
+
function placeName(bg, pos) {
|
|
322
|
+
if (!pos) return 'the field';
|
|
323
|
+
let best = null, bd = 60;
|
|
324
|
+
for (const z of zonesOf(bg)) { const d = dist(z.pos, pos); if (d < bd) { bd = d; best = z; } }
|
|
325
|
+
if (best) return best.label;
|
|
326
|
+
if (bg.isAb) return 'the field';
|
|
327
|
+
return (pos.x < WSG_GEOMETRY.mid.x ? 'horde' : 'alliance') === bg.team ? 'our half' : 'their half';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---- the map strip -----------------------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
function mapWidth(app) { return Math.max(40, Math.min(72, app.term.cols - 6)); }
|
|
333
|
+
|
|
334
|
+
/** The gulch as a strip: flags, graveyards (G), buffs (s r z), each side's bodies, fights, and you as @. */
|
|
335
|
+
function wsgMapRows(app, bg) {
|
|
336
|
+
const { world } = bg;
|
|
337
|
+
const W = mapWidth(app), F = WSG_FIELD;
|
|
338
|
+
const colYd = (F.x1 - F.x0) / W, rowYd = (F.y1 - F.y0) / MAP_ROWS;
|
|
339
|
+
const colOf = (pos) => Math.min(W - 1, Math.max(0, Math.floor((pos.x - F.x0) / colYd)));
|
|
340
|
+
const rowOf = (pos) => Math.min(MAP_ROWS - 1, Math.max(0, Math.floor((F.y1 - pos.y) / rowYd)));
|
|
341
|
+
const cells = Array.from({ length: MAP_ROWS }, () => Array.from({ length: W }, () => ({ ch: ' ', paint: null, prio: -1 })));
|
|
342
|
+
const put = (pos, ch, paint, prio) => {
|
|
343
|
+
const cell = cells[rowOf(pos)][colOf(pos)];
|
|
344
|
+
if (prio > cell.prio) { cell.ch = ch; cell.paint = paint; cell.prio = prio; }
|
|
345
|
+
};
|
|
346
|
+
const midCol = colOf(WSG_GEOMETRY.mid);
|
|
347
|
+
for (let r = 0; r < MAP_ROWS; r++) put({ x: F.x0 + (midCol + 0.5) * colYd, y: F.y1 - (r + 0.5) * rowYd }, GLYPH.mid, dim, 0);
|
|
348
|
+
for (const side of ['alliance', 'horde']) put(WSG_GEOMETRY.graveyard[side], 'G', dim, 1);
|
|
349
|
+
for (const o of world.objectives.values()) {
|
|
350
|
+
if (o.kind === 'powerup') put(o.pos, POWERUP_CHAR[o.id.split('-')[0]] ?? '?', o.state === 'up' ? green : dim, 1);
|
|
351
|
+
else if (o.kind === 'flag' && o.owner) {
|
|
352
|
+
put(WSG_GEOMETRY.flagBase[o.owner], GLYPH.flag, o.state === 'base' ? (o.owner === 'horde' ? red : cyan) : dim, 2);
|
|
353
|
+
if (o.state === 'ground') put(o.pos, GLYPH.flag, yellow, 3);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Bodies, tallied per cell. Ours in full; theirs only as far as our side can see them (BgView).
|
|
357
|
+
const tally = new Map();
|
|
358
|
+
const add = (u, ours) => {
|
|
359
|
+
const key = rowOf(u.pos) * W + colOf(u.pos);
|
|
360
|
+
const t = tally.get(key) ?? { pos: u.pos, ours: 0, theirs: 0, carrier: null, self: false };
|
|
361
|
+
if (ours) t.ours++; else t.theirs++;
|
|
362
|
+
if (u.carryingFlag) t.carrier = ours ? bg.team : bg.foe;
|
|
363
|
+
if (u.id === bg.selfId) t.self = true;
|
|
364
|
+
tally.set(key, t);
|
|
365
|
+
};
|
|
366
|
+
for (const u of world.livingOf(bg.team)) add(u, true);
|
|
367
|
+
for (const e of world.viewFor(bg.team).enemies) if (e.alive) add(e, false);
|
|
368
|
+
for (const t of tally.values()) {
|
|
369
|
+
if (t.self) put(t.pos, '@', bold, 6);
|
|
370
|
+
else if (t.ours && t.theirs) put(t.pos, GLYPH.fight, (s) => bold(red(s)), 5);
|
|
371
|
+
else if (t.carrier) put(t.pos, t.carrier[0].toUpperCase(), (s) => bold(yellow(s)), 5);
|
|
372
|
+
else {
|
|
373
|
+
const side = t.ours ? bg.team : bg.foe, n = t.ours || t.theirs;
|
|
374
|
+
put(t.pos, n > 1 ? (n > 9 ? '+' : String(n)) : side[0], (s) => paintTeam(side, s), 4);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const left = 'Horde ', right = ' Alliance', midLabel = ' mid ';
|
|
378
|
+
const midStart = Math.max(left.length + 1, Math.min(W - right.length - midLabel.length - 1, midCol - 2));
|
|
379
|
+
const header = red(left) + dim(GLYPH.dash.repeat(midStart - left.length) + midLabel + GLYPH.dash.repeat(Math.max(0, W - right.length - midStart - midLabel.length))) + cyan(right);
|
|
380
|
+
return [H(` ${header}`), ...cells.map((row) => H(` ${row.map((c) => (c.paint ? c.paint(c.ch) : c.ch)).join('')}`))];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Lines under the map: one per flag carrier, then one per fight, largest first. */
|
|
384
|
+
const FIELD_LINES = 4;
|
|
385
|
+
|
|
386
|
+
// Always FIELD_LINES rows, blank when quiet, so the cursor does not jump as fights start and end under it.
|
|
387
|
+
function fieldLines(bg) {
|
|
388
|
+
const { world } = bg;
|
|
389
|
+
const lines = [];
|
|
390
|
+
if (!bg.isAb) {
|
|
391
|
+
for (const o of world.objectives.values()) {
|
|
392
|
+
if (o.kind !== 'flag' || o.state !== 'carried') continue;
|
|
393
|
+
const c = world.units.get(o.carrier);
|
|
394
|
+
if (!c || !world.alive(c)) continue;
|
|
395
|
+
const ours = c.team === bg.team;
|
|
396
|
+
const near = world.unitsNear(c.pos.x, c.pos.y, 40).filter((u) => u.team !== c.team && world.alive(u)).length;
|
|
397
|
+
lines.push(`${paintTeam(c.team, c.name)} ${ours ? 'carries their flag' : 'has our flag'} near ${placeName(bg, c.pos)}, ${Math.round((c.hp / c.maxHp) * 100)}%${near ? `, ${near} ${ours ? 'chasing' : 'of ours on it'}` : ''}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const shards = [];
|
|
401
|
+
for (const s of world.currentShards()) {
|
|
402
|
+
let ours = 0, theirs = 0, self = false, cx = 0, cy = 0, n = 0;
|
|
403
|
+
for (const id of s.units) {
|
|
404
|
+
const u = world.units.get(id);
|
|
405
|
+
if (!u || !world.alive(u)) continue;
|
|
406
|
+
if (u.team === bg.team) ours++; else theirs++;
|
|
407
|
+
if (id === bg.selfId) self = true;
|
|
408
|
+
cx += u.pos.x; cy += u.pos.y; n++;
|
|
409
|
+
}
|
|
410
|
+
if (ours && theirs) shards.push({ ours, theirs, self, pos: { x: cx / n, y: cy / n } });
|
|
411
|
+
}
|
|
412
|
+
shards.sort((a, b) => (b.ours + b.theirs) - (a.ours + a.theirs));
|
|
413
|
+
for (const s of shards) lines.push(`${paintTeam(bg.team, String(s.ours))}v${paintTeam(bg.foe, String(s.theirs))} at ${placeName(bg, s.pos)}${s.self ? bold(' you are in it') : ''}`);
|
|
414
|
+
while (lines.length < FIELD_LINES) lines.push('');
|
|
415
|
+
return lines.slice(0, FIELD_LINES);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ---- orders ------------------------------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
const goTo = (z) => ({ label: `going to ${z.label}`, make: () => ({ order: { kind: 'attack-move', to: z.pos } }) });
|
|
421
|
+
|
|
422
|
+
/** Their flag, wherever it is: at its base or on the ground, walk to it; carried by one of ours, escort them. */
|
|
423
|
+
const takeTheirFlag = (bg) => ({
|
|
424
|
+
label: 'taking their flag',
|
|
425
|
+
make: () => {
|
|
426
|
+
const { world } = bg;
|
|
427
|
+
const me = world.units.get(bg.selfId);
|
|
428
|
+
if (me?.carryingFlag) return { label: 'running the flag home', order: { kind: 'move', to: xy(WSG_GEOMETRY.flagBase[bg.team]) } };
|
|
429
|
+
const f = world.objectives.get(flagObjectiveId(bg.foe));
|
|
430
|
+
if (f?.state === 'carried') {
|
|
431
|
+
const c = world.units.get(f.carrier);
|
|
432
|
+
return c ? { label: `escorting ${c.name}`, order: { kind: 'follow', target: c.id } } : null;
|
|
433
|
+
}
|
|
434
|
+
return { order: { kind: 'attack-move', to: f?.state === 'ground' ? xy(f.pos) : xy(WSG_GEOMETRY.flagBase[bg.foe]) } };
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const followUnit = (bg, id, verb) => ({
|
|
439
|
+
label: `${verb} ${unitName(bg, id)}`,
|
|
440
|
+
make: () => { const u = bg.world.units.get(id); return u && bg.world.alive(u) ? { order: { kind: 'follow', target: id } } : null; },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
function setMyOrder(app, bg, standing) {
|
|
444
|
+
const me = bg.world.units.get(bg.selfId);
|
|
445
|
+
if (!me) return;
|
|
446
|
+
me.human = true;
|
|
447
|
+
bg.my = standing;
|
|
448
|
+
bg.myLabel = standing.label;
|
|
449
|
+
app.say(dim(`You are ${standing.label}.`));
|
|
450
|
+
driveSelf(app, bg);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function pickOrder(app, bg) {
|
|
454
|
+
const { world } = bg;
|
|
455
|
+
const me = world.units.get(bg.selfId);
|
|
456
|
+
const opts = [];
|
|
457
|
+
if (me?.carryingFlag) opts.push({ label: 'Run the flag home', value: { label: 'running the flag home', make: () => ({ order: { kind: 'move', to: xy(WSG_GEOMETRY.flagBase[bg.team]) } }) } });
|
|
458
|
+
if (!bg.isAb) opts.push({ label: 'Take their flag', value: takeTheirFlag(bg) });
|
|
459
|
+
for (const z of zonesOf(bg)) opts.push({ label: `Go to ${z.label}`, value: goTo(z) });
|
|
460
|
+
opts.push({ label: 'Follow an ally…', value: 'follow' }, { label: 'Attack an enemy you can see…', value: 'attack' }, { label: `Autopilot: ${me?.human ? 'hand your seat back to a job' : ROLE_TEXT[me?.role] ?? 'pick a job'}`, value: 'auto' });
|
|
461
|
+
app.pick('Your orders', opts, (v) => {
|
|
462
|
+
if (v === 'auto') return pickRole(app, bg);
|
|
463
|
+
if (v === 'follow') {
|
|
464
|
+
const allies = world.livingOf(bg.team).filter((u) => u.id !== bg.selfId);
|
|
465
|
+
if (!allies.length) return app.say(dim('Nobody of yours is up.'));
|
|
466
|
+
return app.pick('Follow', allies.map((u) => ({ label: `${u.name} ${dim(ROLE_TEXT[u.role] ?? u.role)}${u.carryingFlag ? yellow(' carrying the flag') : ''}`, value: u.id })), (id) => setMyOrder(app, bg, followUnit(bg, id, 'following')));
|
|
467
|
+
}
|
|
468
|
+
if (v === 'attack') {
|
|
469
|
+
const seen = world.viewFor(bg.team).enemies.filter((e) => e.alive);
|
|
470
|
+
if (!seen.length) return app.say(dim('You see no enemy right now.'));
|
|
471
|
+
return app.pick('Attack', seen.map((e) => ({ label: `${e.name} ${dim(`${e.hpPct}%, near ${placeName(bg, e.pos)}`)}${e.carryingFlag ? yellow(' has our flag') : ''}`, value: e.id })), (id) => setMyOrder(app, bg, followUnit(bg, id, 'attacking')));
|
|
472
|
+
}
|
|
473
|
+
setMyOrder(app, bg, v);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** A job for the strategy layer to play your seat to; your own orders are dropped. */
|
|
478
|
+
function pickRole(app, bg) {
|
|
479
|
+
const roles = bg.isAb ? ['node-assault', 'node-defense'] : ['flag-carrier', 'fc-escort', 'offense', 'defense', 'mid'];
|
|
480
|
+
app.pick('Your job', roles.map((r) => ({ label: ROLE_TEXT[r], value: r })), (r) => {
|
|
481
|
+
const me = bg.world.units.get(bg.selfId);
|
|
482
|
+
if (!me) return;
|
|
483
|
+
me.role = r; me.human = false; bg.my = null; bg.myLabel = null;
|
|
484
|
+
app.say(dim(`You will ${ROLE_TEXT[r]}.`));
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** One order to everyone of yours who is up, held for CALL_MS, then the strategy layer has them back. */
|
|
489
|
+
function teamCall(app, bg, say, orderFor) {
|
|
490
|
+
const { world } = bg;
|
|
491
|
+
let n = 0;
|
|
492
|
+
for (const u of world.livingOf(bg.team)) {
|
|
493
|
+
if (u.id === bg.selfId) continue;
|
|
494
|
+
const o = orderFor(u);
|
|
495
|
+
if (o && world.issue(u.id, o, bg.team)) { bg.commanded.set(u.id, world.t + CALL_MS); n++; }
|
|
496
|
+
}
|
|
497
|
+
app.say(n ? yellow(say) + dim(` ${n} answer, for ${CALL_MS / 1000} s.`) : dim('Nobody of yours is up to answer.'));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function pickTeamCall(app, bg) {
|
|
501
|
+
const opts = [];
|
|
502
|
+
const call = (label, say, orderFor) => opts.push({ label, value: () => teamCall(app, bg, say, orderFor) });
|
|
503
|
+
if (!bg.isAb) {
|
|
504
|
+
const tc = theirCarrier(bg);
|
|
505
|
+
if (tc) call(`All on their carrier, ${tc.name}`, `You call: all on ${tc.name}!`, () => ({ kind: 'follow', target: tc.id }));
|
|
506
|
+
call('Everyone to mid', 'You call: everyone to mid!', () => ({ kind: 'attack-move', to: xy(WSG_GEOMETRY.mid) }));
|
|
507
|
+
call('Turtle: everyone back to our flag room', 'You call: turtle!', () => ({ kind: 'attack-move', to: xy(WSG_GEOMETRY.flagBase[bg.team]) }));
|
|
508
|
+
call('Push their flag room', 'You call: push their base!', () => ({ kind: 'attack-move', to: xy(WSG_GEOMETRY.flagBase[bg.foe]) }));
|
|
509
|
+
} else {
|
|
510
|
+
for (const n of AB_NODES) call(`Everyone to the ${n.name}`, `You call: everyone to the ${n.name}!`, () => ({ kind: 'attack-move', to: { x: n.x, y: n.y } }));
|
|
511
|
+
}
|
|
512
|
+
call('Escort me', 'You call: on me!', () => ({ kind: 'follow', target: bg.selfId }));
|
|
513
|
+
opts.push({ label: 'Back to your jobs', value: () => { bg.commanded.clear(); app.say(dim('Your side goes back to its jobs.')); } });
|
|
514
|
+
app.pick('Team call', opts, (fn) => fn());
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---- the view ----------------------------------------------------------------------------------------------
|
|
518
|
+
|
|
194
519
|
function bgView(app, bg) {
|
|
195
520
|
const self = () => bg.world.units.get(bg.selfId);
|
|
196
521
|
return {
|
|
197
|
-
id: 'bg', title: bg.def.name, hint: 'Enter:
|
|
522
|
+
id: 'bg', title: bg.def.name, hint: 'Enter: act', noDetail: true,
|
|
198
523
|
onClose: (a) => leave(a, bg),
|
|
199
524
|
keys: {
|
|
525
|
+
o: { hint: 'your orders', fn: (a) => pickOrder(a, bg) },
|
|
526
|
+
t: { hint: 'team call', fn: (a) => pickTeamCall(a, bg) },
|
|
527
|
+
j: { hint: 'autopilot job', fn: (a) => pickRole(a, bg) },
|
|
200
528
|
space: { hint: 'pause', fn: () => { bg.paused = !bg.paused; } },
|
|
201
529
|
'+': { hint: 'faster', fn: () => { bg.speed = Math.min(16, bg.speed * 2); } },
|
|
202
530
|
'-': { fn: () => { bg.speed = Math.max(1, bg.speed / 2); } },
|
|
@@ -206,38 +534,39 @@ function bgView(app, bg) {
|
|
|
206
534
|
const me = self();
|
|
207
535
|
const t = Math.max(0, world.t - COUNTDOWN_MS);
|
|
208
536
|
const clock = `${Math.floor(t / 60000)}:${String(Math.floor((t % 60000) / 1000)).padStart(2, '0')}`;
|
|
209
|
-
const
|
|
537
|
+
const up = `${paintTeam(bg.team, `${world.livingOf(bg.team).length}/${world.unitsOf(bg.team).length} up`)} ${dim('vs')} ${paintTeam(bg.foe, `${world.livingOf(bg.foe).length}/${world.unitsOf(bg.foe).length}`)}`;
|
|
538
|
+
const kills = `${dim('kills')} ${paintTeam(bg.team, String(bg.teamKills[bg.team]))}${dim('-')}${paintTeam(bg.foe, String(bg.teamKills[bg.foe]))}`;
|
|
539
|
+
const rows = [H(`${bold(bg.def.name)} ${scoreText(bg)} ${dim(world.gatesOpen ? clock : 'gates closed')} ${up} ${kills}${bg.paused ? red(' PAUSED') : ''}${bg.speed !== 1 ? dim(` ${bg.speed}x`) : ''}`)];
|
|
210
540
|
if (bg.isAb) {
|
|
541
|
+
rows.push(H(''));
|
|
211
542
|
for (const n of AB_NODES) {
|
|
212
543
|
const o = world.objectives.get(n.id);
|
|
213
544
|
const who = o?.owner ? paintTeam(o.owner, TEAM[o.owner]) : dim('nobody');
|
|
214
545
|
rows.push(H(` ${fit(n.name, 14)} ${who}${o?.state === 'assaulted' ? yellow(' under assault') : ''}`));
|
|
215
546
|
}
|
|
216
547
|
} else {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
548
|
+
rows.push(...wsgMapRows(a, bg));
|
|
549
|
+
const flagText = (side) => {
|
|
550
|
+
const flag = world.objectives.get(flagObjectiveId(side));
|
|
551
|
+
const st = !flag ? '?' : flag.state === 'carried' ? `carried by ${unitName(bg, flag.carrier)}` : flag.state === 'ground' ? 'on the ground' : flag.state === 'respawning' ? 'captured, respawning' : 'at its base';
|
|
552
|
+
return `${paintTeam(side, `${TEAM[side]} flag`)} ${st}`;
|
|
553
|
+
};
|
|
554
|
+
rows.push(H(` ${flagText(bg.team)} ${flagText(bg.foe)}`));
|
|
222
555
|
}
|
|
223
|
-
|
|
556
|
+
for (const l of fieldLines(bg)) rows.push(H(` ${l}`));
|
|
557
|
+
rows.push(H(''));
|
|
224
558
|
if (me) {
|
|
225
559
|
const dead = !world.alive(me);
|
|
226
560
|
rows.push(H(` ${bold(me.name)} ${bar(Math.max(0, me.hp), me.maxHp, 20, dead ? red : green)} ${dead ? red(`dead, back in ${Math.ceil(Math.max(0, (me.deadUntilMs ?? world.t) - world.t) / 1000)} s`) : `${Math.round((me.hp / me.maxHp) * 100)}%`}${me.carryingFlag ? yellow(' carrying the flag!') : ''}`));
|
|
227
|
-
|
|
561
|
+
const what = me.human ? `You: ${cyan(bg.myLabel ?? 'holding here')}` : `Autopilot: ${cyan(ROLE_TEXT[me.role] ?? me.role)}`;
|
|
562
|
+
rows.push({ text: ` ${what} ${dim('Enter: your orders')}`, act: (a2) => pickOrder(a2, bg) });
|
|
563
|
+
const held = [...bg.commanded.keys()].filter((id) => { const u = world.units.get(id); return u && world.alive(u); }).length;
|
|
564
|
+
rows.push({ text: ` Team: ${held ? yellow(`${held} on your call`) : dim('on their jobs')} ${dim('Enter: a team call')}`, act: (a2) => pickTeamCall(a2, bg) });
|
|
228
565
|
}
|
|
229
|
-
rows.push(H(
|
|
230
|
-
if (bg.settled) rows.push(
|
|
231
|
-
else rows.push(
|
|
566
|
+
rows.push(H(dim(` This match: +${bg.honor} honor${bg.kills ? `, ${bg.kills} kills` : ''}${bg.caps ? `, ${bg.caps} ${bg.isAb ? 'bases worked' : 'captures'}` : ''}`)));
|
|
567
|
+
if (bg.settled) rows.push({ text: green('Leave the battleground'), act: (a2) => a2.pop() });
|
|
568
|
+
else rows.push({ text: dim('Leave now (the match goes on without you)'), act: (a2) => a2.confirm(`Leave ${bg.def.name}? You get no win honor and no marks.`, () => a2.pop()) });
|
|
232
569
|
return rows;
|
|
233
570
|
},
|
|
234
571
|
};
|
|
235
572
|
}
|
|
236
|
-
|
|
237
|
-
function pickRole(app, bg) {
|
|
238
|
-
const roles = bg.isAb ? ['node-assault', 'node-defense'] : ['flag-carrier', 'fc-escort', 'offense', 'defense', 'mid'];
|
|
239
|
-
app.pick('Your job', roles.map((r) => ({ label: ROLE_TEXT[r], value: r })), (r) => {
|
|
240
|
-
const me = bg.world.units.get(bg.selfId);
|
|
241
|
-
if (me) { me.role = r; app.say(dim(`You will ${ROLE_TEXT[r]}.`)); }
|
|
242
|
-
});
|
|
243
|
-
}
|