Files
benilla-octo/crates/benilla-ui/src/script/death.rs
T
SamandClaude Fable 5 2ad0043523 benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:27:01 +07:00

152 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The death-arc Lua surface (decision 0308): the release/reclaim/resurrect verbs and the state
//! getters the reference's DEATH / RECOVER_CORPSE / RESURRECT* / XP_LOSS dialogs call.
//!
//! Engine-free seam like [`super::unit`]: the app pushes a [`DeathUiState`] snapshot per frame
//! (the countdowns + offer bits it computes from the wire) and drains the queued
//! [`DeathAction`]s into `ClientCommand`s. The predicates return Lua booleans rather than the
//! client's `1`/nil — truthy either way, the branch shape callers use is identical (the
//! [`super::unit`] convention).
use mlua::{Lua, Value};
use super::Model;
/// The death snapshot the app pushes each frame ([`super::UiScript::set_death`]). Plain data —
/// no mlua handles, no ECS types.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DeathUiState {
/// Seconds until the server force-releases — the client-side mirror of the 6:00
/// `CORPSE_REPOP_TIME` the wire never carries (decision 0308 §4), counted from the death
/// edge. `None` = no release timer (`PLAYER_FIELD_BYTES` bit 0x08 clear — an instanceable
/// map): `GetReleaseTimeRemaining` returns **−1** and the DEATH dialog shows the no-timer
/// text.
pub release_remaining: Option<f32>,
/// Seconds until the corpse becomes reclaimable (`SMSG_CORPSE_RECLAIM_DELAY` anchored at
/// arrival; `0` = reclaimable now). `GetCorpseRecoveryDelay`'s value — the RECOVER_CORPSE /
/// RESURRECT StartDelay gate.
pub recovery_delay: f32,
/// The pending resurrect offer warns of resurrection sickness (`ResurrectHasSickness`).
pub resurrect_sickness: bool,
/// The pending offer still honors the reclaim-delay gate (`ResurrectHasTimer`).
pub resurrect_has_timer: bool,
/// The confirm-owning spirit healer is within dialog range (`CheckSpiritHealerDist` — the
/// XP_LOSS dialogs' OnUpdate auto-hide).
pub spirit_healer_in_range: bool,
/// The "N minutes"/"N seconds" sickness-duration string a spirit-healer res would apply, or
/// `None` below the sickness level (`GetResSicknessDuration` → nil — picks XP_LOSS vs
/// XP_LOSS_NO_SICKNESS, ref UIParent.lua's CONFIRM_XP_LOSS arm).
pub sickness_duration: Option<String>,
}
/// One drained death intent (the app's [`super::UiScript::take_death_actions`] maps each to its
/// `ClientCommand`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeathAction {
/// `RepopMe()` — release the spirit (`CMSG_REPOP_REQUEST`).
Repop,
/// `RetrieveCorpse()` — reclaim the corpse (`CMSG_RECLAIM_CORPSE`).
RetrieveCorpse,
/// `AcceptResurrect()` — accept the pending offer (`CMSG_RESURRECT_RESPONSE` accept).
AcceptResurrect,
/// `DeclineResurrect()` — decline it (`CMSG_RESURRECT_RESPONSE` decline).
DeclineResurrect,
/// `AcceptXPLoss()` — take the spirit healer's res (`CMSG_SPIRIT_HEALER_ACTIVATE`).
AcceptXpLoss,
}
impl super::UiScript {
/// Push this frame's death snapshot (the app's per-frame feed, before the event dispatch).
pub fn set_death(&mut self, state: DeathUiState) {
self.model_mut().death = state;
}
/// Drain the queued death intents (the app's per-frame drain).
pub fn take_death_actions(&mut self) -> Vec<DeathAction> {
std::mem::take(&mut self.model_mut().death_actions)
}
}
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
fn install_action(lua: &Lua, name: &str, action: DeathAction) -> mlua::Result<()> {
lua.globals().set(
name,
lua.create_function(move |lua, ()| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.death_actions.push(action);
Ok(())
})?,
)
}
install_action(lua, "RepopMe", DeathAction::Repop)?;
install_action(lua, "RetrieveCorpse", DeathAction::RetrieveCorpse)?;
install_action(lua, "AcceptResurrect", DeathAction::AcceptResurrect)?;
install_action(lua, "DeclineResurrect", DeathAction::DeclineResurrect)?;
install_action(lua, "AcceptXPLoss", DeathAction::AcceptXpLoss)?;
// GetReleaseTimeRemaining() → seconds until the forced release, or −1 = no timer (the DEATH
// dialog's no-timer text pick, ref StaticPopup.lua:380-387).
g.set(
"GetReleaseTimeRemaining",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(model
.death
.release_remaining
.map_or(-1.0, |s| f64::from(s.max(0.0))))
})?,
)?;
// GetCorpseRecoveryDelay() → whole seconds until reclaimable (0 = now) — the StartDelay gate.
g.set(
"GetCorpseRecoveryDelay",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(f64::from(model.death.recovery_delay.max(0.0).ceil()))
})?,
)?;
g.set(
"ResurrectHasSickness",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(model.death.resurrect_sickness)
})?,
)?;
g.set(
"ResurrectHasTimer",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(model.death.resurrect_has_timer)
})?,
)?;
g.set(
"CheckSpiritHealerDist",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(model.death.spirit_healer_in_range)
})?,
)?;
// GetResSicknessDuration() → the duration string, or nil below the sickness level (the
// CONFIRM_XP_LOSS variant pick, ref UIParent.lua:399-408).
g.set(
"GetResSicknessDuration",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(match &model.death.sickness_duration {
Some(s) => Value::String(lua.create_string(s)?),
None => Value::Nil,
})
})?,
)?;
// HasSoulstone() → nil: soulstones are a named 0308 deferral, and nil is exactly the real
// return for a player with no active soulstone — the DEATH dialog's button2 stays hidden and
// its OnCancel falls through to RepopMe(), both ref-faithful.
g.set("HasSoulstone", lua.create_function(|_, ()| Ok(Value::Nil))?)?;
Ok(())
}