Files
benilla-octo/crates/benilla-ui/src/script/duel.rs
T
SamandClaude Opus 5 959fb8ba08 duels, inspecting other players, and water that ends at the bank
- Duels. Challenge another player from their right-click menu, or with `/duel`, and
  `/forfeit` to give it up. Both popups are there — the challenge you accept or decline, and
  the warning when you leave the field — with the flag countdown between them, and the outcome
  printed in chat when someone is knocked out or flees. Read off the reference client's own
  duel packets and the three globals it keeps client-side, which is why the challenger reads
  "You have requested a duel." even though the server never echoes that back.
- Inspect. Right-click a player and open their paper doll: their nineteen publicly visible
  equipment slots with icon, name, quality and a linkable item, and their body standing in
  the pane, which follows along if you retarget while the window is open. No stack counts and
  no durability — those are not fields another player publishes, which is exactly why the
  reference client's inspect doll has never shown them either.
- Water knows where it stops. Swimming used to trigger in the dry tunnel thirty yards under
  Stormwind's canal, and in mid-air over Blackrock's lava. A liquid is now the grid the map
  file actually stores — one wet flag per cell, and the surface height a bilinear over the
  four corners of the cell you are standing in, the same sample the reference client takes —
  so the water starts at the bank and its surface sits where your eye puts it. Lava is a
  liquid you swim in, an indoor liquid asks the building you are inside, and `/liquid` prints
  every surface claiming your feet and where each one came from.
- Creature parts authored to appear only on death — the voidwalker, the banshee, the ooze,
  the stone elemental, the infernal — were drawing the whole time: material alpha now bakes
  per animation sequence and the unit lane actually evaluates it. The character and inspect
  model panes are lit by the reference widget's own single light instead of blowing out white,
  the rotate arrows spin while you hold them, and the transparent sort ladder had its sign
  backwards, so overhead names draw over water again instead of being painted out by a canal
  behind them. Underneath, the protocol writer's 95-verb drawer and its thousand-line spell
  module split into per-family files — no behaviour change, just a tree you can find things in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 18:22:05 +07:00

99 lines
4.1 KiB
Rust

//! The duel **Era API surface** (decision 0633) — four globals, no state.
//!
//! Duels are the smallest possible shape of the [`super::party`] seam: everything the UI needs to
//! *read* arrives as event arguments (the challenger's name on `DUEL_REQUESTED`), so there is no
//! snapshot to push — only the outbound half. Each call queues a [`DuelRequest`] the app drains
//! ([`UiScript::take_duel_requests`]) and turns into its send, keeping the engine free of ECS/net
//! reach (decision 0068 §3).
//!
//! The four are exactly the reference's own duel bindings, registered adjacent in its Lua API
//! table (`0x849fc8`..`0x849ff0`) and each a one-liner over the same TU: `AcceptDuel` `0x4d4ce0`
//! → `0x4d4830`, `CancelDuel` `0x4d4cf0` → `0x4d48b0`, `StartDuelUnit` `0x4d4c40` (unit token →
//! guid, gated on typemask `0x10` = player), `StartDuel` `0x4d4c90` (name → guid). The two
//! `StartDuel*` calls do **not** send a duel opcode — they cast the duel spell at the guid; the
//! app owns that resolution.
use mlua::Lua;
use super::Model;
/// Outbound duel intents queued by the Era API calls, drained by the app
/// ([`UiScript::take_duel_requests`]). Plain data — [`super::party::PartyRequest`]'s twin.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DuelRequest {
/// `AcceptDuel()` — accept the pending challenge (`CMSG_DUEL_ACCEPTED`).
Accept,
/// `CancelDuel()` — decline, cancel, or forfeit (`CMSG_DUEL_CANCELLED`); which one it means
/// is the server's read of the duel state, not ours.
Cancel,
/// `StartDuel(name)` — challenge a player found by name. The app resolves the name to a guid
/// and casts the duel spell at it; an unresolvable name is dropped (the reference errors the
/// Lua call instead — the deviation is noted in the app-side drain).
StartByName(String),
/// `StartDuelUnit(unit)` — challenge whoever a unit token points at. The app resolves the
/// token to a guid and rejects a non-player (the reference's typemask `0x10` gate).
StartByUnit(String),
}
impl super::UiScript {
/// Drain the duel intents queued since the last call.
pub fn take_duel_requests(&mut self) -> Vec<DuelRequest> {
std::mem::take(&mut self.model_mut().duel_requests)
}
/// Queue an intent from the app side — the slash commands. In the reference these ARE Lua
/// (`SlashCmdList["DUEL"]` calls `StartDuel`, `SlashCmdList["DUEL_CANCEL"]` calls
/// `CancelDuel`); benilla parses slash lines in Rust, so the same intents enter the same
/// queue here rather than through the globals.
pub fn queue_duel_request(&mut self, request: DuelRequest) {
self.model_mut().duel_requests.push(request);
}
}
/// Register the duel globals (the same style/place [`super::party`] registers its actions).
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
// AcceptDuel() — the DUEL_REQUESTED popup's Accept.
g.set(
"AcceptDuel",
lua.create_function(|lua, ()| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.duel_requests.push(DuelRequest::Accept);
Ok(())
})?,
)?;
// CancelDuel() — the popup's Decline, and /forfeit //concede //yield once under way.
g.set(
"CancelDuel",
lua.create_function(|lua, ()| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.duel_requests.push(DuelRequest::Cancel);
Ok(())
})?,
)?;
// StartDuel(name) — /duel <name>.
g.set(
"StartDuel",
lua.create_function(|lua, name: String| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.duel_requests.push(DuelRequest::StartByName(name));
Ok(())
})?,
)?;
// StartDuelUnit(unit) — the unit popup's Duel row.
g.set(
"StartDuelUnit",
lua.create_function(|lua, unit: String| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.duel_requests.push(DuelRequest::StartByUnit(unit));
Ok(())
})?,
)?;
Ok(())
}