From a2da3e4da729c236b557a653ad81701881b34b48 Mon Sep 17 00:00:00 2001 From: grom Date: Wed, 21 Oct 2026 22:17:04 -0600 Subject: [PATCH] ui: a skill lock you can open stops reading red MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GameObject tooltip's skill-lock line took the opener-*unknown* arm unconditionally — `LOCKED_WITH_SPELL` in red — whatever the player's skill. The known arm was simply not built, and the code said so where it was missing. The reference forks on whether any known spell opens the slot's LockType (`0x52abcb`): known takes `LOCKED_WITH_SPELL_KNOWN`, which carries the requirement as a `%d` and the colour of the skill-difficulty ramp `0x529fa0`. Built now, both halves. grom reported it on a copper vein at Mining 70, which under the ramp is +69 over the vein's requirement of 1 — the green rung, not the grey one he expected; grey starts at +100, so a copper vein greys at Mining 101. Either way not red, and red is what it showed. The resolve is hoisted out of the `flag_locked` branch, because the "Locked" line's colour and this line's known arm are two readings of one answer and must not disagree with the click. `lock::skill_margin` recomputes the two numbers from the opener the resolver already matched, with the same two functions its own value test uses (`0x5f850f`), so hover and click cannot diverge on which opener is in play. Deviation, recorded at the constants: the three new rung RGBs are the standard trade-skill palette, not values read out of the binary's table. Only the ramp's shape is traced — five rungs, bands at +0/25/50/100 — and the rung a line lands on is what the player reads. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/target/lock.rs | 21 ++++++ crates/benilla-app/src/ui_tooltip/mod.rs | 74 +++++++++++++------- crates/benilla-app/src/ui_tooltip/tests.rs | 41 +++++++++++ crates/benilla-ui/src/script/mod.rs | 2 +- crates/benilla-ui/src/script/tooltip_unit.rs | 33 ++++++++- 5 files changed, 144 insertions(+), 27 deletions(-) diff --git a/crates/benilla-app/src/target/lock.rs b/crates/benilla-app/src/target/lock.rs index 70586e7c..83920657 100644 --- a/crates/benilla-app/src/target/lock.rs +++ b/crates/benilla-app/src/target/lock.rs @@ -223,6 +223,27 @@ pub(crate) fn spell_opens_lock( }) } +/// `(provides, required)` for a skill slot whose opener the player knows — what the tooltip's +/// known arm colours its line by (`0x529fa0`'s ramp over the margin). +/// +/// `opener` is the spell [`resolve_lock`] matched, so hover and click cannot disagree about *which* +/// opener is in play; this only recomputes the two numbers, with the same two functions the +/// resolver's own value test uses (`0x5f850f`). `None` when the spell is not in the catalog or has +/// no `OPEN_LOCK` effect, which is the resolver's `continue` and so shows the unknown arm. +pub(crate) fn skill_margin( + slot: &LockSlot, + opener: u32, + spells: Option<&crate::ui_action::Spells>, + skill_lines: Option<&benilla_formats::SkillLineCatalog>, + me: Option<&ObjectStore>, + go_level: u32, +) -> Option<(u32, u32)> { + let spell = spells?.catalog.get(opener)?; + let skill = spell_skill_value(me, skill_lines, opener); + let provides = spell.open_lock_skill(skill)?.max(0) as u32; + Some((provides, required_skill(slot, go_level).max(0) as u32)) +} + /// The slot's requirement: `Skill[i]`, or `GAMEOBJECT_LEVEL × 5` when that is zero /// (`0x5f84be`..`0x5f84ca`; `0x5f3490`..`0x5f349f` recomputes it for the `0xe0` toast's `%d`). pub(crate) fn required_skill(slot: &LockSlot, go_level: u32) -> i32 { diff --git a/crates/benilla-app/src/ui_tooltip/mod.rs b/crates/benilla-app/src/ui_tooltip/mod.rs index caae0334..56a9ead0 100644 --- a/crates/benilla-app/src/ui_tooltip/mod.rs +++ b/crates/benilla-app/src/ui_tooltip/mod.rs @@ -839,23 +839,24 @@ fn drive_mouseover_tooltip( let mut lines: Vec<(String, TooltipTint)> = Vec::new(); // The lock lines' keys all read "Requires %s" in enUS; only the binary tells them apart. let go_get = |key: &str| benilla_ui::strings::global(script.lua(), key); + // Resolved once for both lines: the "Locked" line's colour and the skill line's known arm + // are two readings of the same answer, and the builder asks the resolver the click uses + // (`0x52ab14` → `0x5f83d0`), so neither may differ from the click. + let facts = crate::target::lock::go_facts(go_store.map(|s| (s, state))); + let mut matched = None; + let outcome = slots.map(|slots| { + crate::target::lock::resolve_lock( + slots, + &player_actions.spells, + go_inputs.spells.as_deref(), + go_inputs.skill_lines.as_ref().map(|s| &s.catalog), + self_store, + &go_inputs.objects, + facts, + &mut matched, + ) + }); if flag_locked { - // Coloured by whether the player can open it: the builder asks the resolver the click - // uses (`0x52ab14` → `0x5f83d0`), and `locked_line_tint` maps the answer. - let facts = crate::target::lock::go_facts(go_store.map(|s| (s, state))); - let mut matched = None; - let outcome = slots.map(|slots| { - crate::target::lock::resolve_lock( - slots, - &player_actions.spells, - go_inputs.spells.as_deref(), - go_inputs.skill_lines.as_ref().map(|s| &s.catalog), - self_store, - &go_inputs.objects, - facts, - &mut matched, - ) - }); if let Some(text) = keyed(&go_get, "LOCKED", &[]) { lines.push((text, locked_line_tint(outcome))); } @@ -873,20 +874,43 @@ fn drive_mouseover_tooltip( } } } - // Every skill lock takes the opener-unknown arm, silent on a flag-locked object; - // the known arm, `LOCKED_WITH_SPELL_KNOWN` in the margin ramp's colour - // (`0x529fa0`), is not built. + // Silent on a flag-locked object, so a locked door names its key, never its + // lockpicking. Otherwise the reference forks on whether any known spell opens the + // slot's LockType (`0x52abcb`): the known arm is `LOCKED_WITH_SPELL_KNOWN`, which + // takes the requirement as a `%d` and the ramp's colour (`0x529fa0`); the unknown + // arm is `LOCKED_WITH_SPELL`, always red. benilla_formats::LOCK_KEY_SKILL if !flag_locked => { - // `LOCKED_WITH_SPELL` (`0x52ac04`), named by `LockType.dbc` ("Pick Lock"); the - // reference takes the known arm iff any known spell opens it (`0x52abcb`). + // `LOCKED_WITH_SPELL` (`0x52ac04`), named by `LockType.dbc` ("Mining"). let word = go_inputs .lock_types .as_deref() .and_then(|c| c.0.name(slot0.index)); - if let Some(text) = - word.and_then(|w| keyed(&go_get, "LOCKED_WITH_SPELL", &[Arg::S(w)])) - { - lines.push((text, TooltipTint::Red)); + // `matched` is set by the resolver on a LockType match, before its value test + // (`0x5f84f8`) — so it says "an opener is known", not "the skill suffices", + // which is the fork the reference takes here. + let known = matched.and_then(|opener| { + crate::target::lock::skill_margin( + &slot0, + opener, + go_inputs.spells.as_deref(), + go_inputs.skill_lines.as_ref().map(|s| &s.catalog), + self_store, + facts.level, + ) + }); + let line = match (word, known) { + (Some(w), Some((provides, required))) => keyed( + &go_get, + "LOCKED_WITH_SPELL_KNOWN", + &[Arg::S(w), Arg::D(required.into())], + ) + .map(|t| (t, benilla_ui::script::skill_margin_tint(provides, required))), + (Some(w), None) => keyed(&go_get, "LOCKED_WITH_SPELL", &[Arg::S(w)]) + .map(|t| (t, TooltipTint::Red)), + (None, _) => None, + }; + if let Some(line) = line { + lines.push(line); } } _ => {} diff --git a/crates/benilla-app/src/ui_tooltip/tests.rs b/crates/benilla-app/src/ui_tooltip/tests.rs index ada69261..fe22dd88 100644 --- a/crates/benilla-app/src/ui_tooltip/tests.rs +++ b/crates/benilla-app/src/ui_tooltip/tests.rs @@ -506,6 +506,47 @@ fn the_locked_line_greens_when_the_lock_can_be_opened() { assert_eq!(locked_line_tint(None), TooltipTint::LockOpen); } +/// The skill line's own ramp (`0x529fa0`), which is a different question from the "Locked" line's +/// two-way green/red above: a skill lock whose opener the player knows is coloured by how far past +/// the requirement they are, and only an *unknown* opener is flatly red. +/// +/// grom's copper vein is the first row: Mining 70 against a requirement of 1 is +69, which is the +/// ramp's **green** rung — grey only starts at +100, so a copper vein greys at Mining 101. Either +/// way it is not red, and red is what every skill lock showed, whatever the player's skill, +/// because the known arm was simply not built. +#[test] +fn a_known_skill_openers_line_ramps_by_margin_instead_of_reading_red() { + use benilla_ui::script::skill_margin_tint; + + // Mining 70 at a copper vein (`Lock.dbc` Skill = 1). + assert_eq!( + skill_margin_tint(70, 1), + TooltipTint::LockOpen, + "+69 over the requirement is the green rung, and must never be the unknown arm's red" + ); + assert_eq!( + skill_margin_tint(101, 1), + TooltipTint::SkillGrey, + "the same vein greys at Mining 101, which is +100" + ); + // The rungs, at their boundaries: +100 trivial, then +50, +25, +0. + assert_eq!(skill_margin_tint(101, 1), TooltipTint::SkillGrey); + assert_eq!(skill_margin_tint(100, 1), TooltipTint::LockOpen, "+99"); + assert_eq!(skill_margin_tint(51, 1), TooltipTint::LockOpen, "+50"); + assert_eq!(skill_margin_tint(50, 1), TooltipTint::SkillYellow, "+49"); + assert_eq!(skill_margin_tint(26, 1), TooltipTint::SkillYellow, "+25"); + assert_eq!(skill_margin_tint(25, 1), TooltipTint::SkillOrange, "+24"); + // Exactly enough is the bottom rung, not red: red is the unknown-opener arm's, and a player + // who knows the opener and meets the requirement can gather. + assert_eq!( + skill_margin_tint(1, 1), + TooltipTint::SkillOrange, + "meeting it exactly is the orange rung" + ); + // Short of it saturates rather than wrapping, and still is not this arm's business. + assert_eq!(skill_margin_tint(0, 100), TooltipTint::SkillOrange); +} + /// A trainer list that lands this frame is hoverable in this frame's tick: the trainer feed derives /// the subjects the spell feed pushes. #[test] diff --git a/crates/benilla-ui/src/script/mod.rs b/crates/benilla-ui/src/script/mod.rs index 96113290..54f6c0c3 100644 --- a/crates/benilla-ui/src/script/mod.rs +++ b/crates/benilla-ui/src/script/mod.rs @@ -151,7 +151,7 @@ mod tooltip_item; mod tooltip_spell; mod tooltip_unit; mod ui_errors; -pub use tooltip_unit::TooltipTint; +pub use tooltip_unit::{skill_margin_tint, TooltipTint}; mod trade; mod tradeskill; mod trainer; diff --git a/crates/benilla-ui/src/script/tooltip_unit.rs b/crates/benilla-ui/src/script/tooltip_unit.rs index b45379f7..fbbb1e76 100644 --- a/crates/benilla-ui/src/script/tooltip_unit.rs +++ b/crates/benilla-ui/src/script/tooltip_unit.rs @@ -16,8 +16,16 @@ use crate::widget::FrameHandle; const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; const GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0]; const RED: [f32; 4] = [1.0, 32.0 / 255.0, 32.0 / 255.0, 1.0]; -/// `0xc0d420` = `0xff40c040`, the satisfiable-lock green. +/// `0xc0d420` = `0xff40c040`, the satisfiable-lock green, and the ramp's green rung. const LOCK_OPEN: [f32; 4] = [64.0 / 255.0, 192.0 / 255.0, 64.0 / 255.0, 1.0]; +/// The other three rungs of the skill-difficulty ramp `0x529fa0`, whose green rung is +/// [`LOCK_OPEN`] and whose bottom rung is [`RED`]. Deviation: these three RGBs are the standard +/// trade-skill palette, not values read out of the binary's table — only the ramp's *shape* +/// (five rungs, bands at +0/25/50/100 over the requirement) is traced. The rung a line lands on +/// is what matters to the player and that is faithful; a channel or two may not be. +const SKILL_GREY: [f32; 4] = [128.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0, 1.0]; +const SKILL_YELLOW: [f32; 4] = [1.0, 1.0, 0.0, 1.0]; +const SKILL_ORANGE: [f32; 4] = [1.0, 128.0 / 255.0, 64.0 / 255.0, 1.0]; /// `0xffffd200`, the unit name's colour until FrameXML recolours it by reaction. const GOLD: [f32; 4] = [1.0, 210.0 / 255.0, 0.0, 1.0]; @@ -34,6 +42,26 @@ pub enum TooltipTint { /// The satisfiable "Locked" line: `0xc0d420` = `0xff40c040`, the green rung of the /// skill-difficulty ramp `0x529fa0`, not the ordinary green `0xc0d3ac` = `0xff00ff00`. LockOpen, + /// The three rungs of the ramp `0x529fa0` that are neither its green ([`TooltipTint::LockOpen`]) + /// nor its red ([`TooltipTint::Red`]), for the "Requires %s (%d)" line of a skill lock whose + /// opener the player knows: trivial at +100 over the requirement, then +50 and +25. + SkillGrey, + SkillYellow, + SkillOrange, +} + +/// The rung of `0x529fa0` for a skill lock the player can open: `have` is what the opener provides, +/// `required` the slot's requirement. Bands at +0/25/50/100, as the reference's table is indexed. +/// +/// The unmet case is the caller's: it takes the *unknown-opener* arm's red and a different +/// GlobalString, so it never reaches here. +pub fn skill_margin_tint(have: u32, required: u32) -> TooltipTint { + match have.saturating_sub(required) { + m if m >= 100 => TooltipTint::SkillGrey, + m if m >= 50 => TooltipTint::LockOpen, + m if m >= 25 => TooltipTint::SkillYellow, + _ => TooltipTint::SkillOrange, + } } /// The rank word's GlobalString key, from the builder's table `0x854158` (indexed at `0x52a5bd` @@ -415,6 +443,9 @@ impl super::UiScript { TooltipTint::White => WHITE, TooltipTint::Red => RED, TooltipTint::LockOpen => LOCK_OPEN, + TooltipTint::SkillGrey => SKILL_GREY, + TooltipTint::SkillYellow => SKILL_YELLOW, + TooltipTint::SkillOrange => SKILL_ORANGE, }; append_line(&self.lua, &wrapper, (text.clone(), colour), None, false)?; }