- forty files that carried the reference's literal Lua are rewritten as our own expression of the behaviour — worst identical run 61 lines down to 7 — and the README now says exactly that - the overlap gate measures the code itself across all four reference corpora, and ratchets: the number may only go down - a lost session ends at the login screen instead of silently re-authenticating and stealing the account back - a building's skybox is a real model: Caverns of Time's white ceiling becomes its painted nebula and additive stars Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
298 lines
16 KiB
XML
298 lines
16 KiB
XML
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||
<!--
|
||
The death arc's UI (decision 0308 slices 1/3): our death dialogs, registered on the shared
|
||
StaticPopup engine (UiPanels.xml), plus the event driver that raises and clears them.
|
||
|
||
The patch-chain FrameXML (mpqx extraction, decision 0246) is the SPECIFICATION for what
|
||
each dialog says, offers and answers — StaticPopup.lua l.375-505 (DEATH + RESURRECT×3),
|
||
l.1134-1201 (XP_LOSS×2 + RECOVER_CORPSE*), UIParent.lua l.237-262 + l.399-436 (the event
|
||
arms). The field names below are the published registry API that addons write into, so
|
||
they are the reference's and cannot be renamed; the code that fills them is ours. Six of
|
||
the eight dialogs share one disposition and are stamped with it rather than repeating it,
|
||
the three resurrect offers and the two spirit-healer confirms are each one shape built
|
||
more than once, and the driver is a single event switch. Strings are the reference's own
|
||
(GlobalStrings.lua), which is data, not authorship.
|
||
|
||
The engine-side events feeding this: PLAYER_DEAD/ALIVE/UNGHOST +
|
||
RESURRECT_REQUEST + CONFIRM_XP_LOSS fire from `death.rs`'s state machine;
|
||
CORPSE_IN_RANGE/OUT_OF_RANGE/IN_INSTANCE join with the slice-2 corpse-run range gate
|
||
(registered now — they simply don't fire yet). Deferred with 0308 §8: the SKINNED/
|
||
SKINNED_REPOP pair (battleground insignia) and UseSoulstone (HasSoulstone() is nil).
|
||
-->
|
||
<Script><![CDATA[
|
||
-- ── Strings (GlobalStrings.lua, verbatim; %% escapes as shipped) ─────────────────────────
|
||
local DEATH_RELEASE_TIMER = "%d %s until release"
|
||
local DEATH_RELEASE_NOTIMER = "You have died. Release to the nearest graveyard?"
|
||
local DEATH_RELEASE = "Release Spirit"
|
||
local USE_SOULSTONE = "Reincarnate" -- the shaman default; OnShow overrides with HasSoulstone()'s text
|
||
local RECOVER_CORPSE_TEXT = "Resurrect now?"
|
||
local RECOVER_CORPSE_TIMER = "%d %s until resurrection"
|
||
local RECOVER_CORPSE_INSTANCE_TEXT = "You must enter the instance to recover your corpse"
|
||
local RESURRECT_REQUEST_TEXT = "%s wants to resurrect you. You will be afflicted with resurrection sickness."
|
||
local RESURRECT_REQUEST_TIMER = "%s wants to resurrect you and will be able to in %d %s. You will be afflicted with resurrection sickness."
|
||
local RESURRECT_REQUEST_NO_SICKNESS = "%s wants to resurrect you"
|
||
local RESURRECT_REQUEST_NO_SICKNESS_TIMER = "%s wants to resurrect you and will be able to in %d %s"
|
||
local CONFIRM_XP_LOSS = "If you find your corpse, you can resurrect for no penalty. If I resurrect you all of your items will take 25%% durability damage (equipped and inventory) and you will be afflicted by %s of Resurrection Sickness."
|
||
local CONFIRM_XP_LOSS_AGAIN = "Remember, if you find your corpse there is no penalty. Are you sure you want to incur %s of Resurrection Sickness and have all your items take 25%% durability damage?"
|
||
local CONFIRM_XP_LOSS_NO_SICKNESS = "If you find your corpse, you can resurrect for no penalty. If I resurrect you all of your items will take 25%% durability damage (equipped and inventory)."
|
||
local CONFIRM_XP_LOSS_AGAIN_NO_SICKNESS = "Remember, if you find your corpse there is no penalty. Are you sure you want to have all your items take 25% durability damage?"
|
||
local ACCEPT = "Accept"
|
||
local DECLINE = "Decline"
|
||
local CANCEL = "Cancel"
|
||
|
||
-- ── The corpse-side disposition ──────────────────────────────────────────────────────────
|
||
-- Six of these eight dialogs are about a body on the floor, and every one of them wants
|
||
-- the same three things from the engine: it must be legal to raise while you are dead
|
||
-- (`whileDead` — that is the whole point of the arc), it takes precedence over a running
|
||
-- cinematic, and a logout must not quietly close it out from under you. Stamped on here
|
||
-- instead of retyped six times; the fields still land on the entry, so an addon reading
|
||
-- StaticPopupDialogs["DEATH"].whileDead sees exactly what the reference's does.
|
||
local function register_corpse_dialog(which, entry)
|
||
entry.whileDead = 1
|
||
entry.interruptCinematic = 1
|
||
entry.notClosableByLogout = 1
|
||
StaticPopupDialogs[which] = entry
|
||
end
|
||
|
||
-- How long a resurrect offer stands once it becomes answerable. It is both the engine's
|
||
-- own countdown (`timeout`) and the value OnShow re-seeds past the recovery delay.
|
||
local RESURRECT_OFFER_SECONDS = 60
|
||
|
||
-- ── The release dialog (spec: StaticPopup.lua l.375-414) ─────────────────────────────────
|
||
-- OnShow seeds `timeleft` from GetReleaseTimeRemaining(), which the engine renders through
|
||
-- its per-tick DEATH text; −1 means this map has no forced release (an instance), so the
|
||
-- static wording goes in and nothing counts down. `timeout = 0` because the countdown is
|
||
-- the SERVER's to enforce — it force-releases at 6:00 (0308 §1) — and there is deliberately
|
||
-- no hideOnEscape: you cannot dismiss dying. Button2 appears only with a soulstone on you,
|
||
-- which is never in v1 (HasSoulstone() is nil), so a cancel falls through to the release.
|
||
register_corpse_dialog("DEATH", {
|
||
text = DEATH_RELEASE_TIMER,
|
||
button1 = DEATH_RELEASE,
|
||
button2 = USE_SOULSTONE,
|
||
DisplayButton2 = function()
|
||
return HasSoulstone()
|
||
end,
|
||
OnShow = function()
|
||
local name = this:GetName()
|
||
this.timeleft = GetReleaseTimeRemaining()
|
||
if this.timeleft == -1 then
|
||
getglobal(name .. "Text"):SetText(DEATH_RELEASE_NOTIMER)
|
||
end
|
||
-- The soulstone names its own button ("Reincarnate", "Use Soulstone", …).
|
||
local soulstone = HasSoulstone()
|
||
if soulstone then
|
||
getglobal(name .. "Button2"):SetText(soulstone)
|
||
end
|
||
end,
|
||
OnAccept = function()
|
||
RepopMe()
|
||
end,
|
||
OnCancel = function(data, reason)
|
||
-- Only a real click on button2 is an answer. "override" and "timeout" are the
|
||
-- engine taking the dialog away, and neither is the player choosing anything.
|
||
if reason ~= "clicked" then
|
||
return
|
||
end
|
||
if HasSoulstone() then
|
||
UseSoulstone()
|
||
else
|
||
RepopMe()
|
||
end
|
||
end,
|
||
timeout = 0,
|
||
cancels = "RECOVER_CORPSE",
|
||
})
|
||
|
||
-- ── At the corpse (spec: StaticPopup.lua l.1182-1202) — slice 2's range gate raises these.
|
||
-- Accept is gated behind the recovery delay and, once taken, the dialog STAYS: reclaiming
|
||
-- is the server's to confirm, and it does so with the descriptor deltas that stand you up.
|
||
register_corpse_dialog("RECOVER_CORPSE", {
|
||
StartDelay = GetCorpseRecoveryDelay,
|
||
delayText = RECOVER_CORPSE_TIMER,
|
||
text = RECOVER_CORPSE_TEXT,
|
||
button1 = ACCEPT,
|
||
OnAccept = function()
|
||
RetrieveCorpse()
|
||
return 1
|
||
end,
|
||
timeout = 0,
|
||
})
|
||
-- Your body is inside a dungeon you are standing outside of: a notice, not an offer, so
|
||
-- it carries no button at all.
|
||
register_corpse_dialog("RECOVER_CORPSE_INSTANCE", {
|
||
text = RECOVER_CORPSE_INSTANCE_TEXT,
|
||
timeout = 0,
|
||
})
|
||
|
||
-- ── Somebody else's offer (spec: StaticPopup.lua l.416-505) ──────────────────────────────
|
||
-- Three registry keys, one shape: with sickness, without, and without a recovery timer to
|
||
-- wait out. They differ only in the wording and in whether the offer is delay-gated, so
|
||
-- they are built rather than written three times.
|
||
--
|
||
-- Refusing an offer (or letting it lapse) has to undo the `cancels = "DEATH"` that raising
|
||
-- it did — otherwise a declined resurrect leaves you dead with nothing on screen to
|
||
-- release from.
|
||
local function restore_release_dialog()
|
||
if UnitIsDead("player") then
|
||
StaticPopup_Show("DEATH")
|
||
end
|
||
end
|
||
local function resurrect_offer(offer_text, delay_text)
|
||
return {
|
||
text = offer_text,
|
||
button1 = ACCEPT,
|
||
button2 = DECLINE,
|
||
OnAccept = function()
|
||
AcceptResurrect()
|
||
end,
|
||
OnCancel = function()
|
||
DeclineResurrect()
|
||
restore_release_dialog()
|
||
end,
|
||
-- The countdown. Given a `delay_text` the offer is gated: Accept stays dead and
|
||
-- that text counts the corpse-recovery cooldown down; without one (the no-timer
|
||
-- variant) Accept is live at once. Either way the offer lapses a minute after it
|
||
-- becomes answerable, which is why OnShow re-seeds `timeleft` past the gate
|
||
-- instead of leaving `timeout`'s minute to start the moment it appears.
|
||
StartDelay = delay_text and GetCorpseRecoveryDelay or nil,
|
||
delayText = delay_text,
|
||
OnShow = function()
|
||
this.timeleft = GetCorpseRecoveryDelay() + RESURRECT_OFFER_SECONDS
|
||
end,
|
||
timeout = RESURRECT_OFFER_SECONDS,
|
||
cancels = "DEATH",
|
||
hideOnEscape = 1,
|
||
}
|
||
end
|
||
register_corpse_dialog("RESURRECT",
|
||
resurrect_offer(RESURRECT_REQUEST_TEXT, RESURRECT_REQUEST_TIMER))
|
||
register_corpse_dialog("RESURRECT_NO_SICKNESS",
|
||
resurrect_offer(RESURRECT_REQUEST_NO_SICKNESS, RESURRECT_REQUEST_NO_SICKNESS_TIMER))
|
||
register_corpse_dialog("RESURRECT_NO_TIMER",
|
||
resurrect_offer(RESURRECT_REQUEST_NO_SICKNESS))
|
||
|
||
-- ── The spirit healer's price (spec: StaticPopup.lua l.1134-1180) ────────────────────────
|
||
-- Both variants ask twice. The first Accept rewrites the dialog to a blunter second ask
|
||
-- and clears `data`; the truthy return keeps it open. The second Accept — now with no data
|
||
-- — sends the answer. So `data` is at once the payload and the "which ask is this" flag,
|
||
-- which is why the driver below hands the no-sickness variant a bare 1.
|
||
--
|
||
-- The second ask is a FUNCTION, not a string, because only one of the two has anything to
|
||
-- substitute: the no-sickness wording carries a bare "25% durability" that format() would
|
||
-- read as a conversion and choke on.
|
||
--
|
||
-- Neither offer outlives walking away from the healer, so both poll the distance.
|
||
local function close_when_out_of_reach(elapsed, dialog)
|
||
if not CheckSpiritHealerDist() then
|
||
dialog:Hide()
|
||
end
|
||
end
|
||
local function spirit_healer_confirm(first_ask, second_ask)
|
||
return {
|
||
text = first_ask,
|
||
button1 = ACCEPT,
|
||
button2 = CANCEL,
|
||
OnAccept = function(data)
|
||
if not data then
|
||
AcceptXPLoss()
|
||
return
|
||
end
|
||
-- `this` is the button the engine clicked; the dialog is its parent.
|
||
local dialog = this:GetParent()
|
||
getglobal(dialog:GetName() .. "Text"):SetText(second_ask(data))
|
||
dialog.data = nil
|
||
return 1
|
||
end,
|
||
OnUpdate = close_when_out_of_reach,
|
||
timeout = 0,
|
||
exclusive = 1,
|
||
whileDead = 1,
|
||
showAlert = 1,
|
||
hideOnEscape = 1,
|
||
}
|
||
end
|
||
StaticPopupDialogs["XP_LOSS"] = spirit_healer_confirm(CONFIRM_XP_LOSS, function(sickness)
|
||
return format(CONFIRM_XP_LOSS_AGAIN, sickness)
|
||
end)
|
||
StaticPopupDialogs["XP_LOSS_NO_SICKNESS"] = spirit_healer_confirm(
|
||
CONFIRM_XP_LOSS_NO_SICKNESS,
|
||
function()
|
||
return CONFIRM_XP_LOSS_AGAIN_NO_SICKNESS
|
||
end)
|
||
|
||
-- ── The driver (spec: UIParent.lua l.237-262 + l.399-436) ───────────────────────────────
|
||
-- One switch over the arc's events. The reference spreads these arms through the whole of
|
||
-- UIParent_OnEvent; ours has a frame of its own (below) and nothing else to share it with.
|
||
function BenillaDeathDriver_OnEvent()
|
||
if event == "PLAYER_DEAD" then
|
||
if not StaticPopup_Visible("DEATH") then
|
||
CloseAllWindows(1)
|
||
if GetReleaseTimeRemaining() > 0 or GetReleaseTimeRemaining() == -1 then
|
||
StaticPopup_Show("DEATH")
|
||
end
|
||
end
|
||
elseif event == "PLAYER_ALIVE" then
|
||
StaticPopup_Hide("DEATH")
|
||
StaticPopup_Hide("RESURRECT_NO_SICKNESS")
|
||
elseif event == "PLAYER_UNGHOST" then
|
||
StaticPopup_Hide("RESURRECT")
|
||
StaticPopup_Hide("RESURRECT_NO_SICKNESS")
|
||
StaticPopup_Hide("RESURRECT_NO_TIMER")
|
||
elseif event == "RESURRECT_REQUEST" then
|
||
if ResurrectHasSickness() then
|
||
StaticPopup_Show("RESURRECT", arg1)
|
||
elseif ResurrectHasTimer() then
|
||
StaticPopup_Show("RESURRECT_NO_SICKNESS", arg1)
|
||
else
|
||
StaticPopup_Show("RESURRECT_NO_TIMER", arg1)
|
||
end
|
||
elseif event == "CONFIRM_XP_LOSS" then
|
||
-- If the healer would charge resurrection sickness, its duration goes into the
|
||
-- text; if not there is nothing to quote, and the bare 1 handed over instead is
|
||
-- only the "this is still the first ask" marker the two-step above reads.
|
||
local sickness = GetResSicknessDuration()
|
||
local dialog
|
||
if sickness then
|
||
dialog = StaticPopup_Show("XP_LOSS", sickness)
|
||
else
|
||
dialog = StaticPopup_Show("XP_LOSS_NO_SICKNESS")
|
||
end
|
||
if dialog then
|
||
dialog.data = sickness or 1
|
||
end
|
||
-- The confirm covers the healer's gossip window (spec: UIParent.lua l.412).
|
||
local gossip = getglobal("GossipFrame")
|
||
if gossip and gossip:IsVisible() then
|
||
HideUIPanel(gossip)
|
||
end
|
||
elseif event == "CORPSE_IN_RANGE" then
|
||
StaticPopup_Show("RECOVER_CORPSE")
|
||
elseif event == "CORPSE_IN_INSTANCE" then
|
||
StaticPopup_Show("RECOVER_CORPSE_INSTANCE")
|
||
elseif event == "CORPSE_OUT_OF_RANGE" then
|
||
StaticPopup_Hide("RECOVER_CORPSE")
|
||
StaticPopup_Hide("RECOVER_CORPSE_INSTANCE")
|
||
StaticPopup_Hide("XP_LOSS")
|
||
end
|
||
end
|
||
]]></Script>
|
||
|
||
<Frame name="BenillaDeathDriver" hidden="true">
|
||
<Size><AbsDimension x="1" y="1"/></Size>
|
||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||
<Scripts>
|
||
<OnLoad>
|
||
self:RegisterEvent("PLAYER_DEAD")
|
||
self:RegisterEvent("PLAYER_ALIVE")
|
||
self:RegisterEvent("PLAYER_UNGHOST")
|
||
self:RegisterEvent("RESURRECT_REQUEST")
|
||
self:RegisterEvent("CONFIRM_XP_LOSS")
|
||
self:RegisterEvent("CORPSE_IN_RANGE")
|
||
self:RegisterEvent("CORPSE_IN_INSTANCE")
|
||
self:RegisterEvent("CORPSE_OUT_OF_RANGE")
|
||
</OnLoad>
|
||
<OnEvent>BenillaDeathDriver_OnEvent()</OnEvent>
|
||
</Scripts>
|
||
</Frame>
|
||
</Ui>
|