the foam stops at the waterline, and skill bonuses show the right halves
- the foam still washed past the waterline onto the sand after the shore fix: the slope-scaled depth bias ported from the reference's `glPolygonOffset(-4.0, …)` pulls toward the eye as the square of view distance, and what it spends is the wet-cell skirt — whole cells are the unit of liquid geometry, so a shoreline carries liquid-lattice triangles over dry sand (23 cells, up to 7.1 yd inland at the reported beach) held back by nothing but the terrain having drawn first; the tie it would settle does not exist here (our foam patch IS the liquid surface's own triangles — same cells, same winding, identity transform — so the depths agree exactly), and −4.0 is a hardcoded immediate every consumer of that state gets, not the foam lane's number; the slope term is disarmed with an `== 0.0` assert, reproduced three ways at the director's pin (slope 0 stops dead at the waterline, 4.0 washes onto the sand, 64.0 does it wide) - weapon and defense skill bonuses showed the halves the wrong way round: the reader every skill-shaped binding goes through (`0x5ea460`) writes `value + permBonus` and `tempBonus`; ours returned `(value, temp + perm)` — right total, wrong split, which is why no fixture caught it — so a 300 weapon skill with a +5 talent and a +10 aura read 300 (+15) where the reference prints 305 (+10), on the sheet's Attack row and in the Defense tooltip's `(base +x)`; `UnitRangedAttack` is also pinned as a direct call gated on the PLAYER typemask alone, so a pet gets `(0, 0)` - instruments: `water_here` also reports WET cells the ground climbs out of (the skirt a depth bias spends), the foam wake can be laid along a bank (`WOW_WFX_HEAD`) since no shoreline in the game is axis-aligned, `fxview` can stand its subject at any world point (`WOW_FX_AT`) so a blob shadow can be photographed on any terrain, and `WOW_MAP` refuses a name instead of silently falling through to Eastern Kingdoms — `WOW_MAP=Kalimdor` had produced a subject floating in empty ocean that read as a renderer bug Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -150,24 +150,35 @@ pub struct UnitCombatStats {
|
||||
/// Ranged damage range (`UNIT_FIELD_MINRANGEDDAMAGE`/`MAXRANGEDDAMAGE`).
|
||||
pub ranged_min_damage: f32,
|
||||
pub ranged_max_damage: f32,
|
||||
/// The equipped main-hand weapon's skill line as `(value, temp+perm bonus)` — the app resolves
|
||||
/// WHICH skill via [`weapon_subclass_skill`] (unarmed = [`SKILL_UNARMED`]) and reads the pair
|
||||
/// from `PLAYER_SKILL_INFO`; `UnitAttackBothHands` serves it verbatim.
|
||||
pub main_weapon_skill: (u32, i32),
|
||||
/// The equipped main-hand weapon's skill line as **`(value + PERM bonus, TEMP bonus)`** — the
|
||||
/// app resolves WHICH skill via [`weapon_subclass_skill`] (unarmed = [`SKILL_UNARMED`]) and
|
||||
/// reads the pair from `PLAYER_SKILL_INFO`; `UnitAttackBothHands` serves it verbatim.
|
||||
///
|
||||
/// **The permanent half belongs to the BASE, not to the modifier** (`0x5ea460`, VERIFIED —
|
||||
/// wow-re `ui/scratch/unitrangedattack-skill-pair.md` §3; corrected here by decision 1812).
|
||||
/// Every one of these four pairs comes out of that one reader, so all four carry the split.
|
||||
pub main_weapon_skill: (i32, i32),
|
||||
/// The OFF hand's, read the same way. `UnitAttackBothHands` pushes both hands (§4 of wow-re's
|
||||
/// `pet-paperdoll-stat-api.md`: `0x518810` calls `[vtbl+0xb0]` twice, hand 0 then hand 1), and
|
||||
/// an empty or non-weapon off hand is Unarmed exactly as the main hand is.
|
||||
pub offhand_weapon_skill: (u32, i32),
|
||||
/// The ranged weapon's skill pair (`UnitRangedAttack`).
|
||||
pub ranged_weapon_skill: (u32, i32),
|
||||
/// The [`SKILL_DEFENSE`] line's `(value, temp+perm bonus)` pair, read out of `PLAYER_SKILL_INFO`
|
||||
/// like the two weapon pairs above; `UnitDefense` serves it verbatim.
|
||||
pub offhand_weapon_skill: (i32, i32),
|
||||
/// The ranged weapon's skill pair (`UnitRangedAttack`), same split.
|
||||
///
|
||||
/// **Player-only, and by a different gate than its two neighbours.** `UnitRangedAttack`
|
||||
/// `0x518b90` is a DIRECT call gated on the PLAYER typemask alone — no vtable slot and **no
|
||||
/// `CGUnit_C` fallback body** (wow-re `unitrangedattack-skill-pair.md` §4). So where
|
||||
/// `UnitDefense` and `UnitAttackBothHands` pass a pet through SELF-OR-MINE and answer
|
||||
/// `level * 5`, this one answers `(0, 0)` for a pet and for every other player too. Mirroring
|
||||
/// [`cgunit_skill`] here would be wrong.
|
||||
pub ranged_weapon_skill: (i32, i32),
|
||||
/// The [`SKILL_DEFENSE`] line's pair, in the same `(value + perm, temp)` split as the three
|
||||
/// above and out of the same reader; `UnitDefense` serves it verbatim.
|
||||
///
|
||||
/// **The player's only.** A creature has no skill block, and the client does not read one for
|
||||
/// it: `UnitDefense` forks on the resolved unit's vtable and gives a non-player
|
||||
/// `UNIT_FIELD_LEVEL * 5` instead ([`cgunit_skill`]), so the pet feed never fills this and the
|
||||
/// binding never reads it for a pet.
|
||||
pub defense_skill: (u32, i32),
|
||||
pub defense_skill: (i32, i32),
|
||||
/// Whether a wand is equipped (`HasWandEquipped` — the ref swaps the ranged-attack action for
|
||||
/// wand Shoot on it).
|
||||
pub has_wand: bool,
|
||||
@@ -496,7 +507,7 @@ fn with_unit_stats<T>(
|
||||
/// — so the pair can never sum below zero (`0x519298` for `UnitDefense`, `0x5188a7` per hand for
|
||||
/// `UnitAttackBothHands`; wow-re `ui/scratch/pet-paperdoll-stat-api.md` §4). A debuff deeper than
|
||||
/// the skill itself reads as "reduced to 0", never as a negative total.
|
||||
fn skill_clamped((base, modifier): (u32, i32)) -> (i64, i64) {
|
||||
fn skill_clamped((base, modifier): (i32, i32)) -> (i64, i64) {
|
||||
let base = i64::from(base);
|
||||
let modifier = i64::from(modifier);
|
||||
(base, if modifier + base < 0 { -base } else { modifier })
|
||||
@@ -1503,6 +1514,52 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// **`UnitRangedAttack` does NOT take the pet fork its two neighbours take** (decision 1812).
|
||||
///
|
||||
/// `UnitDefense` `0x519200` and `UnitAttackBothHands` `0x518810` gate on SELF-OR-MINE and then
|
||||
/// dispatch through the resolved unit's vtable, so a pet passes and lands on `CGUnit_C`'s
|
||||
/// `level * 5`. `UnitRangedAttack` `0x518b90` is a **direct call** gated on the PLAYER typemask
|
||||
/// alone, with no vtable slot and **no `CGUnit_C` fallback body at all** (VERIFIED, wow-re
|
||||
/// `ui/scratch/unitrangedattack-skill-pair.md` §4 — `0x612b40`/`0x5edae0`/`0x5ea460` appear as
|
||||
/// a dword zero times image-wide, against a positive control of one hit each for the four
|
||||
/// addresses that ARE in a vtable).
|
||||
///
|
||||
/// So a pet's ranged attack is `(0, 0)`, not `level * 5`. The whole reason this is a test and
|
||||
/// not a comment is that the three verbs look interchangeable from the FrameXML side, and
|
||||
/// mirroring the defense handling here would be a plausible, silent, wrong number.
|
||||
#[test]
|
||||
fn ranged_attack_gives_a_pet_zeros_where_defense_gives_it_level_times_five() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_player_combat_stats(Some(stats()));
|
||||
s.set_pet_combat_stats(Some(pet_stats()));
|
||||
s.set_unit(
|
||||
"pet",
|
||||
Some(super::super::UnitState {
|
||||
exists: true,
|
||||
level: 60,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>(r#"return UnitDefense("pet")"#)
|
||||
.unwrap(),
|
||||
(300, 0),
|
||||
"the vtable fork: level * 5"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>(r#"return UnitRangedAttack("pet")"#)
|
||||
.unwrap(),
|
||||
(0, 0),
|
||||
"no fork, no fallback body — a pet fails the PLAYER typemask and gets zeros"
|
||||
);
|
||||
// …and so does another PLAYER, which is the same gate seen from the other side.
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>(r#"return UnitRangedAttack("target")"#)
|
||||
.unwrap(),
|
||||
(0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
/// **`UnitAttackBothHands` pushes FOUR values, one pair per hand — and it pushed two until
|
||||
/// decision 1810.**
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user