Repository navigation
Expand file tree
/
Copy pathwarrior_guide.lua
More file actions
1802 lines (1730 loc) · 92.9 KB
/
Copy pathwarrior_guide.lua
File metadata and controls
1802 lines (1730 loc) · 92.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--=========================================================================================
-- Warrior next-action guide: pure decision engine, no WoW API and no clock.
--
-- The second provider for the frozen ST.<CLASS>.guide contract (nextaction_ui.lua's
-- header; the paladin ships the first). The live reads live in classes/warrior.lua
-- (ST.WARRIOR.gather_guide_inputs); this module only consumes the gathered inputs, so it
-- is unit-tested offline by tests/warrior_guide_state.lua.
--
-- FROZEN SPEC - display slots (ST.WARRIOR.guide.slots, ordered):
-- base set (6, the contract maximum: op, execute, slam, hs, cleave +
-- the Hamstring weave filler 7373): every family without a core reader
-- carries the filler in its sixth slot (Forever swings, so the 7373
-- weave filler has room there); the slot shows the GCD swipe until the
-- weave prompt + threshold/toggle options wiring lands (filed).
-- op Overpower 7384
-- execute Execute 5308
-- slam Slam 1464 (icon rank-resolved at post_init)
-- hs Heroic Strike 78 (icon rank-resolved at post_init)
-- cleave Cleave 845 (icon rank-resolved at post_init)
-- hamstring Hamstring 7373 (icon rank-resolved at post_init)
-- TBC set (7): op, execute, slam, hs, bt, ww, sunder. The cleave
-- slot is replaced, not added past: cleave is the least-TBC-relevant slot (no
-- rule ever outputs "cleave" - the slot only ever showed the Cleave swipe, and
-- Cleave has no cooldown of its own on the classic line), while Bloodthirst and
-- Whirlwind are the TBC fury core. Sunder upkeep (stacks < 5 / refresh
-- <= 10 s, the TBC + Forever APL single-target groups) takes the seventh
-- slot. The cleave readers stay in the gather
-- (cost/queue/cooldown), so the RIDE path and a future slot re-add keep working.
-- The TBC HUD keeps the six-ability row (hud_state.lua pins it and that
-- suite is owned by another lane): the sunder slot is guide-only until
-- the HUD lane mirrors it (the shaman 13-vs-8 deferral precedent).
-- Era set (7): op, execute, slam, hs, bt, ww, hamstring - the same
-- keys as TBC. Bloodthirst and Whirlwind are vanilla spells that exist on Era
-- (resolved through the same rank tables), so Era shares the TBC slot shape;
-- the cleave replacement reason and the reader retention above apply verbatim.
-- The Hamstring weave filler (7373 above 80 rage, sim-cited) takes the
-- seventh slot; every other family keeps the filler on the base set only
-- (no number cited / toggle-gated, slot + threshold filed).
-- Wrath fury set (6): op, execute, slam, hs, bt, ww - the same keys as
-- TBC/Era. The fury priority (BT > WW > Slam-proc > Execute-demoted > HS
-- dump) names exactly these; the conditional Rend (only with Improved Rend,
-- no Bloodsurge, BT/WW not coming off CD) stays tracker-only (the Rend
-- debuff bar already covers it) with no guide slot. No target-count source
-- exists on the client, so the WW-first-on-2+ rule is documented, not built.
-- Wrath arms set (6): op, execute, rend, ms, bladestorm, hs. Bloodthirst /
-- Whirlwind / Cleave have no arms rule (arms never prompts them), Slam is
-- the deferred early-tier filler (readers kept, rule gated off, HS covers
-- the filler window), and Rend / Mortal Strike / Bladestorm take their
-- places. Spec rule (documented): Mortal Strike known = arms (Mortal
-- Strike ranks 12294 / 21551 / 21552 / 21553 / 25248 / 30330 / 47485 /
-- 47486 per the Taste for Blood affected-spells list, wowhead.com/wotlk/
-- spell=56638). Slots are picked at post_init from the frozen spec probe;
-- a respec or rune change mid-session needs /reload (standard for slot
-- shapes; the engine flags freeze with the slots so the two never disagree
-- about which actions exist).
-- SoD set (8, ERA + runes): execute, slam, hs, bt, ww, raging_blow, rend,
-- op. Overpower returns for the stance-dance source (11585 + the gate aura
-- 426969 UNCONFIRMED); Rend upkeep (11574 + rune 412507 CONFIRMED, Blood
-- Frenzy) takes the seventh slot. SoD detection (documented): st.family
-- == "ERA" and (Raging Blow 402911 or Consumed by Rage 425440,
-- wowhead.com/classic/spell=425440, known via api.is_spell_known). Hamstring
-- fishing stays filed (no SoD threshold cited - the slot waits on its
-- number); Sunder upkeep is TBC + Forever only (no SoD APL group).
-- SoD Rend fires on the rune alone (the APL gates on the rune, not a
-- toggle); the Forever Rend toggle is filed with the options lane, so no
-- slot names "rend" on Forever yet.
-- Forever set (7): op, execute, slam, hs, cleave, hamstring, sunder.
-- Victory Rush reuses the execute slot (the engine's victory_rush bypass
-- names "execute", so no extra slot is needed), Bloodthrill reuses the OP
-- dodge-window reader (it arms the same window), and Hamstring rides the
-- sixth slot (the Forever 7373 weave filler). Sunder upkeep (stacks < 5 /
-- refresh <= 10 s) takes the seventh slot. Forever Rend upkeep stays filed
-- (toggle-gated in the sim, no toggle in-tree: the window rides the gather
-- for the toggle lane, no slot names "rend" on Forever yet).
--
-- FROZEN SPEC - inputs (all optional, nil never errors):
-- speed, swing_remaining, gcd_remaining swing/GCD (api.get_time() domain)
-- rage, hs_cost, cleave_cost rage and the on-next-attack costs
-- hs_queued, cleave_queued on-next-attack queue state
-- insufficient_rage the bar-colour shortfall flag
-- has_bloodsurge, slam_casting Bloodsurge proc / Slam cast state
-- overpower_remaining dodge-window countdown (nil once over)
-- target_health_pct nil = unknown (no target / no answer)
-- execute_pct db.guide_execute_pct, default 20
-- bt_remaining, ww_remaining Bloodthirst / Whirlwind cooldowns (TBC
-- and Era readers; both are vanilla
-- spells that exist on Era and resolve
-- through the same rank tables);
-- nil = no reader on this family; the
-- BT/WW rules require a known reading,
-- so nil never prompts
-- bt_cost, ww_cost rage costs through the current ranks;
-- nil falls back to the fixed costs
-- below (BT 30, WW 25 - the same on Era)
-- hs_rage_floor HS dump floor (rage to spare): 40 on TBC,
-- 40 on Era, 50 on Wrath (TBC-carried,
-- HELD - no Wrath sim source), 60 on SoD
-- (Consumed by Rage pooling, HELD -
-- needs a design decision, not a copy);
-- nil = no floor (every other family)
-- core_reserve_window Era BT/WW buffer (2.5 s, the midpoint of
-- the 2-3 s ground-truth window); nil =
-- the 1.5 s TBC default below (Wrath fury
-- and SoD carry their family's default:
-- 1.5 s on Wrath, 2.5 s on SoD)
-- execute_spam Era Execute spam below 20 % (true skips
-- the rule-2 core reservation); nil/false
-- keeps the TBC reservation. SoD feeds
-- true as well (Sudden Death dumps and
-- the sub-20 % spam never wait on the
-- core)
-- execute_phase_dump_gate the APL's execute-phase clause on the
-- rage dump (provider-fed true only on
-- the families whose APL forbids the
-- dump inside the execute phase - TBC
-- and Forever; nil everywhere else, so
-- the legacy shape holds there): a
-- KNOWN sub-threshold target withholds
-- the dump (rule 7)
-- wrath_fury, wrath_arms Wrath branch flags (provider-fed from the
-- frozen spec probe; nil everywhere else,
-- so all other families keep the legacy
-- matrix byte-identically)
-- sod SoD branch flag (ERA + runes; nil
-- elsewhere)
-- sudden_death Sudden Death proc active (aura 52437 on
-- Wrath, wowhead.com/wotlk/spell=52437;
-- 440495 plus the literal name fallback on
-- SoD - in-game to confirm which SoD aura
-- id fires; nil = no proc)
-- tfb Taste for Blood proc active (aura 60503,
-- wowhead.com/wotlk/spell=56638; nil = no
-- proc, so Overpower still needs its dodge
-- window; Rend-tick inference deferred)
-- sunder_stacks, sunder_remaining, sunder_duration
-- Sunder Armor upkeep window (TBC +
-- Forever only, single-target APL
-- groups; nil on every other family
-- and when the target scan finds
-- nothing). No rule consumes these
-- yet (no slot names "sunder") -
-- engine groundwork, slot + options
-- filed.
-- stance_form warrior stance form index via
-- api.GetShapeshiftForm (nil when
-- the probe answers 0/absent; the
-- per-family stance index map is
-- UNCONFIRMED). Consumed by rule D
-- (any non-zero form reads as "in a
-- stance" - the map stays filed).
-- stance the CURRENT stance, as a key:
-- "battle" | "berserker" |
-- "defensive" | nil (unknown).
-- The provider reads it from the
-- client's own stance auras (the
-- sim's stance ids: Battle 2457,
-- Defensive 71, Berserker 2458 -
-- wowsims/classic sim/warrior/
-- stances.go, fetched 2026-10-19),
-- NOT from the form index (that map
-- stays UNCONFIRMED). nil = unknown
-- = no gate (the client's own stance
-- rules then decide). Consumed by
-- the Overpower and Execute stance
-- gates below: the classic, sod and
-- tbc-new sims all register
-- Overpower with a Battle-stance
-- requirement, and Execute with
-- Battle|Berserker, so a prompt for
-- an uncastable spell is a false
-- prompt. The provider feeds the
-- stance only on the families whose
-- sims carry the requirement (ERA
-- incl. SoD, TBC); every other
-- family feeds nil, so the gate is
-- inert there.
-- sod_stance_aura SoD OP stance-dance gate aura live
-- (426969 UNCONFIRMED; nil = absent).
-- Consumed by rule D.
-- sod_rend_rune_known SoD Rend rune known (412507
-- CONFIRMED, Blood Frenzy,
-- operator in-client 2026-10-19;
-- nil = unknown). Fed separately
-- from the SoD branch flag (never
-- widens SoD detection); gates rule
-- R on SoD.
-- rend_upkeep_enabled Rend upkeep outside arms enabled
-- (provider-fed true only on SoD
-- with the rune known; nil/false
-- never prompts - the Forever
-- toggle is filed with the options
-- lane, so Forever feeds nil).
-- hamstring_known Hamstring weave filler known
-- (post_init-frozen knowledge; nil =
-- unlearned/unknown). Consumed by
-- rule W with the threshold below.
-- hamstring_rage_threshold Hamstring weave rage threshold
-- (provider-fed 80 on Era-base,
-- sim-cited; nil everywhere else -
-- no number cited / toggle-gated,
-- so the weave rule stays dormant
-- there until the options lane
-- ships the tuning).
-- rend_remaining, rend_duration Rend debuff window (nil = missing /
-- unknown, which prompts like the druid
-- rip/rake nil rule); rend_refresh is the
-- reapply threshold, default 3 (wotlk Rend
-- 47465, 15 s duration,
-- wowhead.com/wotlk/spell=47465;
-- in-game to confirm the window)
-- ms_remaining, ms_duration Mortal Strike cooldown (6 s,
-- wowhead.com/wotlk/spell=47486; nil =
-- no reader, never prompts - the known-
-- reading requirement, like BT/WW)
-- bladestorm_remaining, bladestorm_duration
-- Bladestorm cooldown (63784,
-- wowhead.com/wotlk/spell=63784; swipe
-- fallback 90 s, NEEDS IN-CLIENT
-- CONFIRMATION)
-- bladestorm_known Bladestorm talented (fresh
-- api.is_spell_known read; nil/false
-- never prompts an untalented slot)
-- raging_blow_remaining, raging_blow_duration
-- Raging Blow cooldown (8 s,
-- wowhead.com/classic/spell=402911;
-- nil = no reader)
-- execute_spam Era Execute spam below 20 % (true skips
-- the rule-2 core reservation); nil/false
-- keeps the TBC reservation
-- hs_cancel_cue TBC HS-cancel cue (provider-fed true only
-- on TBC): when rule 2 fires Execute with
-- HS/Cleave already queued, the overlay
-- reads CANCEL (cancel the queued
-- on-next-attack, don't spend its rage)
-- instead of riding it; nil everywhere
-- else (byte-identical)
-- victory_rush Forever Victory Rush ready (provider-fed
-- true only on FOREVER via the
-- family + knowledge + usable probe,
-- UNCONFIRMED id): Execute at any health,
-- like the SoD Sudden Death bypass
-- (still behind the OP window, still
-- before Slam/BT/WW); nil everywhere
-- else (byte-identical)
-- op_remaining, execute_remaining, slam_remaining, hs_remaining, cleave_remaining,
-- hamstring_remaining client cooldowns; gather filters the GCD
-- echo (duration <= 1.51 s) before they
-- reach here; the rule matrix reads
-- op_remaining / execute_remaining.
-- ms_remaining / bladestorm_remaining /
-- raging_blow_remaining ride the same
-- filter; rend_remaining is a debuff
-- window, not a cooldown.
-- utility_order, utility_order_low the utility spine's APL order for the running
-- family, split into the rows the APL puts
-- above the core rotation and the rows it
-- puts below. nil = no utility spine on
-- this family (the engine stays inert).
-- utility_lines' reads the utility spine's per-line reads (the
-- names live in st.warrior_guide.
-- UTILITY_LINES, so the tick path never
-- builds a string):
-- <key>_known (the client has the spell),
-- <key>_remaining (the spell's own
-- cooldown, 0 = ready, nil = unknown),
-- <key>_duration (the client-reported
-- cooldown length, the slot's swipe),
-- <key>_buff (false = the buff is not up,
-- true = it is, nil = unknown),
-- <key>_rage_max (the APL row's own rage
-- ceiling, nil = no rage gate).
-- utility_stance the CURRENT stance, read from the client's
-- own stance auras, on every family the
-- utility spine can ask a stance on (the
-- existing inputs.stance gate stays on the
-- families whose sims gate Overpower and
-- Execute on a stance, so that gate keeps
-- its byte-identical shape).
-- utility_wanted_stance the stance the APL's upkeep row wants
-- ("battle" / "berserker"), nil when the
-- family carries no stance row.
-- utility_weave true when the running family's APL weaves
-- into Battle Stance for the Overpower
-- (the SoD strictSequence, the TBC
-- Overpower Weaving group, the WotLK
-- strictSequence).
-- reaction optional, default 0.1
--
-- FROZEN SPEC - rule order (first match wins; the legacy rules keep their numbers,
-- the branch rules slot between them as lettered inserts):
-- A. (arms only) Rend upkeep: rend_remaining nil or <= rend_refresh ->
-- action "rend". Nil prompts (missing Rend, like the druid rip/rake nil
-- rule); exactly the threshold refreshes. Inert everywhere else (no Rend
-- upkeep outside arms).
-- B. (arms only) Execute: Sudden Death active (sudden_death) OR target below
-- the threshold, with Execute off cooldown -> action "execute". Outranks
-- Overpower (rule 1): SD-Execute above 20 % and the sub-20 % spam both
-- come first; below 20 % almost exclusively Execute except OP if
-- insufficient rage. No core reservation (arms runs no BT/WW core).
-- 1. the Overpower window is active (overpower_remaining > 0) - or, on arms,
-- the Taste for Blood proc is active (tfb) - and Overpower is off
-- cooldown -> action "op". A window of exactly 0 is already over.
-- The stance gate applies: the classic, sod and tbc-new sims all register
-- Overpower with a Battle-stance requirement, so a known non-Battle
-- stance withholds the prompt (an unknown stance never withholds).
-- D. (SoD only) the stance-dance source: the gate aura is live
-- (sod_stance_aura), the client reports a real stance form and
-- Overpower is ready -> action "op". The dodge window stays suppressed
-- on SoD (no dodge source in the SoD priority); every other family
-- keeps the legacy window shape (sod_dance is nil there).
-- 2. target_health_pct is known and < execute_pct and Execute is off cooldown and
-- the core does not claim the next GCD (see the reservation below) -> action
-- "execute". The stance gate applies (the sims allow Execute in Battle or
-- Berserker only): a known stance outside that set withholds the prompt
-- (an unknown stance never withholds). A Sudden Death proc (sod and sudden_death) or a ready Victory
-- Rush (victory_rush, Forever only) bypasses the health gate entirely
-- (Execute regardless of target health, still before Slam/BT/WW). When the
-- TBC HS-cancel cue is armed (hs_cancel_cue) and HS/Cleave is already
-- queued, the prompt carries hold = true, hold_text = "CANCEL": cancel the
-- queued on-next-attack before it spends its rage (tbc_minmax.md), instead
-- of the rule-4 RIDE. Era spam (execute_spam) skips the reservation: below
-- 20 % the guide spams Execute even when BT/WW is ready/soon, unlike the
-- TBC gate which never spends at the core's expense. SoD feeds
-- execute_spam as well. Skipped on arms (rule B above) and on wrath fury
-- (rule F below demotes it there).
-- 3. has_bloodsurge and not slam_casting -> action "slam". On wrath fury the
-- delay gate applies: withheld while Bloodthirst or Whirlwind is ready
-- (the 5 s proc window covers the wait - BT/WW first, Slam woven between).
-- Skipped on arms (no slam slot; the early-tier filler is deferred and HS
-- covers the window). Inert on TBC and
-- Era: Blood Surge is a WotLK talent proc (aura 46916), so the aura read
-- never fires on 2.5.6 or 1.15.9 and this rule can never match there; the
-- logic is untouched. SoD's Blood Surge rune shares aura 46916, so the
-- same rule serves SoD (Execute-SD above it, BT/WW below it, matching the
-- SoD priority).
-- 4. hs_queued or cleave_queued -> no action, hold = true, hold_text = "RIDE" (the
-- guide rides the queue the player already placed).
-- S. Sunder upkeep (TBC + Forever APL groups): should_refresh_sunder
-- (stacks < 5, or remaining inside the final 10 s) -> action "sunder".
-- Yields to procs, Execute range and a queued ride; outranks the core
-- rotation and the HS dump (maintenance before rotation, like the arms
-- Rend rule A). Nil on every other family (unknown hides). No
-- target-count source exists, so the single-target gate is documented,
-- not built (multi-target over-prompting is the known residual).
-- R. Rend upkeep outside arms: should_maintain_rend_outside_arms with
-- rend_upkeep_enabled (SoD rune-gated; the Forever toggle is filed, so
-- the rule stays dormant on Forever) -> action "rend". Missing applies
-- once enabled; yields to the same ride/proc/Execute shapes as rule S.
-- 5. Bloodthirst is known (a TBC, Era, Wrath-fury or SoD reader exists) and ready
-- (bt_remaining <= 0) -> action "bt". Skipped on arms (no BT rule there).
-- 6. Whirlwind is known and ready (ww_remaining <= 0) and no known
-- Bloodthirst lands inside the WW_DELAY window (1.5 s, the sim Delay
-- Check on Era / TBC / Forever - firing WW just before BT wastes the
-- GCD, so the guide waits) -> action "ww". A NaN BT reads as unknown.
-- Skipped on arms. (No target-count source exists, so the WW-first-on-2+
-- fury rule is documented, not built.)
-- F. (wrath fury only) Execute demotion: target below the threshold, Execute
-- off cooldown, BT/WW both cooling (neither ready), rage at/above the
-- floor and the core claiming nothing next (the rule-2 reservation with
-- the Execute spend) -> action "execute". Lowest priority even in Execute
-- phase: only with excess rage, no BT/WW up and everything else on CD.
-- G. (SoD only) Raging Blow: off cooldown -> action "raging_blow" (no rage
-- gate: the ability costs none). Sits after WW, before the HS dump,
-- matching the SoD priority.
-- H. (arms only) Bladestorm: bladestorm_known and off cooldown ->
-- action "bladestorm". Ordered by the earlier rules: Rend maintained
-- (rule A fires first when it lapses), no SD proc pending (rule B fires
-- first), Overpower current in its TfB cycle (rule 1 fires first). The
-- OP-used-in-cycle guard is deferred (no TfB cycle state yet).
-- I. (arms only) Mortal Strike: known and ready (ms_remaining <= 0) ->
-- action "ms" (the 6 s filler).
-- W. Hamstring rage-dump weave: should_weave_hamstring (rage above the
-- threshold with the filler known - Era 80 live, nil elsewhere) ->
-- action "hamstring". Below the core (BT/WW fire first), above the HS
-- dump: excess rage weaves instead of dumping.
-- W0. (utility spine) The Overpower weave's first step: the dance into Battle
-- Stance (utility_weave_stance). It sits ahead of rule 1 because the weave IS
-- the rule-1 cast - the sims register Overpower with a Battle-stance
-- requirement. Only where the running family's APL carries the weave, and
-- only while the Overpower window is live.
-- U1. (utility spine) The APL's HIGH utility rows, in the APL's own priority
-- order: the first row whose read says "ask for it" wins (utility_spine_pick).
-- The reads: the client knows the spell, the cooldown is ready, the row's buff
-- is not up and the rage pool is at or under the row's own ceiling. Every
-- gate is a known-gate (unknown hides).
-- M. (utility spine) The stance upkeep (utility_stance_action): the APL's last
-- row on the families that carry one - "cast the stance when its aura is not
-- active". It sits above the core rotation and yields to the procs, the
-- Execute range and a queued ride.
-- U2. (utility spine) The APL's LOW utility rows (the rows the APL puts below the
-- core rotation), in the APL's own priority order: the same reads as U1.
-- 7. rage is known:
-- * insufficient (rage < hs_cost when both are known, else the module's
-- insufficient_rage flag) -> no action, hold = true, hold_text = "HOLD";
-- * else, when the floor is set (hs_rage_floor: 40 TBC, 40 Era) and rage
-- is below it -> no action and no hold: rage is being pooled for
-- Bloodthirst/Whirlwind, not missing;
-- * else, when the core claims the next GCD (ready now, or landing
-- inside the reserve window - 1.5 s TBC, 2.5 s Era via
-- core_reserve_window - with too little rage left after the HS spend
-- to cover its cost) -> no action and no hold, for the same reason;
-- * else, when the family forbids the dump inside the execute phase
-- (execute_phase_dump_gate - the provider arms it on TBC and Forever,
-- whose APL rows gate the dump on NOT isExecutePhase E20) and the
-- target's health is KNOWN and below execute_pct -> no action and no
-- hold: the phase is the APL's move, not the filler's. An unknown
-- target health is not a phase, so the dump keeps its legacy shape;
-- * else hs_cost known, rage >= hs_cost and the swing window allows queueing
-- -> action "hs";
-- * else falls through to rule 8.
-- The swing window mirrors the paladin engine's rel_swing landmark (nextaction.lua:
-- swing_remaining - reaction / 2): a prompt inside the final reaction half-step of
-- the swing cannot ride a swing the player can still react to, so it is withheld.
-- Heroic Strike is off the GCD, so a running gcd_remaining never closes the window
-- (the input rides the same landmark set for tuning).
-- 8. no action.
-- "off cooldown" = the *_remaining input is nil or <= 0. The BT/WW/MS rules are
-- the exception: they additionally require a known reading (type() == "number"),
-- so a family without the core readers keeps the legacy six-rule behaviour
-- exactly. Bladestorm additionally requires bladestorm_known (an untalented
-- slot must stay dark). Sunder needs no known-reading gate beyond its own
-- helper (unknown stacks + fresh window hides, like the rule matrix pins).
-- Rend outside arms needs rend_upkeep_enabled == true (the provider feeds
-- it only on SoD with the rune known). Hamstring needs a numeric
-- hamstring_rage_threshold (the provider feeds 80 on Era-base only). The
-- family gating lives in the provider
-- (classes/warrior.lua feeds the bt/ww inputs, the floors, the Era window,
-- the Era/SoD spam flag, the Wrath/SoD branch flags, the upkeep windows
-- and the weave threshold/enable per family/spec);
-- this engine stays family-agnostic and pure.
--
-- Retail (12.x) inertness: st.family == "RETAIL" short-circuits evaluate to the
-- empty display shape before any rule below. The inert stand-in never reports a
-- swing, but target health (rule 2), the Blood Surge aura (rule 3), the queue
-- flags (rule 4) and rage/costs (rule 7, including its HOLD shortfall) all answer
-- on Retail, so those rules would prompt with no swing at all. Every other
-- family - including Forever, which swings - runs the matrix above unchanged; a
-- nil family (offline callers that predate it) keeps the old behaviour too.
--
-- Output: the frozen display fields are overwritten on every call:
-- action, hold, hold_text, filler_slot = nil, judge_slot = nil, rotation_key = nil,
-- waiting_judgement = false.
--=========================================================================================
local _, st = ...
st.warrior_guide = st.warrior_guide or {}
-- Rule constants (frozen; the options panel exposes only the execute threshold).
st.warrior_guide.EXECUTE_PCT_DEFAULT = 20
st.warrior_guide.REACTION_DEFAULT = 0.1
-- TBC fury core constants (sourced ground truth for the 2.5.6 fury priority:
-- Bloodthirst on CD > Whirlwind on CD > Heroic Strike dump only with rage to
-- spare, Execute below 20 % instead of HS and never at BT/WW's expense).
-- Bloodthirst costs 30 rage and Whirlwind 25 on TBC (fixed costs, used only when
-- the provider's live cost read is nil); Execute's base spend is 15 (it drains
-- extra rage into damage on top). The reserve window is one GCD: a core ability
-- landing inside it needs its rage now, so the dump must not take it.
-- Era (1.15.9) shares the costs (BT 30 / WW 25 / Execute 15 are vanilla values)
-- but feeds its own floor (40) and window (2.5 s) through the inputs above, and
-- Execute spam (no reservation) through execute_spam. Wrath fury carries a
-- 50-rage floor and the 1.5 s default window (TBC-carried, HELD: no Wrath sim
-- source in the warrior-rotfix re-verification, so the floor stays put rather
-- than guessing - warrior-rotfix journal 2026-10-19); SoD feeds
-- floor 60 (Consumed by Rage pooling, HELD: the SoD APLs gate 2H at rage >= 30
-- / DW unconditional + stance-dance rage management, which is a design
-- decision, not a number to copy - journal 2026-10-19) with the Era 2.5 s
-- window and spam.
local BT_RAGE_COST = 30
local WW_RAGE_COST = 25
local EXECUTE_RAGE_SPEND = 15
local CORE_RESERVE_WINDOW = 1.5
-- The reserve window, exposed for the practice surface (the core-order drill
-- asks whether a dump would have starved a core sitting inside it).
st.warrior_guide.CORE_RESERVE_WINDOW = CORE_RESERVE_WINDOW
-- Whirlwind delay (the sim "Delay Check"): Whirlwind fires only while
-- Bloodthirst is more than this far out. Era (classic dps_no_reck.apl.json:
-- spellTimeToReady BT > 1.5 s), TBC fury (BT 30335 >= 1.5 s) and Forever
-- (BT 23894 >= 1.5 s) all gate the WW line the same way - firing WW 0.5 s
-- before BT is a wasted GCD the sims never spend. Withheld while a known BT
-- lands inside the window (an unknown BT never withholds, like the
-- known-reading rule shape); the fall-through below re-waits instead of
-- spending (HS reservation covers the rage, or there is simply no action).
local WW_DELAY = 1.5
-- The Delay Check window, exposed for the practice surface (the drill that
-- grades the Whirlwind press measures its margin against this number).
st.warrior_guide.WW_DELAY = WW_DELAY
-- Rend upkeep window (arms rule A): reapply inside the final 3 s, never early.
-- Provisional (the guides only say "maintain / don't reapply early"); Rend's 15 s
-- duration is the wotlk 47465 value above. In-game to confirm.
local REND_REFRESH_DEFAULT = 3
-- Sunder upkeep window (TBC + Forever APL sunder groups, warrior-rotfix
-- journal 2026-10-19: re-stack below 5, refresh inside the final 10 s,
-- single-target only). Consumed by rule S (the TBC sunder slot + the
-- Forever sunder slot); the TBC HUD mirror waits on the HUD lane
-- (guide-only there, shaman 13-vs-8 precedent).
st.warrior_guide.SUNDER_STACKS_MAX = 5
st.warrior_guide.SUNDER_REFRESH_DEFAULT = 10
-- Tactical Mastery rage cap (tbc_minmax.md: cancel queued HS/Cleave before
-- Execute/stance swap, Tactical Mastery keeps at most 25 rage across the swap,
-- Sweeping Strikes costs 30). Pure and family-agnostic; the stance-event
-- wiring is deferred (no stance reads exist in the provider yet), so no rule
-- consumes this today - it pins the 25-cap maths for that wiring.
st.warrior_guide.TACTICAL_MASTERY_RAGE_CAP = 25
-- Rage surviving a stance swap: at most the cap with Tactical Mastery, nothing
-- without it (an unknown talent reads as no talent). Unknown rage answers nil.
function st.warrior_guide.rage_after_stance_swap(rage, tactical_mastery)
if type(rage) ~= "number" or rage ~= rage then
return nil
end
if rage < 0 then
return 0
end
if tactical_mastery ~= true then
return 0
end
if rage > st.warrior_guide.TACTICAL_MASTERY_RAGE_CAP then
return st.warrior_guide.TACTICAL_MASTERY_RAGE_CAP
end
return rage
end
-- Sunder upkeep verdict (TBC + Forever APL groups): true when the stack count
-- is known-below-cap (re-stack regardless of the window) or the remaining
-- time is known-inside the refresh window, false otherwise. Unknown hides: a
-- non-numeric stack with a non-numeric remaining never prompts (an unreadable
-- debuff is not a readable need). A NaN input answers like unknown, never
-- like a need.
function st.warrior_guide.should_refresh_sunder(stacks, remaining, refresh_window)
if type(refresh_window) ~= "number" or refresh_window ~= refresh_window then
refresh_window = st.warrior_guide.SUNDER_REFRESH_DEFAULT
end
if type(stacks) == "number" and stacks == stacks
and stacks < st.warrior_guide.SUNDER_STACKS_MAX then
return true
end
if type(remaining) ~= "number" or remaining ~= remaining then
return false
end
return remaining <= refresh_window
end
-- Rend upkeep outside arms (Forever Rend-weave 11574 at <= 3 s, toggle-gated;
-- SoD Rend 11574 + rune 412507 CONFIRMED, Blood Frenzy): the same 3 s window
-- as arms rule A. The helper takes an explicit enabled flag - nil/false never
-- prompts; the provider feeds true on SoD with the rune known and nil
-- everywhere else (the Forever toggle + the single-target gate live with the
-- options lane, OUT). A missing Rend (nil remaining) reads as apply once
-- enabled, like the arms/druid nil rule. Consumed by rule R (the SoD rend
-- slot; no slot names "rend" on Forever yet - filed).
function st.warrior_guide.should_maintain_rend_outside_arms(remaining, enabled, refresh_window)
if enabled ~= true then
return false
end
if type(refresh_window) ~= "number" or refresh_window ~= refresh_window then
refresh_window = REND_REFRESH_DEFAULT
end
if remaining == nil then
return true
end
if type(remaining) ~= "number" or remaining ~= remaining then
return false
end
return remaining <= refresh_window
end
-- SoD Overpower stance-dance gate (warrior-rotfix journal 2026-10-19: OP R4
-- 11585, already in op_ids, + stances 2457/2458, gated by aura 426969 - all
-- three UNCONFIRMED on SoD, in-client to confirm). True only when the gate
-- aura is live, the client reports a real stance form (0 = unshifted/probe
-- missing, never a stance) and Overpower itself is ready. The from/to stance
-- index map is UNCONFIRMED per family (compat api.GetShapeshiftForm exists;
-- the warrior map needs in-client confirmation), so the helper requires "in
-- a stance", not a named one. Consumed by rule D (the SoD op slot).
st.warrior_guide.SOD_OP_STANCE_AURA_ID = 426969 -- UNCONFIRMED
function st.warrior_guide.should_stance_dance_op(has_gate_aura, stance_form, op_ready)
if has_gate_aura ~= true then
return false
end
if type(stance_form) ~= "number" or stance_form ~= stance_form then
return false
end
if stance_form == 0 then
return false
end
return op_ready == true
end
-- Hamstring rage-dump weave verdict (sim-cited ranks: Era 7373 above 80 rage,
-- TBC 18499 with Improved Hamstring 20500, Forever 7373 toggle-gated; the SoD
-- stance-dance OP path rides the gate above, not this). The threshold arrives
-- as a parameter (the provider feeds 80 on Era-base, sim-cited; nil
-- everywhere else - no number cited / toggle-gated, so the weave rule stays
-- dormant there until the options lane ships the tuning). Unknown rage or an
-- unlearned Hamstring never prompts either. Consumed by rule W (the Era
-- hamstring slot + the base/Forever sixth slot).
st.warrior_guide.HAMSTRING_RAGE_THRESHOLD_ERA = 80
function st.warrior_guide.should_weave_hamstring(rage, hamstring_known, rage_threshold)
if hamstring_known ~= true then
return false
end
if type(rage) ~= "number" or rage ~= rage then
return false
end
if type(rage_threshold) ~= "number" or rage_threshold ~= rage_threshold then
return false
end
return rage > rage_threshold
end
-- The APL's execute-phase clause on the rage dump. The TBC and Forever APLs gate
-- their Heroic Strike row on NOT being inside the execute phase (wowsims/tbc-new
-- ui/specs/warrior/dps/apls/fury.apl.json [18] - `numberTargets == 1 AND NOT
-- isExecutePhase E20 AND currentRage >= 40`; wowsims/forever
-- ui/specs/warrior/dps/apls/arms.apl.json [27] - `NOT isExecutePhase E20 AND
-- currentRage >= 90`), so inside the phase the dump is not the APL's move. The
-- Era APL's row explicitly ALLOWS the dump inside the phase (wowsims/classic
-- ui/warrior/apls/dps_no_reck.apl.json [18] - `currentRage >= 40 OR isExecutePhase
-- E20`) and the SoD / WotLK APLs carry no such clause, so the PROVIDER arms the
-- gate only on the families that carry it (classes/warrior.lua feeds
-- execute_phase_dump_gate).
--
-- The phase read is the one the Execute rule already uses: the target's health
-- fraction against the class's execute percentage. A target with no known health
-- is not in the phase (the clause holds, the dump keeps its legacy behaviour); a
-- KNOWN sub-threshold health withholds the dump. The gate is a prohibition on a
-- filler, not a spend the player cannot afford, so an unreadable phase keeps the
-- legacy shape instead of silencing the dump.
function st.warrior_guide.dump_blocked_in_execute(inputs, execute_pct)
if type(inputs) ~= "table" then
return false
end
if inputs.execute_phase_dump_gate ~= true then
return false
end
local hp = inputs.target_health_pct
if type(hp) ~= "number" or hp ~= hp then
return false
end
if type(execute_pct) ~= "number" or execute_pct ~= execute_pct then
return false
end
return hp < execute_pct
end
--=========================================================================================
-- Practice-facing pure helpers (the rotation scenario + the warrior's own drills).
--
-- The shared practice engine drives the class through three surfaces (the
-- orchestrator's fixed contract): ST.WARRIOR.practice_rotation (the rotation
-- table), ST.WARRIOR.practice_scenario_step (one expected action per tick) and
-- ST.WARRIOR.practice_drills (the class's own drills, each with a grade
-- function). The pure maths those surfaces need lives here, so it is unit
-- tested offline (tests/warrior_guide_state.lua) and never reaches for a client
-- read. The sources are the sim APLs and spell files (fetched 2026-10-19, the
-- wowsims master branches), the audited drill set
-- (bench/class_features_drills_2026-10-19.md section 2.2) and the operator's
-- own Heroic Strike / Cleave queue drill (the section further down, with its
-- own sim citations).
--=========================================================================================
-- The stance keys the provider reads (the sim's stance auras). The ids come
-- from the classic sim's stance registration (Battle 2457, Defensive 71,
-- Berserker 2458 - wowsims/classic sim/warrior/stances.go, fetched
-- 2026-10-19); the tbc-new fury APL switches with 2457 (Battle) and 2458
-- (Berserker), so the same two ids carry the TBC client too.
st.warrior_guide.STANCE_BATTLE = "battle"
st.warrior_guide.STANCE_BERSERKER = "berserker"
st.warrior_guide.STANCE_DEFENSIVE = "defensive"
-- The SoD rune stance (wowsims/sod proto/warrior.proto:131 - RuneGladiatorStance =
-- 412513; wowsims/sod sim/warrior/stances.go:212 - the Gladiator Stance aura's
-- ActionID). The client reports it as a stance aura like the other three, so the
-- provider's own stance walk reads it (classes/warrior.lua's WARRIOR_STANCES.ids).
st.warrior_guide.STANCE_GLADIATOR = "gladiator"
-- The action key each stance asks for (the utility spine's stance rows). The engine
-- names the row, never the client's spell id.
st.warrior_guide.STANCE_ACTIONS = {
battle = "battle_stance",
berserker = "berserker_stance",
gladiator = "gladiator_stance",
}
-- Overpower's stance gate (the classic, sod and tbc-new sims all register
-- Overpower with a Battle-stance requirement: classic sim/warrior/overpower.go
-- `RegisterSpell(BattleStance, ...)`, sod sim/warrior/overpower.go the same,
-- tbc-new sim/warrior/overpower.go `ExtraCastCondition = StanceMatches(
-- BattleStance)`). An unknown stance never withholds (unknown hides, and the
-- legacy behaviour stays byte-identical where the provider feeds no stance).
function st.warrior_guide.stance_allows_op(stance)
if stance == nil then
return true
end
return stance == st.warrior_guide.STANCE_BATTLE
end
-- Execute's stance gate (the classic sim registers Execute with
-- `BattleStance|BerserkerStance` and the tbc-new sim with
-- `StanceMatches(BerserkerStance|BattleStance)`). An unknown stance never
-- withholds.
function st.warrior_guide.stance_allows_execute(stance)
if stance == nil then
return true
end
return stance == st.warrior_guide.STANCE_BATTLE
or stance == st.warrior_guide.STANCE_BERSERKER
end
-- The rage pool after an Execute cast. The classic sim spends the WHOLE pool
-- (sim/warrior/execute.go: `extraRage := spell.Unit.CurrentRage()` then
-- `SpendRage(sim, extraRage)`, damage `600 + 15*extraRage`) and the tbc-new
-- sim caps the extra rage at `maxRage - cost` and spends that too (damage
-- `925 + 21*extraRage`), so the pool is empty afterwards on the draining
-- families. A family whose sim does not drain keeps the base spend. An
-- unreadable pool answers nil (never a guess).
function st.warrior_guide.rage_after_execute(rage, cost, drains_all)
if type(rage) ~= "number" or rage ~= rage then
return nil
end
if drains_all == true then
return 0
end
local spend = cost
if type(spend) ~= "number" or spend ~= spend then
spend = EXECUTE_RAGE_SPEND
end
local left = rage - spend
if left < 0 then
left = 0
end
return left
end
-- The short reason each action carries in the practice surface (the scenario
-- step's "reason" field). The text names the rule that fired, not a judgement.
st.warrior_guide.ACTION_REASONS = {
op = "Overpower window (dodge proc)",
execute = "Execute below 20 %",
slam = "Bloodsurge Slam",
sunder = "Sunder upkeep (stacks / refresh window)",
rend = "Rend upkeep",
bt = "Bloodthirst",
ww = "Whirlwind (the core is more than 1.5 s out)",
raging_blow = "Raging Blow (costs no rage)",
bladestorm = "Bladestorm",
ms = "Mortal Strike",
hamstring = "Hamstring weave (rage dump)",
hs = "Heroic Strike dump",
cleave = "Cleave dump",
-- The utility spine (the APL's utility rows): the text names the row.
battle_stance = "Battle Stance (the Overpower stance)",
berserker_stance = "Berserker Stance (the damage stance)",
gladiator_stance = "Gladiator Stance (the SoD rune stance)",
bloodrage = "Bloodrage (the rage cooldown)",
battle_shout = "Battle Shout (the attack-power buff)",
recklessness = "Recklessness (the burst cooldown)",
death_wish = "Death Wish (the damage cooldown)",
sweeping_strikes = "Sweeping Strikes (the multi-target cooldown)",
thunder_clap = "Thunder Clap (the area stun)",
shattering_throw = "Shattering Throw (the ranged armour break)",
heroic_throw = "Heroic Throw (the ranged filler)",
berserker_rage = "Berserker Rage (the rage cooldown)",
blood_fury = "Blood Fury (the Orc racial)",
}
-- The reason for an action, or nil for an unknown key (the caller shows no
-- reason rather than an invented one).
function st.warrior_guide.action_reason(action)
if type(action) ~= "string" then
return nil
end
return st.warrior_guide.ACTION_REASONS[action]
end
--=========================================================================================
-- The utility spine: the APL's utility rows, and the arm that plays them.
--
-- The rotation table's second block (classes/warrior.lua) carries the APL's utility
-- rows (the stance upkeep, the cooldown pops, the racial) as documentation; this is
-- the arm. The engine stays pure and family-agnostic: the provider
-- (classes/warrior.lua) feeds the per-line reads and the running family's APL order,
-- and the engine walks the order and asks each line.
--
-- THE PER-LINE READS (the flat input names; the table below holds them, so the tick
-- path never builds a string):
-- <key>_known the client knows the spell. The family gate lives in the provider
-- (a line the running family's APL does not carry is never fed at
-- all), so this only answers "does this client have the spell".
-- <key>_remaining the spell's own cooldown in seconds (0 = ready, nil = unknown).
-- <key>_duration the client-reported cooldown length (the guide slot's swipe).
-- <key>_buff the line's buff: false = not up, true = up, nil = unknown.
-- <key>_rage_max the APL's own rage ceiling for the row (nil = no rage gate).
--
-- A LINE IS ASKED FOR only when every one of these holds:
-- * the client knows the spell (a spell the client lacks is never asked for),
-- * the cooldown is READ and ready (an unknown cooldown never prompts),
-- * the buff is READ and not up (an unreadable buff never prompts: unknown hides),
-- * the rage pool is known and at or under the APL's ceiling (when the row has one).
--
-- THE ORDER: the provider feeds the running family's APL order, split in two:
-- utility_order the utility rows the APL puts ABOVE the core rotation
-- utility_order_low the utility rows the APL puts BELOW it (the Era APL's rows,
-- which come after the filler, and the TBC / Forever rows that
-- close the list)
--
-- THE STANCE ROWS are not cooldown rows, so they carry their own arms (below): the
-- sims register every stance spell with
-- `ExtraCastCondition: !warrior.StanceMatches(stance)` (wowsims/classic
-- sim/warrior/stances.go) and the APLs gate their rows on `auraIsInactive(auraId)`,
-- so a stance the player is ALREADY in is never asked for.
--=========================================================================================
-- The utility line's input names (the tick path never builds a string: the names live
-- here). `rage_max` is present only on the rows whose APL row carries a rage ceiling.
st.warrior_guide.UTILITY_LINES = {
bloodrage = {
remaining = "bloodrage_remaining",
duration = "bloodrage_duration",
buff = "bloodrage_buff",
known = "bloodrage_known",
rage_max = "bloodrage_rage_max"
},
battle_shout = {
remaining = "battle_shout_remaining",
duration = "battle_shout_duration",
buff = "battle_shout_buff",
known = "battle_shout_known"
},
recklessness = {
remaining = "recklessness_remaining",
duration = "recklessness_duration",
buff = "recklessness_buff",
known = "recklessness_known"
},
death_wish = {
remaining = "death_wish_remaining",
duration = "death_wish_duration",
buff = "death_wish_buff",
known = "death_wish_known"
},
sweeping_strikes = {
remaining = "sweeping_strikes_remaining",
duration = "sweeping_strikes_duration",
buff = "sweeping_strikes_buff",
known = "sweeping_strikes_known"
},
thunder_clap = {
remaining = "thunder_clap_remaining",
duration = "thunder_clap_duration",
buff = "thunder_clap_buff",
known = "thunder_clap_known"
},
shattering_throw = {
remaining = "shattering_throw_remaining",
duration = "shattering_throw_duration",
buff = "shattering_throw_buff",
known = "shattering_throw_known"
},
heroic_throw = {
remaining = "heroic_throw_remaining",
duration = "heroic_throw_duration",
buff = "heroic_throw_buff",
known = "heroic_throw_known"
},
berserker_rage = {
remaining = "berserker_rage_remaining",
duration = "berserker_rage_duration",
buff = "berserker_rage_buff",
known = "berserker_rage_known"
},
blood_fury = {
remaining = "blood_fury_remaining",
duration = "blood_fury_duration",
buff = "blood_fury_buff",
known = "blood_fury_known"
},
}
-- Pure: the utility row's verdict. True only when the APL's row asks for the line
-- right now. Every gate is a "known" gate (unknown hides): a spell the client does
-- not have, an unreadable cooldown, an unreadable buff and an unreadable rage pool
-- all withhold the prompt.
function st.warrior_guide.utility_line_due(line, inputs)
if type(line) ~= "table" or type(inputs) ~= "table" then
return false
end
if inputs[line.known] ~= true then
return false
end
local remaining = inputs[line.remaining]
if type(remaining) ~= "number" or remaining ~= remaining or remaining > 0 then
return false
end
if inputs[line.buff] ~= false then
return false
end
if line.rage_max then
local ceiling = inputs[line.rage_max]
local rage = inputs.rage
if type(ceiling) ~= "number" or ceiling ~= ceiling then
return false
end
if type(rage) ~= "number" or rage ~= rage then
return false
end
if rage > ceiling then
return false
end
end
return true
end
-- Pure: the utility spine's pick - the first line of the fed order whose read says
-- the APL wants it now. nil when nothing is due (or the provider fed no order).
function st.warrior_guide.utility_spine_pick(order, inputs)
if type(order) ~= "table" or type(inputs) ~= "table" then
return nil
end
for i = 1, #order do
local key = order[i]
local line = st.warrior_guide.UTILITY_LINES[key]
if line and st.warrior_guide.utility_line_due(line, inputs) then
return key
end
end
return nil
end
-- Pure: the stance upkeep row ("cast the stance when its aura is not active"). The
-- wanted stance rides in as an input (the provider picks it per family: Berserker
-- Stance on Era / SoD / TBC / WotLK, Battle Stance on Forever, whose Stance Dance
-- toggle ships off). nil when the stance is unknown (the guide would not know the
-- player is already in it) or when the player is already in it.
function st.warrior_guide.utility_stance_action(stance, wanted)
if type(stance) ~= "string" or stance == "" then
return nil
end
if type(wanted) ~= "string" or wanted == "" then
return nil
end
if stance == wanted then
return nil
end
return st.warrior_guide.STANCE_ACTIONS[wanted]
end
-- Pure: the Overpower weave's first step (the dance into Battle Stance). The sims
-- register Overpower with a Battle-stance requirement (wowsims/classic
-- sim/warrior/overpower.go; wowsims/tbc-new sim/warrior/overpower.go -
-- `ExtraCastCondition = StanceMatches(BattleStance)`), so the dance is what makes the
-- Overpower castable; the APL's own rows are the strictSequence (SoD:
-- [2457, 11585] behind the gate aura 426969) and the Overpower Weaving group (TBC:
-- wait -> 2457 -> 11585 -> 2458). The weave only runs where the running family's APL
-- carries it (the provider's family gate) and only while the Overpower window is
-- live. An unknown stance never asks (unknown hides).
function st.warrior_guide.utility_weave_stance(stance, weave, op_context)
if weave ~= true or op_context ~= true then
return nil
end
if type(stance) ~= "string" or stance == "" then
return nil
end
if stance == st.warrior_guide.STANCE_BATTLE then
return nil
end
return "battle_stance"
end
-- The audited drill bands (bench/class_features_drills_2026-10-19.md section
-- 2.2 (C)). Every table here is the audit's [DESIGN] band list, not a sim
-- number: the sims fix the rotation and the costs, the band edges are the
-- audit's grading design.
-- D-W1 (the core rotation), a RUN-level band over the mis-picks: S at no
-- mis-pick and no reserve violation, A at one, B at two, C at three, D at
-- five, F beyond that or at any reserve violation (a spend that starved a
-- core sitting inside the reserve window).
st.warrior_guide.CORE_ORDER_BANDS = {
{ max = 0, grade = "S" },
{ max = 1, grade = "A" },
{ max = 2, grade = "B" },
{ max = 3, grade = "C" },
{ max = 5, grade = "D" },
}
-- The run-level band for the core-order drill. A negative or unreadable
-- mis-pick count answers nil (never a grade on a guess); any reserve
-- violation is an F whatever the count.
function st.warrior_guide.core_order_band(mis_picks, violations)
if type(mis_picks) ~= "number" or mis_picks ~= mis_picks or mis_picks < 0 then
return nil
end
if type(violations) == "number" and violations == violations and violations > 0 then