Repository navigation
Expand file tree
/
Copy pathrogue_guide.lua
More file actions
2609 lines (2499 loc) · 112 KB
/
Copy pathrogue_guide.lua
File metadata and controls
2609 lines (2499 loc) · 112 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
--=========================================================================================
-- Rogue next-action guide: pure decision engine, no WoW API and no clock.
--
-- The sixth provider's decision model for the frozen ST.<CLASS>.guide contract
-- (nextaction_ui.lua's header; the paladin ships the first provider, the warrior
-- the second, the druid the third, the shaman the fourth, the hunter the fifth).
-- The live reads will live in classes/rogue.lua (ST.ROGUE.gather_guide_inputs -
-- owned by the parallel skeleton lane); this module only consumes the gathered
-- inputs, so it is unit-tested offline by tests/rogue_guide_state.lua.
--
-- Evidence (bench/rogue_research_2026-10-19.md, bench/rogue_deep_2026-10-19.md,
-- bench/rogue_rotation_audit_2026-10-19.md — the wowsims-sim comparison that
-- re-sourced every default below; sim spell ids are sim-side evidence only and
-- are never proposed for our tables):
-- pooling rogue_research §1.3: never pool above 65 pre-tick / 85 post-tick;
-- Relentless Strikes refunds 25 on finishers - finisher BEFORE the tick
-- that would cap you. TICK 20 / 2 s is Era+TBC CONFIRMED (§§1.1/1.3);
-- retail pooling is phase-based, not tick-based (§1.6).
-- Rotation audit: TBC sim pools on a single 71 line (T-M1);
-- Forever cap-danger is 75 (V-G6); ERA keeps the tick-gated 65/85
-- (F2 MATCH); the Era sweaty APL spends at energy>=79 regardless of
-- swing phase (E-G1).
-- rupture rogue_research §1.3: TBC Rupture > Eviscerate generally (T-M2
-- MATCH); Era sims are Evisc-only — Rupture-first is sim-contradicted
-- there (E-C1) so ERA plans Eviscerate; TBC/FOREVER recast when
-- missing, no early refresh (T-G3). Finisher floors are per-family
-- (ERA 8 / TBC 5 / WRATH 4 / FOREVER 6 — T-C1/W-M2/V-G3); the Wrath
-- 1CP 8 s / 5CP 16 s durations ship as DATA below (2 of 5 rows
-- sim-sourced, W-M2; 2-4CP UNCONFIRMED — filed; no sim rule
-- consumes the table yet so the flat floors still govern).
-- envenom rogue_research §3.5: the rules DIFFER per client - never share one
-- Envenom rule. TBC: only on dying targets (consumes stacks, mid-fight
-- DPS loss). WotLK sin: maintain the buff, let it EXPIRE before
-- re-apply unless energy-capping (W-M1 STRONG MATCH). Retail sin:
-- chain deliberately. Forever beta: Eviscerate dump, no Envenom
-- prompt. SoD (ERA family): the SOD primary table below, reached
-- only behind the provider's sod_envenom latch (ERA + the SoD-only
-- Envenom rune known - bench/sod_detect); without the latch ERA
-- keeps its dying-only shape (no global SoD token).
-- snd Rotation audit F0/H1: EVERY sim APL maintains SnD (classic 1+CP
-- early, TBC 10 s minimum + EA/SnD-safe gates, Forever 1+CP clip 0,
-- WotLK <=1 s). Per-CP durations [9,12,15,18,21] s for CP 1-5
-- (classic/TBC sim-exact; WotLK +ImpSnD/glyph, Forever 1CP-clip,
-- SoD CttC filed as follow-ups). TBC 10 s minimum excludes 1CP
-- (9 s < 10 s) so TBC min CP is 2. Emitted as "snd" with NO slot yet
-- (the 7-slot contract is frozen — icon wiring is a filed follow-up
-- for a wider lane); the rotation label names it until then.
-- ea rogue_research §1.3: Imp-EA 100%-uptime juggle (refresh before
-- falloff; 2-5 CP SnDs between). Rotation audit H4: the window is
-- per-client — ERA 3 s (E-G4 MATCH), TBC imminent 6 s (T-G2),
-- FOREVER lockout 16 s (V-G2); the TBC 15 s minimum below plus the
-- TBC wait-HOLD / pool-release (rule 1a) and the Forever finisher
-- lockout ride the same duty (H6).
-- shiv rogue_research §1.3/§1.4: Shiv keeps Deadly stacks alive / stacks the
-- OH on demand. Watch-list window: Shiv-ready when <2 s to falloff
-- (rogue_research §4). Rotation audit H7/T-G4: sim support is TBC
-- combat non-Mutilate only below 5CP, never WotLK/Forever defaults;
-- the engine gates to ERA+TBC (ERA kept ungated: no sim either way,
-- under-prompts nowhere) plus the TBC Mutilate/CP gates; the
-- EA-context check (incl. the 4 s Shiv minimum) is filed.
-- tea rogue_deep §2.3: Thistle Tea item cue on low energy (Sno tracks Tea
-- on the PvE cooldown bar). Rotation audit H10: the line is
-- per-client — ERA 29 / TBC 30 / FOREVER 15 (E-G3/T-G5/V-G4) /
-- SOD 10 + 40-end (S-M1: 40 while ttd<=15 s, via the latch + TTD);
-- WRATH keeps 40 (the 50 line is potion, not Tea); Eureka-25 needs
-- the Eureka in-client probe (1259812 CLIENT-UNCONFIRMED) — filed.
-- ar/bf Rotation audit F3/H11: sim AR/BF are energy-gated behind upkeep
-- (TBC AR<=40, Forever AR<=50 w/ SnD, classic AR energy<59+tick<1,
-- WotLK AR<=50; TBC BF w/ SnD+EA). Emitted as "ar"/"bf" with NO slot
-- yet (same filed icon wiring as SnD). Cold Blood ships slot-less
-- too (TBC w/ SnD, Forever at 5CP — audit F3/V-G5; lockout-yielding;
-- AR-buff sequencing filed). Tricks / Riposte / Killing Spree / HfB
-- / Overkill / Vanish need slots + APL-literal design — filed.
-- stacked rogue_deep §2.7 + competitor_gaps R3: multi-CD yellow glow when two
-- or more offensive CDs ride at once (Sno buff tracker). The counted
-- set (AR + BF) is UNCONFIRMED beyond Sno's list.
--
-- FROZEN SPEC - display slots (ST.ROGUE.guide.slots, owned by the skeleton lane;
-- suggested keys - this engine only emits these action names):
-- builder the family builder (Sinister Strike / Backstab / Mutilate /
-- Saber Slash / Hemorrhage - the winner of the class's candidate
-- chain, family-gated by BUILDER_GATED_FAMILIES above)
-- rupture Rupture
-- eviscerate
-- Eviscerate
-- envenom Envenom (per-client rule below)
-- expose Expose Armor (EA-duty only)
-- shiv Shiv (Deadly upkeep)
-- tea Thistle Tea (item deficit cue)
-- Slot-less (rotation audit F0/H1 + F3/H11 — emitted with NO slot until a wider
-- lane wires icons + options; the frame shows NOW, the rotation label names
-- them, never a guess, never a crash):
-- snd Slice and Dice upkeep (per-client window + min-CP data below)
-- ar Adrenaline Rush (per-client energy gate below)
-- bf Blade Flurry (behind SnD + EA)
-- coldblood Cold Blood (TBC w/ SnD, Forever at 5CP — audit F3/V-G5)
-- vanish Vanish (the stealth opener - the SoD's Vanish -> Envenom row, the
-- WotLK's Vanish pop, the Forever client's Vanish rank 2)
-- ambush Ambush (the Forever client's opener, behind the 462707 aura)
-- garrote Garrote (the Forever client's opener, while its dot is down)
-- preparation Preparation (the Forever client's prep, when Vanish is not ready)
-- premeditation Premeditation (the Forever client's CP opener at cp <= 3)
-- venom Venom (the Forever client's 1310703 finisher, while its buff is down)
-- poisoned_knife Poisoned Knife (the SoD's 4-stack Deadly Poison window)
-- blood_fury Blood Fury (the Forever client's paired pop with Adrenaline Rush)
-- elunes_light Elune's Light (the Forever client's paired pop, id 1259799)
-- gnome_eureka Eureka! (the Forever client's Gnome racial, id 1259812)
-- tricks_of_the_trade Tricks of the Trade (the WotLK energy pop)
-- killing_spree Killing Spree (the WotLK energy pop)
-- Labels (skeleton lane): labels = { hold = "POOL", ride = "NOW" } - the HOLD
-- overlay text below is already explicit, so the labels only cover fallbacks.
--
-- The APL utility layer (the operator's "a rotation that only carries the damage
-- abilities is not the class's rotation", bench/rotation_gap_vs_apl_2026-10-20.md
-- §3): the arms below are the sim APLs' own rows, re-fetched 2026-10-20 from
-- wowsims/{sod,forever,wotlk}/master. Every arm is family-gated and every arm
-- hides when its read cannot prove the line (unknown-hides - no advice beats
-- wrong advice). The rotation TABLE lives in classes/rogue.lua
-- (ST.ROGUE.practice_rotation): each row carries its APL citation, its
-- per-family spell id + name, its family gate and the two flags (live = the
-- read exists, engine = the engine has an arm).
--
-- FROZEN SPEC - inputs (all optional, nil never errors):
-- family compat.lua's st.family ("ERA", "TBC",
-- "WRATH", "FOREVER", "RETAIL", ...);
-- selects the pooling band and the
-- Envenom rule. Unknown (or nil) runs
-- no client-specific rule: Envenom
-- never prompts and pooling stays
-- silent (never a guessed verdict).
-- energy_now, energy_tick_in, max_energy energy + seconds to the next 20-tick
-- (api.get_time() domain; the provider
-- converts); max_energy default 100
-- (Vigor +10 arrives through this input)
-- cp combo points (nil = unreadable)
-- ttd target seconds-to-die, NUMERIC INPUT
-- ONLY (the rogue-owned EMA below,
-- vendored from the druid-D4
-- precedent; this module never reads
-- units/health). nil = unknown. MUST be
-- live-provenance or EMA velocity -
-- never the enemy_swing 2.0 s fallback
-- (M7 corollary, rogue_readiness §4).
-- rupture_remains, rupture_refresh Rupture seconds / refresh window
-- (default 4 - NEEDS IN-CLIENT
-- CONFIRMATION; no rogue source).
-- rupture_min_ttd TTD at/below which Rupture cannot pay
-- (default 8 = the sourced 1CP row,
-- rogue_research §5.2; the Wrath
-- per-CP durations ship as
-- RUPTURE_DURATION_BY_CP DATA
-- (1CP 8 s / 5CP 16 s, W-M2;
-- 2-4CP UNCONFIRMED) and no sim
-- rule consumes them yet, so the
-- flat floors still govern).
-- finisher_min_cp CP needed for a planned finisher
-- (default 5 - the 5-CP Rupture /
-- Eviscerate / Envenom shape on every
-- classic-line client).
-- deadly_stacks Deadly Poison stacks (nil = unknown)
-- envenom_buff_remains Envenom buff seconds (nil = absent or
-- unanswered - prompts like the druid
-- rip/rake nil rule)
-- ea_duty, ea_remains, ea_refresh Imp-EA duty flag + Expose Armor
-- seconds + refresh window (default is
-- per-family: ERA 3 / TBC 6 /
-- WRATH 3 / FOREVER 16 — audit H4;
-- explicit input still wins). The TBC
-- 15 s minimum, the TBC wait/release
-- and the Forever lockout below all
-- ride this same flag (H6, default
-- off -> inert).
-- sod_envenom provider-owned SoD latch: exactly
-- true means this ERA player knows the
-- SoD-only Envenom rune (frozen at
-- post_init; nil/false reads
-- Era-base). Only rule 3 reads it.
-- snd_usable, snd_remains, SnD upkeep: spell resolved + player
-- snd_refresh, snd_min_cp buff seconds + refresh window +
-- min CP (per-family: ERA 3s/1CP,
-- TBC 0s/2CP, WRATH 1s/1CP, FOREVER
-- 0s/1CP, SOD 1s/4CP — audit F0/H1
-- + S-G1 (SOD via the latch);
-- explicit inputs still win;
-- missing (nil)
-- prompts — an upkeep gap, like the
-- druid rip/rake nil rule).
-- ar_ready, bf_ready AR / BF usability (known + usable +
-- no cooldown; the provider proves
-- each — nil never prompts).
-- coldblood_ready Cold Blood usability (known + usable +
-- no cooldown; the provider proves it —
-- nil never prompts; TBC + FOREVER
-- only — audit F3/V-G5).
-- shiv_ready, deadly_remaining, Shiv usability + Deadly seconds to
-- shiv_refresh, mutilate_known, falloff + refresh window (default 2,
-- cp (TBC gates) rogue_research §4 watch-list) +
-- Mutilate-known through the client's
-- own spell data (strict true reads
-- Mutilate; nil reads unknown — the
-- TBC Shiv gate withholds only on
-- exactly true) + CP (TBC Shivs
-- below 5CP).
-- tea_ready, tea_energy_max Thistle Tea usability + the low-energy
-- line (per-family: ERA 29 / TBC 30 /
-- WRATH 40 / FOREVER 15 / SOD 10 +
-- 40-end — audit H10 + S-M1 (SOD via
-- the latch); explicit input still
-- wins).
-- inputs_unknown explicit provider-owned M6 bit: true means
-- the live reads are unusable (or the secret
-- regime makes the auras unreadable), so no
-- rule may emit a verdict - the frame renders
-- "..." instead (no advice beats wrong advice).
-- pool_pre, pool_post optional band override (options tuning;
-- defaults come from POOL_BANDS below).
-- vanish_ready Vanish is known + usable + off
-- cooldown (the provider proves it — nil
-- never prompts). Per family the id
-- differs (SoD 1856 / Wrath 26889 /
-- Forever 1857 rank 2).
-- master_of_subtlety_active the SoD's Master of Subtlety rune
-- aura (425096) is up - the SoD APL's
-- Vanish row wants it DOWN.
-- vanished_active the WotLK's 58426 aura is up - the
-- WotLK APL's Vanish row wants it
-- DOWN (the aura's NAME is
-- UNCONFIRMED; the id is the APL's).
-- ambush_ready, garrote_ready, the Forever client's opener spells
-- preparation_ready, premeditation_ready, (known + usable + off cooldown);
-- venom_ready, blood_fury_ready, venom_ready is the 1310703 spell,
-- elunes_light_ready blood_fury_ready the Orc racial
-- 20572, elunes_light_ready the
-- Forever client's 1259799.
-- ambush_cue_active the Forever client's 462707 aura is
-- up (the APL's Ambush cue - the
-- aura's NAME is UNCONFIRMED).
-- garrote_dot_active the target still carries the
-- Garrote dot (11290) - the Forever
-- APL's Garrote row wants it DOWN.
-- garrote_energy_cost the Garrote's CURRENT energy cost in
-- energy points (the APL's
-- spellCurrentCost(11290)) - the
-- Forever APL's Vanish row (i=3) fires
-- only while the energy covers it. nil
-- = the client cannot read the cost
-- (unknown-hides).
-- venom_buff_active the player carries the Venom aura
-- (1310703) - the Forever APL's Venom
-- row wants it DOWN.
-- ar_cd_remaining the Adrenaline Rush cooldown's
-- remaining seconds (nil = the AR is
-- off cooldown). The Forever APL pairs
-- its Blood Fury / Elune's Light rows
-- with the AR's time to ready.
-- ar_known the Adrenaline Rush (13750) is known
-- (true) or not (false) - the split
-- ar_ready cannot make (the APL's
-- NOT(spellIsKnown) disjunct).
-- ar_buff_active the Adrenaline Rush's buff aura
-- (13750) is up (the APL's
-- auraIsActive(13750) disjunct).
-- gnome_eureka_ready the Gnome racial Eureka! (1259812)
-- is known + usable + off cooldown.
-- gnome_eureka_full_cd the Eureka! racial's FULL cooldown
-- in seconds (the APL's
-- spellFullCooldown(1259812)); nil =
-- the client cannot read it.
-- poisoned_knife_ready the SoD's Poisoned Knife rune spell
-- (425012) is known + usable + off
-- cooldown.
-- tricks_ready, killing_spree_ready the WotLK's Tricks of the Trade
-- (57934) and Killing Spree (51690)
-- are known + usable + off cooldown.
-- envenom_ready Envenom (399963 on the SoD) is
-- known + usable + off cooldown - the
-- SoD APL's Vanish row is the
-- Vanish -> Envenom opener.
--
-- FROZEN SPEC - rule order (first match wins; exactly these rules):
-- 0. unknown: inputs_unknown is true, or energy_now/cp is unusable -> out.unknown =
-- true, no action, no hold (the caller shows "..." - no advice beats wrong advice,
-- M6).
-- 1. EA duty: ea_duty and (ea_remains nil or <= ea_refresh) -> "expose".
-- Nil prompts (missing upkeep, like the druid rip/rake nil rule). On
-- Forever the refresh prompts at 5CP only (the sim's Should-Cast-Check);
-- below 5CP the low-EA bar runs the lockout instead.
-- 1a. TBC EA-duty window (H6): 5CP + ea_duty + EA known but below the
-- family minimum (EA_MINIMUM_BY_FAMILY) and above the imminent window:
-- SnD short of EA_WAIT_SND_MINIMUM (or energy at/above the pool post
-- line) releases into "expose" now (the sim's immediate cast); SnD
-- healthy + energy below the pool line holds (the sim's waitUntil).
-- Sits above SnD (the sim group outranks the SnD entry).
-- 1b. SnD upkeep: snd_usable and cp >= snd_min_cp and (snd_remains nil or
-- <= snd_refresh) -> "snd". Nil prompts (missing upkeep). Slot-less until
-- the icon wiring lands (filed follow-up). Yields to the 1a hold and to
-- the Forever lockout (both sim-literal).
-- 1x. The Forever opener cluster (the APL utility layer): the Forever APL
-- casts Ambush (i=1), Garrote (i=2), Vanish (i=3) and Preparation (i=5)
-- ABOVE its Slice and Dice upkeep (i=18), so the cluster sits above the
-- 1b upkeep. Each row is its own APL condition (see the arms below).
-- 1c. Hunger for Blood upkeep (Wrath sin, audit W-G1 / the class-feature
-- audit R-19): hfb_usable and (hfb_remains nil or <= hfb_refresh) ->
-- "hfb". The wowsims wotlk APL refreshes the buff when its aura has <= 1 s
-- left (ui/rogue/apls/mutilate.apl.json: auraRemainingTime(51662) <= 1s
-- -> cast 51662) - the same "refresh at the tail" shape as SnD. WRATH
-- only (the sim source is the wotlk repo); the spell id is sim-side
-- evidence only - the provider resolves the spell by its English name
-- through the client's own spell data and never ships an id. Nil prompts
-- (a missing buff is an upkeep gap, the SnD convention); an unreadable
-- clock hides. Yields to the 1a hold, to the Forever lockout and to the
-- dying-target skip (the SnD rule's own shape).
-- 1d. Vanish (the stealth opener): the SoD APL's i=7 row (NOT the Master of
-- Subtlety aura AND cp == 5 AND Envenom castable) and the WotLK APL's
-- mutilate i=4 row (NOT the 58426 aura). Both need the read to prove the
-- spell is ready; an unreadable state hides.
-- 1e. Poisoned Knife (the SoD's damage window): the SoD APL's i=5 row - the
-- target carries 4+ Deadly Poison stacks AND cp < 5 AND energy <= 80.
-- 2. Shiv: shiv_ready with deadly falloff imminent
-- (deadly_remaining <= shiv_refresh) -> "shiv". A nil clock never prompts
-- (H4: ambiguous timing hides, never guesses). ERA+TBC only (audit H7);
-- TBC additionally needs Mutilate unknown (mutilate_known ~= true) and
-- cp below 5CP (audit T-G4); the EA-context check is filed.
-- 3. Envenom: the per-client rule (ENVENOM_RULES - never shared):
-- ERA/TBC dying_only (ttd <= ttd_max, cp >= min_cp, stacks >= min_stacks);
-- WRATH let_expire (cp + 5 stacks prove it, buff active withholds unless
-- energy would cap); FOREVER never (beta dumps Eviscerate); RETAIL chain
-- (cp >= min_cp unless stacks are proven empty); SOD primary reached only
-- as ERA + sod_envenom == true (the provider's SoD latch). Yields to the
-- 1a hold (the sim waits at 5CP instead of dumping).
-- 3a. Premeditation (the Forever client's CP opener): the Forever APL's i=17
-- row - cast while cp <= 3. The Forever client's 14183 is Premeditation
-- (the Era/SoD/Wrath 14183 is the same spell; the Shadowstep id is
-- different on every client).
-- 3b. Venom (the Forever client's 1310703 finisher): the Forever APL's i=19
-- row - the buff is DOWN AND cp >= 3. It sits above the dying dump and
-- the planned finisher because the APL casts it (i=19) before Rupture
-- (i=20).
-- 4. dying-target dump: ttd known <= rupture_min_ttd (per-family: ERA 8 /
-- TBC 5 / WRATH 4 / FOREVER 6) and cp >= dump floor (ERA 1 / TBC 5 /
-- WRATH 4 / FOREVER 3) -> "eviscerate" (Era any-CP rule, §1.1, split per
-- T-C1). Yields to the 1a hold and to the Forever lockout.
-- 5. planned finisher: cp >= finisher_min_cp (per-family: ERA 5 / TBC 5 /
-- WRATH 4 / FOREVER 3) -> ERA "eviscerate" (E-C1: Era sims are Evisc-only)
-- or finisher_for_ttd on the TTD (rupture-first elsewhere):
-- unknown TTD never guesses (nil); TTD at/below rupture_min_ttd ->
-- "eviscerate" (the target dies before a fresh finisher pays); Rupture
-- missing or inside its refresh window (per-family: TBC/FOREVER
-- recast-when-missing) -> "rupture"; otherwise -> "eviscerate".
-- 5b. Adrenaline Rush: ar_ready and energy at/below the per-family line
-- (ERA <59 with tick<1 / TBC <=40 / WRATH <=50 / FOREVER <=50 w/ SnD) ->
-- "ar". Slot-less until the icon wiring lands (filed follow-up).
-- 5c. Blade Flurry: bf_ready with SnD active (and EA healthy when on EA
-- duty) -> "bf". Slot-less until the icon wiring lands (filed).
-- 5d. Cold Blood: coldblood_ready with SnD active (TBC) and at/above the
-- family CP line (FOREVER 5CP — audit V-G5) -> "coldblood". Slot-less
-- until the icon wiring lands (filed). Yields to the 1a hold and to
-- the Forever lockout (the sim suppresses CB while EA is low).
-- 5e. RETIRED (the WotLK's Hypersharp row): the row's spell (54758) is the
-- Engineering belt enchant's EFFECT, not a spell the rogue casts, so
-- there is nothing to ask for and the arm is gone (the row keeps its
-- citation and its filed reason in classes/rogue.lua's rotation table).
-- 5f. Tricks of the Trade (the WotLK's energy pop): the WotLK combat APL's
-- i=13 and mutilate i=8 rows - energy <= 50.
-- 5g. Killing Spree (the WotLK's energy pop): the WotLK combat APL's i=14
-- row - energy <= 50.
-- 5h. Blood Fury (the Forever client's paired pop): the Forever APL's i=11
-- row - the Adrenaline Rush's time to ready is at/above 1.9 m.
-- 5i. Elune's Light (the Forever client's paired pop, id 1259799): the
-- Forever APL's i=12 row - the same Adrenaline Rush gate.
-- 5j. Eureka! (the Forever client's Gnome racial, id 1259812): the Forever
-- APL's i=13 row (the APL's own "Gnome Eureka - Should Cast" variable) -
-- the racial is known + off cooldown AND one of the three readable
-- disjuncts holds (the Adrenaline Rush is unknown / its buff rides / its
-- time to ready outlasts the racial's full cooldown). The APL's fourth
-- disjunct (the fight is about to end) has no read, so the row can only
-- under-prompt.
-- 6. Thistle Tea: tea_ready and energy_now <= tea_energy_max -> "tea".
-- SoD reads 10, or 40 while ttd <= 15 s (S-M1 fight-end exception).
-- 7. pool-vs-spend (pool_verdict): the 1a hold reads "POOL" first; then
-- "spend" -> action "builder"; "pool" -> hold = true, hold_text = "POOL";
-- hold = true, hold_text = "POOL"; nil (no band for this client - RETAIL
-- pools by phase, not by tick) -> silent. ERA spends at >=79 regardless
-- of tick phase (sweaty override, E-G1).
-- 8. no action.
--
-- 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, unknown = bool.
--=========================================================================================
local _, st = ...
st.rogue_guide = st.rogue_guide or {}
-- Classic tick cadence: 20 energy / 2 s on the classic line (Era + TBC CONFIRMED,
-- rogue_research §§1.1/1.3). Retail is phase-based (rogue_research §1.6).
st.rogue_guide.TICK_ENERGY_DEFAULT = 20
st.rogue_guide.MAX_ENERGY_DEFAULT = 100
-- Pooling bands per client, as DATA (never bare constants): never pool above
-- `pre` with a tick coming, never sit above `post`. ERA keeps the tick-gated
-- 65/85 (classic MATCH, audit F2); TBC is the sim-validated single 71 line
-- (pre == post: at/above always spends — audit T-M1); WRATH keeps 65/85 (the
-- 85 cap-exception MATCHES W-M1); FOREVER caps at the sim's 75 cap-danger
-- line (audit V-G6). RETAIL is nil: phase-based pooling has no tick line.
st.rogue_guide.POOL_BANDS = {
ERA = { pre = 65, post = 85 },
TBC = { pre = 71, post = 71 },
WRATH = { pre = 65, post = 85 },
FOREVER = { pre = 65, post = 75 },
}
-- Planned-finisher spine per client (audit H2): ERA sims are Evisc-only, so
-- ERA plans Eviscerate; every other classic-line client plans Rupture-first.
-- Unknown/RETAIL falls back to the rupture-first shape (every rogue client
-- has Rupture/Eviscerate — F6 pin).
st.rogue_guide.FINISHER_MODES = {
ERA = "eviscerate",
TBC = "rupture",
WRATH = "rupture",
FOREVER = "rupture",
}
-- Planned-finisher CP line per client (audit V-G3: Forever spends from 3CP;
-- W-G2: WotLK combat spends from 4CP; ERA/TBC hold 5CP; SoD 4CP needs a SoD
-- family token — filed).
st.rogue_guide.FINISHER_MIN_CP_BY_FAMILY = {
ERA = 5,
TBC = 5,
WRATH = 4,
FOREVER = 3,
}
-- Rupture refresh window per client (audit T-G3/H9): TBC and FOREVER sims
-- recast when missing (no early refresh — window 0); ERA/WRATH keep 4
-- (no sim refresh row there — status quo, UNCONFIRMED).
st.rogue_guide.RUPTURE_REFRESH_BY_FAMILY = {
ERA = 4,
TBC = 0,
WRATH = 4,
FOREVER = 0,
}
-- Finisher floor per client (audit T-C1/W-M2/V-G3): the TTD at/below which a
-- fresh finisher is not worth starting, and the CP the dying-target dump
-- needs. ERA any-CP at 8 (the sourced 1CP row + the Era any-CP rule, §1.1);
-- TBC 5CP at 5 s (sim execute is 5CP/remaining<=5s — the shared any-CP rule
-- leaking here was the T-C1 CONFLICT); WRATH 4CP at 4 s (combat Evisc 4+CP at
-- fight<=4s; recast-missing Rupture above it; the 2-4CP duration rows are
-- UNCONFIRMED — filed); FOREVER 3CP at 6 s (sim 3CP fight-end Evisc <=6s).
st.rogue_guide.RUPTURE_MIN_TTD_BY_FAMILY = {
ERA = 8,
TBC = 5,
WRATH = 4,
FOREVER = 6,
}
st.rogue_guide.DUMP_MIN_CP_BY_FAMILY = {
ERA = 1,
TBC = 5,
WRATH = 4,
FOREVER = 3,
}
-- Rupture duration per CP as DATA (audit W-M2 — sim-sourced rows only):
-- wotlk rupture.go ships RuptureTicks = 3+CP (+2 Glyph of Rupture) at 2 s
-- ticks, i.e. 1CP 8 s … 5CP 16 s (20 s glyphed). Only the 1CP 8 s and 5CP
-- 16 s endpoint rows are sim-quoted durations, so only those two ship;
-- [2]/[3]/[4] stay absent (UNCONFIRMED — never interpolated, never
-- guessed). Glyph +2 ticks, the Forever fight-end exclusion and CttC
-- interplay are filed (no sim rule consumes this table yet; the flat
-- RUPTURE_MIN_TTD_* floors above still govern every verdict).
st.rogue_guide.RUPTURE_DURATION_BY_CP = { [1] = 8, [5] = 16 }
-- SnD upkeep per client (audit F0/H1): refresh window + min CP. TBC casts
-- only behind its 10 s minimum, which excludes 1CP (9 s < 10 s) — min CP 2.
-- Per-CP durations [9,12,15,18,21] s for CP 1-5 (classic/TBC sim-exact;
-- WotLK glyph/ImpSnD rows, Forever 1CP-clip and SoD CttC filed). SoD upkeep
-- casts at 4+CP with remains<=1s (S-G1, sim P5_Saber/mutilate) — reached
-- only as ERA + sod_envenom (the SOD rows below); CttC interplay filed.
st.rogue_guide.SND_REFRESH_BY_FAMILY = {
ERA = 3,
TBC = 0,
WRATH = 1,
FOREVER = 0,
SOD = 1,
}
st.rogue_guide.SND_MIN_CP_BY_FAMILY = {
ERA = 1,
TBC = 2,
WRATH = 1,
FOREVER = 1,
SOD = 4,
}
st.rogue_guide.SND_DURATION_BY_CP = { [1] = 9, [2] = 12, [3] = 15, [4] = 18, [5] = 21 }
st.rogue_guide.SND_MIN_CP_DEFAULT = 1
-- Hunger for Blood upkeep (Wrath sin; audit W-G1, the class-feature audit's
-- R-19): the wowsims wotlk APL refreshes the buff when its aura has <= 1 s
-- left (ui/rogue/apls/mutilate.apl.json: auraRemainingTime(51662) <= 1s ->
-- cast 51662 - fetched 2026-10-19). The same "refresh at the tail" shape as
-- Slice and Dice, and the same rule order (SnD first, then Hunger for Blood).
-- WRATH only: the sim source is the wowsims wotlk repo, so no other family
-- prompts. The spell id is sim-side evidence only - the provider resolves the
-- spell through the client's own spell data by its English name and never
-- ships an id (the remembered-id rule).
st.rogue_guide.HFB_FAMILIES = {
WRATH = true,
}
st.rogue_guide.HFB_REFRESH_BY_FAMILY = {
WRATH = 1,
}
st.rogue_guide.HFB_REFRESH_DEFAULT = 1
-- EA-duty refresh window per client (audit H4): ERA 3 s (E-G4 MATCH), TBC
-- imminent 6 s (T-G2), FOREVER lockout 16 s (V-G2), WRATH unsourced — keeps 3.
-- The TBC 15 s minimum (EA_MINIMUM_BY_FAMILY), the TBC wait/release (rule 1a)
-- and the Forever finisher lockout (EA_LOCKOUT_FAMILIES) ship below (H6),
-- all behind the same ea_duty flag.
st.rogue_guide.EA_REFRESH_BY_FAMILY = {
ERA = 3,
TBC = 6,
WRATH = 3,
FOREVER = 16,
}
-- EA-duty minimum per client (audit H6, sim-literal): TBC 15 s non-Shiv (the
-- sim's "Minimum EA Time"; the Shiv-context 4 s needs the filed Mutilate-known
-- probe, so only the non-Shiv row ships). A family with no row has no minimum
-- concept: the wait/release below never applies there.
st.rogue_guide.EA_MINIMUM_BY_FAMILY = {
TBC = 15,
}
-- SnD cover for the TBC EA wait (sim-literal): the sim's "Minimum SnD Time"
-- 10 s ("SnD Above Minimum" gates the wait — a short SnD releases into an
-- immediate Expose instead of waiting). The Shiv-context 1 s is filed with
-- the Mutilate probe above.
st.rogue_guide.EA_WAIT_SND_MINIMUM = 10
-- EA-duty finisher lockout per client (audit H6, sim-literal): while on duty
-- with EA low, the Forever sim withholds SnD, Riposte, Rupture, Cold Blood
-- and Eviscerate (every branch incl. the fight-end dump) but keeps Expose,
-- builders, Tea and AR live. Riposte / Cold Blood need slots + design and
-- are filed; the engine suppresses the shipped spender set (snd, dying dump,
-- planned finisher) in evaluate. A family with no row never locks out.
st.rogue_guide.EA_LOCKOUT_FAMILIES = {
FOREVER = true,
}
-- Thistle-Tea line per client (audit H10): ERA 29 / TBC 30 / FOREVER 15;
-- WRATH keeps 40 (the sim's 50 line is potion, not Tea). SoD drinks at 10,
-- or 40 when the fight ends inside 15 s (S-M1, sim P5_Saber) — the SOD row
-- plus the fight-end data below, reached only as ERA + sod_envenom.
-- Eureka-25 needs the Eureka in-client probe (1259812 CLIENT-UNCONFIRMED) —
-- filed.
st.rogue_guide.TEA_ENERGY_MAX_BY_FAMILY = {
ERA = 29,
TBC = 30,
WRATH = 40,
FOREVER = 15,
SOD = 10,
}
-- SoD Tea fight-end exception (S-M1): energy<=40 while ttd<=15 s. Unknown
-- TTD never guesses the exception (stays on the 10 line).
st.rogue_guide.SOD_TEA_FIGHT_END_TTD = 15
st.rogue_guide.SOD_TEA_FIGHT_END_MAX = 40
-- AR energy line per client (audit F3/H11): ERA <59 gated on tick<1 (classic
-- tick-gated AR); TBC <=40; WRATH <=50; FOREVER <=50 with SnD riding.
-- RETAIL has no sim source — nil, never prompts (filed with the retail sim).
st.rogue_guide.AR_ENERGY_MAX_BY_FAMILY = {
ERA = 59,
TBC = 40,
WRATH = 50,
FOREVER = 50,
}
-- Era sweaty override (audit E-G1): at energy>=79 the sweaty APL fires the
-- builder regardless of swing phase — a spend, never a pool. The 0.5 s window
-- widening needs in-client proof and is filed; only the override ships here.
st.rogue_guide.SWEATY_SPEND_BY_FAMILY = {
ERA = 79,
}
-- Shiv family gate (audit H7/T-G4): sim support is TBC combat non-Mutilate
-- only, below 5CP, and never WotLK/Forever defaults. ERA is kept ungated
-- (no sim either way, and withholding there under-prompts a real upkeep).
-- The EA-context check (Shiv inside EA windows incl. the 4 s Shiv minimum)
-- needs APL-literal EA-window design — filed.
st.rogue_guide.SHIV_FAMILIES = {
ERA = true,
TBC = true,
}
-- TBC Shiv gates (audit T-G4): withhold while Mutilate is known, and at/above
-- 5CP (the sim Shivs below 5CP). Unknown Mutilate (nil) never withholds —
-- an unproven book keeps the upkeep.
st.rogue_guide.SHIV_MUTILATE_GATE_FAMILIES = {
TBC = true,
}
st.rogue_guide.SHIV_CP_MAX_BY_FAMILY = {
TBC = 4,
}
-- Envenom rules per client, as DATA (rogue_research §3.5 - NEVER one shared
-- rule; each family owns its table, even where the fields coincide, so a tuning
-- pass cannot silently recouple them):
-- dying_only TBC CONFIRMED (§1.3): only on dying targets (consumes stacks).
-- ERA carries the same shape for combat (dumps Eviscerate); SoD
-- mid-fight Envenom rides the SOD table via the sod_envenom latch.
-- let_expire WotLK-sin CONFIRMED (§1.4): buff active withholds re-apply
-- unless energy would cap waiting.
-- never Forever beta (§1.5): the beta rotation dumps Eviscerate.
-- chain Retail-sin CONFIRMED (§1.6): 5+CP chaining while the buff rides.
-- sod_primary SoD sim-CONFIRMED (P5_Saber/mutilate APLs): 4+CP primary dump
-- behind poison stacks, with the 2+CP fight-end exception. Reached
-- only as ERA + sod_envenom == true (the provider's SoD latch);
-- the Vanish→Envenom sequence and CttC ride the same latch as
-- follow-ups.
st.rogue_guide.ENVENOM_RULES = {
ERA = { mode = "dying_only", ttd_max = 8, min_cp = 5, min_stacks = 1 },
TBC = { mode = "dying_only", ttd_max = 8, min_cp = 5, min_stacks = 1 },
WRATH = { mode = "let_expire", min_cp = 4, min_stacks = 5 },
FOREVER = { mode = "never" },
RETAIL = { mode = "chain", min_cp = 5 },
SOD = { mode = "sod_primary", ttd_max = 4, min_cp = 4, min_stacks = 1 },
}
-- The builder family gate, as DATA (BS-6, bench/filed_closure_sweep_2026-10-19.md
-- §3 U-04 / §7 BS-6): the candidate builders that are not a builder on every
-- client. Hemorrhage is a TBC/Forever/Era builder - the name is certain
-- ("Hemorrhage") and the ids are web-confirmed by wowhead's own redirect slugs
-- (TBC 26864, Era/Forever 16511; the bench corpus had the two swapped - 26862
-- is Sinister Strike). The class resolves the NAME through the client's own
-- spell data, so a wrong id can never produce a wrong builder. The SoD client
-- (the ERA token with the SoD-only Envenom rune known - the provider's frozen
-- latch) builds with a RUNE, never with Hemorrhage. A candidate with no row
-- here is a builder on every client that knows it (Saber Slash, Mutilate).
st.rogue_guide.BUILDER_GATED_FAMILIES = {
["Hemorrhage"] = { ERA = true, TBC = true, FOREVER = true },
}
-- Is this builder candidate a builder on this client? Pure: an ungated
-- candidate is always allowed; a gated candidate needs its family row and must
-- not be the SoD client (the "SOD" token, or the ERA token behind the
-- sod_envenom latch). An unknown family answers false - never a guess.
function st.rogue_guide.builder_candidate_allowed(name, family, sod_envenom)
if type(name) ~= "string" then
return false
end
local gated = st.rogue_guide.BUILDER_GATED_FAMILIES[name]
if gated == nil then
return true
end
if type(family) ~= "string" then
return false
end
if family == "SOD" then
return false
end
if family == "ERA" and sod_envenom == true then
return false
end
return gated[family] == true
end
-- Refresh / threshold defaults. SHIV 2 s is the §4 watch-list window;
-- RUPTURE 4 s mirrors the druid rip/rake refresh, EA 3 s the warrior Rend
-- window, TEA 40 the deficit cue - all three NEED IN-CLIENT CONFIRMATION
-- (no rogue source). RUPTURE_MIN_TTD 8 s is the sourced 1CP row
-- (rogue_research §5.2); the per-CP durations ship as
-- RUPTURE_DURATION_BY_CP DATA (1CP 8 s / 5CP 16 s sim-sourced, W-M2;
-- 2-4CP UNCONFIRMED — never interpolated).
st.rogue_guide.RUPTURE_REFRESH_DEFAULT = 4
st.rogue_guide.RUPTURE_MIN_TTD_DEFAULT = 8
st.rogue_guide.FINISHER_MIN_CP_DEFAULT = 5
st.rogue_guide.EA_REFRESH_DEFAULT = 3
st.rogue_guide.SHIV_REFRESH_DEFAULT = 2
st.rogue_guide.TEA_ENERGY_MAX_DEFAULT = 40
-- Time-to-die EMA (rogue-owned; vendored from the druid D4 precedent in
-- druid_energy.lua - same smoothing, same inputs/outputs, behavior-preserving.
-- Each class is self-contained: no cross-class runtime reads on the guide hot
-- path (bench/rogue_client_isolation_2026-10-19.md F1). The velocity feeding
-- the estimator is an exponential moving average of HP/s (verify_bank §4.1 -
-- raw samples spike on crits and DoT ticks), never a raw sample. The EMA
-- weight of the newest sample: 0.25 keeps a ~4-sample horizon.
st.rogue_guide.TTD_EMA_ALPHA = 0.25
-- Minimum seconds between HP samples: the live read is per-frame, and dividing
-- by a near-zero dt would manufacture velocity noise; the throttle gives the
-- EMA a stable dt.
st.rogue_guide.TTD_SAMPLE_SECONDS = 0.25
-- Pure: one EMA step of HP/s. hp / prev_hp are hit points, dt the seconds
-- between the samples, alpha the weight of the newest sample. A heal
-- (hp > prev_hp) contributes zero velocity: healing does not resurrect a
-- time-to-die, and a negative velocity would flip the division below into
-- nonsense. The first sample (a non-number ema) seeds from the raw velocity;
-- every later sample smooths. Anything unreadable (missing/non-number/NaN hp,
-- a non-positive dt) returns nil, which the caller reads as "drop the baseline
-- and re-seed" - a poisoned sample must never linger in the EMA.
function st.rogue_guide.update_hp_velocity(ema, hp, prev_hp, dt, alpha)
if type(hp) ~= "number" or type(prev_hp) ~= "number"
or hp ~= hp or prev_hp ~= prev_hp
or type(dt) ~= "number" or dt <= 0 or dt ~= dt then
return nil
end
local weight = alpha
if type(weight) ~= "number" or weight <= 0 or weight > 1 or weight ~= weight then
weight = st.rogue_guide.TTD_EMA_ALPHA
end
local velocity = (prev_hp - hp) / dt
if velocity < 0 then
velocity = 0
end
if type(ema) ~= "number" or ema ~= ema then
return velocity
end
return weight * velocity + (1 - weight) * ema
end
-- Pure: the time-to-die estimate from a smoothed HP/s and the live HP:
-- hp / velocity while the velocity is a positive number (a zero/negative
-- velocity has no finite crossing). Unreadable or NaN inputs read nil - the
-- caller treats that as unknown, never as an estimate.
function st.rogue_guide.get_time_to_die(hp, hp_velocity)
if type(hp) ~= "number" or type(hp_velocity) ~= "number"
or hp ~= hp or hp_velocity ~= hp_velocity or hp_velocity <= 0 then
return nil
end
return hp / hp_velocity
end
-- Numeric and usable: the NaN-safe number check every rule below funnels through.
local function is_num(x)
return type(x) == "number" and x == x
end
-- Pooling band for a family token (nil for RETAIL / unknown: no tick line there,
-- so pool_verdict stays silent instead of guessing).
function st.rogue_guide.band_for(family)
return st.rogue_guide.POOL_BANDS[family]
end
-- Pool-vs-spend verdict: "spend" (a finisher/builder before the tick that would
-- cap you - the Relentless-Strikes-25 timing, §1.3), "pool" (hold for the tick),
-- nil (no band for this client, or an unusable energy read - never a guess).
-- tick_in unknown degrades to "pool": pooling wastes nothing by itself, while a
-- timed spend without a clock would be a guess.
function st.rogue_guide.pool_verdict(energy, tick_in, band, max_energy)
if not is_num(energy) then
return nil
end
if type(band) ~= "table" then
return nil
end
if not is_num(band.pre) or not is_num(band.post) then
return nil
end
if not is_num(max_energy) then
max_energy = st.rogue_guide.MAX_ENERGY_DEFAULT
end
if energy >= max_energy then
return "spend"
end
if energy + st.rogue_guide.TICK_ENERGY_DEFAULT > max_energy then
return "spend"
end
if energy >= band.post then
return "spend"
end
if energy >= band.pre then
if is_num(tick_in) then
return "spend"
end
return "pool"
end
return "pool"
end
-- Rupture-vs-Eviscerate decider on a NUMERIC TTD input: "rupture",
-- "eviscerate", or nil (unknown TTD or CP below the planned-finisher line -
-- never a guessed finisher; the dying-target any-CP dump lives in evaluate).
function st.rogue_guide.finisher_for_ttd(cp, ttd, rupture_remains, opts)
opts = opts or {}
local min_cp = opts.min_cp
if not is_num(min_cp) then
min_cp = st.rogue_guide.FINISHER_MIN_CP_DEFAULT
end
local refresh = opts.rupture_refresh
if not is_num(refresh) then
refresh = st.rogue_guide.RUPTURE_REFRESH_DEFAULT
end
local min_ttd = opts.rupture_min_ttd
if not is_num(min_ttd) then
min_ttd = st.rogue_guide.RUPTURE_MIN_TTD_DEFAULT
end
if not is_num(cp) or cp < min_cp then
return nil
end
if not is_num(ttd) then
return nil
end
if ttd <= min_ttd then
return "eviscerate"
end
if rupture_remains == nil then
return "rupture"
end
if not is_num(rupture_remains) then
return nil
end
if rupture_remains <= refresh then
return "rupture"
end
return "eviscerate"
end
-- Rupture duration lookup per CP: the sourced row or nil. Unknown-hides —
-- 2-4CP (UNCONFIRMED), out-of-range, nil/NaN CP never guess and never
-- interpolate; the flat RUPTURE_MIN_TTD_* floors still govern every verdict
-- (no sim rule consumes RUPTURE_DURATION_BY_CP yet — W-M2 filed follow-up).
function st.rogue_guide.rupture_duration_for_cp(cp)
if not is_num(cp) then
return nil
end
local d = st.rogue_guide.RUPTURE_DURATION_BY_CP[cp]
if not is_num(d) then
return nil
end
return d
end
-- Per-client Envenom verdict: "envenom" or nil. Reads ONLY its family's rule
-- table (unknown family never prompts). args is the caller-owned inputs table;
-- band is the resolved pooling band (for the Wrath cap exception).
function st.rogue_guide.envenom_verdict(family, args, band)
args = args or {}
local rule = st.rogue_guide.ENVENOM_RULES[family]
if type(rule) ~= "table" then
return nil
end
local mode = rule.mode
if mode == "never" then
return nil
end
local cp = args.cp
local stacks = args.deadly_stacks
if mode == "dying_only" then
local ttd_max = rule.ttd_max
if not is_num(ttd_max) then
return nil
end
local min_cp = rule.min_cp
if not is_num(min_cp) then
return nil
end
local min_stacks = rule.min_stacks
if not is_num(min_stacks) then
return nil
end
if not is_num(cp) or cp < min_cp then
return nil
end
if not is_num(stacks) or stacks < min_stacks then
return nil
end
if not is_num(args.ttd) or args.ttd > ttd_max then
return nil
end
return "envenom"
end
if mode == "let_expire" then
local min_cp = rule.min_cp
if not is_num(min_cp) then
return nil
end
local min_stacks = rule.min_stacks
if not is_num(min_stacks) then
return nil
end
if not is_num(cp) or cp < min_cp then
return nil
end
if not is_num(stacks) or stacks < min_stacks then
return nil
end
local buff = args.envenom_buff_remains
if is_num(buff) and buff > 0 then
-- Buff rides: let it expire - unless energy would cap waiting.
local energy = args.energy_now
if type(band) == "table" and is_num(band.post)
and is_num(energy) and energy >= band.post then
return "envenom"
end
return nil
end
if buff ~= nil and not is_num(buff) then
return nil
end
return "envenom"
end
if mode == "chain" then
local min_cp = rule.min_cp
if not is_num(min_cp) then
return nil
end
if not is_num(cp) or cp < min_cp then
return nil
end
if is_num(stacks) and stacks <= 0 then
return nil
end
return "envenom"
end
if mode == "sod_primary" then
-- SoD primary (sim-CONFIRMED): 4+CP primary dump behind poison stacks,
-- with the 2+CP fight-end exception. Reached only as ERA +
-- sod_envenom == true (evaluate maps the family; direct callers pass
-- "SOD" themselves).
local ttd_max = rule.ttd_max
if not is_num(ttd_max) then
return nil
end
local min_cp = rule.min_cp
if not is_num(min_cp) then
return nil
end
local min_stacks = rule.min_stacks
if not is_num(min_stacks) then
return nil
end
local punch = is_num(cp) and cp >= min_cp
if not punch and is_num(cp) and cp >= 2
and is_num(args.ttd) and args.ttd <= ttd_max then
punch = true
end
if not punch then
return nil
end
if not is_num(stacks) or stacks < min_stacks then
return nil
end
return "envenom"
end
return nil
end
-- Shiv upkeep verdict: "shiv" when Shiv is usable and the Deadly clock proves a
-- falloff inside the window; nil otherwise (a nil clock never prompts - H4).
-- deadly_stacks rides the call shape for the provider but is intentionally
-- unread: a running falloff clock already proves stacks exist.
function st.rogue_guide.shiv_verdict(deadly_stacks, deadly_remaining, shiv_ready, window)
if shiv_ready ~= true then
return nil
end
if not is_num(window) then
window = st.rogue_guide.SHIV_REFRESH_DEFAULT
end
if deadly_remaining == nil then
return nil
end
if not is_num(deadly_remaining) then
return nil
end
if deadly_remaining <= window then
return "shiv"
end
return nil
end
-- Shiv family gate (audit T-G4): true when the family prompts Shiv at all and
-- the TBC Mutilate/CP gates pass. ERA carries no rows (ungated); an unknown
-- Mutilate (nil) never withholds; an unknown CP hides (never a guessed Shiv).
function st.rogue_guide.shiv_family_ok(family, args)
args = args or {}
if st.rogue_guide.SHIV_FAMILIES[family] ~= true then
return nil
end
if st.rogue_guide.SHIV_MUTILATE_GATE_FAMILIES[family] == true
and args.mutilate_known == true then
return nil
end
local cp_max = st.rogue_guide.SHIV_CP_MAX_BY_FAMILY[family]
if is_num(cp_max) then
local cp = args.cp
if not is_num(cp) or cp > cp_max then
return nil
end
end
return true
end
-- EA-duty refresh verdict: "expose" while on duty and the armor clock is missing
-- (nil prompts, like the druid rip/rake nil rule) or inside the window.
function st.rogue_guide.ea_verdict(ea_duty, ea_remains, window)
if ea_duty ~= true then
return nil
end