-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCore.lua
More file actions
514 lines (454 loc) · 19.9 KB
/
Copy pathCore.lua
File metadata and controls
514 lines (454 loc) · 19.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
-- TrueShot: AssistedCombat rotation overlay with cast-tracked state
-- Copyright (C) 2026 itsDNNS
-- Licensed under GPL-3.0-or-later. See LICENSE.
------------------------------------------------------------------------
-- Global namespace & saved variables
------------------------------------------------------------------------
TrueShot = TrueShot or {}
TrueShotDB = TrueShotDB or {}
local DEFAULTS = {
iconCount = 1,
iconSize = 40,
iconSpacing = 4,
locked = false,
enableDiagnostics = false,
showCooldownSwipe = true,
showCooldownText = false,
showCastFeedback = true,
showWhyOverlay = false,
showPhaseIndicator = false,
showOverrideIndicator = true,
showKeybinds = true,
showRangeIndicator = true,
showIdleState = true,
combatOnly = false,
enemyTargetOnly = false,
overlayScale = 1.0,
overlayOpacity = 1.0,
hidden = false,
firstIconScale = 1.3,
orientation = "LEFT",
showBackdrop = true,
showAoeHint = true,
reasonPosition = "BELOW", -- BELOW or ABOVE
aoeHintPosition = "BELOW", -- BELOW or LEFT
showLoginMessage = false,
showScorecard = true,
showHeartbeat = false,
strictCompliance = true,
}
local optionCallbacks = {}
function TrueShot.GetOpt(key)
if TrueShotDB[key] ~= nil then return TrueShotDB[key] end
return DEFAULTS[key]
end
function TrueShot.SetOpt(key, value)
local prev = TrueShot.GetOpt(key)
if prev == value then return end
TrueShotDB[key] = value
for _, callback in ipairs(optionCallbacks) do
callback(key, value, prev)
end
end
function TrueShot.RegisterOptCallback(callback)
optionCallbacks[#optionCallbacks + 1] = callback
end
function TrueShot.DiagnosticsEnabled()
return TrueShot.GetOpt("enableDiagnostics") and true or false
end
TrueShot.RegisterOptCallback(function(key, value)
if key == "enableDiagnostics" and value ~= true
and TrueShot.Engine and TrueShot.Engine.ClearDecisionHistory then
TrueShot.Engine:ClearDecisionHistory()
end
end)
local function AssistedCombatAvailable()
if not C_AssistedCombat or not C_AssistedCombat.IsAvailable then return false end
local ok, available = pcall(C_AssistedCombat.IsAvailable)
if not ok or (issecretvalue and issecretvalue(available)) then return false end
return available == true
end
------------------------------------------------------------------------
-- Lifecycle
------------------------------------------------------------------------
local Engine -- resolved after all files load
local Display
local function GetActiveSpecID()
local specIndex = GetSpecialization()
if not specIndex then return nil end
return GetSpecializationInfo(specIndex)
end
local function TryActivate()
Engine = TrueShot.Engine
Display = TrueShot.Display
local specID = GetActiveSpecID()
if not specID then
Display:Disable()
return false
end
if not Engine:ActivateProfile(specID) then
Display:Disable()
return false
end
if not AssistedCombatAvailable() then
Display:Disable()
return false
end
-- Visibility handled by ReconcileVisibility after activation
return true
end
------------------------------------------------------------------------
-- Events
------------------------------------------------------------------------
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
eventFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
eventFrame:RegisterEvent("PLAYER_TALENT_UPDATE")
eventFrame:RegisterEvent("SPELLS_CHANGED")
eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
eventFrame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
local function ShouldShowOverlay()
if TrueShot.GetOpt("hidden") then return false end
if not AssistedCombatAvailable() then return false end
if TrueShot.GetOpt("enemyTargetOnly") then
if UnitExists("target") and UnitCanAttack("player", "target") then return true end
return UnitAffectingCombat("player")
end
if TrueShot.GetOpt("combatOnly") then
return UnitAffectingCombat("player")
end
return true
end
local function ReconcileVisibility()
if not TrueShot.Engine or not TrueShot.Engine.activeProfile then return end
if not TrueShot.Display then return end
if ShouldShowOverlay() then
TrueShot.Display:Enable()
else
TrueShot.Display:Disable()
end
end
TrueShot.ReconcileVisibility = ReconcileVisibility
eventFrame:SetScript("OnEvent", function(self, event, ...)
Engine = TrueShot.Engine
Display = TrueShot.Display
if event == "PLAYER_ENTERING_WORLD" then
if TrueShot.CDLedger and TrueShot.CDLedger.ReseedFromCooldownAPI then
TrueShot.CDLedger:ReseedFromCooldownAPI()
end
if TryActivate() then
if TrueShot.GetOpt("showLoginMessage") then
local profile = Engine.activeProfile
local name = profile and (profile.displayName or profile.id) or "unknown"
print("|cff00ff00[TrueShot]|r Ready. |cffffff00" .. name .. "|r active. Type |cffffff00/ts help|r for commands.")
end
else
if TrueShot.GetOpt("showLoginMessage") then
local specID = GetActiveSpecID()
if not TrueShot.Profiles[specID or 0] then
print("|cffaaaaaa[TrueShot]|r No profile for current spec. Addon inactive.")
else
print("|cffff0000[TrueShot]|r Assisted Combat not available.")
end
end
end
ReconcileVisibility()
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
local unit, _, spellID = ...
if unit == "player" and spellID then
-- CDLedger runs alongside the profile dispatch so timer state is
-- updated before any condition (including the one that just fired
-- this cast through MarkDirty) re-evaluates.
if TrueShot.CDLedger and TrueShot.CDLedger.OnSpellCastSucceeded then
TrueShot.CDLedger:OnSpellCastSucceeded(spellID)
end
Engine:OnSpellCast(spellID)
if Display then
if Display.OnSpellCastSucceeded then
Display:OnSpellCastSucceeded(spellID)
end
if Display.MarkDirty then Display:MarkDirty() end
end
end
elseif event == "SPELL_UPDATE_COOLDOWN" then
-- Best-effort reconcile: prunes ledger entries whose cooldowns are
-- demonstrably finished per a non-secret C_Spell read. Cast-event
-- entries with secret/unreadable responses are left intact so the
-- tier-3 visual swipe fallback keeps animating.
if TrueShot.CDLedger and TrueShot.CDLedger.ReconcileFromCooldownAPI then
TrueShot.CDLedger:ReconcileFromCooldownAPI()
end
if Display and Display.MarkDirty then Display:MarkDirty() end
elseif event == "PLAYER_REGEN_DISABLED" then
Engine.combatStartTime = GetTime()
if TrueShot.CombatTrace then TrueShot.CombatTrace:Reset() end
if Display and Display.ResetQueueStabilization then
Display:ResetQueueStabilization()
end
if Display and Display.MarkDirty then Display:MarkDirty() end
ReconcileVisibility()
elseif event == "PLAYER_REGEN_ENABLED" then
if TrueShot.Scorecard and Engine.combatStartTime then
local combatDuration = GetTime() - Engine.combatStartTime
TrueShot.Scorecard:OnCombatEnd(combatDuration)
end
Engine.combatStartTime = nil
Engine:OnCombatEnd()
if Display and Display.ResetQueueStabilization then
Display:ResetQueueStabilization()
end
if Display and Display.MarkDirty then Display:MarkDirty() end
ReconcileVisibility()
elseif event == "PLAYER_TARGET_CHANGED" then
if Display and Display.MarkDirty then Display:MarkDirty() end
ReconcileVisibility()
elseif event == "PLAYER_SPECIALIZATION_CHANGED"
or event == "PLAYER_TALENT_UPDATE"
or event == "SPELLS_CHANGED" then
local prev = Engine.activeProfile
TryActivate()
local curr = Engine.activeProfile
if curr and curr ~= prev and TrueShot.GetOpt("showLoginMessage") then
local name = curr.displayName or curr.id or "unknown"
print("|cff00ff00[TrueShot]|r Profile switched: " .. name)
end
ReconcileVisibility()
-- Force immediate display refresh after any spell/talent change
if Display and Display.container and Display.container:IsShown() then
local queue = Engine:ComputeQueue(TrueShot.GetOpt("iconCount"))
if Display.RenderQueueNow then
Display:RenderQueueNow(queue)
else
Display:UpdateQueue(queue)
end
end
-- Delayed re-check: spellbook may still be updating
C_Timer.After(0.5, function()
local prevDelayed = Engine.activeProfile
TryActivate()
local currDelayed = Engine.activeProfile
if currDelayed and currDelayed ~= prevDelayed and TrueShot.GetOpt("showLoginMessage") then
print("|cff00ff00[TrueShot]|r Profile switched: " .. (currDelayed.displayName or currDelayed.id or "unknown"))
end
ReconcileVisibility()
if Display and Display.container and Display.container:IsShown() then
local queue = Engine:ComputeQueue(TrueShot.GetOpt("iconCount"))
if Display.RenderQueueNow then
Display:RenderQueueNow(queue)
else
Display:UpdateQueue(queue)
end
end
end)
end
end)
------------------------------------------------------------------------
-- Slash commands
------------------------------------------------------------------------
local function SafeDebugText(value, fallback)
if issecretvalue and issecretvalue(value) then return "<secret>" end
if value == nil then return fallback or "none" end
local valueType = type(value)
if valueType == "string" or valueType == "number" or valueType == "boolean" then
return tostring(value)
end
return fallback or "unavailable"
end
local function SafeDebugSpell(spellID)
if issecretvalue and issecretvalue(spellID) then return "<secret>" end
if spellID == nil or type(spellID) ~= "number" then return "none" end
local name = nil
if C_Spell and C_Spell.GetSpellName then
local ok, result = pcall(C_Spell.GetSpellName, spellID)
if ok and not (issecretvalue and issecretvalue(result)) and type(result) == "string" then
name = result
end
end
return (name or "Spell") .. " (" .. tostring(spellID) .. ")"
end
SLASH_TRUESHOT1 = "/ts"
SLASH_TRUESHOT2 = "/trueshot"
SlashCmdList["TRUESHOT"] = function(msg)
Engine = TrueShot.Engine
Display = TrueShot.Display
msg = strtrim(msg:lower())
if msg == "lock" then
TrueShot.SetOpt("locked", true)
Display:SetClickThrough(true)
print("|cff00ff00[TS]|r Frame locked (click-through).")
elseif msg == "unlock" then
TrueShot.SetOpt("locked", false)
Display:SetClickThrough(false)
print("|cff00ff00[TS]|r Frame unlocked. Drag to reposition.")
elseif msg == "burst" then
Engine.burstModeActive = not Engine.burstModeActive
if Display and Display.MarkDirty then Display:MarkDirty() end
if Engine.burstModeActive then
print("|cff00ff00[TS]|r Burst mode ON")
else
print("|cff00ff00[TS]|r Burst mode OFF")
end
elseif msg == "hide" then
TrueShot.SetOpt("hidden", true)
Display:Disable()
print("|cff00ff00[TS]|r Hidden. /ts show to restore.")
elseif msg == "show" then
TrueShot.SetOpt("hidden", false)
ReconcileVisibility()
if not Display.container:IsShown() then
print("|cff00ff00[TS]|r Overlay will show when conditions are met (target/combat).")
end
elseif msg == "options" or msg == "config" then
if TrueShot.OpenSettingsPanel then
TrueShot.OpenSettingsPanel()
else
print("|cffff0000[TS]|r Settings panel unavailable.")
end
elseif msg == "diagnostics on" or msg == "diag on" then
TrueShot.SetOpt("enableDiagnostics", true)
print("|cff00ff00[TS]|r Diagnostics enabled. `/ts probe ...` is now available.")
elseif msg == "diagnostics off" or msg == "diag off" then
TrueShot.SetOpt("enableDiagnostics", false)
print("|cff00ff00[TS]|r Diagnostics disabled.")
elseif msg == "diagnostics" or msg == "diag" then
local state = TrueShot.DiagnosticsEnabled() and "ON" or "OFF"
print("|cff00ff00[TS]|r Diagnostics: " .. state)
print(" Use `/ts diagnostics on` or `/ts diagnostics off`.")
elseif msg == "strict on" then
TrueShot.SetOpt("strictCompliance", true)
if Display and Display.MarkDirty then Display:MarkDirty() end
print("|cff00ff00[TS]|r Strict compliance mode ON. Experimental overrides disabled.")
elseif msg == "strict off" or msg == "experimental on" then
TrueShot.SetOpt("strictCompliance", false)
if Display and Display.MarkDirty then Display:MarkDirty() end
print("|cffffff00[TS]|r Experimental override mode ON. Use only for validation.")
elseif msg == "strict" or msg == "experimental" then
local strict = TrueShot.GetOpt("strictCompliance") ~= false
print("|cff00ff00[TS]|r Strict compliance mode: " .. (strict and "ON" or "OFF"))
print(" /ts strict on - Disable experimental overrides")
print(" /ts strict off - Enable experimental overrides for validation")
elseif msg == "debug" then
local queue = Engine:ComputeQueue(TrueShot.GetOpt("iconCount"))
print("|cff00ff00[TS] Queue:|r")
for i, id in ipairs(queue) do
local castable = Engine:IsSpellCastable(id) and "usable" or "not usable"
print(" " .. i .. ": " .. SafeDebugSpell(id) .. " [" .. castable .. "]")
end
local meta = Engine.lastQueueMeta or {}
print("|cff00ff00[TS] Decision:|r")
print(" Raw AC: " .. SafeDebugSpell(meta.rawACSpell)
.. " [" .. SafeDebugText(meta.rawACStatus, "unavailable") .. "]")
print(" Final primary: " .. SafeDebugSpell(meta.finalPrimarySpell))
print(" Source/reason: " .. SafeDebugText(meta.source)
.. " / " .. SafeDebugText(meta.reasonCode))
print(" Detail: reason=" .. SafeDebugText(meta.reason)
.. ", fallback/drop=" .. SafeDebugText(meta.fallbackDropReason))
local catalog = meta.rotationCatalogSnapshot
local catalogCount = 0
if not (issecretvalue and issecretvalue(catalog)) and type(catalog) == "table" then
catalogCount = #catalog
end
print(" Strict: " .. SafeDebugText(meta.strictState)
.. ", rotation catalog context: " .. tostring(catalogCount))
if Display and Display.GetStabilizationSnapshot then
local stabilization = Display:GetStabilizationSnapshot()
if not (issecretvalue and issecretvalue(stabilization)) and type(stabilization) == "table" then
print(" Display: shown=" .. SafeDebugSpell(stabilization.displayedPrimary)
.. ", pending=" .. SafeDebugSpell(stabilization.pendingPrimary)
.. ", ticks=" .. SafeDebugText(stabilization.pendingTicks, "0")
.. ", age=" .. SafeDebugText(stabilization.pendingAge, "0")
.. ", deadlineForced=" .. SafeDebugText(stabilization.staleDeadlineForcedLastCommit, "false"))
end
end
local recentCount = Engine.GetDecisionHistoryCount and Engine:GetDecisionHistoryCount() or 0
print(" Diagnostic decision changes retained: " .. tostring(recentCount))
local profile = Engine.activeProfile
if profile and profile.GetDebugLines then
print("|cff00ff00[TS] Profile State:|r")
for _, line in ipairs(profile:GetDebugLines()) do
print(line)
end
end
print(" Burst mode: " .. tostring(Engine.burstModeActive))
elseif msg == "smoke" then
if TrueShot.SmokeTest and TrueShot.SmokeTest.Run then
TrueShot.SmokeTest:Run()
else
print("|cffff0000[TS]|r SmokeTest not loaded.")
end
elseif msg == "combat-smoke" or msg == "combat smoke" then
if TrueShot.SmokeTest and TrueShot.SmokeTest.Run then
TrueShot.SmokeTest:Run({ mode = "combat", requireCombat = true })
else
print("|cffff0000[TS]|r SmokeTest not loaded.")
end
elseif msg:sub(1, 5) == "probe" then
if not TrueShot.DiagnosticsEnabled() then
print("|cffffff00[TS]|r Probe diagnostics are disabled. Enable them via `/ts diagnostics on` or in `/ts options`.")
return
end
local probeArgs = msg:sub(7) or ""
TrueShot.SignalProbe:HandleCommand(probeArgs)
elseif msg == "score" or msg == "scores" then
if TrueShot.Scorecard then
TrueShot.Scorecard:PrintHistory(5)
else
print("|cff00ff00[TS]|r Scorecard not loaded.")
end
elseif msg == "export" then
if TrueShot.ProfileIO and TrueShot.ProfileIO.ShowExport then
TrueShot.ProfileIO:ShowExport()
else
print("|cffff0000[TS]|r ProfileIO not loaded.")
end
elseif msg == "import" then
if TrueShot.ProfileIO and TrueShot.ProfileIO.ShowImport then
TrueShot.ProfileIO:ShowImport()
else
print("|cffff0000[TS]|r ProfileIO not loaded.")
end
elseif msg == "rules" then
if TrueShot.RuleBuilder and TrueShot.RuleBuilder.Toggle then
TrueShot.RuleBuilder:Toggle()
else
print("|cffff0000[TS]|r Rule Builder not loaded.")
end
elseif msg == "profiles" then
if TrueShot.RuleBuilder and TrueShot.RuleBuilder.Toggle then
TrueShot.RuleBuilder:Toggle()
else
print("|cffff0000[TS]|r Rule Builder not loaded.")
end
elseif msg == "browse" then
if TrueShot.ProfileIO and TrueShot.ProfileIO.ToggleBrowser then
TrueShot.ProfileIO:ToggleBrowser()
else
print("|cffff0000[TS]|r ProfileIO not loaded.")
end
elseif msg == "help" then
print("|cff00ff00[TrueShot]|r Commands:")
print(" /ts lock - Lock frame (click-through)")
print(" /ts unlock - Unlock frame for dragging")
print(" /ts options - Open the TrueShot settings panel")
print(" /ts burst - Toggle burst mode")
print(" /ts hide - Hide the display")
print(" /ts show - Show the display")
print(" /ts debug - Print queue and profile state")
print(" /ts smoke - Run in-client strict compliance smoke test")
print(" /ts combat-smoke - Run strict compliance smoke test requiring combat")
print(" /ts score - Show recent alignment scores")
print(" /ts rules - Open the Visual Rule Builder")
print(" /ts profiles - Open the Visual Rule Builder (alias for /ts rules)")
print(" /ts browse - Browse all profiles (Class > Spec > Hero Talent)")
print(" /ts export - Export custom profile as shareable string")
print(" /ts import - Import a profile from a shared string")
print(" /ts diagnostics on|off - Enable or disable probe diagnostics")
print(" /ts strict on|off - Toggle strict compliance / experimental override mode")
print(" /ts probe - Signal validation probes (only when diagnostics are enabled)")
else
print("|cff00ff00[TrueShot]|r Use /ts help for commands.")
end
end