MONKEY (daylight): district window split (oversized EXT batches become per-pane apertures), sky rooms include EXT-class windows, lamp occlusion scan, B1 hill scenarios
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
310bed8941
commit
1cdbf2aabf
@@ -1390,10 +1390,10 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
name: "daylight-sw-cathedral", map: Some(MAP_AZEROTH),
|
||||
eye: [-8556.0, 826.0, 109.0], look: [-8515.0, 862.0, 112.0], minute: 720, ui: None,
|
||||
},
|
||||
// A Trade District shop (group g123, one exterior portal).
|
||||
// A Trade District house (group g68 NEH02, one exterior portal).
|
||||
Scenario {
|
||||
name: "daylight-sw-shop", map: Some(MAP_AZEROTH),
|
||||
eye: [-8776.0, 722.0, 101.5], look: [-8764.0, 711.0, 100.2], minute: 720, ui: None,
|
||||
eye: [-8799.0, 696.0, 104.3], look: [-8786.0, 707.0, 103.0], minute: 720, ui: None,
|
||||
},
|
||||
// Ironforge: the Great Forge hall (g64, no opening to the sky) and the gate hall (g7, the
|
||||
// city's one exterior portal).
|
||||
@@ -1409,6 +1409,16 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
Scenario {
|
||||
name: "daylight-torch-goldshire", map: Some(MAP_AZEROTH),
|
||||
eye: [-9460.0, 70.0, 58.0], look: [-9450.0, 20.0, 61.0], minute: 0, ui: None,
|
||||
}, // Two Elwynn road lampposts whose own light the terrain blocks most (the census's
|
||||
// `lamp_terrain_occlusion_scan`: 18 % and 16 % of the ground within 25 yd), at midnight, framed
|
||||
// across the lamp toward the blocked side.
|
||||
Scenario {
|
||||
name: "daylight-torch-hill-a", map: Some(MAP_AZEROTH),
|
||||
eye: [-9314.8, 134.1, 70.0], look: [-9320.4, 165.6, 64.0], minute: 0, ui: None,
|
||||
},
|
||||
Scenario {
|
||||
name: "daylight-torch-hill-b", map: Some(MAP_AZEROTH),
|
||||
eye: [-9163.1, 181.1, 77.0], look: [-9147.1, 153.4, 71.0], minute: 0, ui: None,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -315,12 +315,20 @@ const DISTRICT_SKY_HOPS: u8 = 1;
|
||||
/// pool dies half-way into a 40 yd hall.
|
||||
///
|
||||
/// The rule is the portal graph's, not a box test: a room is SKY-CONNECTED when it authors an
|
||||
/// exterior-facing portal (the same relation the PORTAL seed stands the sun in) or sits within
|
||||
/// exterior-facing portal (the relation the PORTAL seed stands the sun in) or an EXT-class batch (the
|
||||
/// APERTURE population: a window), or sits within
|
||||
/// [`DISTRICT_SKY_HOPS`] interior portals of such a room. That lights the shops, taverns and their
|
||||
/// back rooms and leaves what the data says is deep inside alone: the Stormwind canal tunnels,
|
||||
/// the Slaughtered Lamb's cellar, and all of Ironforge except the gate halls (the city's ONE
|
||||
/// exterior portal is `p0`, into `g7`).
|
||||
pub fn district_sky_rooms(groups: &[WmoGroupInfo], portals: PortalGraph<'_>) -> Vec<bool> {
|
||||
pub fn district_sky_rooms<'a, I>(
|
||||
groups: &[WmoGroupInfo],
|
||||
portals: PortalGraph<'_>,
|
||||
batches: I,
|
||||
) -> Vec<bool>
|
||||
where
|
||||
I: IntoIterator<Item = (u16, bool, &'a [[f32; 3]])>,
|
||||
{
|
||||
if groups.iter().filter(|g| g.interior).count() <= DAYLIGHT_DISTRICT_ROOMS {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -352,6 +360,14 @@ pub fn district_sky_rooms(groups: &[WmoGroupInfo], portals: PortalGraph<'_>) ->
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// …and a room with an EXT-class batch — a window or an open side (the APERTURE population).
|
||||
for (g, ext_class, _) in batches {
|
||||
if ext_class && interior(g) {
|
||||
if let Some(h) = hop.get_mut(usize::from(g)) {
|
||||
*h = Some(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
for level in 0..DISTRICT_SKY_HOPS {
|
||||
let mut next = Vec::new();
|
||||
for &(a, b) in &edges {
|
||||
@@ -690,6 +706,9 @@ where
|
||||
let portal_seeds = out.len();
|
||||
|
||||
// --- 2. APERTURE seeds: an EXT-class batch of an INTERIOR group (see the module doc).
|
||||
// MONKEY (daylight: window split): on a DISTRICT placement an EXT batch wider than
|
||||
// [`APERTURE_MAX_DIAG`] is split into its spatial clusters first ([`window_boxes`]).
|
||||
let district = groups.iter().filter(|g| g.interior).count() > DAYLIGHT_DISTRICT_ROOMS;
|
||||
if want_apertures {
|
||||
for &(gi, ext_class, positions) in &batches {
|
||||
if !ext_class || interior_of(gi) != Some(true) {
|
||||
@@ -698,35 +717,34 @@ where
|
||||
let Some(g) = groups.get(usize::from(gi)) else {
|
||||
continue;
|
||||
};
|
||||
let Some((lo, hi)) = bounds(positions.iter().copied()) else {
|
||||
continue;
|
||||
};
|
||||
let diag = diagonal(lo, hi);
|
||||
if !(APERTURE_MIN_DIAG..=APERTURE_MAX_DIAG).contains(&diag) {
|
||||
continue;
|
||||
for (lo, hi) in window_boxes(positions, district) {
|
||||
let diag = diagonal(lo, hi);
|
||||
if !(APERTURE_MIN_DIAG..=APERTURE_MAX_DIAG).contains(&diag) {
|
||||
continue;
|
||||
}
|
||||
let (c, hz) = seed_point(lo, hi);
|
||||
let pos = nudge_inward(c, g, None);
|
||||
// The authored portal wins a shared opening — see [`SEED_DEDUPE_YD`].
|
||||
let dup = out[..portal_seeds].iter().any(|s| {
|
||||
(s.pos[0] - pos[0]).powi(2)
|
||||
+ (s.pos[1] - pos[1]).powi(2)
|
||||
+ (s.pos[2] - pos[2]).powi(2)
|
||||
< SEED_DEDUPE_YD * SEED_DEDUPE_YD
|
||||
});
|
||||
if dup {
|
||||
continue;
|
||||
}
|
||||
out.push(DaylightSeed {
|
||||
group: gi,
|
||||
portal: None,
|
||||
how: DaylightHow::Aperture,
|
||||
pos,
|
||||
diag,
|
||||
area: opening_area(lo, hi),
|
||||
hz,
|
||||
round: 0,
|
||||
});
|
||||
}
|
||||
let (c, hz) = seed_point(lo, hi);
|
||||
let pos = nudge_inward(c, g, None);
|
||||
// The authored portal wins a shared opening — see [`SEED_DEDUPE_YD`].
|
||||
let dup = out[..portal_seeds].iter().any(|s| {
|
||||
(s.pos[0] - pos[0]).powi(2)
|
||||
+ (s.pos[1] - pos[1]).powi(2)
|
||||
+ (s.pos[2] - pos[2]).powi(2)
|
||||
< SEED_DEDUPE_YD * SEED_DEDUPE_YD
|
||||
});
|
||||
if dup {
|
||||
continue;
|
||||
}
|
||||
out.push(DaylightSeed {
|
||||
group: gi,
|
||||
portal: None,
|
||||
how: DaylightHow::Aperture,
|
||||
pos,
|
||||
diag,
|
||||
area: opening_area(lo, hi),
|
||||
hz,
|
||||
round: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,6 +796,108 @@ where
|
||||
out
|
||||
}
|
||||
|
||||
/// MONKEY (daylight: window split): the openings one EXT-class batch stands for, as boxes.
|
||||
///
|
||||
/// MEASURED (`benilla-extract wmolights` on Stormwind, 2026-09-25): the Cathedral of Light's nave
|
||||
/// authors ALL its stained-glass windows as ONE EXT batch per wall — `g135 b11` spans
|
||||
/// 15 x 44 x 20 yd (diag 51), `g146 b10` 30 x 44 x 10 yd (diag 54) — so [`APERTURE_MAX_DIAG`]
|
||||
/// refused the batch whole and the nave took no daylight through a single window. Its windows are
|
||||
/// separate panes between pillars, so the batch's vertices fall into separate clusters at the
|
||||
/// plan linkage of [`plan_clusters`], and each window-sized cluster is an aperture.
|
||||
///
|
||||
/// District placements only, and only for an oversized batch: every building (the inn, the abbey)
|
||||
/// keeps its calibrated seed list byte for byte, and a batch that is already window-sized is one
|
||||
/// box as before. At most [`WINDOW_SPLIT_MAX`] boxes per batch, largest first.
|
||||
fn window_boxes(positions: &[[f32; 3]], district: bool) -> Vec<([f32; 3], [f32; 3])> {
|
||||
let Some((lo, hi)) = bounds(positions.iter().copied()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if !district || diagonal(lo, hi) <= APERTURE_MAX_DIAG {
|
||||
return vec![(lo, hi)];
|
||||
}
|
||||
let mut boxes: Vec<([f32; 3], [f32; 3])> = plan_clusters(positions)
|
||||
.into_iter()
|
||||
.filter(|(l, h)| (APERTURE_MIN_DIAG..=APERTURE_MAX_DIAG).contains(&diagonal(*l, *h)))
|
||||
.collect();
|
||||
// Stable: `cluster_boxes` is already sorted, and ties keep that order.
|
||||
boxes.sort_by(|a, b| opening_area(b.0, b.1).total_cmp(&opening_area(a.0, a.1)));
|
||||
boxes.truncate(WINDOW_SPLIT_MAX);
|
||||
boxes
|
||||
}
|
||||
|
||||
/// MONKEY (daylight: window split): single-link clustering of `points` in PLAN (x, y) at
|
||||
/// [`WINDOW_GAP_YD`], one 3-D `(lo, hi)` box per cluster. In plan because a pane's top and bottom
|
||||
/// corners share their plan position however tall it is (a stained-glass pane is often one quad,
|
||||
/// its vertices 8 yd apart vertically), while two panes of one wall are split by a pillar.
|
||||
fn plan_clusters(points: &[[f32; 3]]) -> Vec<([f32; 3], [f32; 3])> {
|
||||
fn find(parent: &mut [usize], mut i: usize) -> usize {
|
||||
while parent[i] != i {
|
||||
parent[i] = parent[parent[i]];
|
||||
i = parent[i];
|
||||
}
|
||||
i
|
||||
}
|
||||
let cell = |p: [f32; 3]| {
|
||||
[
|
||||
(p[0] / WINDOW_GAP_YD).floor() as i32,
|
||||
(p[1] / WINDOW_GAP_YD).floor() as i32,
|
||||
]
|
||||
};
|
||||
let mut index: HashMap<[i32; 2], usize> = HashMap::new();
|
||||
let mut cells: Vec<[i32; 2]> = Vec::new();
|
||||
let of_point: Vec<usize> = points
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let c = cell(*p);
|
||||
*index.entry(c).or_insert_with(|| {
|
||||
cells.push(c);
|
||||
cells.len() - 1
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut parent: Vec<usize> = (0..cells.len()).collect();
|
||||
for (i, c) in cells.iter().enumerate() {
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
if let Some(&j) = index.get(&[c[0] + dx, c[1] + dy]) {
|
||||
let (a, b) = (find(&mut parent, i), find(&mut parent, j));
|
||||
if a != b {
|
||||
parent[a] = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut boxes: HashMap<usize, ([f32; 3], [f32; 3])> = HashMap::new();
|
||||
for (p, ci) in points.iter().zip(of_point.iter()) {
|
||||
let root = find(&mut parent, *ci);
|
||||
let e = boxes.entry(root).or_insert((*p, *p));
|
||||
for a in 0..3 {
|
||||
e.0[a] = e.0[a].min(p[a]);
|
||||
e.1[a] = e.1[a].max(p[a]);
|
||||
}
|
||||
}
|
||||
let mut out: Vec<([f32; 3], [f32; 3])> = boxes.into_values().collect();
|
||||
// Deterministic: a `HashMap` walk is not.
|
||||
out.sort_by(|a, b| {
|
||||
a.0[0]
|
||||
.total_cmp(&b.0[0])
|
||||
.then(a.0[1].total_cmp(&b.0[1]))
|
||||
.then(a.0[2].total_cmp(&b.0[2]))
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// MONKEY (daylight: window split): the plan-cell size (yd) that links one pane's vertices and no
|
||||
/// more: two points in neighbouring cells are at most ~4 yd apart, and a pillar between two panes
|
||||
/// is wider than the one-cell gap it takes to split them.
|
||||
const WINDOW_GAP_YD: f32 = 1.5;
|
||||
|
||||
/// MONKEY (daylight: window split): the most apertures one EXT batch may become. A nave wall is
|
||||
/// three to five panes; the cap keeps a pathological batch (a lattice of posts) from flooding the
|
||||
/// placement's quota, which ranks one room's second opening after every room's first anyway.
|
||||
const WINDOW_SPLIT_MAX: usize = 6;
|
||||
|
||||
/// MONKEY (daylight fixtures: boundary): every stitched interior<->exterior opening of one root, as
|
||||
/// `(interior group, cluster lo, cluster hi, inward unit direction)` in WMO model space.
|
||||
///
|
||||
@@ -2147,11 +2267,36 @@ mod tests {
|
||||
slices[2] = (2, 2);
|
||||
slices[3] = (4, 1);
|
||||
let portals = PortalGraph { vertices: &vertices, infos: &infos, refs: &refs, slices: &slices };
|
||||
let sky = district_sky_rooms(&groups, portals);
|
||||
let none: [(u16, bool, &[[f32; 3]]); 0] = [];
|
||||
let sky = district_sky_rooms(&groups, portals, none);
|
||||
assert!(sky[1] && sky[2], "the doorway room and its neighbour");
|
||||
assert!(!sky[3], "two hops in stays dark");
|
||||
assert!(!sky[0], "the shell is not a room");
|
||||
assert!(district_sky_rooms(&groups[..5], portals).is_empty(), "a building is untouched");
|
||||
assert!(district_sky_rooms(&groups[..5], portals, none).is_empty(), "a building is untouched");
|
||||
// A window (EXT-class batch) makes g3 a sky room too.
|
||||
let window = [[0.0f32; 3], [1.0, 1.0, 1.0]];
|
||||
let sky = district_sky_rooms(&groups, portals, [(3u16, true, &window[..])]);
|
||||
assert!(sky[3], "a room with a window");
|
||||
}
|
||||
|
||||
/// MONKEY (daylight: window split): a district's wall-long EXT batch becomes its panes; a
|
||||
/// building's stays one box (and is then refused as before).
|
||||
#[test]
|
||||
fn a_district_window_wall_splits_into_panes() {
|
||||
// Three 2 x 0.2 x 4 yd panes, 10 yd apart along +Y: one batch, diag > 24.
|
||||
let mut pts = Vec::new();
|
||||
for i in 0..3 {
|
||||
let y = i as f32 * 12.0;
|
||||
for (dy, dz) in [(0.0, 0.0), (2.0, 0.0), (0.0, 4.0), (2.0, 4.0), (1.0, 2.0)] {
|
||||
pts.push([0.0, y + dy, dz]);
|
||||
pts.push([0.2, y + dy, dz]);
|
||||
}
|
||||
}
|
||||
pts.push([0.0, 0.0, 0.0]);
|
||||
assert_eq!(window_boxes(&pts, false).len(), 1, "a building keeps the whole batch");
|
||||
let panes = window_boxes(&pts, true);
|
||||
assert_eq!(panes.len(), 3, "a district splits it into its panes");
|
||||
assert!(panes.iter().all(|(l, h)| diagonal(*l, *h) < 6.0));
|
||||
}
|
||||
|
||||
/// The reach formula: linear in the opening's diagonal, floored at 6 and capped at 20.
|
||||
|
||||
@@ -15,7 +15,13 @@ struct Loaded {
|
||||
portals: benilla_formats::WmoPortals,
|
||||
slices: Vec<(u16, u16)>,
|
||||
/// `(group, class, window, sidn, positions)` per render batch.
|
||||
batches: Vec<(u16, benilla_formats::WmoBatchClass, bool, bool, Vec<[f32; 3]>)>,
|
||||
batches: Vec<(
|
||||
u16,
|
||||
benilla_formats::WmoBatchClass,
|
||||
bool,
|
||||
bool,
|
||||
Vec<[f32; 3]>,
|
||||
)>,
|
||||
}
|
||||
|
||||
fn group_names(bytes: &[u8]) -> Vec<String> {
|
||||
@@ -96,7 +102,11 @@ fn census(path: &str) {
|
||||
.collect();
|
||||
let verts: usize = feed.iter().map(|b| b.2.len()).sum();
|
||||
let rooms = l.groups.iter().filter(|g| g.interior).count();
|
||||
println!("=== {path}: {} groups, {rooms} interior, {} batches, {verts} batch verts", l.groups.len(), feed.len());
|
||||
println!(
|
||||
"=== {path}: {} groups, {rooms} interior, {} batches, {verts} batch verts",
|
||||
l.groups.len(),
|
||||
feed.len()
|
||||
);
|
||||
|
||||
let t = std::time::Instant::now();
|
||||
let ranked = daylight_seeds_ranked(&l.groups, graph, feed.iter().copied());
|
||||
@@ -125,10 +135,12 @@ fn census(path: &str) {
|
||||
let mut day_claim: HashMap<u16, Vec<String>> = HashMap::new();
|
||||
for s in &day {
|
||||
for c in daylight_claims(&l.groups, graph, s) {
|
||||
day_claim
|
||||
.entry(c.group)
|
||||
.or_default()
|
||||
.push(format!("{}@g{}{:?}", s.how.tag(), s.group, c.how));
|
||||
day_claim.entry(c.group).or_default().push(format!(
|
||||
"{}@g{}{:?}",
|
||||
s.how.tag(),
|
||||
s.group,
|
||||
c.how
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut bleed_claim: HashSet<u16> = HashSet::new();
|
||||
@@ -157,8 +169,17 @@ fn census(path: &str) {
|
||||
};
|
||||
let (start, count) = l.slices[gi];
|
||||
let mut nb = Vec::new();
|
||||
for r in l.portals.refs.iter().skip(start as usize).take(count as usize) {
|
||||
let other = l.groups.get(r.group as usize).map_or('?', |o| if o.interior { 'i' } else { 'e' });
|
||||
for r in l
|
||||
.portals
|
||||
.refs
|
||||
.iter()
|
||||
.skip(start as usize)
|
||||
.take(count as usize)
|
||||
{
|
||||
let other =
|
||||
l.groups
|
||||
.get(r.group as usize)
|
||||
.map_or('?', |o| if o.interior { 'i' } else { 'e' });
|
||||
let area = benilla_formats::room_claim::portal_area(&graph, r.portal).unwrap_or(0.0);
|
||||
nb.push(format!("p{}->g{}{}({:.0})", r.portal, r.group, other, area));
|
||||
}
|
||||
@@ -191,8 +212,10 @@ fn census(path: &str) {
|
||||
day_claim.get(&gid).map(|v| v.iter().take(3).cloned().collect::<Vec<_>>().join(",")).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
println!("interior rooms: {n_day} reached by daylight, {n_bleed} by bleed only, {n_none} by neither");
|
||||
let sky = district_sky_rooms(&l.groups, graph);
|
||||
println!(
|
||||
"interior rooms: {n_day} reached by daylight, {n_bleed} by bleed only, {n_none} by neither"
|
||||
);
|
||||
let sky = district_sky_rooms(&l.groups, graph, feed.iter().copied());
|
||||
let enclosed = (0..l.groups.len() as u16)
|
||||
.filter(|g| benilla_formats::room_claim::enclosed_by_building_shell(&l.groups, *g))
|
||||
.count();
|
||||
@@ -210,12 +233,20 @@ fn census(path: &str) {
|
||||
fn placement() -> Option<bevy::math::Affine3A> {
|
||||
let spec = std::env::var("WOW_CENSUS_PLACE").ok()?;
|
||||
let p: Vec<&str> = spec.split(',').collect();
|
||||
let (map, tx, ty, uid) = (p[0], p[1].parse().ok()?, p[2].parse().ok()?, p[3].parse::<u32>().ok()?);
|
||||
let (map, tx, ty, uid) = (
|
||||
p[0],
|
||||
p[1].parse().ok()?,
|
||||
p[2].parse().ok()?,
|
||||
p[3].parse::<u32>().ok()?,
|
||||
);
|
||||
let data = benilla_formats::wow_data()?;
|
||||
let mut chain = open_chain(&data).ok()?;
|
||||
let tile = benilla_formats::load_tile_mesh(&mut chain, map, tx, ty).ok()?;
|
||||
let w = tile.wmos.iter().find(|w| w.unique_id == uid)?;
|
||||
println!("placement uid {uid} pos {:?} rot {:?}", w.position, w.rotation);
|
||||
println!(
|
||||
"placement uid {uid} pos {:?} rot {:?}",
|
||||
w.position, w.rotation
|
||||
);
|
||||
Some(bevy::math::Affine3A::from_scale_rotation_translation(
|
||||
Vec3::ONE,
|
||||
benilla_assets::coords::placement_rotation(w.rotation),
|
||||
@@ -234,3 +265,79 @@ fn city_daylight_census() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MONKEY (daylight: terrain torch casters): which lampposts have ground they cannot see — the
|
||||
/// share of terrain points within 25 yd whose line to the lamp head (3.5 yd up) is blocked by the
|
||||
/// terrain itself. Picks a vantage for the B1 capture. `WOW_LAMP_SCAN=map,x,y,radius,filter`.
|
||||
#[test]
|
||||
#[ignore = "instrument: needs WOW_DATA; run by hand with --ignored --nocapture"]
|
||||
fn lamp_terrain_occlusion_scan() {
|
||||
let spec = std::env::var("WOW_LAMP_SCAN")
|
||||
.unwrap_or_else(|_| "Azeroth,-9300,150,1,lamppost".into());
|
||||
let p: Vec<&str> = spec.split(',').collect();
|
||||
let (map, cx, cy, r, filter) = (
|
||||
p[0],
|
||||
p[1].parse::<f32>().unwrap(),
|
||||
p[2].parse::<f32>().unwrap(),
|
||||
p[3].parse::<i32>().unwrap(),
|
||||
p[4].to_ascii_lowercase(),
|
||||
);
|
||||
let data = benilla_formats::wow_data().expect("WOW_DATA");
|
||||
let mut chain = open_chain(&data).expect("chain");
|
||||
let (tx, ty) = benilla_formats::world_to_tile(cx, cy);
|
||||
let mut chunks = Vec::new();
|
||||
let mut lamps = Vec::new();
|
||||
for x in tx as i32 - r..=tx as i32 + r {
|
||||
for y in ty as i32 - r..=ty as i32 + r {
|
||||
let Ok(t) = benilla_formats::load_tile_mesh(&mut chain, map, x as u32, y as u32) else {
|
||||
continue;
|
||||
};
|
||||
for d in &t.doodads {
|
||||
if d.model.to_ascii_lowercase().contains(&filter) {
|
||||
lamps.push(d.position);
|
||||
}
|
||||
}
|
||||
chunks.push(t.chunks);
|
||||
}
|
||||
}
|
||||
let h = |x: f32, y: f32| {
|
||||
chunks
|
||||
.iter()
|
||||
.find_map(|c| benilla_formats::terrain_height_at(c, [x, y, 1.0e4]))
|
||||
};
|
||||
let mut rows = Vec::new();
|
||||
for l in lamps {
|
||||
let head = Vec3::new(l[0], l[1], l[2] + 3.5);
|
||||
let (mut total, mut blocked) = (0u32, 0u32);
|
||||
let mut best = (0.0f32, 0.0f32);
|
||||
for ring in 1..=10 {
|
||||
let d = ring as f32 * 2.5;
|
||||
for k in 0..36 {
|
||||
let a = k as f32 * std::f32::consts::TAU / 36.0;
|
||||
let (x, y) = (l[0] + d * a.cos(), l[1] + d * a.sin());
|
||||
let Some(z) = h(x, y) else { continue };
|
||||
total += 1;
|
||||
let target = Vec3::new(x, y, z + 0.3);
|
||||
let steps = (d / 0.5) as i32;
|
||||
let hit = (1..steps).any(|s| {
|
||||
let q = head.lerp(target, s as f32 / steps as f32);
|
||||
h(q.x, q.y).is_some_and(|g| g > q.z + 0.05)
|
||||
});
|
||||
if hit {
|
||||
blocked += 1;
|
||||
best = (a.to_degrees(), d);
|
||||
}
|
||||
}
|
||||
}
|
||||
if total > 0 {
|
||||
rows.push((blocked as f32 / total as f32, l, best));
|
||||
}
|
||||
}
|
||||
rows.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
for (f, l, (a, d)) in rows.iter().take(12) {
|
||||
println!(
|
||||
"lamp ({:8.1},{:7.1},{:6.1}) terrain-blocked {:5.1}% e.g. bearing {a:5.0} deg at {d:4.1} yd",
|
||||
l[0], l[1], l[2], f * 100.0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +340,13 @@ pub(super) fn spawn_loaded_placements(
|
||||
refs: &m.portal_refs,
|
||||
slices: &sky_slices,
|
||||
},
|
||||
m.submeshes.iter().zip(m.submesh_group.iter()).map(|(s, g)| {
|
||||
(
|
||||
*g,
|
||||
matches!(s.wmo_batch, Some(benilla_formats::WmoBatchClass::Ext)),
|
||||
&s.geometry.positions[..],
|
||||
)
|
||||
}),
|
||||
);
|
||||
let instance = (has_portals || m.wmo_id != 0).then(|| {
|
||||
commands
|
||||
|
||||
Reference in New Issue
Block a user