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)?; }