azeroth-shell 0.1.2 → 0.3.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 +362 -85
- package/bin/azsh.mjs +35 -19
- package/package.json +12 -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 +145 -0
- package/src/cli/iconGrid.mjs +100 -0
- package/src/cli/modelArt.mjs +298 -0
- package/src/cli/oocPrep.mjs +32 -0
- package/src/cli/paperdoll.mjs +76 -0
- package/src/cli/perfCheck.mjs +56 -0
- package/src/cli/repl.mjs +48 -7
- package/src/cli/rotationText.mjs +6 -6
- package/src/cli/sixel.mjs +319 -0
- package/src/cli/term.mjs +106 -12
- package/src/cli/text.mjs +21 -3
- package/src/cli/tui.mjs +262 -102
- package/src/cli/tuiAutopilot.mjs +356 -0
- package/src/cli/tuiBags.mjs +37 -4
- package/src/cli/tuiBg.mjs +370 -41
- package/src/cli/tuiCheck.mjs +633 -37
- package/src/cli/tuiCity.mjs +146 -41
- package/src/cli/tuiCompanionKit.mjs +6 -6
- package/src/cli/tuiCompanions.mjs +211 -41
- package/src/cli/tuiCreate.mjs +230 -0
- package/src/cli/tuiDungeon.mjs +57 -21
- package/src/cli/tuiFight.mjs +467 -88
- package/src/cli/tuiFightPanes.mjs +42 -14
- package/src/cli/tuiFloor.mjs +21 -9
- package/src/cli/tuiGear.mjs +17 -14
- package/src/cli/tuiLoot.mjs +6 -6
- package/src/cli/tuiMap.mjs +214 -0
- package/src/cli/tuiPaperdoll.mjs +108 -0
- package/src/cli/tuiPartyStage.mjs +206 -0
- package/src/cli/tuiPet.mjs +16 -8
- package/src/cli/tuiProfessions.mjs +27 -25
- package/src/cli/tuiRotationCond.mjs +390 -0
- package/src/cli/tuiSaves.mjs +24 -1
- package/src/cli/tuiSearch.mjs +79 -27
- package/src/cli/tuiSettings.mjs +3 -4
- package/src/cli/tuiStage.mjs +360 -0
- package/src/cli/tuiTalentTree.mjs +124 -0
- package/src/cli/tuiTalents.mjs +3 -4
- package/src/cli/tuiViews.mjs +633 -172
- package/src/cli/tuiWiki.mjs +2 -3
- package/src/cli/tuiWorld.mjs +90 -7
- 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-worn.bin.br +0 -0
- package/src/data/model-art.bin.gz +0 -0
- package/src/data/rotations/pve/paladin/retribution.json +2 -2
- package/src/data/tables/creatures.json.gz +0 -0
- package/src/data/world-map-sixel.bin.br +0 -0
- package/src/data/world-map.bin.gz +0 -0
- 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/character/character.ts +6 -0
- package/src/domain/pet/pet.ts +31 -0
- 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 +3494 -3449
- package/src/engine/combat/procsDamage.ts +31 -1
- package/src/engine/combat/sessionChase.ts +19 -2
- package/src/engine/combat/sessionMotionKit.ts +40 -2
- package/src/engine/game/loadReconcile.ts +21 -1
- package/src/engine/game/pets.ts +33 -1
- package/src/engine/game/types.ts +4 -0
- package/src/quests/quests.ts +60 -0
|
@@ -27,6 +27,19 @@ const enemyBase = (view: BgView): BgPos => {
|
|
|
27
27
|
return { x: g.x, y: g.y };
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
// Each side's flag runs keep to one edge of the field, past the brawl at the mid point: Alliance
|
|
31
|
+
// along the north side, Horde along the south, so the two runs never cross each other either.
|
|
32
|
+
// A run straight through mid put the carrier inside the mid fight's 30 yd shard every time.
|
|
33
|
+
const LANE_DY: Record<BgTeam, number> = { alliance: 90, horde: -90 };
|
|
34
|
+
const laneWaypoint = (team: BgTeam): BgPos => ({ x: WSG_GEOMETRY.mid.x, y: WSG_GEOMETRY.mid.y + LANE_DY[team] });
|
|
35
|
+
/** Toward `to` by the lane: the lane point while it is still ahead on the way, then `to` itself. */
|
|
36
|
+
const viaLane = (unit: BgUnitView, team: BgTeam, to: BgPos): BgPos => {
|
|
37
|
+
const wp = laneWaypoint(team);
|
|
38
|
+
const dir = Math.sign(to.x - unit.pos.x);
|
|
39
|
+
if (dir !== 0 && Math.sign(wp.x - unit.pos.x) === dir && Math.abs(wp.x - unit.pos.x) > 15) return wp;
|
|
40
|
+
return to;
|
|
41
|
+
};
|
|
42
|
+
|
|
30
43
|
const ourFlag = (view: BgView) => view.objectives.find((o) => o.id === flagObjectiveId(view.team));
|
|
31
44
|
const theirFlag = (view: BgView) =>
|
|
32
45
|
view.objectives.find((o) => o.id === flagObjectiveId(OTHER_TEAM[view.team]));
|
|
@@ -37,6 +50,21 @@ const ourCarrier = (view: BgView): BgUnitView | undefined =>
|
|
|
37
50
|
const theirCarrier = (view: BgView): BgUnitView | undefined =>
|
|
38
51
|
view.enemies.find((u) => u.carryingFlag && u.alive);
|
|
39
52
|
|
|
53
|
+
// Both flags carried: neither side can score until the other carrier dies and the flag returns. Left to
|
|
54
|
+
// escorting and holding, both teams sat on their own carrier at home and a 25-minute match had two
|
|
55
|
+
// pickups. The field goes to kill their carrier; the defenders stay with ours.
|
|
56
|
+
const doubleCarry = (view: BgView): boolean => !!ourCarrier(view) && !!theirCarrier(view);
|
|
57
|
+
|
|
58
|
+
const nearestOf = <T extends { pos: BgPos }>(from: BgPos, list: readonly T[]): T | undefined => {
|
|
59
|
+
let best: T | undefined;
|
|
60
|
+
let bd = Infinity;
|
|
61
|
+
for (const o of list) {
|
|
62
|
+
const d = dist(from, o.pos);
|
|
63
|
+
if (d < bd) { bd = d; best = o; }
|
|
64
|
+
}
|
|
65
|
+
return best;
|
|
66
|
+
};
|
|
67
|
+
|
|
40
68
|
const nearestVisibleEnemy = (unit: BgUnitView, view: BgView): BgUnitView | undefined => {
|
|
41
69
|
let best: BgUnitView | undefined;
|
|
42
70
|
let bd = Infinity;
|
|
@@ -55,7 +83,7 @@ const runFlagHome: BgAction = {
|
|
|
55
83
|
name: 'runFlagHome',
|
|
56
84
|
relevance: () => 100,
|
|
57
85
|
available: (unit) => !!unit.carryingFlag,
|
|
58
|
-
order: (
|
|
86
|
+
order: (unit, view) => ({ kind: 'move', to: viaLane(unit, view.team, homeBase(view)) }),
|
|
59
87
|
};
|
|
60
88
|
|
|
61
89
|
// Our own flag is on the ground: touching it returns it. Urgent — a dropped flag returns itself in 10 s,
|
|
@@ -70,18 +98,38 @@ const returnOurFlag: BgAction = {
|
|
|
70
98
|
},
|
|
71
99
|
};
|
|
72
100
|
|
|
101
|
+
/** How far the flag runner will step aside to help kill the enemy carrier; beyond it they keep running. */
|
|
102
|
+
const RUNNER_PEEL_YD = 60;
|
|
103
|
+
/** An enemy carrier this close pulls a body out of the fight it is in. */
|
|
104
|
+
const CARRIER_PEEL_YD = 60;
|
|
105
|
+
/** A carrier of ours this close does too. */
|
|
106
|
+
const CARRIER_HELP_YD = 100;
|
|
107
|
+
|
|
73
108
|
// Chase whoever has our flag. Its FLOOR must stay above holdOurBase (70): at 50, defenders
|
|
74
109
|
// preferred an already-safe base over retrieving the flag, and both teams sat at 0-0 forever.
|
|
110
|
+
// The flag runner is the exception: a cross-carry is how a stalemate breaks, so they only peel onto
|
|
111
|
+
// a carrier who runs past them.
|
|
75
112
|
const chaseEnemyCarrier: BgAction = {
|
|
76
113
|
name: 'chaseEnemyCarrier',
|
|
77
114
|
relevance: (unit, view) => {
|
|
78
115
|
const c = theirCarrier(view);
|
|
79
116
|
if (!c) return 0;
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
117
|
+
if (unit.role === 'flag-carrier') return 90;
|
|
118
|
+
// A carrier passing close outranks everything but the flag actions; a far one still outranks
|
|
119
|
+
// holding a base (70), but not the fight this body is in (82): with every mid fighter peeling
|
|
120
|
+
// onto a carrier 200 yd away, the whole brawl migrated and the runs always died in it.
|
|
121
|
+
return dist(unit.pos, c.pos) < CARRIER_PEEL_YD ? 95 : 75;
|
|
122
|
+
},
|
|
123
|
+
available: (unit, view) => {
|
|
124
|
+
if (unit.carryingFlag) return false;
|
|
125
|
+
const c = theirCarrier(view);
|
|
126
|
+
if (!c) return false;
|
|
127
|
+
const d = dist(unit.pos, c.pos);
|
|
128
|
+
if (unit.role === 'flag-carrier') return d < RUNNER_PEEL_YD;
|
|
129
|
+
// In a double carry a defender guards our carrier instead of crossing the map.
|
|
130
|
+
if (unit.role === 'defense' && doubleCarry(view)) return d < CARRIER_PEEL_YD;
|
|
131
|
+
return true;
|
|
83
132
|
},
|
|
84
|
-
available: (unit, view) => !unit.carryingFlag && !!theirCarrier(view),
|
|
85
133
|
// `follow`, NOT attack-move to a position: the strategy layer runs at 1 Hz, so a position order
|
|
86
134
|
// sends an equal-speed chaser to where the carrier was a second ago and never closes.
|
|
87
135
|
order: (_unit, view) => {
|
|
@@ -92,13 +140,18 @@ const chaseEnemyCarrier: BgAction = {
|
|
|
92
140
|
|
|
93
141
|
// Stay on our carrier and kill what touches them. Open to every non-defense role: with escorts
|
|
94
142
|
// alone a carrier ran 620 yd through the enemy team by itself and died within ~50 yd every time.
|
|
143
|
+
// A carrier within reach outranks the fight a body is in (82), as a passing enemy carrier does;
|
|
144
|
+
// one far away does not, so the mid fight is not abandoned for a run on the other side of the map.
|
|
95
145
|
const escortCarrier: BgAction = {
|
|
96
146
|
name: 'escortCarrier',
|
|
97
|
-
relevance: () =>
|
|
147
|
+
relevance: (unit, view) => {
|
|
148
|
+
const c = ourCarrier(view);
|
|
149
|
+
return c && dist(unit.pos, c.pos) < CARRIER_HELP_YD ? 84 : 80;
|
|
150
|
+
},
|
|
98
151
|
available: (unit, view) => {
|
|
99
152
|
if (unit.carryingFlag) return false;
|
|
100
153
|
if (unit.role === 'defense') return false;
|
|
101
|
-
return !!ourCarrier(view);
|
|
154
|
+
return !!ourCarrier(view) && !theirCarrier(view);
|
|
102
155
|
},
|
|
103
156
|
order: (_unit, view) => {
|
|
104
157
|
const c = ourCarrier(view);
|
|
@@ -106,18 +159,73 @@ const escortCarrier: BgAction = {
|
|
|
106
159
|
},
|
|
107
160
|
};
|
|
108
161
|
|
|
109
|
-
|
|
162
|
+
// Our flag runner, alive and not yet carrying: the body the escorts walk in with.
|
|
163
|
+
const ourRunner = (view: BgView): BgUnitView | undefined =>
|
|
164
|
+
view.own.find((u) => u.role === 'flag-carrier' && u.alive && !u.carryingFlag);
|
|
165
|
+
|
|
166
|
+
// Walk in with the runner. Escorts used to wait at mid for a carrier to exist, so the runner went
|
|
167
|
+
// into the enemy flag room alone, picked up, and died to the two defenders before anyone arrived.
|
|
168
|
+
const escortRunner: BgAction = {
|
|
169
|
+
name: 'escortRunner',
|
|
170
|
+
relevance: () => 58,
|
|
171
|
+
available: (unit, view) => !unit.carryingFlag && !ourCarrier(view) && !!ourRunner(view) && ourRunner(view)!.id !== unit.id,
|
|
172
|
+
order: (_unit, view) => {
|
|
173
|
+
const r = ourRunner(view);
|
|
174
|
+
return r ? { kind: 'follow', target: r.id } : null;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** An escort this close is with the runner; farther, the runner goes back for them. */
|
|
179
|
+
const ESCORT_WITH_YD = 25;
|
|
180
|
+
/** An escort farther than this from the runner is somebody the runner should wait for. */
|
|
181
|
+
const ESCORT_FAR_YD = 40;
|
|
182
|
+
|
|
183
|
+
/** Our living escorts who are not carrying anything. */
|
|
184
|
+
const ourEscorts = (view: BgView): BgUnitView[] =>
|
|
185
|
+
view.own.filter((u) => u.role === 'fc-escort' && u.alive && !u.carryingFlag);
|
|
186
|
+
|
|
187
|
+
// The runner does not walk into the enemy flag room alone. With escorts alive but elsewhere (held in
|
|
188
|
+
// a fight at mid, or just back from the graveyard) the runner goes to the nearest one; once an escort
|
|
189
|
+
// is beside them, grabEnemyFlag takes over and the group walks in together. Alone, a level-22 runner
|
|
190
|
+
// picked up and died to the two defenders in nine seconds, sixteen times in a row: 0-0 after 20 min.
|
|
191
|
+
const waitForEscort: BgAction = {
|
|
192
|
+
name: 'waitForEscort',
|
|
193
|
+
relevance: () => 86,
|
|
194
|
+
available: (unit, view) => {
|
|
195
|
+
if (unit.role !== 'flag-carrier' || unit.carryingFlag) return false;
|
|
196
|
+
const escorts = ourEscorts(view).filter((e) => e.id !== unit.id);
|
|
197
|
+
if (!escorts.length) return false;
|
|
198
|
+
if (escorts.some((e) => dist(e.pos, unit.pos) < ESCORT_WITH_YD)) return false;
|
|
199
|
+
return escorts.some((e) => dist(e.pos, unit.pos) > ESCORT_FAR_YD);
|
|
200
|
+
},
|
|
201
|
+
order: (unit, view) => {
|
|
202
|
+
const e = nearestOf(unit.pos, ourEscorts(view).filter((o) => o.id !== unit.id));
|
|
203
|
+
return e ? { kind: 'follow', target: e.id } : null;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** A dropped flag this close is anyone's to pick up before it returns itself in 10 s. */
|
|
208
|
+
const GROUND_FLAG_REACH_YD = 60;
|
|
209
|
+
|
|
210
|
+
// Go take their flag. The runner walks across the map for it; a fighter in reach of a dropped one
|
|
211
|
+
// picks it up (a pass, in WSG terms); everyone else only takes it from the ground when it is near.
|
|
110
212
|
const grabEnemyFlag: BgAction = {
|
|
111
213
|
name: 'grabEnemyFlag',
|
|
112
|
-
relevance: (unit) =>
|
|
214
|
+
relevance: (unit, view) => {
|
|
215
|
+
const f = theirFlag(view);
|
|
216
|
+
if (f?.state === 'ground' && unit.role !== 'defense' && dist(unit.pos, f.pos) < GROUND_FLAG_REACH_YD) return 90;
|
|
217
|
+
return unit.role === 'flag-carrier' ? 85 : unit.role === 'offense' ? 42 : 20;
|
|
218
|
+
},
|
|
113
219
|
available: (unit, view) => {
|
|
114
220
|
if (unit.carryingFlag) return false;
|
|
115
221
|
const f = theirFlag(view);
|
|
116
222
|
return f?.state === 'base' || f?.state === 'ground';
|
|
117
223
|
},
|
|
118
|
-
order: (
|
|
224
|
+
order: (unit, view) => {
|
|
119
225
|
const f = theirFlag(view);
|
|
120
|
-
|
|
226
|
+
const to = f ? f.pos : enemyBase(view);
|
|
227
|
+
// The runner walks the lane in; a dropped flag is wherever it fell, so it is walked to straight.
|
|
228
|
+
return { kind: 'attack-move', to: unit.role === 'flag-carrier' && f?.state !== 'ground' ? viaLane(unit, view.team, to) : to };
|
|
121
229
|
},
|
|
122
230
|
};
|
|
123
231
|
|
|
@@ -138,21 +246,38 @@ const holdOurBase: BgAction = {
|
|
|
138
246
|
},
|
|
139
247
|
};
|
|
140
248
|
|
|
141
|
-
/**
|
|
249
|
+
/** Standing this close to the mid point is holding it. */
|
|
250
|
+
const MID_HELD_YD = 40;
|
|
251
|
+
|
|
252
|
+
// Contest the middle — where the two flag runs cross. Walking there outranks an enemy in reach;
|
|
253
|
+
// once there, an enemy in reach outranks standing on the point, or two groups stood 50 yd apart
|
|
254
|
+
// at mid for a whole match and watched each other's runners walk past.
|
|
142
255
|
const contestMid: BgAction = {
|
|
143
256
|
name: 'contestMid',
|
|
144
|
-
relevance: (unit) =>
|
|
257
|
+
relevance: (unit) => {
|
|
258
|
+
if (unit.role !== 'mid') return 25;
|
|
259
|
+
return dist(unit.pos, WSG_GEOMETRY.mid) > MID_HELD_YD ? 55 : 30;
|
|
260
|
+
},
|
|
145
261
|
available: (unit) => !unit.carryingFlag,
|
|
146
262
|
order: () => ({ kind: 'attack-move', to: { ...WSG_GEOMETRY.mid } }),
|
|
147
263
|
};
|
|
148
264
|
|
|
149
|
-
/**
|
|
265
|
+
/** Touching distance: a fight you are in. */
|
|
266
|
+
const ENGAGE_CONTACT_YD = 25;
|
|
267
|
+
/** An enemy this close is worth walking over to. */
|
|
268
|
+
const ENGAGE_REACH_YD = 60;
|
|
269
|
+
|
|
270
|
+
// Fight what is in front of you. In contact it outranks escorting (80) and the offense grab (42), so
|
|
271
|
+
// a body finishes the fight it is in instead of walking off mid-swing with the enemy on its back; it
|
|
272
|
+
// stays under the runner's grab (85), the carrier chase and the flag actions. In reach it outranks
|
|
273
|
+
// holding mid; far away it is the floor.
|
|
150
274
|
const engageNearest: BgAction = {
|
|
151
275
|
name: 'engageNearest',
|
|
152
276
|
relevance: (unit, view) => {
|
|
153
277
|
const foe = nearestVisibleEnemy(unit, view);
|
|
154
278
|
if (!foe) return 0;
|
|
155
|
-
|
|
279
|
+
const d = dist(unit.pos, foe.pos);
|
|
280
|
+
return d < ENGAGE_CONTACT_YD ? 82 : d < ENGAGE_REACH_YD ? 45 : 10;
|
|
156
281
|
},
|
|
157
282
|
available: (unit, view) => !unit.carryingFlag && !!nearestVisibleEnemy(unit, view),
|
|
158
283
|
// Same reason as chaseEnemyCarrier: track the unit, not a one-second-old point.
|
|
@@ -174,7 +299,7 @@ const carrierDisengage: BgAction = {
|
|
|
174
299
|
);
|
|
175
300
|
return !helpNear;
|
|
176
301
|
},
|
|
177
|
-
order: (
|
|
302
|
+
order: (unit, view) => ({ kind: 'retreat', to: viaLane(unit, view.team, homeBase(view)) }),
|
|
178
303
|
};
|
|
179
304
|
|
|
180
305
|
// ---- Arathi Basin: nodes, not flags ----------------------------------------
|
|
@@ -192,16 +317,6 @@ const takeableNodes = (view: BgView) =>
|
|
|
192
317
|
const nodesUnderAssault = (view: BgView) =>
|
|
193
318
|
nodes(view).filter((o) => o.state === 'assaulted' && o.prevOwner === view.team);
|
|
194
319
|
|
|
195
|
-
const nearestOf = <T extends { pos: BgPos }>(from: BgPos, list: readonly T[]): T | undefined => {
|
|
196
|
-
let best: T | undefined;
|
|
197
|
-
let bd = Infinity;
|
|
198
|
-
for (const o of list) {
|
|
199
|
-
const d = dist(from, o.pos);
|
|
200
|
-
if (d < bd) { bd = d; best = o; }
|
|
201
|
-
}
|
|
202
|
-
return best;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
320
|
const abHome = (view: BgView): BgPos => {
|
|
206
321
|
const s = AB_GEOMETRY.start[view.team];
|
|
207
322
|
return { x: s.x, y: s.y };
|
|
@@ -475,7 +590,7 @@ export const AV_MID: BgPos = { ...AV_GEOMETRY.mid };
|
|
|
475
590
|
export const BG_STRATEGIES = {
|
|
476
591
|
'wsg.offense': {
|
|
477
592
|
name: '~wsg.offense',
|
|
478
|
-
actions: [runFlagHome, carrierDisengage, escortCarrier, chaseEnemyCarrier, grabEnemyFlag, engageNearest, contestMid],
|
|
593
|
+
actions: [runFlagHome, carrierDisengage, escortCarrier, chaseEnemyCarrier, waitForEscort, grabEnemyFlag, engageNearest, contestMid],
|
|
479
594
|
},
|
|
480
595
|
'wsg.defense': {
|
|
481
596
|
name: '~wsg.defense',
|
|
@@ -483,11 +598,11 @@ export const BG_STRATEGIES = {
|
|
|
483
598
|
},
|
|
484
599
|
'wsg.fc-escort': {
|
|
485
600
|
name: '~wsg.fc-escort',
|
|
486
|
-
actions: [runFlagHome, carrierDisengage, escortCarrier, chaseEnemyCarrier, engageNearest, contestMid],
|
|
601
|
+
actions: [runFlagHome, carrierDisengage, escortCarrier, chaseEnemyCarrier, grabEnemyFlag, escortRunner, engageNearest, contestMid],
|
|
487
602
|
},
|
|
488
603
|
'wsg.mid': {
|
|
489
604
|
name: '~wsg.mid',
|
|
490
|
-
actions: [returnOurFlag, chaseEnemyCarrier, escortCarrier, contestMid, engageNearest],
|
|
605
|
+
actions: [runFlagHome, carrierDisengage, returnOurFlag, chaseEnemyCarrier, escortCarrier, grabEnemyFlag, contestMid, engageNearest],
|
|
491
606
|
},
|
|
492
607
|
// Arathi Basin. No flag actions at all: the WSG bundles walk to WSG_GEOMETRY, which is map 489,
|
|
493
608
|
// so an AB unit on one of them marched off the plate and never touched a banner.
|
package/src/domain/bg/types.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface BgUnit {
|
|
|
75
75
|
order: BgOrder;
|
|
76
76
|
/** Combat spec — only needed when a shard is folded into a real createSession. */
|
|
77
77
|
spec?: CombatantSpec;
|
|
78
|
+
/** Does this seat heal? The attrition resolver reads it; absent, it falls back to the class. */
|
|
79
|
+
heals?: boolean;
|
|
78
80
|
/** Movement speed, yards/second. */
|
|
79
81
|
speedYd: number;
|
|
80
82
|
/** Set while this unit carries an enemy flag (the aura id, 23333 / 23335). */
|
package/src/domain/bg/wsg.ts
CHANGED
|
@@ -178,27 +178,19 @@ export function installWsg(world: BgWorld): WsgEvent[] {
|
|
|
178
178
|
|
|
179
179
|
if (obj.state !== 'base' && obj.state !== 'ground') continue;
|
|
180
180
|
|
|
181
|
-
// Someone standing on it either takes it (enemy) or returns it (owner, dropped only).
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
returnFlag(team, u);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
// Enemy flag: pick it up. One flag per unit.
|
|
195
|
-
if (u.carryingFlag) continue;
|
|
196
|
-
obj.state = 'carried';
|
|
197
|
-
obj.carrier = u.id;
|
|
198
|
-
u.carryingFlag = flagSpellCarriedBy(u.team);
|
|
199
|
-
log({ t: w.t, kind: 'pickup', team: u.team, unit: u.id });
|
|
200
|
-
break;
|
|
181
|
+
// Someone standing on it either takes it (enemy) or returns it (owner, dropped only). With
|
|
182
|
+
// both sides on a dropped flag the nearest hand wins; the first body in roster order used to,
|
|
183
|
+
// so the team added to the world first won every contested flag and every match.
|
|
184
|
+
const u = flagToucher(w, obj, team);
|
|
185
|
+
if (!u) continue;
|
|
186
|
+
if (u.team === team) {
|
|
187
|
+
returnFlag(team, u);
|
|
188
|
+
continue;
|
|
201
189
|
}
|
|
190
|
+
obj.state = 'carried';
|
|
191
|
+
obj.carrier = u.id;
|
|
192
|
+
u.carryingFlag = flagSpellCarriedBy(u.team);
|
|
193
|
+
log({ t: w.t, kind: 'pickup', team: u.team, unit: u.id });
|
|
202
194
|
}
|
|
203
195
|
|
|
204
196
|
// A carrier standing on its OWN flag, with that flag at base, scores.
|
|
@@ -257,6 +249,29 @@ export function installWsg(world: BgWorld): WsgEvent[] {
|
|
|
257
249
|
return events;
|
|
258
250
|
}
|
|
259
251
|
|
|
252
|
+
// The body that gets a flag everyone within reach wants. The owner's side can only return a dropped
|
|
253
|
+
// flag; the other side's runner is preferred over its escorts at the base, since a pickup by the
|
|
254
|
+
// escort sent the runner home empty-handed. Exact ties (bodies stacked on one point) alternate by
|
|
255
|
+
// tick, so neither side is favoured by roster order.
|
|
256
|
+
function flagToucher(w: BgWorld, obj: { pos: BgPos; state: string }, owner: BgTeam): BgUnit | null {
|
|
257
|
+
let best: BgUnit | null = null;
|
|
258
|
+
let bd = Infinity;
|
|
259
|
+
let bestRunner = false;
|
|
260
|
+
const odd = Math.floor(w.t / Math.max(1, w.tickMs)) % 2 === 1;
|
|
261
|
+
for (const u of w.units.values()) {
|
|
262
|
+
if (!w.alive(u)) continue;
|
|
263
|
+
const own = u.team === owner;
|
|
264
|
+
if (own ? obj.state !== 'ground' : !!u.carryingFlag) continue;
|
|
265
|
+
const d = detHypot(u.pos.x - obj.pos.x, u.pos.y - obj.pos.y);
|
|
266
|
+
if (d > WS_FLAG_INTERACT_YD) continue;
|
|
267
|
+
const runner = !own && u.role === 'flag-carrier';
|
|
268
|
+
if (!best || (runner && !bestRunner) || (runner === bestRunner && (d < bd || (d === bd && odd)))) {
|
|
269
|
+
best = u; bd = d; bestRunner = runner;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return best;
|
|
273
|
+
}
|
|
274
|
+
|
|
260
275
|
/** Where a dead unit of `team` comes back. Cite: world_safe_locs 771 / 772. */
|
|
261
276
|
export function wsgGraveyard(team: BgTeam): BgPos {
|
|
262
277
|
const gy = WSG_GEOMETRY.graveyard[team];
|
|
@@ -195,6 +195,9 @@ export interface PetState {
|
|
|
195
195
|
loyaltyPoints?: number;
|
|
196
196
|
/** Unspent training points for pet-trainer abilities. */
|
|
197
197
|
trainingPoints?: number;
|
|
198
|
+
// Pet untrain fee ladder: the last fee paid (copper) and when (epoch ms). Pet::m_resetTalentsCost/Time.
|
|
199
|
+
resetTalentsCost?: number;
|
|
200
|
+
resetTalentsTime?: number;
|
|
198
201
|
/** Owner XP remaining before the next loyalty level-up is allowed (SetRequiredXpForNextLoyaltyLevel). */
|
|
199
202
|
loyaltyXpRequired?: number;
|
|
200
203
|
// Ability ids the player right-click-disabled on the pet bar, rank-agnostic by name chain.
|
|
@@ -206,6 +209,9 @@ export interface PetState {
|
|
|
206
209
|
// Hunter pets: innate ability ids the OWNER can still learn from this pet, each cast rolling 10%
|
|
207
210
|
// to enter the Beast Training repertoire. Mirrors Pet::m_teachspells.
|
|
208
211
|
teachSpells?: number[];
|
|
212
|
+
// Hunter pets: the innate kit its tame charged training points for (InitPetCreateSpells). Absent on
|
|
213
|
+
// a pet tamed while the bake still handed every beast rank 1 of each family chain.
|
|
214
|
+
innateKit?: number[];
|
|
209
215
|
// Pet bar command: 'follow' (default) or 'stay'. 'attack' is a one-shot order and is never stored. Cite:
|
|
210
216
|
// Unit.h:819-824 CommandStates; PetActionBarFrame.lua PET_FOLLOW/PET_WAIT.
|
|
211
217
|
command?: 'follow' | 'stay';
|
package/src/domain/pet/pet.ts
CHANGED
|
@@ -667,6 +667,36 @@ export function grantPetLevelTrainingPoints(
|
|
|
667
667
|
return { ...pet, trainingPoints: (pet.trainingPoints ?? 0) + gain };
|
|
668
668
|
}
|
|
669
669
|
|
|
670
|
+
const PET_UNTRAIN_DAY_MS = 24 * 60 * 60 * 1000;
|
|
671
|
+
|
|
672
|
+
// Next pet-untrain fee in copper, Pet::resetTalentsCost: 10s, then 50s, then 1g, then +1g to a 10g
|
|
673
|
+
// cap; a day since the last untrain drops it back to 10s. Cite: Pet.cpp resetTalentsCost.
|
|
674
|
+
export function petUntrainCost(pet: PetState, nowMs: number): number {
|
|
675
|
+
const prev = pet.resetTalentsCost ?? 0;
|
|
676
|
+
const days = Math.floor((nowMs - (pet.resetTalentsTime ?? 0)) / PET_UNTRAIN_DAY_MS);
|
|
677
|
+
if (prev < 1000 || days > 0) return 1000;
|
|
678
|
+
if (prev < 5000) return 5000;
|
|
679
|
+
if (prev < 10000) return 10000;
|
|
680
|
+
return Math.min(100000, prev + 10000);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// HandlePetUnlearnOpcode: every taught ability goes and TP resets to level × (loyalty − 1). The innate
|
|
684
|
+
// kit is the creature's here (petKnownSpells reads petDefaultSpells), so it stays and keeps its tame
|
|
685
|
+
// charge, where CMaNGOS unlearns it too. Cite: PetHandler.cpp HandlePetUnlearnOpcode; Pet.cpp SetTP.
|
|
686
|
+
export function untrainPetState(pet: PetState, cdef: CreatureDef, petLevel: number, ds: DataStore): PetState {
|
|
687
|
+
const innate = pet.innateKit ?? cdef.petDefaultSpells ?? [];
|
|
688
|
+
const loyalty = pet.loyalty ?? LOYALTY_REBELLIOUS;
|
|
689
|
+
const { autocastOff, ...rest } = pet;
|
|
690
|
+
const keptOff = (autocastOff ?? []).filter((id) => innate.some((k) => ds.spell(k)?.name === ds.spell(id)?.name));
|
|
691
|
+
return {
|
|
692
|
+
...rest,
|
|
693
|
+
...(keptOff.length ? { autocastOff: keptOff } : {}),
|
|
694
|
+
loyalty,
|
|
695
|
+
taughtSpells: [],
|
|
696
|
+
trainingPoints: petLevel * (loyalty - 1) - tpSpentOnKnown(innate, ds),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
670
700
|
// Abilities the active pet can cast: the innate kit plus taught ranks, intersected with the
|
|
671
701
|
// creature's petSpells, highest LEVEL-ELIGIBLE rank per name. Generic-line passives are excluded.
|
|
672
702
|
export function petKnownSpells(char: Character, cdef: CreatureDef, ds: DataStore): SpellDef[] {
|
|
@@ -863,6 +893,7 @@ export function newPet(creature: number, ds: DataStore, char: Character): PetSta
|
|
|
863
893
|
// Training repertoire are taught per-pet for TP (game.teachPetSpell).
|
|
864
894
|
taughtSpells: [],
|
|
865
895
|
teachSpells,
|
|
896
|
+
innateKit: [...innate],
|
|
866
897
|
};
|
|
867
898
|
}
|
|
868
899
|
|
|
@@ -44,6 +44,8 @@ import { auraIdsForSnapshot, itemGrantsAdCommissionAura, snapshotHasAdCommission
|
|
|
44
44
|
import {
|
|
45
45
|
lootDropAllowed,
|
|
46
46
|
compareQuestDroppers,
|
|
47
|
+
pickQuestDropper,
|
|
48
|
+
dropperPickSalt,
|
|
47
49
|
creatureFarmLevel,
|
|
48
50
|
creatureFarmRank,
|
|
49
51
|
canTakeQuestForCondition,
|
|
@@ -935,6 +937,8 @@ function fightStep(
|
|
|
935
937
|
const skip = opts?.skipCreatures;
|
|
936
938
|
const hunt = huntableCreatures(ds, state.char.zoneId, state.questlog, state.char.race);
|
|
937
939
|
const summoned = new Set(corpseUseSummonedEntries(ds, def));
|
|
940
|
+
const salt = dropperPickSalt(state);
|
|
941
|
+
const killable: { entry: number; level: number; chance: number; rank: number }[] = [];
|
|
938
942
|
for (const kt of creatureCreditTargets(def)) {
|
|
939
943
|
if ((qs.kills[String(kt.creatureEntry)] ?? 0) >= kt.count) continue;
|
|
940
944
|
if (skip?.has(kt.creatureEntry)) continue;
|
|
@@ -949,7 +953,17 @@ function fightStep(
|
|
|
949
953
|
// Friendly + noXp: beginFight refuses it as script credit, so proposing it strands the quest.
|
|
950
954
|
if (isScriptCreditOnlyTarget(ds, kt.creatureEntry, state.questlog)) continue;
|
|
951
955
|
// Named kill target is the quest — fight it even when it is not on zone.huntable.
|
|
952
|
-
|
|
956
|
+
const cr = ds.creature(kt.creatureEntry);
|
|
957
|
+
if (cr) killable.push({ entry: kt.creatureEntry, level: creatureFarmLevel(cr), chance: 0, rank: creatureFarmRank(cr) });
|
|
958
|
+
}
|
|
959
|
+
if (killable.length > 0) {
|
|
960
|
+
// Several unmet kill objectives: rotate among the near-equal ones that are actually here, so a
|
|
961
|
+
// multi-template objective does not open on its first-listed entry every pull.
|
|
962
|
+
const here = new Set(hunt.map((c) => c.entry));
|
|
963
|
+
const local = killable.filter((k) => here.has(k.entry));
|
|
964
|
+
if (local.length === 0) return { kind: 'fight', creatureEntry: killable[0].entry };
|
|
965
|
+
local.sort((a, b) => compareQuestDroppers(a, b, state.char.level) || a.entry - b.entry);
|
|
966
|
+
return { kind: 'fight', creatureEntry: pickQuestDropper(local, state.char.level, salt)!.entry };
|
|
953
967
|
}
|
|
954
968
|
const farm = corpseUseFightEntries(ds, def, qs, state.inventory);
|
|
955
969
|
if (farm.length) {
|
|
@@ -986,7 +1000,8 @@ function fightStep(
|
|
|
986
1000
|
}
|
|
987
1001
|
const usable = [...byEntry.values()];
|
|
988
1002
|
usable.sort((a, b) => compareQuestDroppers(a, b, state.char.level) || a.entry - b.entry);
|
|
989
|
-
|
|
1003
|
+
const dropper = pickQuestDropper(usable, state.char.level, salt);
|
|
1004
|
+
if (dropper) return { kind: 'fight', creatureEntry: dropper.entry };
|
|
990
1005
|
if (unmetItems.length === 0) return null;
|
|
991
1006
|
// farm[] is one union over every itemTarget, and many named templates are not on zone.huntable.
|
|
992
1007
|
// Prefer an entry that drops what we still need; 40 of 1366 farm quests gate it behind a condition.
|
|
@@ -1008,7 +1023,8 @@ function fightStep(
|
|
|
1008
1023
|
? farmDrop
|
|
1009
1024
|
: farmScored.filter((s) => !farmCandidateIsHopeless(ds, def, s.ce, unmetItems, owned, snap));
|
|
1010
1025
|
farmPool.sort((a, b) => compareQuestDroppers(a, b, state.char.level) || a.ce - b.ce);
|
|
1011
|
-
|
|
1026
|
+
const farmPick = pickQuestDropper(farmPool, state.char.level, salt);
|
|
1027
|
+
if (farmPick) return { kind: 'fight', creatureEntry: farmPick.ce };
|
|
1012
1028
|
for (const it of unmetItems) {
|
|
1013
1029
|
const fight = pickFightable(ds, state, creatureDroppersForItem(ds, it.item, snap, state.char.level), skip);
|
|
1014
1030
|
if (fight != null) return { kind: 'fight', creatureEntry: fight };
|
|
@@ -1055,13 +1071,14 @@ function dungeonOnlyWork(
|
|
|
1055
1071
|
}
|
|
1056
1072
|
|
|
1057
1073
|
// Farm entries for one item, ordered the way fightStep picks: noise floor first, then compareQuestDroppers,
|
|
1058
|
-
// so the travel hop names the mob the fight will actually open on.
|
|
1074
|
+
// then the salted pick moved to the front, so the travel hop names the mob the fight will actually open on.
|
|
1059
1075
|
function rankFarmForItem(
|
|
1060
1076
|
ds: DataStore,
|
|
1061
1077
|
farm: readonly number[],
|
|
1062
1078
|
itemId: number,
|
|
1063
1079
|
snap: ConditionSnapshot,
|
|
1064
1080
|
playerLevel: number,
|
|
1081
|
+
salt: number,
|
|
1065
1082
|
): number[] {
|
|
1066
1083
|
const scored: { ce: number; chance: number; level: number; rank: number }[] = [];
|
|
1067
1084
|
const floor = dropperNoiseFloor(ds, itemId, snap);
|
|
@@ -1079,7 +1096,8 @@ function rankFarmForItem(
|
|
|
1079
1096
|
const drops = scored.filter((s) => s.chance > 0);
|
|
1080
1097
|
const pool = drops.length > 0 ? drops : scored;
|
|
1081
1098
|
pool.sort((a, b) => compareQuestDroppers(a, b, playerLevel) || a.ce - b.ce);
|
|
1082
|
-
|
|
1099
|
+
const first = pickQuestDropper(pool, playerLevel, salt);
|
|
1100
|
+
return first ? [first.ce, ...pool.filter((s) => s !== first).map((s) => s.ce)] : [];
|
|
1083
1101
|
}
|
|
1084
1102
|
|
|
1085
1103
|
function farmTravelStep(
|
|
@@ -1108,7 +1126,7 @@ function farmTravelStep(
|
|
|
1108
1126
|
if (countItem(ownedOf(state), it.item) >= it.count) continue;
|
|
1109
1127
|
if (isProvidedItem(def, it.item)) continue;
|
|
1110
1128
|
const ces = (def.farm ?? []).length
|
|
1111
|
-
? rankFarmForItem(ds, def.farm ?? [], it.item, snap, state.char.level)
|
|
1129
|
+
? rankFarmForItem(ds, def.farm ?? [], it.item, snap, state.char.level, dropperPickSalt(state))
|
|
1112
1130
|
: creatureDroppersForItem(ds, it.item, snap, state.char.level);
|
|
1113
1131
|
for (const ce of ces) {
|
|
1114
1132
|
if (opts?.skipCreatures?.has(ce)) continue;
|