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:
@@ -56,7 +56,7 @@ pub(crate) fn drive_fx_view(
|
||||
return;
|
||||
};
|
||||
let root = *state.root.get_or_insert_with(|| {
|
||||
let mut pos = benilla_assets::coords::wow_to_bevy(FXVIEW_POS);
|
||||
let mut pos = benilla_assets::coords::wow_to_bevy(req.at.unwrap_or(FXVIEW_POS));
|
||||
// `WOW_FX_DISPLAY`: the UNIT lane. Spawn the live component set a streamed creature gets
|
||||
// (`net::apply`'s, the same one the `vplates` fixture's wolf uses) and let the ordinary
|
||||
// unit pipeline build the model — so what this shoots is a creature, not a model, and
|
||||
|
||||
@@ -227,6 +227,9 @@ fn glue_screen() -> Option<GlueScreen> {
|
||||
/// surface decals, `crate::ground_fx`; default 0 = the mid-air point), `WOW_FX_HOLD` (=1 keeps
|
||||
/// the fixture alive past one sequence pass — previewing a persistent HOLD kit's steady state;
|
||||
/// default 0 = the game's discrete-instance reap at one pass, then the pool drains),
|
||||
/// `WOW_FX_AT` (`x,y,z` — plant the fixture at a real world point instead of the Northshire
|
||||
/// hillside, with `WOW_MAP` picking the map and `WOW_FX_MINUTE` the clock; the ground-decal lanes
|
||||
/// need real terrain under the subject),
|
||||
/// `WOW_FX_UP` (yards to raise the fixture above its resolved seat — for models authored below
|
||||
/// their anchor, whose opening frames the terrain otherwise swallows; default 0).
|
||||
#[derive(Resource)]
|
||||
@@ -264,6 +267,14 @@ pub(crate) struct FxViewRequest {
|
||||
pub(crate) ground: bool,
|
||||
/// See `WOW_FX_HOLD` above.
|
||||
pub(crate) hold: bool,
|
||||
/// `WOW_FX_AT=x,y,z` — plant the fixture at this raw WoW point instead of [`FXVIEW_POS`]
|
||||
/// (with `WOW_MAP` picking the map, like `vista` and `waterfx`). The ground-decal lanes —
|
||||
/// blob shadow, footprints, the selection ring — only exist *on* terrain, and which terrain
|
||||
/// decides whether they exist at all (footprints gate on the surface's `TerrainType.Flags & 1`,
|
||||
/// i.e. snow and sand) and how hard the coplanar tie is (a steep slope seen at a grazing
|
||||
/// angle is the whole difficulty). A fixture nailed to one Northshire hillside can photograph
|
||||
/// none of that.
|
||||
pub(crate) at: Option<[f32; 3]>,
|
||||
/// `WOW_FX_UP` — raise the fixture this many yards above its resolved seat (default 0).
|
||||
/// The escape hatch for models authored BELOW their anchor (Arcane Intellect's star cluster
|
||||
/// starts 1.6 yd under its attach point): at the default seat the terrain swallows the
|
||||
@@ -586,6 +597,10 @@ impl Plugin for CapturePlugin {
|
||||
None
|
||||
} else {
|
||||
Some(if name == "fxview" {
|
||||
let at = std::env::var("WOW_FX_AT").ok().and_then(|v| {
|
||||
let c: Vec<f32> = v.split(',').filter_map(|p| p.trim().parse().ok()).collect();
|
||||
(c.len() == 3).then(|| [c[0], c[1], c[2]])
|
||||
});
|
||||
let display: Option<u32> = std::env::var("WOW_FX_DISPLAY")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse().ok());
|
||||
@@ -627,21 +642,28 @@ impl Plugin for CapturePlugin {
|
||||
ground: knob("WOW_FX_GROUND", 0.0) > 0.5,
|
||||
hold: knob("WOW_FX_HOLD", 0.0) > 0.5,
|
||||
up: knob("WOW_FX_UP", 0.0),
|
||||
// `WOW_FX_AT=x,y,z` plants the fixture anywhere in the real world, with
|
||||
// `WOW_MAP` picking the map — the same pair `vista` and `waterfx` take. The
|
||||
// default hillside can photograph an effect but not a ground decal: which
|
||||
// surface is under the subject decides whether footprints exist at all, and
|
||||
// how steeply it falls away decides how hard the coplanar tie is.
|
||||
at,
|
||||
})
|
||||
.init_resource::<FxViewState>();
|
||||
Scenario {
|
||||
name: "fxview",
|
||||
map: scenarios::MAP_AZEROTH, // the fixture spawns over the Northshire slope
|
||||
eye: GROUND_EYE, // overridden per frame by the orbit in `pin_scene`
|
||||
look: FXVIEW_POS,
|
||||
minute: 720,
|
||||
map: None,
|
||||
eye: GROUND_EYE, // overridden per frame by the orbit in `pin_scene`
|
||||
look: at.unwrap_or(FXVIEW_POS),
|
||||
minute: knob("WOW_FX_MINUTE", 720.0) as u32,
|
||||
ui: None,
|
||||
}
|
||||
} else if name == "waterfx" {
|
||||
// The water-foam viewer (see `capture::waterfx`): a wading unit over a synthetic
|
||||
// wet lattice — or, with WOW_WFX_AT=x,y,z, in the real streamed liquid at that pin
|
||||
// — framed by a fixed orbit around the rig centre. Knobs: WOW_WFX_MODE
|
||||
// (ring|wake|turn), WOW_WFX_SPEED (yd/s), WOW_WFX_AGE (s), WOW_WFX_DEPTH (yd),
|
||||
// (ring|wake|turn), WOW_WFX_SPEED (yd/s), WOW_WFX_HEAD (deg), WOW_WFX_AGE (s),
|
||||
// WOW_WFX_DEPTH (yd),
|
||||
// camera WOW_WFX_AZ/EL/DIST. Not a golden scenario.
|
||||
let knob = |k: &str, d: f32| {
|
||||
std::env::var(k)
|
||||
@@ -676,6 +698,7 @@ impl Plugin for CapturePlugin {
|
||||
app.insert_resource(waterfx::WaterFxView {
|
||||
mode,
|
||||
speed: knob("WOW_WFX_SPEED", 4.0),
|
||||
heading: knob("WOW_WFX_HEAD", 0.0).to_radians(),
|
||||
age: knob("WOW_WFX_AGE", 1.5),
|
||||
center,
|
||||
depth: knob("WOW_WFX_DEPTH", 0.5),
|
||||
@@ -686,10 +709,7 @@ impl Plugin for CapturePlugin {
|
||||
name: "waterfx",
|
||||
// The synthetic lattice sits over the Northshire slope; a live rig
|
||||
// (`WOW_WFX_AT`) goes wherever its pin is, so its map is a knob like `vista`'s.
|
||||
map: std::env::var("WOW_MAP")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(scenarios::MAP_AZEROTH),
|
||||
map: None,
|
||||
eye,
|
||||
look: center,
|
||||
minute: 720,
|
||||
@@ -733,10 +753,7 @@ impl Plugin for CapturePlugin {
|
||||
name: "vista",
|
||||
// The arbitrary-viewpoint instrument goes anywhere, so its map is a knob: a
|
||||
// horizon report from Kalimdor is `WOW_MAP=1` (a `Map.dbc` id).
|
||||
map: std::env::var("WOW_MAP")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(scenarios::MAP_AZEROTH),
|
||||
map: None,
|
||||
eye,
|
||||
look: [
|
||||
eye[0] + d * pitch.cos() * face.cos(),
|
||||
@@ -772,10 +789,12 @@ impl Plugin for CapturePlugin {
|
||||
// terrain/WDL streamers and per-map lighting all key off it. Raw WoW coords repeat on every
|
||||
// continent, so a scenario that could not say which map it meant would stream the wrong
|
||||
// world's tiles and photograph a void (Felwood's tile `33_24` exists in Azeroth, empty).
|
||||
// Written back for `vista` too, which is where the value came from — a harmless no-op that
|
||||
// keeps one path for "which map is this run on" (decision 0743). A glue screen has no map.
|
||||
if let Some(s) = &scenario {
|
||||
std::env::set_var("WOW_MAP", s.map.to_string());
|
||||
// The arbitrary-viewpoint instruments (`vista`, `waterfx`, `fxview`) carry `map: None`
|
||||
// instead: their map IS the knob, so writing it back would be a round trip through a second
|
||||
// parser — and a second parser is how `WOW_MAP=Kalimdor` came to photograph Azeroth in
|
||||
// silence. `world_map` is the one reader (decision 0743). A glue screen has no map.
|
||||
if let Some(m) = scenario.as_ref().and_then(|s| s.map) {
|
||||
std::env::set_var("WOW_MAP", m.to_string());
|
||||
}
|
||||
let shot_name = scenario.map(|s| s.name).or(glue.map(|g| g.name)).unwrap();
|
||||
let out = std::env::var("WOW_CAPTURE_OUT")
|
||||
|
||||
@@ -11,7 +11,11 @@ pub(super) struct Scenario {
|
||||
/// Felwood spot's tile (`33_24`) exists in Azeroth too, empty — so a scenario that could not
|
||||
/// name its map silently photographed the wrong world. The harness seeds
|
||||
/// [`benilla_world::world_map::CurrentMap`] from this before streaming starts (decision 0743).
|
||||
pub(super) map: u32,
|
||||
/// Which `Map.dbc` map this scenario photographs. `None` = **the `$WOW_MAP` knob decides** —
|
||||
/// the arbitrary-viewpoint instruments (`vista`, `waterfx`, `fxview`) go anywhere, so they have
|
||||
/// no map of their own and must not write one back over the knob. A named golden scenario
|
||||
/// always says, because raw WoW coords repeat on every continent.
|
||||
pub(super) map: Option<u32>,
|
||||
/// Camera eye, raw WoW coords `(x, y, z)`.
|
||||
pub(super) eye: [f32; 3],
|
||||
/// Camera look-at target, raw WoW coords.
|
||||
@@ -304,7 +308,7 @@ pub(super) const GLUE_SCENARIOS: &[GlueScenario] = &[
|
||||
pub(super) const SCENARIOS: &[Scenario] = &[
|
||||
Scenario {
|
||||
name: "water-noon",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: WATER_EYE,
|
||||
look: WATER_LOOK,
|
||||
minute: 720,
|
||||
@@ -312,7 +316,7 @@ pub(super) const SCENARIOS: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "water-night",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: WATER_EYE,
|
||||
look: WATER_LOOK,
|
||||
minute: 0,
|
||||
@@ -320,7 +324,7 @@ pub(super) const SCENARIOS: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "felwood-noon",
|
||||
map: MAP_KALIMDOR,
|
||||
map: Some(MAP_KALIMDOR),
|
||||
eye: FELWOOD_EYE,
|
||||
look: FELWOOD_LOOK,
|
||||
minute: 720,
|
||||
@@ -328,7 +332,7 @@ pub(super) const SCENARIOS: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "felwood-night",
|
||||
map: MAP_KALIMDOR,
|
||||
map: Some(MAP_KALIMDOR),
|
||||
eye: FELWOOD_EYE,
|
||||
look: FELWOOD_LOOK,
|
||||
minute: 0,
|
||||
@@ -444,7 +448,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// shape 0688 wired and nothing has ever photographed it.
|
||||
Scenario {
|
||||
name: "tram-undersea",
|
||||
map: MAP_DEEPRUN_TRAM,
|
||||
map: Some(MAP_DEEPRUN_TRAM),
|
||||
eye: TRAM_EYE,
|
||||
look: TRAM_LOOK,
|
||||
minute: 720,
|
||||
@@ -458,7 +462,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// that a half-clouded zone hides. Kept as the standing reproducer for the cloud palette.
|
||||
Scenario {
|
||||
name: "tainted-scar-noon",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: SCAR_EYE,
|
||||
look: SCAR_LOOK,
|
||||
minute: 720,
|
||||
@@ -473,7 +477,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// of one build) — same coplanar-batch draw-order defect as `chest-indoor-*` below (0815 Open).
|
||||
Scenario {
|
||||
name: "overlook-noon",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: OVERLOOK_EYE,
|
||||
look: OVERLOOK_LOOK,
|
||||
minute: 720,
|
||||
@@ -481,7 +485,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "overlook-night",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: OVERLOOK_EYE,
|
||||
look: OVERLOOK_LOOK,
|
||||
minute: 0,
|
||||
@@ -491,7 +495,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `inn-noon` — the room is lit by hearth and candles, so the clock barely reaches it).
|
||||
Scenario {
|
||||
name: "inn-noon",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: INN_EYE,
|
||||
look: INN_LOOK,
|
||||
minute: 720,
|
||||
@@ -502,7 +506,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// slides off the rails and the shot degrades into an ordinary fence.
|
||||
Scenario {
|
||||
name: "fence-shadowline-day",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: FENCE_EYE,
|
||||
look: FENCE_LOOK,
|
||||
minute: 624,
|
||||
@@ -516,7 +520,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// about the light curves, and a baseline is the right place to hold the answer (0749 addendum).
|
||||
Scenario {
|
||||
name: "fence-shadowline-night",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: FENCE_EYE,
|
||||
look: FENCE_LOOK,
|
||||
minute: 0,
|
||||
@@ -524,7 +528,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-sun-front",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9496.46, 59.54, 58.28],
|
||||
look: [-9500.00, 56.00, 57.28],
|
||||
minute: 720,
|
||||
@@ -535,7 +539,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-sun-rear",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9503.54, 52.46, 58.28],
|
||||
look: [-9500.00, 56.00, 57.28],
|
||||
minute: 720,
|
||||
@@ -546,7 +550,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-shade-front",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9496.46, 47.54, 57.75],
|
||||
look: [-9500.00, 44.00, 56.75],
|
||||
minute: 720,
|
||||
@@ -557,7 +561,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-shade-rear",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9503.54, 40.46, 57.75],
|
||||
look: [-9500.00, 44.00, 56.75],
|
||||
minute: 720,
|
||||
@@ -568,7 +572,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-indoor-front",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9466.57, 34.73, 59.70],
|
||||
look: [-9469.40, 31.90, 58.70],
|
||||
minute: 720,
|
||||
@@ -579,7 +583,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "creature-indoor-rear",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9472.23, 29.07, 59.70],
|
||||
look: [-9469.40, 31.90, 58.70],
|
||||
minute: 720,
|
||||
@@ -590,7 +594,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-sun-front",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9496.82, 59.18, 57.98],
|
||||
look: [-9500.00, 56.00, 56.93],
|
||||
minute: 720,
|
||||
@@ -601,7 +605,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-sun-rear",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9503.18, 52.82, 57.98],
|
||||
look: [-9500.00, 56.00, 56.93],
|
||||
minute: 720,
|
||||
@@ -612,7 +616,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-shade-front",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9496.82, 47.18, 57.45],
|
||||
look: [-9500.00, 44.00, 56.40],
|
||||
minute: 720,
|
||||
@@ -623,7 +627,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-shade-rear",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9503.18, 40.82, 57.45],
|
||||
look: [-9500.00, 44.00, 56.40],
|
||||
minute: 720,
|
||||
@@ -641,7 +645,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// they are deterministic.
|
||||
Scenario {
|
||||
name: "chest-indoor-front",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9466.85, 34.45, 59.40],
|
||||
look: [-9469.40, 31.90, 58.35],
|
||||
minute: 720,
|
||||
@@ -652,7 +656,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "chest-indoor-rear",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9471.95, 29.35, 59.40],
|
||||
look: [-9469.40, 31.90, 58.35],
|
||||
minute: 720,
|
||||
@@ -663,7 +667,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "house-north",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: HOUSE_EYE,
|
||||
look: [-9389.1, 71.2, 58.0],
|
||||
minute: 720,
|
||||
@@ -671,7 +675,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "house-south",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: HOUSE_EYE,
|
||||
look: [-9489.1, 71.2, 58.0],
|
||||
minute: 720,
|
||||
@@ -679,7 +683,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "house-west",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: HOUSE_EYE,
|
||||
look: [-9439.1, 121.2, 58.0],
|
||||
minute: 720,
|
||||
@@ -687,7 +691,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "house-east",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: HOUSE_EYE,
|
||||
look: [-9439.1, 21.2, 58.0],
|
||||
minute: 720,
|
||||
@@ -700,7 +704,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// every diff unseen.
|
||||
Scenario {
|
||||
name: "house-north-midnight",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: HOUSE_EYE,
|
||||
look: [-9389.1, 71.2, 58.0],
|
||||
minute: 0,
|
||||
@@ -716,7 +720,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// client does the same — the audit's "faithful-cull residue"), which vanishes the room.
|
||||
Scenario {
|
||||
name: "inn-interior",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-9463.3, 4.4, 58.8],
|
||||
look: [-9462.1, -5.6, 58.5],
|
||||
minute: 720,
|
||||
@@ -727,7 +731,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// stays as the warm-light/fog fixture.
|
||||
Scenario {
|
||||
name: "northshire-dusk",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 1170, // 19:30 — warm dusk light + fog
|
||||
@@ -735,7 +739,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "northshire-sky-noon",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: SKY_EYE,
|
||||
look: SKY_LOOK,
|
||||
minute: 720, // day sky-dome gradient + fog horizon
|
||||
@@ -743,7 +747,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "northshire-sky-dusk",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: SKY_EYE,
|
||||
look: SKY_LOOK,
|
||||
minute: 1170, // dusk dome warp + low sun + stars emerging
|
||||
@@ -757,7 +761,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// was cut along a giant faceted circle).
|
||||
Scenario {
|
||||
name: "northshire-sun-flare",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: SKY_EYE,
|
||||
look: [-8797.0, 23.0, 264.0], // eye + 300·(elev 30°, az 45°) — the sun's spot at 17:30
|
||||
minute: 1050,
|
||||
@@ -772,7 +776,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// unit tests (`flare_ray_*`) and the sun fixtures.
|
||||
Scenario {
|
||||
name: "northshire-moonrise",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: SKY_EYE,
|
||||
look: [-8775.0, 45.0, 190.0], // eye + 300·(elev 15°, az 45°) — the moon's spot at 22:44
|
||||
minute: 1364,
|
||||
@@ -784,7 +788,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// the moonrise fixture above proves its absence early, this one its presence at depth of night.
|
||||
Scenario {
|
||||
name: "northshire-moon-halo",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: SKY_EYE,
|
||||
look: [-8858.0, -38.0, 358.0], // eye + 300·(elev 55°, az 45°) — the moon's spot at 00:00
|
||||
minute: 0,
|
||||
@@ -796,7 +800,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// load is resident here; Northshire scenes never exercise that scale.
|
||||
Scenario {
|
||||
name: "stormwind",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-8833.38, 628.63, 96.0],
|
||||
look: [-8809.1, 672.3, 94.0],
|
||||
minute: 720,
|
||||
@@ -808,7 +812,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// director look pass, so "looks like shit" gets caught in-loop, not on the director's screen.
|
||||
Scenario {
|
||||
name: "ui-merchant",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -816,7 +820,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "ui-gossip",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -824,7 +828,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "ui-bank",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -832,7 +836,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "ui-quest",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -840,7 +844,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "ui-questgreeting",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -848,7 +852,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "ui-questlog",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -856,7 +860,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "ui-loot",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -864,7 +868,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
},
|
||||
Scenario {
|
||||
name: "ui-bag",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -874,7 +878,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-tooltip`.
|
||||
Scenario {
|
||||
name: "ui-tooltip",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -884,7 +888,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// seeded hostile wolf. Run with `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-tooltip-world`.
|
||||
Scenario {
|
||||
name: "ui-tooltip-world",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -894,7 +898,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-char`.
|
||||
Scenario {
|
||||
name: "ui-char",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -905,7 +909,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-unitframes`.
|
||||
Scenario {
|
||||
name: "ui-unitframes",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -917,7 +921,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-combopoints`.
|
||||
Scenario {
|
||||
name: "ui-combopoints",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -927,7 +931,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// Run with `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-partyinvite`.
|
||||
Scenario {
|
||||
name: "ui-partyinvite",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -940,7 +944,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-actionbar`.
|
||||
Scenario {
|
||||
name: "ui-actionbar",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -952,7 +956,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE=vplates` (main.rs sizes this window 1024×768 — the 1:1 gx window).
|
||||
Scenario {
|
||||
name: "vplates",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: [-8956.5, -137.5, 85.6],
|
||||
look: [-8949.95, -132.49, 84.8],
|
||||
minute: 720,
|
||||
@@ -963,7 +967,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-worldmap`.
|
||||
Scenario {
|
||||
name: "ui-worldmap",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -972,7 +976,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// The spellbook over a seeded mage book. Run with `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-spellbook`.
|
||||
Scenario {
|
||||
name: "ui-spellbook",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -982,7 +986,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-macro`.
|
||||
Scenario {
|
||||
name: "ui-macro",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -992,7 +996,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-macro-popup`.
|
||||
Scenario {
|
||||
name: "ui-macro-popup",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1002,7 +1006,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-chatedit`.
|
||||
Scenario {
|
||||
name: "ui-chatedit",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1013,7 +1017,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-chat-tabhover`.
|
||||
Scenario {
|
||||
name: "ui-chat-tabhover",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1023,7 +1027,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-social`.
|
||||
Scenario {
|
||||
name: "ui-social",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1033,7 +1037,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-options`.
|
||||
Scenario {
|
||||
name: "ui-options",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1043,7 +1047,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-options-audio`.
|
||||
Scenario {
|
||||
name: "ui-options-audio",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1053,7 +1057,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-options-graphics`.
|
||||
Scenario {
|
||||
name: "ui-options-graphics",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1063,7 +1067,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-options-chat`.
|
||||
Scenario {
|
||||
name: "ui-options-chat",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1073,7 +1077,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-color-picker`.
|
||||
Scenario {
|
||||
name: "ui-color-picker",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1083,7 +1087,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// with `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-options-dropdown`.
|
||||
Scenario {
|
||||
name: "ui-options-dropdown",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1093,7 +1097,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-keybindings`.
|
||||
Scenario {
|
||||
name: "ui-keybindings",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1103,7 +1107,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-options-search`.
|
||||
Scenario {
|
||||
name: "ui-options-search",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: GROUND_EYE,
|
||||
look: GROUND_LOOK,
|
||||
minute: 720,
|
||||
@@ -1114,7 +1118,7 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
// out in the water. Run with `WOW_CAPTURE=name-water`.
|
||||
Scenario {
|
||||
name: "name-water",
|
||||
map: MAP_AZEROTH,
|
||||
map: Some(MAP_AZEROTH),
|
||||
eye: WATER_EYE,
|
||||
look: WATER_LOOK,
|
||||
minute: 720,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! shipped systems; the rig only supplies a unit, water, and motion.
|
||||
//!
|
||||
//! `WOW_CAPTURE=waterfx` + knobs: `WOW_WFX_MODE` (`ring`|`wake`|`turn`), `WOW_WFX_SPEED` (yd/s),
|
||||
//! `WOW_WFX_HEAD` (wake heading, WoW degrees, 0 = +X — point it along a real bank),
|
||||
//! `WOW_WFX_AGE` (s of motion before the shot), `WOW_WFX_DEPTH` (yd below the surface; > ~0.8
|
||||
//! also exercises the step-in one-shot), camera `WOW_WFX_AZ`/`EL`/`DIST`. Not a golden scenario —
|
||||
//! output depends on the knobs.
|
||||
@@ -41,8 +42,12 @@ pub(crate) enum WfxMode {
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct WaterFxView {
|
||||
pub(crate) mode: WfxMode,
|
||||
/// Translation speed for [`WfxMode::Wake`] (yd/s, WoW +X).
|
||||
/// Translation speed for [`WfxMode::Wake`] (yd/s, along [`Self::heading`]).
|
||||
pub(crate) speed: f32,
|
||||
/// Which way a [`WfxMode::Wake`] walks, as a WoW yaw in radians (0 = +X). No shoreline in the
|
||||
/// game is axis-aligned, so a wake that can only run along +X can never be laid **along** a
|
||||
/// bank — the one arrangement where a trail's whole length ties against the waterline at once.
|
||||
pub(crate) heading: f32,
|
||||
/// Seconds the unit moves/stands before the shot (foam accumulates).
|
||||
pub(crate) age: f32,
|
||||
/// Rig centre in raw WoW coords `(x, y, surface_z)` — where the unit ends up at shot time.
|
||||
@@ -148,10 +153,14 @@ pub(crate) fn spawn(
|
||||
/// subsystem never gives it a fallback cube over the foam). A wake starts far enough back that its
|
||||
/// trail ENDS at the rig centre after `age` seconds.
|
||||
fn spawn_dummy(commands: &mut Commands, view: &WaterFxView, cx: f32, cy: f32, surf: f32) {
|
||||
let start_x = if view.mode == WfxMode::Wake {
|
||||
cx - view.speed * view.age
|
||||
let (start_x, start_y) = if view.mode == WfxMode::Wake {
|
||||
let back = view.speed * view.age;
|
||||
(
|
||||
cx - back * view.heading.cos(),
|
||||
cy - back * view.heading.sin(),
|
||||
)
|
||||
} else {
|
||||
cx
|
||||
(cx, cy)
|
||||
};
|
||||
commands.spawn((
|
||||
WaterFxDummy,
|
||||
@@ -173,7 +182,7 @@ fn spawn_dummy(commands: &mut Commands, view: &WaterFxView, cx: f32, cy: f32, su
|
||||
bound: None,
|
||||
},
|
||||
crate::entities::VisualAttached,
|
||||
Transform::from_translation(wow_to_bevy([start_x, cy, surf - view.depth])),
|
||||
Transform::from_translation(wow_to_bevy([start_x, start_y, surf - view.depth])),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -197,8 +206,11 @@ pub(crate) fn drive(
|
||||
for mut t in &mut units {
|
||||
match view.mode {
|
||||
WfxMode::Wake => {
|
||||
// WoW +X = Bevy −Z (bevy = (−wow.y, wow.z, −wow.x)).
|
||||
t.translation.z -= view.speed * time.delta_secs();
|
||||
// bevy = (−wow.y, wow.z, −wow.x), so a WoW heading (cos h, sin h) walks the dummy
|
||||
// along Bevy (−sin h, 0, −cos h).
|
||||
let step = view.speed * time.delta_secs();
|
||||
t.translation.x -= step * view.heading.sin();
|
||||
t.translation.z -= step * view.heading.cos();
|
||||
}
|
||||
WfxMode::Turn => {
|
||||
t.rotate_y(2.0 * time.delta_secs());
|
||||
|
||||
@@ -409,16 +409,38 @@ fn weapon_skill_id(
|
||||
weapon_subclass_skill(t.subclass).unwrap_or(SKILL_UNARMED)
|
||||
}
|
||||
|
||||
/// Read a skill line's `(value, temp+perm bonus)` pair from the `PLAYER_SKILL_INFO` triplets;
|
||||
/// `(0, 0)` when the line isn't known (a ranged pair with nothing equipped).
|
||||
fn skill_pair(store: &ObjectStore, skill_id: u32) -> (u32, i32) {
|
||||
/// Read a skill line's `(value + PERM bonus, TEMP bonus)` pair from the `PLAYER_SKILL_INFO`
|
||||
/// triplets; `(0, 0)` when the line isn't known (a ranged pair with nothing equipped).
|
||||
///
|
||||
/// **The split is not `(value, temp + perm)`, which is what this returned until decision 1812.**
|
||||
/// The reader every skill-shaped binding goes through is `0x5ea460`, and it writes
|
||||
/// `*out1 = value + permBonus` / `*out2 = tempBonus` — the permanent half folds into the BASE and
|
||||
/// only the temporary half is the modifier (VERIFIED, wow-re
|
||||
/// `ui/scratch/unitrangedattack-skill-pair.md` §3; the field reads are `+0x84C` `movzx` value,
|
||||
/// `+0x850` `movsx` temp — the block's only sign-extended read — and `+0x852` `movzx` perm).
|
||||
///
|
||||
/// The old shape had the right TOTAL and the wrong halves, which is why nothing caught it: the
|
||||
/// paper doll's Attack row prints `base` and colours `base + mod`, so a 300-value line with a +5
|
||||
/// talent and a +10 aura read "300" with a green +15 where the reference reads "305" with a green
|
||||
/// +10. wow-re's own `object-layer/scratch/w2b2.md` had the two inverted as well, and that is
|
||||
/// where ours came from; the same round corrected it there.
|
||||
///
|
||||
/// **The perm add is skipped when the value is 0** (`0x5ea4b4 jle`), so an unknown line stays a
|
||||
/// flat 0 rather than reading back a bare talent bonus.
|
||||
///
|
||||
/// Signed, because the sum is: `fild dword` on both pushes, and a perm malus deeper than the skill
|
||||
/// is representable on the wire even if no live server sends one.
|
||||
fn skill_pair(store: &ObjectStore, skill_id: u32) -> (i32, i32) {
|
||||
for i in 0..PLAYER_SKILL_SLOTS {
|
||||
if let Some(s) = store.0.player_skill(i) {
|
||||
if u32::from(s.skill_id) == skill_id {
|
||||
return (
|
||||
u32::from(s.value),
|
||||
i32::from(s.temp_bonus) + i32::from(s.perm_bonus),
|
||||
);
|
||||
let value = i32::from(s.value);
|
||||
let base = if value == 0 {
|
||||
0
|
||||
} else {
|
||||
value + i32::from(s.perm_bonus)
|
||||
};
|
||||
return (base, i32::from(s.temp_bonus));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1223,6 +1245,57 @@ mod tests {
|
||||
use benilla_protocol::messages::ObjectFields;
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
|
||||
/// **The skill pair splits `(value + PERM, TEMP)`, not `(value, temp + perm)`** — the reader
|
||||
/// every skill-shaped binding goes through (`0x5ea460`, VERIFIED: wow-re
|
||||
/// `ui/scratch/unitrangedattack-skill-pair.md` §3).
|
||||
///
|
||||
/// It reported the right TOTAL and the wrong halves until decision 1812, which is exactly why
|
||||
/// nothing caught it: `UnitAttackBothHands`'s caller prints the base and colours `base + mod`,
|
||||
/// so both shapes agree on the number a player adds up and disagree on the two they read. The
|
||||
/// fixture below is the one the record quotes — 300 / +5 talent / +10 aura — where the
|
||||
/// reference shows **305** with a green **+10** and ours showed 300 with a green +15.
|
||||
///
|
||||
/// The zero-value leg is the other half of the law (`0x5ea4b4 jle`): an unknown line must stay
|
||||
/// a flat 0 rather than reading back a bare talent bonus.
|
||||
#[test]
|
||||
fn a_skill_pair_folds_the_permanent_bonus_into_the_base() {
|
||||
/// `PLAYER_SKILL_INFO_1_1` — id|step, then value|max, then temp|perm, per slot.
|
||||
const F_SKILL: u16 = 718;
|
||||
let pair = |lo: u16, hi: u16| u32::from(lo) | (u32::from(hi) << 16);
|
||||
// Slot 0: Defense (95) at 300, +10 temporary, +5 permanent.
|
||||
// Slot 1: Swords (43) at 0 — known id, no value, a +7 permanent that must NOT surface.
|
||||
let store = ObjectStore(ObjectFields::from_pairs(&[
|
||||
(F_SKILL, pair(95, 0)),
|
||||
(F_SKILL + 1, pair(300, 300)),
|
||||
(F_SKILL + 2, pair(10, 5)),
|
||||
(F_SKILL + 3, pair(43, 0)),
|
||||
(F_SKILL + 4, pair(0, 300)),
|
||||
(F_SKILL + 5, pair(0, 7)),
|
||||
]));
|
||||
assert_eq!(
|
||||
skill_pair(&store, 95),
|
||||
(305, 10),
|
||||
"the permanent bonus belongs to the base and the temporary one is the modifier"
|
||||
);
|
||||
assert_eq!(
|
||||
skill_pair(&store, 43),
|
||||
(0, 0),
|
||||
"a line at value 0 skips the perm add — a bare talent bonus is not a skill"
|
||||
);
|
||||
assert_eq!(
|
||||
skill_pair(&store, 162),
|
||||
(0, 0),
|
||||
"a line the player does not have at all"
|
||||
);
|
||||
// A malus deeper than the skill is representable, which is why the pair is signed.
|
||||
let cursed = ObjectStore(ObjectFields::from_pairs(&[
|
||||
(F_SKILL, pair(95, 0)),
|
||||
(F_SKILL + 1, pair(5, 300)),
|
||||
(F_SKILL + 2, pair(0, u16::MAX - 9)), // perm = -10
|
||||
]));
|
||||
assert_eq!(skill_pair(&cursed, 95), (-5, 0));
|
||||
}
|
||||
|
||||
/// `PLAYER_FIELD_BANK_BAG_SLOT_1` — bank bag slot 0's guid pair (protocol test `bank.rs`).
|
||||
const F_BANK_BAG_1: u16 = 612;
|
||||
/// `OBJECT_FIELD_ENTRY` on the item object.
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
//! the only thing the renderer draws, so wherever the lattice runs out before the ground climbs
|
||||
//! through the plane, the water ends on a 4.167-yd straight edge in open water: a shape no
|
||||
//! shoreline has;
|
||||
//! - its **mirror** — wet cells the ground climbs out of, which is the skirt of liquid-lattice
|
||||
//! geometry that lies over dry sand. Nothing renders there only because the terrain drew first
|
||||
//! and won the depth test, so this is the exact budget a depth bias spends: a decal pulled `b`
|
||||
//! yards toward the eye paints `b / slope` yards of that skirt;
|
||||
//! - the **shoreline slope** at the pin, and what a coplanarity *lift* costs there. A decal held `l`
|
||||
//! yards above the water plane keeps painting for `l / slope` yards past the waterline, on dry
|
||||
//! ground — the arithmetic behind B348's foam-on-the-sand, and the reason that settle is a depth
|
||||
@@ -256,6 +260,64 @@ fn main() -> anyhow::Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── The mirror: WET cells the ground climbs out of ───────────────────────────────────────
|
||||
// The scan above finds water that is missing; this one finds water geometry that is *over dry
|
||||
// land*. Whole wet cells are the unit the renderer draws, so wherever the ground climbs through
|
||||
// the plane inside a cell, the liquid mesh — and every decal built on the same lattice — carries
|
||||
// a skirt of geometry up the beach. Nothing renders there only because the terrain drew first
|
||||
// and won the depth test, which makes this number the exact **budget a depth bias spends**: a
|
||||
// decal pulled `b` yards toward the eye paints `b / slope` yards of that skirt onto the sand.
|
||||
let mut skirts: Vec<(f32, f32, (i32, i32))> = Vec::new(); // (height above plane, run inland, cell)
|
||||
for (k, idxs) in by_key.iter() {
|
||||
let c = &cells[idxs[0]];
|
||||
if (c.x - x).hypot(c.y - y) > radius {
|
||||
continue;
|
||||
}
|
||||
let mut highest = 0.0_f32;
|
||||
for si in 0..=16 {
|
||||
for sj in 0..=16 {
|
||||
let px = c.x - CELL / 2.0 + CELL * si as f32 / 16.0;
|
||||
let py = c.y - CELL / 2.0 + CELL * sj as f32 / 16.0;
|
||||
if let Some(g) = terrain_height_at(&chunks, [px, py, 0.0]) {
|
||||
highest = highest.max(g - c.surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
if highest > 0.02 {
|
||||
let h = 1.0_f32;
|
||||
let sample = |dx: f32, dy: f32| terrain_height_at(&chunks, [c.x + dx, c.y + dy, 0.0]);
|
||||
let slope = match (
|
||||
sample(h, 0.0),
|
||||
sample(-h, 0.0),
|
||||
sample(0.0, h),
|
||||
sample(0.0, -h),
|
||||
) {
|
||||
(Some(px), Some(mx), Some(py), Some(my)) => {
|
||||
(((px - mx) / (2.0 * h)).powi(2) + ((py - my) / (2.0 * h)).powi(2)).sqrt()
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
let run = if slope > 0.0 {
|
||||
highest / slope
|
||||
} else {
|
||||
f32::INFINITY
|
||||
};
|
||||
skirts.push((highest, run, *k));
|
||||
}
|
||||
}
|
||||
skirts.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("finite"));
|
||||
println!(
|
||||
"\n WET cells with dry ground inside them (the skirt the depth test has to eat): {}",
|
||||
skirts.len()
|
||||
);
|
||||
for (hi, run, k) in skirts.iter().take(6) {
|
||||
println!(
|
||||
" cell {k:?} at ({:.1}, {:.1}) — ground up to {hi:.3} yd above the plane, {run:.2} yd inland",
|
||||
(k.0 as f32 + 0.5) * CELL,
|
||||
(k.1 as f32 + 0.5) * CELL
|
||||
);
|
||||
}
|
||||
|
||||
// ── The waterline's slope, and what a coplanarity lift costs horizontally ────────────────
|
||||
// A decal on the water plane, lifted `l` yards to win the coplanar tie, keeps painting for
|
||||
// `l / slope` yards past the waterline — up the beach, on dry ground. Measure the slope where
|
||||
|
||||
@@ -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.**
|
||||
///
|
||||
|
||||
@@ -264,30 +264,46 @@ impl Rung {
|
||||
/// `0x68fd0f` arms EGxRs id `0` for the foam draw, and arming that id non-zero is what issues
|
||||
/// the client's sole `glPolygonOffset` — `factor` a hardcoded `0xc0800000` = **−4.0**
|
||||
/// (`0x59bf0a`, in capability arm `0x41`, whose only image-wide call site is inside the id-`0`
|
||||
/// applicator), `units` = `footstepBias(0.125) × [0x810390]` ⇒ **−8.0001**. So the reference
|
||||
/// settles this decal with a **slope-scaled** term plus a near-zero constant, which is the
|
||||
/// right instrument and not an accident: a near-horizontal decal seen at a grazing angle has a
|
||||
/// depth gradient a constant cannot follow, and the shoreline is exactly that view.
|
||||
/// applicator), `units` = `footstepBias(0.125) × [0x810390]` ⇒ **−8.0001**. The `-4.0` is not
|
||||
/// this lane's number: it is the immediate every consumer of EGxRs id `0` gets, and the foam
|
||||
/// reaches it by borrowing the **footprint's own cvar** — which is idiom reuse ("this is a
|
||||
/// ground decal, arm the ground-decal offset"), not a tie this decal has to win. Only the
|
||||
/// `units` term is the foam's own.
|
||||
///
|
||||
/// **The signs invert here.** GL depth runs 0 = near, so its negative offset pulls toward the
|
||||
/// eye; ours is reversed-Z (`CompareFunction::GreaterEqual`, near = 1), where toward the eye is
|
||||
/// *larger* depth — so both terms are positive. [`DECAL_RASTER`](Rung::DECAL_RASTER) being
|
||||
/// *larger* depth — so a ported term is positive. [`DECAL_RASTER`](Rung::DECAL_RASTER) being
|
||||
/// positive for the same job corroborates the convention independently.
|
||||
///
|
||||
/// **Two things here are ours, not transcribed.** `factor` multiplies the primitive's *window*
|
||||
/// depth slope, and reversed-Z gives a plane a different gradient than GL's mapping does, so
|
||||
/// 4.0 is the reference's number in a frame where it means something slightly different.
|
||||
/// And the constant is in ULPs of a **float** depth buffer (per-primitive, the quantisation
|
||||
/// 1806 is about) rather than GL's fixed `2⁻²⁴` — same order, not the same unit. The client's
|
||||
/// *D3D* backend arms no slope term at all and compensates with a flat bias ~256× the GL
|
||||
/// constant; which of the two encodings is canonical turns on the `gxApi` device-identity
|
||||
/// question, which is open on wow-re's `gx` board. The GL shape is ported because it is the
|
||||
/// one that is *correct* in any depth format, and because a flat constant is described at the
|
||||
/// bytes as standing in for the missing slope term.
|
||||
/// **The constant is in ULPs of a float depth buffer** (per-primitive — the quantisation 1806
|
||||
/// is about) rather than GL's fixed `2⁻²⁴`: same order, not the same unit. Its world-space pull
|
||||
/// is `z · C · 2⁻²³`, i.e. ~1 µm per yard of view distance at `C = 8` — invisible by
|
||||
/// construction, which is the point. It is a guard against a driver rounding one pipeline's
|
||||
/// coplanar arithmetic differently from another's, not a tie-breaker anything rests on.
|
||||
pub const FOAM_RASTER: i32 = 8;
|
||||
/// The slope-scale half of [`FOAM_RASTER`](Rung::FOAM_RASTER) — see its doc for the bytes,
|
||||
/// the sign inversion, and what is ported vs transcribed.
|
||||
pub const FOAM_RASTER_SLOPE: f32 = 4.0;
|
||||
/// **The slope half is deliberately NOT armed — the tie it settles does not exist here** (B348
|
||||
/// second round, 1811; supersedes 1809 §3's transcription).
|
||||
///
|
||||
/// The reference arms `factor = -4.0` and it is the right instrument *there*. It is the wrong
|
||||
/// one here because our foam patch is not a decal *over* the liquid surface — it **is** the
|
||||
/// liquid surface's own triangles: [`water_fx::build_patch`] emits the wet cells straight out
|
||||
/// of [`WaterChunkInfo`]'s grid, in the liquid mesh's own winding, through the same
|
||||
/// `clip_from_world` (`DECAL_WORLD_CLIP`, 0781) against a mesh whose `Transform` is
|
||||
/// `IDENTITY`. Same vertices, same matrix, same arithmetic — so the depths agree exactly and
|
||||
/// `GreaterEqual` passes the tie on its own. That holds for the fullbright kinds too, which
|
||||
/// are the only liquids that write depth at all (magma/slime are `AlphaMode::Opaque`; water is
|
||||
/// `Blend`, depth-write off, so it never competes at all).
|
||||
///
|
||||
/// What a slope term *does* buy, measured: a near-horizontal decal at a grazing angle has a
|
||||
/// window-depth gradient of `n / (h · f)` per pixel (eye height `h` above the plane, focal
|
||||
/// length `f` in pixels), so `factor · m` is a world-space pull of `factor · z² / (h · f)` —
|
||||
/// **growing as the square of view distance, and the near plane cancels out**. Whole wet cells
|
||||
/// are the unit of liquid geometry, so a shoreline carries a skirt of liquid-lattice triangles
|
||||
/// over dry sand (up to 7 yd of it at the reported beach — `benilla-formats --example
|
||||
/// water_here`), held back by nothing but the terrain having drawn first. Any pull toward the
|
||||
/// eye spends that budget: at `4.0` the wake visibly washed onto the beach, which is what the
|
||||
/// director reported twice.
|
||||
pub const FOAM_RASTER_SLOPE: f32 = 0.0;
|
||||
/// **World text** — above the celestial glare, so a flare never washes a nameplate, and above
|
||||
/// every sky rung. The reference draws its world text late in the frame (decision 0519).
|
||||
/// Small on purpose: 6× the far plane is all the ordering needs, and this same field doubles
|
||||
@@ -330,7 +346,10 @@ const _: () = {
|
||||
// unread — see FOAM_RASTER). Both halves pull toward the eye under reversed-Z, so both are
|
||||
// positive; a negative here would push the decal INTO its receiver.
|
||||
assert!(Rung::FOAM_RASTER > 0 && Rung::FOAM_RASTER < Rung::DECAL_RASTER);
|
||||
assert!(Rung::FOAM_RASTER_SLOPE > 0.0);
|
||||
// The slope half stays disarmed on this lane (1811): its pull grows as z² and it spends the
|
||||
// wet-lattice skirt onto the beach, while the tie it would settle is already won by the patch
|
||||
// being the liquid mesh's own triangles. A nonzero value here is a decision, not a tune.
|
||||
assert!(Rung::FOAM_RASTER_SLOPE == 0.0);
|
||||
|
||||
// ─── The pre-water decal band (B347, 1785/1789) ─────────────────────────────────────────
|
||||
// Internally ordered as the reference's frame emits them — ring then shadow, both from
|
||||
|
||||
@@ -45,9 +45,13 @@
|
||||
//! depth-buffer bias and the foam vertices sit at the queried MCLQ height exactly, so there is no
|
||||
//! geometric lift in the binary to be faithful to (the "−2048 GL units" this file cited for a
|
||||
//! year is refuted; the real term is `footstepBias(0.125) × [0x810390]` = D3D `DEPTHBIAS −1/8192`).
|
||||
//! Ours is the decal family's rasterizer bias plus `DECAL_WORLD_CLIP` — absolute verts through the
|
||||
//! same `clip_from_world` the world meshes run (0781), so the tie is settled in depth ULPs with
|
||||
//! zero horizontal cost.
|
||||
//! Ours needs no settle at all, and that is worth stating rather than assuming (1811): the patch
|
||||
//! is not a decal *over* the surface, it **is** the liquid mesh's own triangles — the same wet
|
||||
//! cells, the same winding, through the same `clip_from_world` (`DECAL_WORLD_CLIP`, 0781) as a
|
||||
//! mesh whose `Transform` is `IDENTITY` — so the depths agree exactly and `GreaterEqual` passes
|
||||
//! the tie unaided. [`Rung::FOAM_RASTER`] stays as a few ULPs of insurance against a driver
|
||||
//! rounding two pipelines differently; the **slope half is disarmed**, because its pull grows as
|
||||
//! z² and the only thing it can reach is the skirt of wet-cell geometry lying over dry sand.
|
||||
//!
|
||||
//! The formulas + lifecycle math live in [`params`]; this half is the ECS: the emitter over the
|
||||
//! avatar + streamed units, the record pool, and one additive effect-stream draw per
|
||||
@@ -598,12 +602,13 @@ fn push_water_foam(
|
||||
lighting: crate::particles::buffer::EffectLighting::None,
|
||||
anchor: centroid / n as f32,
|
||||
bias: FOAM_BIAS,
|
||||
// **The reference's own settle, both halves** (`Rung::FOAM_RASTER`, whose doc
|
||||
// carries the bytes): a near-zero constant plus a slope-scaled term, which is what
|
||||
// a near-horizontal decal at a grazing angle needs — the shoreline view. A nonzero
|
||||
// constant also selects `DECAL_WORLD_CLIP`, so these absolute verts skip the
|
||||
// cam-relative rebase and tie against the ground through the world meshes' own
|
||||
// matrix (0781).
|
||||
// A few ULPs of constant and **no slope term** (`Rung::FOAM_RASTER*`, whose docs
|
||||
// carry the bytes and the reasoning, 1811): the patch is the liquid mesh's own
|
||||
// triangles, so `GreaterEqual` already passes the coplanar tie and the only thing
|
||||
// a pull toward the eye can reach is the wet-lattice skirt over dry sand. The
|
||||
// nonzero constant also selects `DECAL_WORLD_CLIP`, so these absolute verts skip
|
||||
// the cam-relative rebase and go through the world meshes' own matrix (0781) —
|
||||
// which is what makes the two depths agree in the first place.
|
||||
raster_bias: crate::sky_order::Rung::FOAM_RASTER,
|
||||
raster_slope: crate::sky_order::Rung::FOAM_RASTER_SLOPE,
|
||||
cam_relative: false,
|
||||
|
||||
@@ -17,6 +17,23 @@ use benilla_assets::{AssetSet, WorldAssets};
|
||||
/// arrives and tells us the character's real map. A worldport then bumps [`CurrentMap`].
|
||||
pub(crate) const DEFAULT_MAP_ID: u32 = 0;
|
||||
|
||||
/// `$WOW_MAP` — the map a **server-less** run starts on, as a `Map.dbc` id (`0` = Azeroth,
|
||||
/// `1` = Kalimdor). Unset gives [`DEFAULT_MAP_ID`].
|
||||
///
|
||||
/// **Set-but-unparseable is a hard error, deliberately.** This used to fall back to Azeroth, so
|
||||
/// `WOW_MAP=Kalimdor` silently photographed the wrong continent — the capture came back as empty
|
||||
/// ocean with the subject floating in it, which reads as a renderer bug and costs a session the
|
||||
/// time to chase one. An instrument that answers the wrong question quietly is worse than one that
|
||||
/// refuses.
|
||||
fn map_id_from_env() -> u32 {
|
||||
match std::env::var("WOW_MAP") {
|
||||
Err(_) => DEFAULT_MAP_ID,
|
||||
Ok(v) => v.trim().parse().unwrap_or_else(|_| {
|
||||
panic!("WOW_MAP={v:?} is not a Map.dbc id — it takes a NUMBER (0 = Azeroth, 1 = Kalimdor), not a name")
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// The map the player is currently on. Written by `player::control` when it drains the world stream's
|
||||
/// worldport; watched by the terrain + WDL streamers (reload ADTs/WDL for the new map), the loading
|
||||
/// screen (cover the swap), and time-of-day lighting (per-map outdoor light).
|
||||
@@ -93,10 +110,7 @@ fn load_world_map(mut commands: Commands, world_assets: Option<Res<WorldAssets>>
|
||||
// A named capture scenario carries its own map (`capture::scenarios::Scenario::map`)
|
||||
// and writes it here before this runs, so the golden sweep's Kalimdor shots need no env
|
||||
// at all — see `capture::CapturePlugin::build` (decision 0743).
|
||||
let map = std::env::var("WOW_MAP")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(DEFAULT_MAP_ID);
|
||||
let map = map_id_from_env();
|
||||
commands.insert_resource(CurrentMap(map));
|
||||
}
|
||||
Err(e) => error!("failed to load Map.dbc, cross-map teleport disabled: {e:#}"),
|
||||
|
||||
Reference in New Issue
Block a user