3737//! Three lines rather than one, and they are honest about the ownership rather
3838//! than hiding it behind an arena.
3939
40+ use crate :: cliffs:: vulcanus_fields:: CliffinessBasic ;
4041use crate :: distance_from_nearest_point:: distance_from_nearest_point;
4142use crate :: eval:: ctx:: EvalCtx ;
4243use crate :: expressions:: starting_spot_at_angle:: AngleTrig ;
@@ -47,7 +48,11 @@ use crate::expressions::vulcanus_elevation::{ElevationFields, VulcanusElevation}
4748use crate :: expressions:: vulcanus_helpers:: VulcanusHelpers ;
4849use crate :: expressions:: vulcanus_resources:: { OreRegions , ResourceFields , VulcanusResources } ;
4950use crate :: expressions:: vulcanus_spawn:: VulcanusSpawn ;
51+ use crate :: expressions:: vulcanus_spawn:: WobbleSums ;
5052use crate :: multioctave_noise:: Prepared ;
53+ use crate :: resources:: vulcanus_catalog:: sulfuric_acid_geyser_probability;
54+ use crate :: rocks:: vulcanus_field:: { vulcanus_decorative_knockout, VulcanusRockFields } ;
55+ use crate :: tiles:: vulcanus_catalog:: TILE_ORDER ;
5156use crate :: tiles:: vulcanus_catalog:: {
5257 resolve_tile, tile_probabilities, vulcanus_rock_noise, MountainLavaSpots , VulcanusTile ,
5358 VulcanusTileFields ,
@@ -248,8 +253,7 @@ impl<'a> VulcanusStack<'a> {
248253 /// port that dropped the `max` would score identically. The clamp's real
249254 /// test lives in `vulcanus_elevation`, constructing the case the fixture
250255 /// does not.
251- #[ must_use]
252- pub fn elevation_fields ( & self , x : f64 , y : f64 ) -> ElevationFields {
256+ fn elevation_fields ( & self , x : f64 , y : f64 ) -> ElevationFields {
253257 self . elevation . eval ( x, y)
254258 }
255259
@@ -260,8 +264,7 @@ impl<'a> VulcanusStack<'a> {
260264 /// byte-identical pixels say nothing about it. Tier 1 grades it against the
261265 /// game (244 of 434) and tier 2 is the only thing that would notice it
262266 /// diverging from the TypeScript.
263- #[ must_use]
264- pub fn temperature ( & self , x : f64 , y : f64 ) -> f64 {
267+ fn temperature ( & self , x : f64 , y : f64 ) -> f64 {
265268 self . elevation
266269 . temperature ( x, y, self . base . ctx . temperature_bias )
267270 }
@@ -332,6 +335,149 @@ impl<'a> VulcanusStack<'a> {
332335 }
333336}
334337
338+ /// Tier 2's field selector for the Vulcanus graph: one named expression, by index.
339+ ///
340+ /// **This lives here rather than in the wasm crate on purpose.** The selector
341+ /// needs `elevation_fields` and `temperature`, which nothing on a render path
342+ /// reads - temperature is not drawn at all, and `elev` versus its clamp is a
343+ /// distinction no fixture can make. Reaching them from another crate meant two
344+ /// `pub` methods on a library type that existed solely for a test, and a `pub`
345+ /// method cannot be `#[cfg(test)]`-gated because the wasm crate calls it at
346+ /// build time. Keeping the selector in this module makes both private again.
347+ ///
348+ /// The layers it evaluates per point are deliberately NOT shared with each
349+ /// other - `tile_fields` re-evaluates the biome and resource layers, for
350+ /// instance. That is the point: each field is reached through its own entry
351+ /// point, the same one its consumer uses, so a field wired to the wrong layer
352+ /// shows up as a divergence rather than being smoothed over by a shared
353+ /// intermediate. It runs once in a test, so the redundancy is free.
354+ pub struct VulcanusParity < ' a , ' b > {
355+ stack : & ' a VulcanusStack < ' b > ,
356+ rocks : VulcanusRockFields < ' a , ' b > ,
357+ knockout : Prepared ,
358+ cliffiness : CliffinessBasic ,
359+ }
360+
361+ impl < ' a , ' b > VulcanusParity < ' a , ' b > {
362+ /// How many named fields [`Self::field`] can select, `0..FIELD_COUNT`.
363+ ///
364+ /// The order is the order the chain evaluates in: helpers, spawn, cracks,
365+ /// climate, biomes, elevation, resources, the tile-support fields, the two
366+ /// overlay fields, then the 19 tile probabilities and the argmax over them.
367+ pub const FIELD_COUNT : u32 = 74 ;
368+
369+ #[ must_use]
370+ pub fn new ( stack : & ' a VulcanusStack < ' b > , seed0 : u32 ) -> Self {
371+ Self {
372+ stack,
373+ // The overlay fields hang off the same stack rather than rebuilding
374+ // the chain, which is what their own constructors are for.
375+ rocks : VulcanusRockFields :: new ( stack, seed0) ,
376+ knockout : vulcanus_decorative_knockout ( seed0) ,
377+ cliffiness : CliffinessBasic :: for_vulcanus ( seed0) ,
378+ }
379+ }
380+
381+ /// One named field at `(x, y)`. Out-of-range indices resolve to the argmax,
382+ /// matching the exhaustive `match` this was lifted from.
383+ #[ must_use]
384+ #[ allow( clippy:: too_many_lines) ]
385+ pub fn field ( & self , field : u32 , x : f64 , y : f64 ) -> f64 {
386+ let base = self . stack . base ;
387+ let sp = base. spawn . eval ( x, y, WobbleSums :: at ( & base. helpers , x, y) ) ;
388+ let cr = base. cracks . eval ( x, y) ;
389+ let cl = base. climate . eval ( x, y, & cr) ;
390+ let bi = self . stack . biomes . eval ( x, y) ;
391+ let el = self . stack . elevation_fields ( x, y) ;
392+ let res = self . stack . resources ( x, y) ;
393+ let tf = self . stack . tile_fields ( x, y) ;
394+ let rk = self . rocks . eval ( x, y) ;
395+
396+ match field {
397+ 0 => base. helpers . wobble_x ( x, y) ,
398+ 1 => base. helpers . wobble_y ( x, y) ,
399+ 2 => base. helpers . wobble_large_x ( x, y) ,
400+ 3 => base. helpers . wobble_large_y ( x, y) ,
401+ 4 => base. helpers . wobble_huge_x ( x, y) ,
402+ 5 => base. helpers . wobble_huge_y ( x, y) ,
403+ 6 => sp. ashlands_start ,
404+ 7 => sp. basalts_start ,
405+ 8 => sp. mountains_start ,
406+ 9 => sp. starting_area ,
407+ 10 => sp. starting_circle ,
408+ 11 => cr. hairline_cracks ,
409+ 12 => cr. flood_cracks_a ,
410+ 13 => cr. flood_cracks_b ,
411+ 14 => cr. flood_paths ,
412+ 15 => cr. flood_basalts_func ,
413+ 16 => cl. aux ,
414+ 17 => cl. moisture ,
415+ 18 => bi. mountain_volcano_spots ,
416+ 19 => bi. mountains_raw_volcano ,
417+ 20 => bi. mountains_biome_full ,
418+ 21 => bi. ashlands_biome_full ,
419+ 22 => bi. basalts_biome_full ,
420+ 23 => bi. mountains_biome ,
421+ 24 => bi. ashlands_biome ,
422+ 25 => bi. basalts_biome ,
423+ // The RAW `elev` and its clamp, graded separately. No fixture can
424+ // tell them apart - see `VulcanusStack::elevation_fields`.
425+ 26 => el. elev ,
426+ 27 => el. elevation ,
427+ // A genuinely different field, not a rounding of the one above:
428+ // `multisample`'s offsets are in the CONSUMING program's grid units
429+ // and the cliff lattice is 4 tiles wide (#83).
430+ 28 => self . stack . cliff_elevation ( x, y) ,
431+ // Read by no renderer at all, which is half the argument for this
432+ // selector existing.
433+ 29 => self . stack . temperature ( x, y) ,
434+ 30 => res. basalts_favorability ,
435+ 31 => res. mountains_favorability ,
436+ 32 => res. mountains_sulfur_favorability ,
437+ 33 => res. ashlands_favorability ,
438+ 34 => res. starting_tungsten ,
439+ 35 => res. starting_coal ,
440+ 36 => res. starting_calcite ,
441+ 37 => res. starting_sulfur ,
442+ 38 => res. tungsten_region ,
443+ 39 => res. coal_region ,
444+ 40 => res. calcite_region ,
445+ 41 => res. sulfuric_acid_region ,
446+ 42 => res. sulfuric_acid_patches ,
447+ 43 => res. sulfuric_acid_region_patchy ,
448+ 44 => res. metal_tile ,
449+ 45 => sulfuric_acid_geyser_probability ( res. sulfuric_acid_region_patchy ) ,
450+ 46 => tf. mountain_lava_spots ,
451+ 47 => tf. rock_noise ,
452+ 48 => tf. distance ,
453+ 49 => self . cliffiness . eval ( x, y) ,
454+ 50 => f64:: from ( self . knockout . eval ( x, y) ) ,
455+ 51 => rk. rock_huge ,
456+ 52 => rk. rock_big ,
457+ 53 => rk. density ,
458+ // The 19 tile probabilities, in `TILE_ORDER`.
459+ 54 ..=72 => tile_probabilities ( & tf) [ ( field - 54 ) as usize ] ,
460+ // The resolved tile, as its index in `TILE_ORDER`. A number so it
461+ // rides the same comparator as the probabilities it derives from,
462+ // and a DISCRETE one, which is why `resolve_tile` carries
463+ // `poison::index_result` rather than a numeric hook.
464+ _ => tile_index ( resolve_tile ( & tf) ) ,
465+ }
466+ }
467+ }
468+
469+ /// A resolved tile as its position in [`TILE_ORDER`].
470+ ///
471+ /// A linear scan rather than a second table, so it cannot drift from the order
472+ /// the argmax resolves against.
473+ fn tile_index ( tile : crate :: tiles:: vulcanus_catalog:: VulcanusTile ) -> f64 {
474+ #[ allow( clippy:: cast_precision_loss) ]
475+ TILE_ORDER
476+ . iter ( )
477+ . position ( |& t| t == tile)
478+ . map_or ( f64:: NAN , |i| i as f64 )
479+ }
480+
335481#[ cfg( test) ]
336482mod tests {
337483 use super :: * ;
0 commit comments