@@ -30,7 +30,7 @@ use once_cell::sync::Lazy;
3030use crate :: block_map:: { compute_cell_details_for_tile, CellDetails } ;
3131use crate :: collision:: HybridCollisionMap ;
3232use crate :: lighting:: {
33- build_ray_table, trace_visible_cells, Ambient , Color , ColorMode , Fov , Light , RayTable ,
33+ build_ray_table, trace_visible_cells, Ambient , Color , Fov , Light , RayTable ,
3434} ;
3535use crate :: map_grid:: UnionFind ;
3636
@@ -250,67 +250,43 @@ impl LightingEngine {
250250 self . collision . clear ( ) ;
251251 }
252252
253- /// Create or update a rainbow light. Returns a pointer to the rendered
254- /// canvas (used by the WASM shim ). Rust callers should prefer
255- /// [`Self::light_canvas`] after this call.
253+ /// Create or update a light and return a pointer to its transport mask —
254+ /// a white radial-falloff canvas (alpha = attenuation, ADR-0010 ). Rust
255+ /// callers should prefer [`Self::light_canvas`] after this call.
256256 pub fn update_or_add_light ( & mut self , id : u8 , r : i16 , x : i16 , y : i16 ) -> * const Color {
257- self . update_light_with_color_mode ( id, r, x, y, None )
258- }
259-
260- /// Create or update a solid-color light.
261- pub fn update_or_add_light_with_solid_color (
262- & mut self ,
263- id : u8 ,
264- r : i16 ,
265- x : i16 ,
266- y : i16 ,
267- hue : u8 ,
268- ) -> * const Color {
269- self . update_light_with_color_mode ( id, r, x, y, Some ( ColorMode :: Solid ( hue) ) )
270- }
271-
272- /// Create or update a custom-HSV light.
273- pub fn update_or_add_light_with_custom_color (
274- & mut self ,
275- id : u8 ,
276- r : i16 ,
277- x : i16 ,
278- y : i16 ,
279- hue : u8 ,
280- saturation : u8 ,
281- ) -> * const Color {
282- self . update_light_with_color_mode (
283- id,
284- r,
285- x,
286- y,
287- Some ( ColorMode :: Custom { hue, saturation } ) ,
288- )
257+ let clamped_r = r. min ( self . max_dist as i16 ) . max ( 1 ) ;
258+
259+ // Disjoint borrows: `lights` mutably, `collision`+`all_rays` immutably.
260+ let collision = & self . collision ;
261+ let all_rays = & self . all_rays ;
262+ let max_dist = self . max_dist ;
263+ let light = self
264+ . lights
265+ . entry ( id)
266+ . or_insert_with ( || Light :: new ( ( x, y) , clamped_r) ) ;
267+ if light. radius ( ) != clamped_r {
268+ // Radius change resizes the canvas — rebuild.
269+ * light = Light :: new ( ( x, y) , clamped_r) ;
270+ } else {
271+ light. set_pos ( ( x, y) ) ;
272+ }
273+ light. update ( collision, all_rays, max_dist)
289274 }
290275
291276 /// Create or update a room-bounded ambient emitter and return a pointer to
292- /// its full-map canvas (`cells_per_row²` RGBA cells in wasm linear memory).
277+ /// its full-map mask (`cells_per_row²` RGBA cells in wasm linear memory).
293278 ///
294279 /// The emitter floods the same-type [`UnionFind`] room of the tile at
295280 /// `(tile_x, tile_y)` — every cell of every tile sharing that tile's room
296- /// is filled with `Color(r, g, b, 255)`; everything else stays transparent
297- /// `(0, 0, 0, 0)`. Because the room is the `tile_uf` partition (which is
298- /// door-agnostic, per ADR-0003), the fill never crosses a Door, open or
299- /// closed. An emitter on a non-floor tile (`tile <= 0`) or out of range
300- /// emits an empty (fully transparent) canvas.
301- pub fn update_or_add_ambient (
302- & mut self ,
303- id : u8 ,
304- tile_x : i16 ,
305- tile_y : i16 ,
306- r : u8 ,
307- g : u8 ,
308- b : u8 ,
309- ) -> * const Color {
281+ /// is opaque white; everything else stays transparent `(0, 0, 0, 0)`.
282+ /// Colour is applied renderer-side (ADR-0010). Because the room is the
283+ /// `tile_uf` partition (which is door-agnostic, per ADR-0003), the fill
284+ /// never crosses a Door, open or closed. An emitter on a non-floor tile
285+ /// (`tile <= 0`) or out of range emits an empty (fully transparent) canvas.
286+ pub fn update_or_add_ambient ( & mut self , id : u8 , tile_x : i16 , tile_y : i16 ) -> * const Color {
310287 let tiles_per_row = self . tiles_per_row ;
311288 let cells_per_tile = self . cells_per_tile ;
312289 let cells_per_row = self . cells_per_row ( ) ;
313- let color = Color ( r, g, b, 255 ) ;
314290
315291 // Resolve the emitter tile's room first (needs `&mut tile_uf` for
316292 // find()), then collect every tile in that room. Done before borrowing
@@ -338,12 +314,7 @@ impl LightingEngine {
338314 . or_insert_with ( || Ambient :: new ( cells_per_row) ) ;
339315 ambient. clear ( ) ;
340316 for & ti in & room_tiles {
341- ambient. fill_tile (
342- ti % tiles_per_row,
343- ti / tiles_per_row,
344- cells_per_tile,
345- color,
346- ) ;
317+ ambient. fill_tile ( ti % tiles_per_row, ti / tiles_per_row, cells_per_tile) ;
347318 }
348319 ambient. canvas ( ) . as_ptr ( )
349320 }
@@ -416,37 +387,6 @@ impl LightingEngine {
416387 self . lights . get ( & id) . map ( |l| l. radius ( ) )
417388 }
418389
419- fn update_light_with_color_mode (
420- & mut self ,
421- id : u8 ,
422- r : i16 ,
423- x : i16 ,
424- y : i16 ,
425- color_mode : Option < ColorMode > ,
426- ) -> * const Color {
427- let clamped_r = r. min ( self . max_dist as i16 ) . max ( 1 ) ;
428-
429- let needs_new = match self . lights . get ( & id) {
430- Some ( existing) => existing. radius ( ) != clamped_r || existing. color_mode ( ) != & color_mode,
431- None => true ,
432- } ;
433- if needs_new {
434- self . lights
435- . insert ( id, Light :: new ( ( x, y) , clamped_r, color_mode. clone ( ) ) ) ;
436- }
437-
438- // Disjoint borrows: `lights` mutably, `collision`+`all_rays` immutably.
439- let collision = & self . collision ;
440- let all_rays = & self . all_rays ;
441- let max_dist = self . max_dist ;
442- let light = self
443- . lights
444- . get_mut ( & id)
445- . expect ( "just inserted or known to exist" ) ;
446- light. set_state ( ( x, y) , clamped_r, color_mode) ;
447- light. update ( collision, all_rays, max_dist)
448- }
449-
450390 fn recompute_block_map ( & mut self ) {
451391 let tiles_total = self . tiles . len ( ) ;
452392 for tile_index in 0 ..tiles_total {
@@ -765,7 +705,8 @@ impl LightingEngine {
765705 for row in 0 ..size {
766706 for col in 0 ..size {
767707 let pixel = canvas[ row * size + col] ;
768- let brightness = pixel. 0 . max ( pixel. 1 ) . max ( pixel. 2 ) ;
708+ // Transport mask: attenuation lives in alpha (ADR-0010).
709+ let brightness = pixel. 3 ;
769710 let idx = ( brightness as usize * ( ASCII_GRADIENT . len ( ) - 1 ) ) / 255 ;
770711 out. push ( ASCII_GRADIENT [ idx] as char ) ;
771712 }
@@ -822,6 +763,20 @@ mod tests {
822763 ) ;
823764 }
824765
766+ #[ test]
767+ fn light_emits_white_mask_with_falloff_alpha ( ) {
768+ // ADR-0010: transport only — white pixels, attenuation in alpha,
769+ // full-strength at the centre, dimmer toward the rim.
770+ let mut e = LightingEngine :: new ( 6 , 30 ) ;
771+ e. update_or_add_light ( 1 , 3 , 5 , 5 ) ;
772+ let canvas = e. light_canvas ( 1 ) . expect ( "light exists" ) ;
773+ let centre = canvas[ 3 + 3 * 7 ] ; // centre of the radius-3 light's 7×7 canvas
774+ assert_eq ! ( ( centre. 0 , centre. 1 , centre. 2 , centre. 3 ) , ( 255 , 255 , 255 , 255 ) ) ;
775+ let rim = canvas[ 3 + 1 * 7 ] ; // two cells up from centre
776+ assert_eq ! ( ( rim. 0 , rim. 1 , rim. 2 ) , ( 255 , 255 , 255 ) , "mask stays white" ) ;
777+ assert ! ( rim. 3 > 0 && rim. 3 < 255 , "rim alpha attenuated, got {}" , rim. 3 ) ;
778+ }
779+
825780 #[ test]
826781 fn independent_engines_do_not_share_lights ( ) {
827782 let mut a = LightingEngine :: new ( 6 , 30 ) ;
@@ -1072,13 +1027,13 @@ mod tests {
10721027 e. set_tile_map ( tiles) ;
10731028
10741029 // Emitter in the west room (tile (1,1)).
1075- e. update_or_add_ambient ( 0 , 1 , 1 , 140 , 130 , 120 ) ;
1030+ e. update_or_add_ambient ( 0 , 1 , 1 ) ;
10761031
1077- // West-room cells are filled with the authored colour...
1032+ // West-room cells are opaque white (mask; colour is renderer-side) ...
10781033 let cpr = e. cells_per_row ( ) ;
10791034 let cpt = e. cells_per_tile ( ) ;
10801035 let west = e. ambient_canvas ( 0 ) . unwrap ( ) [ ( 1 * cpt + 1 ) * cpr + ( 1 * cpt + 1 ) ] ;
1081- assert_eq ! ( ( west. 0 , west. 1 , west. 2 , west. 3 ) , ( 140 , 130 , 120 , 255 ) ) ;
1036+ assert_eq ! ( ( west. 0 , west. 1 , west. 2 , west. 3 ) , ( 255 , 255 , 255 , 255 ) ) ;
10821037 assert ! ( ambient_cell_opaque( & e, 0 , 0 , 0 ) , "same-room tile filled" ) ;
10831038
10841039 // ...east-room cells (x>=3) stay transparent.
@@ -1104,13 +1059,13 @@ mod tests {
11041059 e. set_tile_map ( tiles) ;
11051060
11061061 // Closed door: east room dark.
1107- e. update_or_add_ambient ( 0 , 1 , 1 , 100 , 100 , 100 ) ;
1062+ e. update_or_add_ambient ( 0 , 1 , 1 ) ;
11081063 assert ! ( ambient_cell_opaque( & e, 0 , 1 , 1 ) , "west room filled" ) ;
11091064 assert ! ( !ambient_cell_opaque( & e, 0 , 3 , 1 ) , "closed door: east dark" ) ;
11101065
11111066 // Open the door between (1,1) and (2,1); re-flood. Still east dark.
11121067 e. set_door_edge ( 1 * 5 + 1 , 1 * 5 + 2 , true ) ;
1113- e. update_or_add_ambient ( 0 , 1 , 1 , 100 , 100 , 100 ) ;
1068+ e. update_or_add_ambient ( 0 , 1 , 1 ) ;
11141069 assert ! ( ambient_cell_opaque( & e, 0 , 1 , 1 ) , "west room still filled" ) ;
11151070 assert ! (
11161071 !ambient_cell_opaque( & e, 0 , 3 , 1 ) ,
@@ -1126,7 +1081,7 @@ mod tests {
11261081 tiles[ 1 * 5 + 1 ] = 0 ;
11271082 e. set_tile_map ( tiles) ;
11281083
1129- e. update_or_add_ambient ( 0 , 1 , 1 , 200 , 50 , 50 ) ;
1084+ e. update_or_add_ambient ( 0 , 1 , 1 ) ;
11301085 let canvas = e. ambient_canvas ( 0 ) . unwrap ( ) ;
11311086 assert ! (
11321087 canvas. iter( ) . all( |c| c. 3 == 0 ) ,
@@ -1138,7 +1093,7 @@ mod tests {
11381093 fn ambient_out_of_range_is_empty ( ) {
11391094 let mut e = LightingEngine :: new ( 2 , 5 ) ;
11401095 e. set_tile_map ( vec ! [ 1u8 ; 25 ] ) ;
1141- e. update_or_add_ambient ( 0 , -1 , 99 , 10 , 20 , 30 ) ;
1096+ e. update_or_add_ambient ( 0 , -1 , 99 ) ;
11421097 let canvas = e. ambient_canvas ( 0 ) . unwrap ( ) ;
11431098 assert ! ( canvas. iter( ) . all( |c| c. 3 == 0 ) , "out-of-range → empty canvas" ) ;
11441099 }
@@ -1153,8 +1108,8 @@ mod tests {
11531108 }
11541109 e. set_tile_map ( tiles) ;
11551110
1156- e. update_or_add_ambient ( 0 , 1 , 1 , 100 , 0 , 0 ) ; // west
1157- e. update_or_add_ambient ( 1 , 3 , 1 , 0 , 0 , 100 ) ; // east
1111+ e. update_or_add_ambient ( 0 , 1 , 1 ) ; // west
1112+ e. update_or_add_ambient ( 1 , 3 , 1 ) ; // east
11581113
11591114 assert ! ( ambient_cell_opaque( & e, 0 , 1 , 1 ) && !ambient_cell_opaque( & e, 0 , 3 , 1 ) ) ;
11601115 assert ! ( ambient_cell_opaque( & e, 1 , 3 , 1 ) && !ambient_cell_opaque( & e, 1 , 1 , 1 ) ) ;
@@ -1166,14 +1121,14 @@ mod tests {
11661121 // the room (and re-flooding) leaves only the emitter's half lit.
11671122 let mut e = LightingEngine :: new ( 2 , 5 ) ;
11681123 e. set_tile_map ( vec ! [ 1u8 ; 25 ] ) ;
1169- e. update_or_add_ambient ( 0 , 0 , 1 , 80 , 80 , 80 ) ;
1124+ e. update_or_add_ambient ( 0 , 0 , 1 ) ;
11701125 assert ! ( ambient_cell_opaque( & e, 0 , 4 , 1 ) , "single room: far tile lit" ) ;
11711126
11721127 // Split with a wall column at x=2.
11731128 for y in 0 ..5 {
11741129 e. set_tile ( 2 , y as u32 , 0 ) ;
11751130 }
1176- e. update_or_add_ambient ( 0 , 0 , 1 , 80 , 80 , 80 ) ;
1131+ e. update_or_add_ambient ( 0 , 0 , 1 ) ;
11771132 assert ! ( ambient_cell_opaque( & e, 0 , 0 , 1 ) , "emitter half stays lit" ) ;
11781133 assert ! ( !ambient_cell_opaque( & e, 0 , 4 , 1 ) , "far half now dark after split" ) ;
11791134 }
0 commit comments