@@ -362,7 +362,36 @@ <h3 id="insptitle">—</h3>
362362 if ( maxh < 400 ) for ( let i = 0 ; i < W . h . length ; i ++ ) W . h [ i ] *= 400 / maxh ;
363363
364364 await progress ( 'Carving the rivers…' , 0.2 ) ;
365- /* --- hydrology: flow accumulation, carve rivers ----------------------- */
365+ /* --- hydrology: fill depressions, then flow accumulation, carve rivers --
366+ Priority-flood fill guarantees every cell drains monotonically to the
367+ map rim or the sea, so rivers always run from the highlands down. */
368+ {
369+ const visited = new Uint8Array ( GRID * GRID ) ;
370+ const heap = [ ] ;
371+ const hpush = ( h , i ) => { heap . push ( [ h , i ] ) ; let k = heap . length - 1 ;
372+ while ( k > 0 ) { const p = ( k - 1 ) >> 1 ; if ( heap [ p ] [ 0 ] <= heap [ k ] [ 0 ] ) break ; const t = heap [ p ] ; heap [ p ] = heap [ k ] ; heap [ k ] = t ; k = p ; } } ;
373+ const hpop = ( ) => { const top = heap [ 0 ] , last = heap . pop ( ) ;
374+ if ( heap . length ) { heap [ 0 ] = last ; let k = 0 ;
375+ for ( ; ; ) { const l = k * 2 + 1 , r = l + 1 ; let m = k ;
376+ if ( l < heap . length && heap [ l ] [ 0 ] < heap [ m ] [ 0 ] ) m = l ;
377+ if ( r < heap . length && heap [ r ] [ 0 ] < heap [ m ] [ 0 ] ) m = r ;
378+ if ( m === k ) break ; const t = heap [ m ] ; heap [ m ] = heap [ k ] ; heap [ k ] = t ; k = m ; } }
379+ return top ; } ;
380+ for ( let cx = 0 ; cx < GRID ; cx ++ ) { for ( const cz of [ 0 , GRID - 1 ] ) { const i = cIdx ( cx , cz ) ; if ( ! visited [ i ] ) { visited [ i ] = 1 ; hpush ( W . h [ i ] , i ) ; } } }
381+ for ( let cz = 0 ; cz < GRID ; cz ++ ) { for ( const cx of [ 0 , GRID - 1 ] ) { const i = cIdx ( cx , cz ) ; if ( ! visited [ i ] ) { visited [ i ] = 1 ; hpush ( W . h [ i ] , i ) ; } } }
382+ while ( heap . length ) {
383+ const [ hh , c ] = hpop ( ) ;
384+ const cx = c % GRID , cz = ( c / GRID ) | 0 ;
385+ for ( let dz = - 1 ; dz <= 1 ; dz ++ ) for ( let dx = - 1 ; dx <= 1 ; dx ++ ) {
386+ if ( ! dx && ! dz ) continue ; const nx = cx + dx , nz = cz + dz ;
387+ if ( ! inB ( nx , nz ) ) continue ; const ni = cIdx ( nx , nz ) ;
388+ if ( visited [ ni ] ) continue ;
389+ visited [ ni ] = 1 ;
390+ if ( W . h [ ni ] < hh + 0.05 ) W . h [ ni ] = hh + 0.05 ; // fill pits just enough to drain
391+ hpush ( W . h [ ni ] , ni ) ;
392+ }
393+ }
394+ }
366395 const order = [ ] ; for ( let i = 0 ; i < GRID * GRID ; i ++ ) order . push ( i ) ;
367396 order . sort ( ( a , b ) => W . h [ b ] - W . h [ a ] ) ;
368397 const down = new Int32Array ( GRID * GRID ) . fill ( - 1 ) ;
@@ -397,6 +426,7 @@ <h3 id="insptitle">—</h3>
397426 if ( path . length > 4 ) W . riverPaths . push ( path ) ;
398427 }
399428 W . coastal = coastal ;
429+ W . down = down ; // drainage graph — reused for river barge routes
400430
401431 await progress ( 'Seeding meadow and forest…' , 0.35 ) ;
402432 /* --- biomes ------------------------------------------------------------ */
@@ -494,25 +524,43 @@ <h3 id="insptitle">—</h3>
494524 return c ;
495525 } ;
496526 function astar ( s , t ) {
497- const open = new Map ( ) , g = new Map ( ) , from = new Map ( ) ;
498- const hx = ( i ) => { const ax = i % GRID , az = ( i / GRID ) | 0 , bx = t % GRID , bz = ( t / GRID ) | 0 ; return Math . hypot ( ax - bx , az - bz ) ; } ;
499- open . set ( s , hx ( s ) ) ; g . set ( s , 0 ) ;
527+ // binary-heap A* — cheap enough to search the whole grid, so mountain
528+ // crossings get real switchback roads instead of falling back
529+ const g = new Float64Array ( GRID * GRID ) . fill ( Infinity ) ;
530+ const from = new Int32Array ( GRID * GRID ) . fill ( - 1 ) ;
531+ const closed = new Uint8Array ( GRID * GRID ) ;
532+ const heap = [ ] ;
533+ const hpush = ( f , i ) => { heap . push ( [ f , i ] ) ; let k = heap . length - 1 ;
534+ while ( k > 0 ) { const p = ( k - 1 ) >> 1 ; if ( heap [ p ] [ 0 ] <= heap [ k ] [ 0 ] ) break ; const t2 = heap [ p ] ; heap [ p ] = heap [ k ] ; heap [ k ] = t2 ; k = p ; } } ;
535+ const hpop = ( ) => { const top = heap [ 0 ] , last = heap . pop ( ) ;
536+ if ( heap . length ) { heap [ 0 ] = last ; let k = 0 ;
537+ for ( ; ; ) { const l = k * 2 + 1 , r = l + 1 ; let m = k ;
538+ if ( l < heap . length && heap [ l ] [ 0 ] < heap [ m ] [ 0 ] ) m = l ;
539+ if ( r < heap . length && heap [ r ] [ 0 ] < heap [ m ] [ 0 ] ) m = r ;
540+ if ( m === k ) break ; const t2 = heap [ m ] ; heap [ m ] = heap [ k ] ; heap [ k ] = t2 ; k = m ; } }
541+ return top ; } ;
542+ const tx = t % GRID , tz = ( t / GRID ) | 0 ;
543+ const hx = ( i ) => Math . hypot ( i % GRID - tx , ( ( i / GRID ) | 0 ) - tz ) ;
544+ g [ s ] = 0 ; hpush ( hx ( s ) , s ) ;
500545 let guard = 0 ;
501- while ( open . size && guard ++ < 26000 ) {
502- let cur = - 1 , cf = 1e18 ; for ( const [ k , f ] of open ) if ( f < cf ) { cf = f ; cur = k ; }
546+ while ( heap . length && guard ++ < 200000 ) {
547+ const cur = hpop ( ) [ 1 ] ;
548+ if ( closed [ cur ] ) continue ;
549+ closed [ cur ] = 1 ;
503550 if ( cur === t ) break ;
504- open . delete ( cur ) ;
505- const cx = cur % GRID , cz = ( cur / GRID ) | 0 , gc = g . get ( cur ) ;
551+ const cx = cur % GRID , cz = ( cur / GRID ) | 0 , gc = g [ cur ] ;
506552 for ( let dz = - 1 ; dz <= 1 ; dz ++ ) for ( let dx = - 1 ; dx <= 1 ; dx ++ ) {
507553 if ( ! dx && ! dz ) continue ; const nx = cx + dx , nz = cz + dz ; if ( ! inB ( nx , nz ) ) continue ;
508- const ni = cIdx ( nx , nz ) , step = cost ( cur , ni ) * ( dx && dz ?1.414 :1 ) ;
554+ const ni = cIdx ( nx , nz ) ;
555+ if ( closed [ ni ] ) continue ;
556+ const step = cost ( cur , ni ) * ( dx && dz ?1.414 :1 ) ;
509557 if ( step > 1e8 ) continue ;
510558 const ng = gc + step ;
511- if ( ng < ( g . get ( ni ) ?? 1e18 ) ) { g . set ( ni , ng ) ; from . set ( ni , cur ) ; open . set ( ni , ng + hx ( ni ) ) ; }
559+ if ( ng < g [ ni ] ) { g [ ni ] = ng ; from [ ni ] = cur ; hpush ( ng + hx ( ni ) , ni ) ; }
512560 }
513561 }
514- if ( ! from . has ( t ) && s !== t ) return null ;
515- const path = [ ] ; let c = t ; while ( c !== undefined && c !== s ) { path . push ( c ) ; c = from . get ( c ) ; } path . push ( s ) ; path . reverse ( ) ;
562+ if ( from [ t ] < 0 && s !== t ) return null ;
563+ const path = [ ] ; let c = t ; while ( c >= 0 && c !== s ) { path . push ( c ) ; c = from [ c ] ; } path . push ( s ) ; path . reverse ( ) ;
516564 return path ;
517565 }
518566 const connected = [ 0 ] ;
@@ -530,7 +578,17 @@ <h3 id="insptitle">—</h3>
530578 else W . roadCells [ c ] = 1 ; // tint the ground under the king's roads
531579 }
532580 W . roads . push ( { a :i , b :bj , path, len} ) ;
533- } else W . roads . push ( { a :i , b :bj , path :[ { x :a . pos . x , z :a . pos . z } , { x :b . pos . x , z :b . pos . z } ] , len :bd } ) ;
581+ } else {
582+ // no path found: lay a terrain-following track so the route is at least visible
583+ const path = [ ] ; const n = Math . max ( 2 , Math . round ( bd / 25 ) ) ;
584+ for ( let k = 0 ; k <= n ; k ++ ) {
585+ const x = lerp ( a . pos . x , b . pos . x , k / n ) , z = lerp ( a . pos . z , b . pos . z , k / n ) ;
586+ path . push ( { x, z} ) ;
587+ const ci2 = cIdx ( toCell ( x ) , toCell ( z ) ) ;
588+ if ( ! W . water [ ci2 ] ) W . roadCells [ ci2 ] = 1 ;
589+ }
590+ W . roads . push ( { a :i , b :bj , path, len :bd } ) ;
591+ }
534592 connected . push ( i ) ;
535593 }
536594 // settlement-graph routing (BFS), route(a,b) => polyline
@@ -557,6 +615,27 @@ <h3 id="insptitle">—</h3>
557615 const res = { poly, len} ; W . routes [ key ] = res ;
558616 return res ;
559617}
618+ /* river barge routing: two settlements are river-linked if their drainage
619+ traces meet — same river, or tributary joining the main stem */
620+ function traceDown ( ci ) {
621+ const seq = [ ] ; let c = ci , g = 0 ;
622+ while ( c >= 0 && g ++ < 3000 ) { seq . push ( c ) ; if ( W . h [ c ] <= SEA ) break ; c = W . down [ c ] ; }
623+ return seq ;
624+ }
625+ function riverRoute ( ai , bi ) {
626+ const A = W . settlements [ ai ] , Bs = W . settlements [ bi ] ;
627+ if ( A . rivCell == null || Bs . rivCell == null ) return null ;
628+ const sa = traceDown ( A . rivCell ) , sb = traceDown ( Bs . rivCell ) ;
629+ const setB = new Map ( sb . map ( ( c , j ) => [ c , j ] ) ) ;
630+ let mi = - 1 , mj = - 1 ;
631+ for ( let i = 0 ; i < sa . length ; i ++ ) { if ( setB . has ( sa [ i ] ) ) { mi = i ; mj = setB . get ( sa [ i ] ) ; break ; } }
632+ if ( mi < 0 ) return null ;
633+ const cells = sa . slice ( 0 , mi + 1 ) . concat ( sb . slice ( 0 , mj ) . reverse ( ) ) ;
634+ if ( cells . length < 4 ) return null ;
635+ const poly = cells . map ( c => ( { x :cellX ( c % GRID ) , z :cellZ ( ( c / GRID ) | 0 ) } ) ) ;
636+ let len = 0 ; for ( let k = 1 ; k < poly . length ; k ++ ) len += dist2d ( poly [ k - 1 ] . x , poly [ k - 1 ] . z , poly [ k ] . x , poly [ k ] . z ) ;
637+ return { poly, len} ;
638+ }
560639
561640/* ---------------- settlement layout: streets, lots, buildings ------------ */
562641function layoutSettlement ( s ) {
@@ -849,6 +928,43 @@ <h3 id="insptitle">—</h3>
849928}
850929
851930/* ---------------- terrain, water, roads ---------------------------------- */
931+ function chaikin ( pts , passes ) { // corner-cutting smoothing
932+ for ( let k = 0 ; k < passes ; k ++ ) {
933+ const out = [ pts [ 0 ] ] ;
934+ for ( let i = 0 ; i < pts . length - 1 ; i ++ ) {
935+ const a = pts [ i ] , b = pts [ i + 1 ] ;
936+ out . push ( { x :a . x * 0.75 + b . x * 0.25 , z :a . z * 0.75 + b . z * 0.25 , w :( a . w || 8 ) * 0.75 + ( b . w || 8 ) * 0.25 } ) ;
937+ out . push ( { x :a . x * 0.25 + b . x * 0.75 , z :a . z * 0.25 + b . z * 0.75 , w :( a . w || 8 ) * 0.25 + ( b . w || 8 ) * 0.75 } ) ;
938+ }
939+ out . push ( pts [ pts . length - 1 ] ) ;
940+ pts = out ;
941+ }
942+ return pts ;
943+ }
944+ function ribbon ( pts , acc , hw , lift ) { // append triangles for a terrain-following strip
945+ for ( let k = 0 ; k < pts . length - 1 ; k ++ ) {
946+ const a = pts [ k ] , b = pts [ k + 1 ] ;
947+ const dx = b . x - a . x , dz = b . z - a . z , L = Math . hypot ( dx , dz ) || 1 ;
948+ const wA = hw ?? ( Math . max ( 10 , a . w || 10 ) / 2 ) , wB = hw ?? ( Math . max ( 10 , b . w || 10 ) / 2 ) ;
949+ const px = - dz / L , pz = dx / L ;
950+ const ya = hAt ( a . x , a . z ) + lift , yb = hAt ( b . x , b . z ) + lift ;
951+ acc . p . push ( a . x + px * wA , ya , a . z + pz * wA , a . x - px * wA , ya , a . z - pz * wA , b . x - px * wB , yb , b . z - pz * wB ,
952+ a . x + px * wA , ya , a . z + pz * wA , b . x - px * wB , yb , b . z - pz * wB , b . x + px * wB , yb , b . z + pz * wB ) ;
953+ for ( let i = 0 ; i < 6 ; i ++ ) acc . n . push ( 0 , 1 , 0 ) ;
954+ }
955+ }
956+ function rebuildRoadMesh ( ) { // king's roads (wide) + settlement streets (narrow), one merged mesh
957+ if ( G . roads ) { scene . remove ( G . roads ) ; G . roads . geometry . dispose ( ) ; }
958+ const rd = { p :[ ] , n :[ ] } ;
959+ for ( const r of W . roads ) ribbon ( chaikin ( r . path , 1 ) , rd , 5.2 , 0.9 ) ;
960+ for ( const s of W . settlements ) if ( s . streets ) for ( const st of s . streets ) ribbon ( st , rd , 2.6 , 0.8 ) ;
961+ const rrg = new THREE . BufferGeometry ( ) ;
962+ rrg . setAttribute ( 'position' , new THREE . Float32BufferAttribute ( rd . p , 3 ) ) ;
963+ rrg . setAttribute ( 'normal' , new THREE . Float32BufferAttribute ( rd . n , 3 ) ) ;
964+ G . roads = new THREE . Mesh ( rrg , G . roadMat || ( G . roadMat = new THREE . MeshLambertMaterial ( { color :0x8f7c55 ,
965+ polygonOffset :true , polygonOffsetFactor :- 2 , polygonOffsetUnits :- 2 } ) ) ) ;
966+ scene . add ( G . roads ) ;
967+ }
852968function colorForVertex ( i , sp , snowT , seed ) {
853969 const bio = W . biome [ i ] , h = W . h [ i ] ;
854970 const nn = Math . sin ( i * 127.1 + ( seed % 97 ) ) * 43758.5453 ; const n = nn - Math . floor ( nn ) ; // stable patternless jitter
@@ -902,32 +1018,6 @@ <h3 id="insptitle">—</h3>
9021018 G . sea = new THREE . Mesh ( sg , new THREE . MeshPhongMaterial ( { color :0x2e6285 , transparent :true , opacity :0.92 , shininess :90 , specular :0x88aabb } ) ) ;
9031019 G . sea . position . y = SEA + 0.5 ; scene . add ( G . sea ) ;
9041020 }
905- // river ribbons — Chaikin-smoothed so the water reads as one stroke
906- function chaikin ( pts , passes ) {
907- for ( let k = 0 ; k < passes ; k ++ ) {
908- const out = [ pts [ 0 ] ] ;
909- for ( let i = 0 ; i < pts . length - 1 ; i ++ ) {
910- const a = pts [ i ] , b = pts [ i + 1 ] ;
911- out . push ( { x :a . x * 0.75 + b . x * 0.25 , z :a . z * 0.75 + b . z * 0.25 , w :( a . w || 8 ) * 0.75 + ( b . w || 8 ) * 0.25 } ) ;
912- out . push ( { x :a . x * 0.25 + b . x * 0.75 , z :a . z * 0.25 + b . z * 0.75 , w :( a . w || 8 ) * 0.25 + ( b . w || 8 ) * 0.75 } ) ;
913- }
914- out . push ( pts [ pts . length - 1 ] ) ;
915- pts = out ;
916- }
917- return pts ;
918- }
919- const ribbon = ( pts , acc , hw , lift ) => { // append triangles for a smoothed strip
920- for ( let k = 0 ; k < pts . length - 1 ; k ++ ) {
921- const a = pts [ k ] , b = pts [ k + 1 ] ;
922- const dx = b . x - a . x , dz = b . z - a . z , L = Math . hypot ( dx , dz ) || 1 ;
923- const wA = hw ?? ( Math . max ( 10 , a . w || 10 ) / 2 ) , wB = hw ?? ( Math . max ( 10 , b . w || 10 ) / 2 ) ;
924- const px = - dz / L , pz = dx / L ;
925- const ya = hAt ( a . x , a . z ) + lift , yb = hAt ( b . x , b . z ) + lift ;
926- acc . p . push ( a . x + px * wA , ya , a . z + pz * wA , a . x - px * wA , ya , a . z - pz * wA , b . x - px * wB , yb , b . z - pz * wB ,
927- a . x + px * wA , ya , a . z + pz * wA , b . x - px * wB , yb , b . z - pz * wB , b . x + px * wB , yb , b . z + pz * wB ) ;
928- for ( let i = 0 ; i < 6 ; i ++ ) acc . n . push ( 0 , 1 , 0 ) ;
929- }
930- } ;
9311021 const riv = { p :[ ] , n :[ ] } ;
9321022 for ( const path of W . riverPaths ) ribbon ( chaikin ( path , 2 ) , riv , null , 1.3 ) ;
9331023 const rg = new THREE . BufferGeometry ( ) ;
@@ -936,16 +1026,7 @@ <h3 id="insptitle">—</h3>
9361026 G . rivers = new THREE . Mesh ( rg , new THREE . MeshPhongMaterial ( { color :0x2e6ea0 , transparent :true , opacity :0.95 , shininess :110 , specular :0x99bbcc ,
9371027 polygonOffset :true , polygonOffsetFactor :- 2 , polygonOffsetUnits :- 2 } ) ) ;
9381028 scene . add ( G . rivers ) ;
939- // roads: king's roads (wide) + settlement streets (narrow), one merged mesh
940- const rd = { p :[ ] , n :[ ] } ;
941- for ( const r of W . roads ) ribbon ( chaikin ( r . path , 1 ) , rd , 5.2 , 0.9 ) ;
942- for ( const s of W . settlements ) if ( s . streets ) for ( const st of s . streets ) ribbon ( st , rd , 2.6 , 0.8 ) ;
943- const rrg = new THREE . BufferGeometry ( ) ;
944- rrg . setAttribute ( 'position' , new THREE . Float32BufferAttribute ( rd . p , 3 ) ) ;
945- rrg . setAttribute ( 'normal' , new THREE . Float32BufferAttribute ( rd . n , 3 ) ) ;
946- G . roads = new THREE . Mesh ( rrg , new THREE . MeshLambertMaterial ( { color :0x8f7c55 ,
947- polygonOffset :true , polygonOffsetFactor :- 2 , polygonOffsetUnits :- 2 } ) ) ;
948- scene . add ( G . roads ) ;
1029+ rebuildRoadMesh ( ) ;
9491030 // bridges
9501031 const bg = new THREE . BoxGeometry ( 1 , 1 , 1 ) ;
9511032 G . bridges = new THREE . InstancedMesh ( bg , new THREE . MeshLambertMaterial ( { color :0x7d6748 } ) , Math . max ( 1 , W . bridges . length ) ) ;
@@ -1169,9 +1250,9 @@ <h3 id="insptitle">—</h3>
11691250 sp . visible = false ; scene . add ( sp ) ; G . caravanDots . push ( sp ) ;
11701251 }
11711252 }
1172- // cogs
1253+ // cogs (sea traders) & river barges share the boat pool
11731254 G . cogs = [ ] ;
1174- for ( let i = 0 ; i < 8 ; i ++ ) {
1255+ for ( let i = 0 ; i < 14 ; i ++ ) {
11751256 const grp = new THREE . Group ( ) ;
11761257 const hull = new THREE . Mesh ( new THREE . BoxGeometry ( 9 , 2.4 , 3.6 ) , new THREE . MeshLambertMaterial ( { color :0x6e5233 } ) ) ; hull . position . y = 1.4 ;
11771258 const sail = new THREE . Mesh ( new THREE . PlaneGeometry ( 5 , 6 ) , new THREE . MeshLambertMaterial ( { color :0xe4dcc4 , side :THREE . DoubleSide } ) ) ; sail . position . y = 6 ;
@@ -1509,7 +1590,7 @@ <h3 id="insptitle">—</h3>
15091590 flyToBtn ( fb , 'Follow' , ( ) => { cam . follow = a ; cam . dist = 260 ; } ) ;
15101591 } else if ( pk . type === 'caravan' ) {
15111592 const c = pk . c ;
1512- title . textContent = ' Caravan';
1593+ title . textContent = c . sea ? 'Trading cog' : ( c . river ? 'River barge' : ' Caravan') ;
15131594 sub . textContent = W . settlements [ c . origin ] . name + ' → ' + W . settlements [ c . dest ] . name ;
15141595 h += row ( 'Cargo' , c . good + ' × ' + Math . round ( c . qty ) ) ;
15151596 h += row ( 'Value' , Math . round ( c . value ) + ' gold' ) ;
@@ -1568,6 +1649,23 @@ <h3 id="insptitle">—</h3>
15681649 if ( b === B . FARM ) farm ++ ; if ( b === B . FOREST || b === B . PINE ) forest ++ ; if ( b === B . ROCK ) ore ++ ;
15691650 }
15701651 s . res = { farm, forest, ore :Math . min ( ore , 14 ) , fish :( s . harbor ?12 :0 ) + ( s . river ?5 :0 ) } ;
1652+ // nearest river cell, for barge landings
1653+ s . rivCell = null ;
1654+ if ( s . river ) {
1655+ let bd = 1e9 ;
1656+ for ( let dz = - 6 ; dz <= 6 ; dz ++ ) for ( let dx = - 6 ; dx <= 6 ; dx ++ ) {
1657+ const ci = s . cx + dx , cj = s . cz + dz ;
1658+ if ( ! inB ( ci , cj ) || W . water [ cIdx ( ci , cj ) ] !== 2 ) continue ;
1659+ const d = dx * dx + dz * dz ; if ( d < bd ) { bd = d ; s . rivCell = cIdx ( ci , cj ) ; }
1660+ }
1661+ }
1662+ }
1663+ // the river is a road that flows: barge routes between river-linked towns
1664+ W . riverPairs = [ ] ;
1665+ for ( let i = 0 ; i < W . settlements . length ; i ++ ) for ( let j = i + 1 ; j < W . settlements . length ; j ++ ) {
1666+ if ( ! W . settlements [ i ] . river || ! W . settlements [ j ] . river ) continue ;
1667+ const r = riverRoute ( i , j ) ;
1668+ if ( r && r . len > 400 ) W . riverPairs . push ( { a :i , b :j , route :r } ) ;
15711669 }
15721670}
15731671
@@ -1669,6 +1767,22 @@ <h3 id="insptitle">—</h3>
16691767 }
16701768 }
16711769 }
1770+ // river barges between river-linked settlements (faster and safer than the road)
1771+ if ( W . riverPairs && W . riverPairs . length && W . caravans . filter ( c => c . river ) . length < 5 && chance ( 'sim' , 0.4 * MOD . trade ) ) {
1772+ const pr = pick ( 'sim' , W . riverPairs ) ;
1773+ const downstream = chance ( 'sim' , 0.5 ) ;
1774+ const oi = downstream ?pr . a :pr . b , di = downstream ?pr . b :pr . a ;
1775+ const o = W . settlements [ oi ] , d2 = W . settlements [ di ] ;
1776+ if ( o . infected / Math . max ( 1 , o . pop ) <= 0.15 && d2 . infected / Math . max ( 1 , d2 . pop ) <= 0.15 && ! o . siegeBy && ! d2 . siegeBy ) {
1777+ const g = GOODS . reduce ( ( a , b ) => o . stores [ a ] > o . stores [ b ] ?a :b ) ;
1778+ if ( o . stores [ g ] > 25 ) {
1779+ const qty = Math . min ( o . stores [ g ] * 0.3 , 70 ) ; o . stores [ g ] -= qty ;
1780+ const poly = downstream ?pr . route . poly :[ ...pr . route . poly ] . reverse ( ) ;
1781+ W . caravans . push ( { origin :oi , dest :di , good :g , qty, value :qty * price ( d2 , g ) , poly, len :pr . route . len ,
1782+ departDay :day ( ) , arriveDay :day ( ) + pr . route . len / 1500 , robbed :false , sea :false , river :true , proxy :null } ) ;
1783+ }
1784+ }
1785+ }
16721786 // cogs between harbors
16731787 const harbors = W . settlements . map ( ( s , i ) => s . harbor ?i :- 1 ) . filter ( i => i >= 0 ) ;
16741788 if ( harbors . length >= 2 && W . caravans . filter ( c => c . sea ) . length < 6 && chance ( 'sim' , 0.3 * MOD . trade ) ) {
@@ -2351,7 +2465,7 @@ <h3 id="insptitle">—</h3>
23512465 }
23522466 // ambush caravans passing near (position computed in sim time, not render time)
23532467 for ( const c of W . caravans ) {
2354- if ( c . sea || c . robbed ) continue ;
2468+ if ( c . sea || c . river || c . robbed ) continue ; // barges are beyond the outlaws' reach
23552469 const f = ( day ( ) - c . departDay ) / Math . max ( 0.01 , c . arriveDay - c . departDay ) ;
23562470 const p = polyPos ( c . poly , f ) ;
23572471 if ( ! p ) continue ;
@@ -2574,9 +2688,17 @@ <h3 id="insptitle">—</h3>
25742688 W . settlements . push ( s ) ;
25752689 const si = W . settlements . length - 1 ; W . adj [ si ] = [ ] ;
25762690 let bj = 0 , bd = 1e18 ; W . settlements . forEach ( ( o , oi ) => { if ( oi !== si ) { const d = dist2d ( p . x , p . z , o . pos . x , o . pos . z ) ; if ( d < bd ) { bd = d ; bj = oi ; } } } ) ;
2577- const path = [ { x :p . x , z :p . z } , { x :W . settlements [ bj ] . pos . x , z :W . settlements [ bj ] . pos . z } ] ;
2691+ const tgt = W . settlements [ bj ] . pos ;
2692+ const path = [ ] ; const nseg = Math . max ( 2 , Math . round ( bd / 25 ) ) ;
2693+ for ( let k = 0 ; k <= nseg ; k ++ ) {
2694+ const x = lerp ( p . x , tgt . x , k / nseg ) , z = lerp ( p . z , tgt . z , k / nseg ) ;
2695+ path . push ( { x, z} ) ;
2696+ const rc = cIdx ( toCell ( x ) , toCell ( z ) ) ;
2697+ if ( ! W . water [ rc ] ) W . roadCells [ rc ] = 1 ;
2698+ }
25782699 W . roads . push ( { a :si , b :bj , path, len :bd } ) ; W . routes = { } ;
25792700 W . adj [ si ] . push ( { to :bj , ri :W . roads . length - 1 } ) ; W . adj [ bj ] . push ( { to :si , ri :W . roads . length - 1 } ) ;
2701+ rebuildRoadMesh ( ) ; lastSeasonKey = '' ; // show the new track and its ground tint
25802702 for ( let i = 0 ; i < 10 ; i ++ ) addBuildingLive ( s ) ;
25812703 const sp = makeTextSprite ( s . name , 1.6 ) ; sp . position . set ( p . x , s . pos . y + 90 , p . z ) ; scene . add ( sp ) ; G . labels . push ( sp ) ;
25822704 const m = new THREE . Mesh ( new THREE . SphereGeometry ( 60 , 8 , 6 ) , new THREE . MeshBasicMaterial ( { visible :false } ) ) ;
@@ -3126,15 +3248,15 @@ <h3 id="insptitle">—</h3>
31263248 const f = ( simDay - c . departDay ) / Math . max ( 0.01 , c . arriveDay - c . departDay ) ;
31273249 const p = polyPos ( c . poly , f ) ;
31283250 if ( ! p ) continue ;
3129- const y = c . sea ?SEA + 1 :hAt ( p . x , p . z ) + 1.0 ; // ride on the road surface, not inside it
3251+ const y = c . sea ?SEA + 1 :( c . river ? hAt ( p . x , p . z ) + 1.2 : hAt ( p . x , p . z ) + 1.0 ) ; // road surface or water line
31303252 c . _rpos = { x :p . x , y :y + 8 , z :p . z } ;
31313253 if ( ! c . proxy ) {
31323254 c . proxy = new THREE . Mesh ( new THREE . SphereGeometry ( 16 , 6 , 5 ) , new THREE . MeshBasicMaterial ( { visible :false } ) ) ;
31333255 c . proxy . userData . pick = { type :'caravan' , c} ; scene . add ( c . proxy ) ;
31343256 }
31353257 c . proxy . position . set ( p . x , y + 6 , p . z ) ;
3136- if ( c . sea ) {
3137- if ( cogI < G . cogs . length ) { const g = G . cogs [ cogI ++ ] ; g . visible = true ; g . position . set ( p . x , y , p . z ) ; g . rotation . y = - ( p . dir || 0 ) ; g . scale . setScalar ( Math . min ( unitScale , 3 ) ) ; }
3258+ if ( c . sea || c . river ) {
3259+ if ( cogI < G . cogs . length ) { const g = G . cogs [ cogI ++ ] ; g . visible = true ; g . position . set ( p . x , y , p . z ) ; g . rotation . y = - ( p . dir || 0 ) ; g . scale . setScalar ( Math . min ( unitScale , c . river ? 2 : 3 ) ) ; }
31383260 } else if ( ci < 48 ) {
31393261 setInst ( G . caravans , ci ++ , p . x , y , p . z , - ( p . dir || 0 ) , unitScale , unitScale , unitScale ) ;
31403262 }
0 commit comments