@@ -870,7 +870,11 @@ <h3 id="insptitle">—</h3>
870870 case B . ROCK :c = C1 . setHex ( sp . rock ) ; break ;
871871 default :c = C1 . setHex ( 0xe8ecef ) ;
872872 }
873- c . multiplyScalar ( 0.88 + n * 0.24 ) ;
873+ c . multiplyScalar ( 0.92 + n * 0.16 ) ;
874+ if ( bio === B . ROCK || bio === B . SNOW ) { // coherent snowcaps: whiten smoothly with height
875+ const t = clamp ( ( h - 470 ) / 130 , 0 , 1 ) ;
876+ c . lerp ( new THREE . Color ( 0xe9edf2 ) , t * t ) ;
877+ }
874878 if ( W . roadCells [ i ] && bio !== B . WATER ) c . lerp ( new THREE . Color ( 0x8a7448 ) , 0.8 ) ; // roads read at map altitude
875879 if ( W . drought && ( bio === B . MEADOW || bio === B . FARM ) ) c . lerp ( new THREE . Color ( 0xb3a35c ) , 0.55 ) ;
876880 // winter snow accumulation on flat/high ground
@@ -929,7 +933,7 @@ <h3 id="insptitle">—</h3>
929933 const rg = new THREE . BufferGeometry ( ) ;
930934 rg . setAttribute ( 'position' , new THREE . Float32BufferAttribute ( riv . p , 3 ) ) ;
931935 rg . setAttribute ( 'normal' , new THREE . Float32BufferAttribute ( riv . n , 3 ) ) ;
932- G . rivers = new THREE . Mesh ( rg , new THREE . MeshPhongMaterial ( { color :0x3a7096 , transparent :true , opacity :0.88 , shininess :110 , specular :0x99bbcc ,
936+ G . rivers = new THREE . Mesh ( rg , new THREE . MeshPhongMaterial ( { color :0x2e6ea0 , transparent :true , opacity :0.95 , shininess :110 , specular :0x99bbcc ,
933937 polygonOffset :true , polygonOffsetFactor :- 2 , polygonOffsetUnits :- 2 } ) ) ;
934938 scene . add ( G . rivers ) ;
935939 // roads: king's roads (wide) + settlement streets (narrow), one merged mesh
@@ -958,17 +962,30 @@ <h3 id="insptitle">—</h3>
958962 const sp = { } ; for ( const k of [ 'mead' , 'farm' , 'forest' , 'pine' , 'rock' , 'marsh' ] ) {
959963 const c0 = new THREE . Color ( sp0 [ k ] ) , c1 = new THREE . Color ( sp1 [ k ] ) ; sp [ k ] = c0 . lerp ( c1 , blend * blend ) . getHex ( ) ;
960964 }
961- const col = G . terrain . geometry . attributes . color ;
962- for ( let i = 0 ; i < col . count ; i ++ ) {
965+ const col = G . terrain . geometry . attributes . color , N = col . count ;
966+ if ( ! G . _cbuf ) G . _cbuf = new Float32Array ( N * 3 ) ;
967+ const buf = G . _cbuf ;
968+ for ( let i = 0 ; i < N ; i ++ ) {
963969 const c = colorForVertex ( i , sp , snowT , SEED ) ;
964- col . setXYZ ( i , c . r , c . g , c . b ) ;
970+ buf [ i * 3 ] = c . r ; buf [ i * 3 + 1 ] = c . g ; buf [ i * 3 + 2 ] = c . b ;
971+ }
972+ // neighbor blend calms biome banding into painterly gradients
973+ for ( let cz = 0 ; cz < GRID ; cz ++ ) for ( let cx = 0 ; cx < GRID ; cx ++ ) {
974+ const i = cIdx ( cx , cz ) ;
975+ let r = buf [ i * 3 ] * 0.44 , g = buf [ i * 3 + 1 ] * 0.44 , b = buf [ i * 3 + 2 ] * 0.44 , ws = 0.44 ;
976+ for ( const [ dx , dz ] of [ [ 1 , 0 ] , [ - 1 , 0 ] , [ 0 , 1 ] , [ 0 , - 1 ] ] ) {
977+ if ( ! inB ( cx + dx , cz + dz ) ) continue ;
978+ const j = cIdx ( cx + dx , cz + dz ) ;
979+ r += buf [ j * 3 ] * 0.14 ; g += buf [ j * 3 + 1 ] * 0.14 ; b += buf [ j * 3 + 2 ] * 0.14 ; ws += 0.14 ;
980+ }
981+ col . setXYZ ( i , r / ws , g / ws , b / ws ) ;
965982 }
966983 col . needsUpdate = true ;
967984 // trees seasonal tint
968985 if ( G . decid ) {
969- const dc = new THREE . Color ( [ 0x4c7a3c , 0x557a36 , 0xa05e24 , 0x6e6250 ] [ si ] ) ;
986+ const dc = new THREE . Color ( [ 0x40682f , 0x4a6c2e , 0x96541e , 0x5e5344 ] [ si ] ) ;
970987 G . decid . material . color . copy ( dc ) ;
971- G . pine . material . color . setHex ( si === 3 ?0x39543f : 0x2e5138 ) ;
988+ G . pine . material . color . setHex ( si === 3 ?0x314a37 : 0x28472f ) ;
972989 }
973990}
974991
@@ -985,11 +1002,11 @@ <h3 id="insptitle">—</h3>
9851002 const i = cIdx ( cx , cz ) , bio = W . biome [ i ] ;
9861003 if ( bio !== B . FOREST && bio !== B . PINE ) continue ;
9871004 if ( W . settlements . some ( s => dist2d ( cellX ( cx ) , cellZ ( cz ) , s . pos . x , s . pos . z ) < s . radius * 0.8 ) ) continue ;
988- const dens = bio === B . FOREST ?2 :2 ;
1005+ const dens = bio === B . FOREST ?3 :2 ;
9891006 for ( let t = 0 ; t < dens ; t ++ ) {
9901007 const x = cellX ( cx ) + ( nj ( cx * 3 + t * 7 , cz * 3 ) - 0.5 ) * CELL * 1.4 , z = cellZ ( cz ) + ( nj ( cx * 3 , cz * 3 + t * 13 ) - 0.5 ) * CELL * 1.4 ;
9911008 const arr = bio === B . FOREST ?spots . dec :spots . pine ;
992- if ( arr . length < 5200 ) arr . push ( { x, z, s :0.8 + nj ( cx + t , cz - t ) * 0.9 } ) ;
1009+ if ( arr . length < 7000 ) arr . push ( { x, z, s :1.0 + nj ( cx + t , cz - t ) * 1.0 } ) ;
9931010 }
9941011 }
9951012 G . decid = new THREE . InstancedMesh ( decidG , new THREE . MeshLambertMaterial ( { color :0x4c7a3c } ) , Math . max ( 1 , spots . dec . length ) ) ;
@@ -2641,8 +2658,14 @@ <h3 id="insptitle">—</h3>
26412658 if ( this . queue . length > 12 ) this . queue . sort ( ( a , b ) => b . ev . pri - a . ev . pri ) . length = 12 ;
26422659 } ,
26432660 update ( now , dt ) {
2644- if ( cam . mode !== 'director' && ! document . body . classList . contains ( 'watch' ) ) return ;
2645- if ( now < cam . manualUntil ) return ;
2661+ if ( cam . mode !== 'director' && ! document . body . classList . contains ( 'watch' ) ) {
2662+ if ( this . shot ) { this . shot = null ; hideBeat ( ) ; } // don't leave a stale card when the user takes the wheel
2663+ return ;
2664+ }
2665+ if ( now < cam . manualUntil ) {
2666+ if ( this . shot ) { this . shot = null ; hideBeat ( ) ; }
2667+ return ;
2668+ }
26462669 // expire stale
26472670 this . queue = this . queue . filter ( q => now - q . born < 50 && ( ! this . shot || q . ev !== this . shot . ev ) ) ;
26482671 // pick a beat
0 commit comments