TerrainGenerator.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. public class TerrainGenerator
  4. {
  5. private NoiseGenerator noiseGenerator;
  6. private List<Vector2Int> originalRiverPositions;
  7. public TerrainGenerator()
  8. {
  9. noiseGenerator = new NoiseGenerator();
  10. originalRiverPositions = new List<Vector2Int>();
  11. }
  12. public void GenerateTerrain(MapData mapData)
  13. {
  14. // Clear previous river positions
  15. originalRiverPositions.Clear();
  16. // Generate base heightmap
  17. GenerateHeightmap(mapData);
  18. // Generate water bodies first (they take priority)
  19. GenerateOceans(mapData);
  20. GenerateLakes(mapData);
  21. GenerateRivers(mapData);
  22. // Store river positions after generation but before forests
  23. StoreRiverPositions(mapData);
  24. // Generate mountains (avoid water areas)
  25. GenerateMountains(mapData);
  26. // Generate forests (can overlap with rivers initially)
  27. GenerateForests(mapData);
  28. // Create river paths through forests (this cuts paths after forests are placed)
  29. CreateRiverPathsThroughForests(mapData);
  30. // Generate shores (around water)
  31. GenerateShores(mapData);
  32. }
  33. private void GenerateHeightmap(MapData mapData)
  34. {
  35. for (int x = 0; x < mapData.Width; x++)
  36. {
  37. for (int y = 0; y < mapData.Height; y++)
  38. {
  39. float height = noiseGenerator.GetNoise(x, y, 0.1f);
  40. mapData.GetTile(x, y).height = height;
  41. }
  42. }
  43. }
  44. private void GenerateOceans(MapData mapData)
  45. {
  46. // Generate one large ocean that extends to map edge
  47. int edgeChoice = Random.Range(0, 4); // 0=left, 1=right, 2=top, 3=bottom
  48. int oceanX, oceanY, oceanWidth, oceanHeight;
  49. switch (edgeChoice)
  50. {
  51. case 0: // Left edge ocean - cover entire left edge
  52. oceanX = 0;
  53. oceanY = 0;
  54. oceanWidth = Random.Range(40, 70);
  55. oceanHeight = mapData.Height;
  56. break;
  57. case 1: // Right edge ocean - cover entire right edge
  58. oceanWidth = Random.Range(40, 70);
  59. oceanX = mapData.Width - oceanWidth;
  60. oceanY = 0;
  61. oceanHeight = mapData.Height;
  62. break;
  63. case 2: // Top edge ocean - cover entire top edge
  64. oceanX = 0;
  65. oceanHeight = Random.Range(40, 70);
  66. oceanY = mapData.Height - oceanHeight;
  67. oceanWidth = mapData.Width;
  68. break;
  69. default: // Bottom edge ocean - cover entire bottom edge
  70. oceanX = 0;
  71. oceanY = 0;
  72. oceanWidth = mapData.Width;
  73. oceanHeight = Random.Range(40, 70);
  74. break;
  75. }
  76. // Create the main ocean shape
  77. List<Vector2Int> oceanTiles = new List<Vector2Int>();
  78. for (int x = oceanX; x < oceanX + oceanWidth && x < mapData.Width; x++)
  79. {
  80. for (int y = oceanY; y < oceanY + oceanHeight && y < mapData.Height; y++)
  81. {
  82. // Calculate distance from the map edge that should be ocean
  83. float distanceFromOceanEdge = 0f;
  84. if (edgeChoice == 0) distanceFromOceanEdge = x; // Left edge
  85. else if (edgeChoice == 1) distanceFromOceanEdge = mapData.Width - 1 - x; // Right edge
  86. else if (edgeChoice == 2) distanceFromOceanEdge = mapData.Height - 1 - y; // Top edge
  87. else distanceFromOceanEdge = y; // Bottom edge
  88. // Create probability that decreases from edge with some noise
  89. float maxDepth = Mathf.Min(oceanWidth, oceanHeight) * 0.8f;
  90. float edgeProbability = 1f - (distanceFromOceanEdge / maxDepth);
  91. // Add noise for coastline variation, but keep it more cohesive
  92. float noise = noiseGenerator.GetNoise(x, y, 0.1f) * 0.2f;
  93. float finalProbability = Mathf.Clamp01(edgeProbability + noise);
  94. // Ensure edge tiles are always ocean for proper ocean coverage
  95. if (distanceFromOceanEdge < 3 || finalProbability > 0.4f)
  96. {
  97. oceanTiles.Add(new Vector2Int(x, y));
  98. }
  99. }
  100. } // Apply ocean tiles
  101. foreach (var pos in oceanTiles)
  102. {
  103. mapData.GetTile(pos.x, pos.y).terrainType = TerrainType.Ocean;
  104. mapData.GetTile(pos.x, pos.y).isWalkable = false;
  105. }
  106. // Fill gaps more aggressively to reduce scattered cells
  107. FillTerrainGaps(mapData, TerrainType.Ocean, 3);
  108. }
  109. private void GenerateLakes(MapData mapData)
  110. {
  111. int lakeCount = Random.Range(3, 6);
  112. for (int i = 0; i < lakeCount; i++)
  113. {
  114. int attempts = 0;
  115. while (attempts < 20)
  116. {
  117. attempts++;
  118. int lakeX = Random.Range(10, mapData.Width - 10);
  119. int lakeY = Random.Range(10, mapData.Height - 10);
  120. int lakeSize = Random.Range(4, 10);
  121. // Check if area is suitable for a lake (no water nearby)
  122. if (IsAreaSuitable(mapData, lakeX, lakeY, lakeSize * 2, TerrainType.Plains))
  123. {
  124. CreateLake(mapData, lakeX, lakeY, lakeSize);
  125. break;
  126. }
  127. }
  128. }
  129. }
  130. private void CreateLake(MapData mapData, int centerX, int centerY, int size)
  131. {
  132. List<Vector2Int> lakeTiles = new List<Vector2Int>();
  133. for (int x = centerX - size; x <= centerX + size; x++)
  134. {
  135. for (int y = centerY - size; y <= centerY + size; y++)
  136. {
  137. if (mapData.IsValidPosition(x, y))
  138. {
  139. float distance = Vector2.Distance(new Vector2(x, y), new Vector2(centerX, centerY));
  140. if (distance <= size * Random.Range(0.8f, 1.2f))
  141. {
  142. lakeTiles.Add(new Vector2Int(x, y));
  143. }
  144. }
  145. }
  146. }
  147. // Apply lake tiles
  148. foreach (var pos in lakeTiles)
  149. {
  150. mapData.GetTile(pos.x, pos.y).terrainType = TerrainType.Lake;
  151. mapData.GetTile(pos.x, pos.y).isWalkable = false;
  152. }
  153. // Fill gaps
  154. FillTerrainGaps(mapData, TerrainType.Lake, 1);
  155. }
  156. private void GenerateRivers(MapData mapData)
  157. {
  158. var riverGenerator = new RiverGenerator();
  159. riverGenerator.GenerateRivers(mapData);
  160. }
  161. private void GenerateForests(MapData mapData)
  162. {
  163. // Generate one huge forest first (if possible)
  164. GenerateHugeForest(mapData);
  165. // Generate medium forests
  166. int mediumForestCount = Random.Range(3, 6);
  167. for (int i = 0; i < mediumForestCount; i++)
  168. {
  169. int attempts = 0;
  170. while (attempts < 20)
  171. {
  172. attempts++;
  173. int forestX = Random.Range(10, mapData.Width - 10);
  174. int forestY = Random.Range(10, mapData.Height - 10);
  175. int forestSize = Random.Range(8, 18);
  176. if (IsAreaSuitableForForest(mapData, forestX, forestY, forestSize))
  177. {
  178. CreateForest(mapData, forestX, forestY, forestSize);
  179. break;
  180. }
  181. }
  182. }
  183. // Generate small forests and forest patches
  184. int smallForestCount = Random.Range(5, 10);
  185. for (int i = 0; i < smallForestCount; i++)
  186. {
  187. int attempts = 0;
  188. while (attempts < 15)
  189. {
  190. attempts++;
  191. int forestX = Random.Range(5, mapData.Width - 5);
  192. int forestY = Random.Range(5, mapData.Height - 5);
  193. int forestSize = Random.Range(3, 8);
  194. if (IsAreaSuitableForForest(mapData, forestX, forestY, forestSize))
  195. {
  196. CreateForest(mapData, forestX, forestY, forestSize);
  197. break;
  198. }
  199. }
  200. }
  201. }
  202. private void GenerateHugeForest(MapData mapData)
  203. {
  204. int attempts = 0;
  205. while (attempts < 10)
  206. {
  207. attempts++;
  208. // Try to place a huge forest away from water and edges
  209. int forestX = Random.Range(25, mapData.Width - 45);
  210. int forestY = Random.Range(25, mapData.Height - 45);
  211. int forestSize = Random.Range(25, 40);
  212. if (IsAreaSuitableForForest(mapData, forestX, forestY, forestSize))
  213. {
  214. CreateHugeForest(mapData, forestX, forestY, forestSize);
  215. break;
  216. }
  217. }
  218. }
  219. private void CreateHugeForest(MapData mapData, int centerX, int centerY, int size)
  220. {
  221. List<Vector2Int> forestTiles = new List<Vector2Int>();
  222. // Create multiple overlapping forest areas for irregular huge forest
  223. int subForests = Random.Range(3, 6);
  224. for (int sf = 0; sf < subForests; sf++)
  225. {
  226. // Each sub-forest has its own center near the main center
  227. int subCenterX = centerX + Random.Range(-size / 3, size / 3);
  228. int subCenterY = centerY + Random.Range(-size / 3, size / 3);
  229. int subSize = Random.Range(size / 2, size);
  230. // Generate organic forest shape for this sub-forest
  231. for (int x = subCenterX - subSize; x <= subCenterX + subSize; x++)
  232. {
  233. for (int y = subCenterY - subSize; y <= subCenterY + subSize; y++)
  234. {
  235. if (mapData.IsValidPosition(x, y))
  236. {
  237. float distance = Vector2.Distance(new Vector2(x, y), new Vector2(subCenterX, subCenterY));
  238. float maxDistance = subSize * Random.Range(0.7f, 1.3f);
  239. // Add some noise for irregular edges
  240. float noise = noiseGenerator.GetNoise(x, y, 0.2f);
  241. maxDistance += noise * (subSize * 0.2f);
  242. if (distance <= maxDistance)
  243. {
  244. MapTile tile = mapData.GetTile(x, y);
  245. // Allow forests on Plains and Rivers - we'll cut river paths later
  246. if (tile.terrainType == TerrainType.Plains || tile.terrainType == TerrainType.River)
  247. {
  248. forestTiles.Add(new Vector2Int(x, y));
  249. }
  250. }
  251. }
  252. }
  253. }
  254. }
  255. // Apply forest tiles (only on Plains now, rivers are left as paths)
  256. foreach (var pos in forestTiles)
  257. {
  258. MapTile tile = mapData.GetTile(pos.x, pos.y);
  259. tile.terrainType = TerrainType.Forest;
  260. }
  261. // Fill gaps more aggressively for huge forests
  262. FillTerrainGaps(mapData, TerrainType.Forest, 2);
  263. }
  264. private bool IsAreaSuitableForForest(MapData mapData, int centerX, int centerY, int size)
  265. {
  266. int plainsCount = 0;
  267. int totalCount = 0;
  268. for (int x = centerX - size / 2; x <= centerX + size / 2; x++)
  269. {
  270. for (int y = centerY - size / 2; y <= centerY + size / 2; y++)
  271. {
  272. if (mapData.IsValidPosition(x, y))
  273. {
  274. totalCount++;
  275. var tile = mapData.GetTile(x, y);
  276. if (tile.terrainType == TerrainType.Plains)
  277. {
  278. plainsCount++;
  279. }
  280. }
  281. }
  282. }
  283. return totalCount > 0 && (float)plainsCount / totalCount > 0.8f;
  284. }
  285. private void CreateForest(MapData mapData, int centerX, int centerY, int size)
  286. {
  287. List<Vector2Int> forestTiles = new List<Vector2Int>();
  288. // Determine forest type - some are dense, some are sparse
  289. bool isDenseForest = Random.value < 0.4f; // 40% chance for dense forest
  290. bool hasClearing = Random.value < 0.3f && size > 6; // 30% chance for clearing in larger forests
  291. // Create organic forest shape with variation
  292. for (int x = centerX - size; x <= centerX + size; x++)
  293. {
  294. for (int y = centerY - size; y <= centerY + size; y++)
  295. {
  296. if (mapData.IsValidPosition(x, y))
  297. {
  298. float distance = Vector2.Distance(new Vector2(x, y), new Vector2(centerX, centerY));
  299. float maxDistance = size * Random.Range(0.6f, 1.2f);
  300. // Add noise for irregular edges
  301. float noise = noiseGenerator.GetNoise(x, y, 0.15f);
  302. float noiseInfluence = isDenseForest ? 0.1f : 0.3f; // Dense forests are more regular
  303. maxDistance += noise * (size * noiseInfluence);
  304. // Create clearings in some forests
  305. bool inClearing = false;
  306. if (hasClearing)
  307. {
  308. float clearingDistance = Vector2.Distance(new Vector2(x, y), new Vector2(centerX, centerY));
  309. inClearing = clearingDistance < size * 0.3f && Random.value < 0.7f;
  310. }
  311. if (distance <= maxDistance && !inClearing)
  312. {
  313. MapTile tile = mapData.GetTile(x, y);
  314. // Allow forests on Plains and Rivers - we'll cut river paths later
  315. if (tile.terrainType == TerrainType.Plains || tile.terrainType == TerrainType.River)
  316. {
  317. // Dense forests fill more completely, sparse forests are more scattered
  318. float fillProbability = isDenseForest ? 0.85f : 0.65f;
  319. if (Random.value < fillProbability)
  320. {
  321. forestTiles.Add(new Vector2Int(x, y));
  322. }
  323. }
  324. }
  325. }
  326. }
  327. }
  328. // Apply forest tiles (only on Plains now, rivers are left as paths)
  329. foreach (var pos in forestTiles)
  330. {
  331. MapTile tile = mapData.GetTile(pos.x, pos.y);
  332. tile.terrainType = TerrainType.Forest;
  333. }
  334. // Dense forests get better gap filling
  335. int gapFillLevel = isDenseForest ? 2 : 1;
  336. FillTerrainGaps(mapData, TerrainType.Forest, gapFillLevel);
  337. }
  338. private void GenerateMountains(MapData mapData)
  339. {
  340. // Generate more varied mountain ranges with different sizes
  341. int mountainCount = Random.Range(2, 5); // Slightly more mountain ranges
  342. for (int i = 0; i < mountainCount; i++)
  343. {
  344. int attempts = 0;
  345. while (attempts < 20)
  346. {
  347. attempts++;
  348. int mountainX = Random.Range(15, mapData.Width - 15);
  349. int mountainY = Random.Range(15, mapData.Height - 15);
  350. // More varied mountain sizes - some small, some large
  351. int mountainSize;
  352. float sizeRoll = Random.value;
  353. if (sizeRoll < 0.3f)
  354. mountainSize = Random.Range(5, 10); // Small mountains
  355. else if (sizeRoll < 0.7f)
  356. mountainSize = Random.Range(10, 18); // Medium mountains
  357. else
  358. mountainSize = Random.Range(18, 28); // Large mountain ranges
  359. if (IsAreaSuitableForMountains(mapData, mountainX, mountainY, mountainSize))
  360. {
  361. CreateMountainRange(mapData, mountainX, mountainY, mountainSize);
  362. break;
  363. }
  364. }
  365. }
  366. }
  367. private bool IsAreaSuitableForMountains(MapData mapData, int centerX, int centerY, int size)
  368. {
  369. // Check if area is mostly plains (not water or already mountains)
  370. int suitableCount = 0;
  371. int totalCount = 0;
  372. for (int x = centerX - size; x <= centerX + size; x++)
  373. {
  374. for (int y = centerY - size; y <= centerY + size; y++)
  375. {
  376. if (mapData.IsValidPosition(x, y))
  377. {
  378. totalCount++;
  379. var tile = mapData.GetTile(x, y);
  380. if (tile.terrainType == TerrainType.Plains || tile.terrainType == TerrainType.Forest)
  381. {
  382. suitableCount++;
  383. }
  384. }
  385. }
  386. }
  387. return totalCount > 0 && (float)suitableCount / totalCount > 0.7f;
  388. }
  389. private void CreateMountainRange(MapData mapData, int centerX, int centerY, int size)
  390. {
  391. List<Vector2Int> mountainTiles = new List<Vector2Int>();
  392. // Create mountain range with multiple peaks - more varied
  393. int peaks = Random.Range(2, 6); // More variation in peak count
  394. for (int p = 0; p < peaks; p++)
  395. {
  396. // More random peak placement instead of evenly spaced
  397. float angle = Random.Range(0f, 360f) * Mathf.Deg2Rad;
  398. float peakDistance = Random.Range(0.2f, 0.5f) * size; // Varied distance from center
  399. int peakX = centerX + Mathf.RoundToInt(Mathf.Cos(angle) * peakDistance);
  400. int peakY = centerY + Mathf.RoundToInt(Mathf.Sin(angle) * peakDistance);
  401. // Each peak has its own size variation
  402. int peakSize = Random.Range(size / 3, size);
  403. for (int x = peakX - peakSize / 2; x <= peakX + peakSize / 2; x++)
  404. {
  405. for (int y = peakY - peakSize / 2; y <= peakY + peakSize / 2; y++)
  406. {
  407. if (mapData.IsValidPosition(x, y))
  408. {
  409. float distance = Vector2.Distance(new Vector2(x, y), new Vector2(peakX, peakY));
  410. // More random distance threshold for irregular mountain shapes
  411. float maxDistance = peakSize * Random.Range(0.3f, 0.9f);
  412. // Add noise for more natural mountain edges
  413. float noise = noiseGenerator.GetNoise(x, y, 0.15f);
  414. maxDistance += noise * (peakSize * 0.2f);
  415. if (distance <= maxDistance)
  416. {
  417. MapTile tile = mapData.GetTile(x, y);
  418. if (!tile.IsWater()) // Don't override water
  419. {
  420. mountainTiles.Add(new Vector2Int(x, y));
  421. }
  422. }
  423. }
  424. }
  425. }
  426. }
  427. // Apply mountain tiles
  428. foreach (var pos in mountainTiles)
  429. {
  430. mapData.GetTile(pos.x, pos.y).terrainType = TerrainType.Mountain;
  431. mapData.GetTile(pos.x, pos.y).isWalkable = false;
  432. }
  433. // Fill gaps
  434. FillTerrainGaps(mapData, TerrainType.Mountain, 1);
  435. }
  436. private void FillTerrainGaps(MapData mapData, TerrainType terrainType, int maxGapSize)
  437. {
  438. for (int x = 0; x < mapData.Width; x++)
  439. {
  440. for (int y = 0; y < mapData.Height; y++)
  441. {
  442. MapTile tile = mapData.GetTile(x, y);
  443. if (tile.terrainType != terrainType)
  444. {
  445. // Check if this tile is surrounded by the target terrain type
  446. int neighborCount = 0;
  447. int targetNeighbors = 0;
  448. for (int dx = -maxGapSize; dx <= maxGapSize; dx++)
  449. {
  450. for (int dy = -maxGapSize; dy <= maxGapSize; dy++)
  451. {
  452. if (dx == 0 && dy == 0) continue;
  453. int checkX = x + dx;
  454. int checkY = y + dy;
  455. if (mapData.IsValidPosition(checkX, checkY))
  456. {
  457. neighborCount++;
  458. if (mapData.GetTile(checkX, checkY).terrainType == terrainType)
  459. {
  460. targetNeighbors++;
  461. }
  462. }
  463. }
  464. }
  465. // If most neighbors are the target terrain, fill this gap
  466. if (neighborCount > 0 && (float)targetNeighbors / neighborCount > 0.6f)
  467. {
  468. if (terrainType == TerrainType.Ocean || terrainType == TerrainType.Lake || terrainType == TerrainType.River)
  469. {
  470. tile.isWalkable = false;
  471. }
  472. if (terrainType == TerrainType.Mountain)
  473. {
  474. tile.isWalkable = false;
  475. }
  476. tile.terrainType = terrainType;
  477. }
  478. }
  479. }
  480. }
  481. }
  482. private void GenerateShores(MapData mapData)
  483. {
  484. for (int x = 0; x < mapData.Width; x++)
  485. {
  486. for (int y = 0; y < mapData.Height; y++)
  487. {
  488. MapTile tile = mapData.GetTile(x, y);
  489. if (tile.terrainType == TerrainType.Plains)
  490. {
  491. if (IsNearWater(mapData, x, y, 2)) // Reduced range for shores
  492. {
  493. tile.terrainType = TerrainType.Shore;
  494. }
  495. }
  496. }
  497. }
  498. }
  499. private bool IsNearWater(MapData mapData, int x, int y, int range)
  500. {
  501. for (int dx = -range; dx <= range; dx++)
  502. {
  503. for (int dy = -range; dy <= range; dy++)
  504. {
  505. int checkX = x + dx;
  506. int checkY = y + dy;
  507. if (mapData.IsValidPosition(checkX, checkY))
  508. {
  509. MapTile checkTile = mapData.GetTile(checkX, checkY);
  510. if (checkTile.IsWater())
  511. {
  512. return true;
  513. }
  514. }
  515. }
  516. }
  517. return false;
  518. }
  519. private bool IsAreaSuitable(MapData mapData, int centerX, int centerY, int size, TerrainType requiredType)
  520. {
  521. int suitableCount = 0;
  522. int totalCount = 0;
  523. for (int x = centerX - size; x <= centerX + size; x++)
  524. {
  525. for (int y = centerY - size; y <= centerY + size; y++)
  526. {
  527. if (mapData.IsValidPosition(x, y))
  528. {
  529. totalCount++;
  530. if (mapData.GetTile(x, y).terrainType == requiredType)
  531. {
  532. suitableCount++;
  533. }
  534. }
  535. }
  536. }
  537. return totalCount > 0 && (float)suitableCount / totalCount > 0.8f;
  538. }
  539. private void StoreRiverPositions(MapData mapData)
  540. {
  541. originalRiverPositions.Clear();
  542. for (int x = 0; x < mapData.Width; x++)
  543. {
  544. for (int y = 0; y < mapData.Height; y++)
  545. {
  546. var tile = mapData.GetTile(x, y);
  547. if (tile.terrainType == TerrainType.River)
  548. {
  549. originalRiverPositions.Add(new Vector2Int(x, y));
  550. }
  551. }
  552. }
  553. }
  554. private void CreateRiverPathsThroughForests(MapData mapData)
  555. {
  556. // Restore rivers that got covered by forests with more natural-looking paths
  557. foreach (var riverPos in originalRiverPositions)
  558. {
  559. var tile = mapData.GetTile(riverPos.x, riverPos.y);
  560. // If this river position got turned into forest, restore it as river
  561. if (tile.terrainType == TerrainType.Forest)
  562. {
  563. tile.terrainType = TerrainType.River;
  564. // Create narrower, more natural river paths
  565. // Only expand along the main river flow directions (not diagonally)
  566. for (int dx = -1; dx <= 1; dx++)
  567. {
  568. for (int dy = -1; dy <= 1; dy++)
  569. {
  570. // Skip center (already handled) and diagonal directions (keep forest)
  571. if ((dx == 0 && dy == 0) || (Mathf.Abs(dx) == 1 && Mathf.Abs(dy) == 1))
  572. continue;
  573. int x = riverPos.x + dx;
  574. int y = riverPos.y + dy;
  575. if (mapData.IsValidPosition(x, y))
  576. {
  577. var neighborTile = mapData.GetTile(x, y);
  578. // Only expand river into forests, and with lower probability for narrower channels
  579. if (neighborTile.terrainType == TerrainType.Forest && Random.value < 0.3f)
  580. {
  581. neighborTile.terrainType = TerrainType.River;
  582. }
  583. }
  584. }
  585. }
  586. }
  587. }
  588. }
  589. }