├── LICENSE └── mapgen.as /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /mapgen.as: -------------------------------------------------------------------------------- 1 | // Generate a fantasy-world map 2 | // Author: amitp@cs.stanford.edu 3 | // License: MIT 4 | 5 | package { 6 | import flash.geom.*; 7 | import flash.display.*; 8 | import flash.filters.*; 9 | import flash.text.*; 10 | import flash.events.*; 11 | import flash.utils.*; 12 | import flash.net.*; 13 | 14 | public class mapgen extends Sprite { 15 | public static var SEED:int = 72689; 16 | // 83980, 59695, 94400, 92697, 30628, 9146, 23896, 60489, 57078, 89680, 10377, 42612, 29732 17 | // NOTE: some sort of bug is triggered for seed 77904, leading to craters on the map 18 | public static var OCEAN_ALTITUDE:int = 1; 19 | public static var SIZE:int = 512; 20 | public static var DETAILSIZE:int = 64; 21 | public static var DETAILMAG:int = 8; 22 | 23 | // Smooth color mode uses a continuous function for non-sand, 24 | // non-water terrain; the regular mode uses discrete terrain 25 | // types. Smooth coloring doesn't work well with the smoothing 26 | // from vertex displacement. 27 | public static var useSmoothColors:Boolean = false; 28 | 29 | public var seed_text:TextField = new TextField(); 30 | public var seed_button:TextField = new TextField(); 31 | public var save_altitude_button:TextField = new TextField(); 32 | public var save_moisture_button:TextField = new TextField(); 33 | public var location_text:TextField = new TextField(); 34 | public var moisture_iterations:TextField = new TextField(); 35 | public var generate_button:TextField = new TextField(); 36 | 37 | public var corner_adjust_text:TextField = new TextField(); 38 | public var random_adjust_text:TextField = new TextField(); 39 | 40 | public var map:Map = new Map(SIZE, SEED); 41 | public var colorMap:BitmapData; 42 | public var lightingMap:BitmapData; 43 | public var moistureBitmap:BitmapData; 44 | public var altitudeBitmap:BitmapData; 45 | 46 | public var detailMap:Shape = new Shape(); 47 | 48 | [Embed("oryx/lofi_environment_a.png")] 49 | public const lofi_environment_a:Class; 50 | public var sprites:Bitmap = new lofi_environment_a(); 51 | 52 | public function mapgen() { 53 | colorMap = new BitmapData(256, 256); 54 | lightingMap = new BitmapData(256, 256); 55 | moistureBitmap = new BitmapData(SIZE, SIZE); 56 | altitudeBitmap = new BitmapData(SIZE, SIZE); 57 | 58 | stage.scaleMode = "noScale"; 59 | stage.align = "TL"; 60 | stage.frameRate = 60; 61 | 62 | addChild(new Debug(this)); 63 | 64 | graphics.beginFill(0xffffff); 65 | graphics.drawRect(-1000, -1000, 2000, 2000); 66 | graphics.endFill(); 67 | 68 | function createLabel(text:String, x:int, y:int):TextField { 69 | var t:TextField = new TextField(); 70 | t.text = text; 71 | t.width = 0; 72 | t.x = x; 73 | t.y = y; 74 | t.autoSize = TextFieldAutoSize.RIGHT; 75 | t.selectable = false; 76 | return t; 77 | } 78 | 79 | function changeIntoEditable(field:TextField, text:String):void { 80 | field.text = text; 81 | field.background = true; 82 | field.backgroundColor = 0xccccdd; 83 | field.autoSize = TextFieldAutoSize.LEFT; 84 | field.type = TextFieldType.INPUT; 85 | } 86 | 87 | function changeIntoButton(button:TextField, text:String):void { 88 | button.text = text; 89 | button.background = true; 90 | button.backgroundColor = 0xbbddbb; 91 | button.selectable = false; 92 | button.autoSize = TextFieldAutoSize.LEFT; 93 | button.filters = [new BevelFilter(1)]; 94 | } 95 | 96 | addChild(createLabel("Generating maps of size " 97 | + SIZE + "x" + SIZE, 255, 2)); 98 | addChild(createLabel("Amit J Patel -- " 99 | + "http://simblob.blogspot.com/", 260+512, 515)); 100 | 101 | changeIntoEditable(seed_text, "" + SEED); 102 | seed_text.restrict = "0-9"; 103 | seed_text.x = 50; 104 | seed_text.y = 40; 105 | addChild(seed_text); 106 | addChild(createLabel("Seed:", 50, 40)); 107 | 108 | changeIntoEditable(moisture_iterations, "4"); 109 | moisture_iterations.restrict = "0-9"; 110 | moisture_iterations.x = 150; 111 | moisture_iterations.y = 40; 112 | addChild(moisture_iterations); 113 | addChild(createLabel("Wind iter:", 150, 40)); 114 | 115 | changeIntoButton(generate_button, " Update Map "); 116 | generate_button.x = 180; 117 | generate_button.y = 40; 118 | generate_button.addEventListener(MouseEvent.MOUSE_UP, 119 | function (e:Event):void { 120 | SEED = int(seed_text.text); 121 | newMap(); 122 | }); 123 | addChild(generate_button); 124 | 125 | changeIntoButton(seed_button, " Randomize "); 126 | seed_button.x = 20; 127 | seed_button.y = 70; 128 | seed_button.addEventListener(MouseEvent.MOUSE_UP, 129 | function (e:Event):void { 130 | SEED = int(100000*Math.random()); 131 | seed_text.text = "" + SEED; 132 | moisture_iterations.text = "" + (1 + int(9*Math.random())); 133 | newMap(); 134 | }); 135 | addChild(seed_button); 136 | 137 | changeIntoButton(save_moisture_button, " Export "); 138 | save_moisture_button.x = 60; 139 | save_moisture_button.y = 380; 140 | save_moisture_button.addEventListener(MouseEvent.MOUSE_UP, 141 | function (e:Event):void { 142 | saveMoistureMap(); 143 | }); 144 | addChild(save_moisture_button); 145 | addChild(createLabel("Moisture:", 60, 380)); 146 | 147 | var b:Bitmap = new Bitmap(moistureBitmap); 148 | b.x = 0; 149 | b.y = 400; 150 | b.scaleX = 128.0/SIZE; 151 | b.scaleY = b.scaleX; 152 | addChild(b); 153 | 154 | changeIntoButton(save_altitude_button, " Export "); 155 | save_altitude_button.x = 190; 156 | save_altitude_button.y = 380; 157 | save_altitude_button.addEventListener(MouseEvent.MOUSE_UP, 158 | function (e:Event):void { 159 | saveAltitudeMap(); 160 | }); 161 | addChild(save_altitude_button); 162 | addChild(createLabel("Altitude:", 190, 380)); 163 | 164 | b = new Bitmap(altitudeBitmap); 165 | b.x = 130; 166 | b.y = 400; 167 | b.scaleX = 128.0/SIZE; 168 | b.scaleY = b.scaleX; 169 | addChild(b); 170 | 171 | // NOTE: Bitmap and Shape objects do not support mouse events, 172 | // so I'm wrapping the bitmap inside a sprite. 173 | var s:Sprite = new Sprite(); 174 | s.x = 2; 175 | s.y = 120; 176 | 177 | s.addEventListener(MouseEvent.MOUSE_DOWN, 178 | function (e:MouseEvent):void { 179 | s.addEventListener(MouseEvent.MOUSE_MOVE, onMapClick); 180 | onMapClick(e); 181 | }); 182 | stage.addEventListener(MouseEvent.MOUSE_UP, 183 | function (e:MouseEvent):void { 184 | s.removeEventListener(MouseEvent.MOUSE_MOVE, onMapClick); 185 | }); 186 | 187 | s.addChild(new Bitmap(colorMap)); 188 | s.addChild(new Bitmap(lightingMap)).blendMode = BlendMode.HARDLIGHT; 189 | addChild(s); 190 | 191 | location_text.x = 20; 192 | location_text.y = 100; 193 | location_text.autoSize = TextFieldAutoSize.LEFT; 194 | addChild(location_text); 195 | 196 | changeIntoEditable(corner_adjust_text, "0.25"); 197 | corner_adjust_text.restrict = "0-9."; 198 | corner_adjust_text.x = 220; 199 | corner_adjust_text.y = 60; 200 | addChild(corner_adjust_text); 201 | addChild(createLabel("Corner:", 200, 60)); 202 | 203 | changeIntoEditable(random_adjust_text, "0.00"); 204 | random_adjust_text.restrict = "0-9."; 205 | random_adjust_text.x = 220; 206 | random_adjust_text.y = 80; 207 | addChild(random_adjust_text); 208 | addChild(createLabel("Random:", 200, 80)); 209 | 210 | detailMap.x = 260; 211 | detailMap.y = 0; 212 | addChild(detailMap); 213 | 214 | newMap(); 215 | } 216 | 217 | public function saveAltitudeMap():void { 218 | // Save the altitude minimap (not the big map, where we don't have altitude) 219 | new FileReference().save(flattenArray(map.altitude, SIZE)); 220 | } 221 | 222 | public function saveMoistureMap():void { 223 | // Save the moisture minimap (not the big map) 224 | new FileReference().save(flattenArray(map.moisture, SIZE)); 225 | } 226 | 227 | public function flattenArray(A:Vector.>, size:int):ByteArray { 228 | var B:ByteArray = new ByteArray(); 229 | for (var x:int = 0; x < size; x++) { 230 | for (var y:int = 0; y < size; y++) { 231 | B.writeByte(A[x][y]); 232 | } 233 | } 234 | return B; 235 | } 236 | 237 | public function onMapClick(event:MouseEvent):void { 238 | // Rescale the mouse click from the minimap size to the internal map size 239 | var x:Number = event.localX * map.SIZE / colorMap.width; 240 | var y:Number = event.localY * map.SIZE / colorMap.height; 241 | location_text.text = "Map @ " + x + ", " + y; 242 | generateDetailMap(detailMap.graphics, x, y); 243 | } 244 | 245 | // We want to incrementally generate the map using onEnterFrame, 246 | // so the remaining commands needed to generate the map are stored here. 247 | // _commands is a an array of ["explanatory text", function]. 248 | private var _commands:Array = []; 249 | public function newMap():void { 250 | // Invariant: if _commands is empty, there is no event listener 251 | if (_commands.length == 0) { 252 | addEventListener(Event.ENTER_FRAME, onEnterFrame); 253 | } 254 | 255 | _commands = []; 256 | _commands.push(["Generating coarse map", 257 | function():void { 258 | map = new Map(128, SEED); 259 | map.generate(); 260 | channelsToLighting(); 261 | }]); 262 | _commands.push(["Generating detail map", 263 | function():void { 264 | map = new Map(SIZE, SEED); 265 | map.generate(); 266 | channelsToLighting(); 267 | arrayToBitmap(map.altitude, altitudeBitmap); 268 | }]); 269 | for (var i:int = 0; i < int(moisture_iterations.text); i++) { 270 | _commands.push(["Wind iteration " + (1+i), 271 | function():void { 272 | map.spreadMoisture(); 273 | map.blurMoisture(); 274 | }]); 275 | } 276 | } 277 | 278 | public function onEnterFrame(event:Event):void { 279 | if (_commands.length > 0) { 280 | var command:Array = _commands.shift(); 281 | location_text.text = command[0]; 282 | command[1](); 283 | 284 | channelsToColors(); 285 | arrayToBitmap(map.moisture, moistureBitmap); 286 | } 287 | 288 | // Invariant: if _commands is empty, there is no event listener 289 | if (_commands.length == 0) { 290 | location_text.text = "(click on minimap to see detail)"; 291 | removeEventListener(Event.ENTER_FRAME, onEnterFrame); 292 | } 293 | } 294 | 295 | public function arrayToBitmap(v:Vector.>, b:BitmapData):void { 296 | b.lock(); 297 | for (var x:int = 0; x < v.length; x++) { 298 | for (var y:int = 0; y < v[x].length; y++) { 299 | var c:int = v[x][y]; 300 | b.setPixel(x, y, (c << 16) | (c << 8) | c); 301 | } 302 | } 303 | b.unlock(); 304 | } 305 | 306 | 307 | public function moistureAndAltitudeToColor(m:Number, a:Number, r:Number):int { 308 | var color:int = 0xff0000; 309 | 310 | if (a < OCEAN_ALTITUDE) color = 0x000099; 311 | //else if (a < OCEAN_ALTITUDE + 3) color = 0xc2bd8c; 312 | else if (a < OCEAN_ALTITUDE + 5) color = 0xae8c4c; 313 | else if (useSmoothColors) { 314 | color = Color.hsvToRgb(40.0 + 100.0 * m/255 + 30 * Math.min(a,m)/255, 315 | 0.8+0.2*m/255-0.7*a/255, 316 | 0.5+0.5*a/255-0.3*m/255); 317 | } else if (a > 220) { 318 | if (a > 250) color = 0xffffff; 319 | else if (a > 240) color = 0xeeeeee; 320 | else if (a > 230) color = 0xddddcc; 321 | else color = 0xccccaa; 322 | if (m > 150) color -= 0x331100; 323 | } 324 | 325 | else if (r > 10) color = 0x00cccc; 326 | 327 | else if (m > 200) color = 0x56821b; 328 | else if (m > 150) color = 0x3b8c43; 329 | else if (m > 100) color = 0x54653c; 330 | else if (m > 50) color = 0x334021; 331 | else if (m > 20) color = 0x989a2d; 332 | else color = 0xc2bd8c; 333 | 334 | return color; 335 | } 336 | 337 | public function channelsToColors():void { 338 | var b:BitmapData = new BitmapData(map.SIZE, map.SIZE); 339 | for (var x:int = 0; x < map.SIZE; x++) { 340 | for (var y:int = 0; y < map.SIZE; y++) { 341 | b.setPixel 342 | (x, y, 343 | moistureAndAltitudeToColor(map.moisture[x][y], 344 | map.altitude[x][y] * (1.0 + 0.1*((x+y)%2)), 345 | map.rivers[x][y])); 346 | } 347 | } 348 | 349 | var m:Matrix = new Matrix(); 350 | m.scale(colorMap.width / b.width, colorMap.height / b.height); 351 | colorMap.draw(b, m, null, null, null, true); 352 | } 353 | 354 | public function channelsToLighting():void { 355 | // From the altitude map, generate a light map that highlights 356 | // northwest sides of hills. Then blur it all to remove sharp edges. 357 | var b:BitmapData = new BitmapData(map.SIZE, map.SIZE); 358 | arrayToBitmap(map.altitude, b); 359 | // NOTE: the scale for the lighting should be changed depending 360 | // on the map size but it's not clear in what way. Alternatively 361 | // we could rescale the lightingMap to a fixed size and always 362 | // use that for lighting. 363 | b.applyFilter(b, b.rect, new Point(0, 0), 364 | new ConvolutionFilter 365 | (3, 3, [-2, -1, 0, 366 | -1, 0, +1, 367 | 0, +1, +2], 2.0, 127)); 368 | b.applyFilter(b, b.rect, new Point(0, 0), 369 | new BlurFilter()); 370 | 371 | var m:Matrix = new Matrix(); 372 | m.scale(lightingMap.width / b.width, lightingMap.height / b.height); 373 | lightingMap.draw(b, m); 374 | } 375 | 376 | public function generateDetailMap(g: Graphics, centerX:Number, centerY:Number):void { 377 | // Parameters 378 | var cornerAdjust:Number = DETAILMAG*Number(corner_adjust_text.text); 379 | var randomAdjust:Number = DETAILMAG*Number(random_adjust_text.text); 380 | 381 | // We are drawing an area DETAILSIZE x DETAILSIZE. 382 | var x:int, y:int; 383 | g.clear(); 384 | 385 | // Coordinates of the detail area: 386 | var bounds:Rectangle = new Rectangle 387 | (int(centerX - DETAILSIZE/2), int(centerY - DETAILSIZE/2), 388 | DETAILSIZE, DETAILSIZE); 389 | 390 | // Make sure that we're entirely within the bounds of the map 391 | bounds = bounds.intersection(new Rectangle(0, 0, SIZE, SIZE)); 392 | 393 | // 2d Array of vertices 394 | var vertices:Array = []; 395 | for (x = bounds.left; x <= bounds.right; x++) { 396 | vertices[x] = []; 397 | for (y = bounds.top; y <= bounds.bottom; y++) { 398 | // TODO: we'd save a lot of allocation if we reused these points 399 | vertices[x][y] = new Point((x-centerX+DETAILSIZE/2)*DETAILMAG, 400 | (y-centerY+DETAILSIZE/2)*DETAILMAG); 401 | } 402 | } 403 | 404 | // Move vertices randomly 405 | var noise:BitmapData = new BitmapData(256, 256); 406 | noise.noise(SEED, 0, 255); 407 | for (x = bounds.left; x <= bounds.right; x++) { 408 | for (y = bounds.top; y <= bounds.bottom; y++) { 409 | var noiseColor:int = noise.getPixel(x % 256, y % 256); 410 | var rand1:Number = ((noiseColor & 0x00ff00) >> 8) / 255.0 - 0.5; 411 | var rand2:Number = (noiseColor & 0xff) / 255.0 - 0.5; 412 | vertices[x][y].x += rand1 * randomAdjust; 413 | vertices[x][y].y += rand2 * randomAdjust; 414 | } 415 | } 416 | 417 | // Alter vertices if 3 of 4 squares has same type 418 | for (x = bounds.left+1; x < bounds.right; x++) { 419 | for (y = bounds.top+1; y < bounds.bottom; y++) { 420 | // Sprites at the four squares touching this vertex 421 | var cTL:int = moistureAndAltitudeToColor(map.moisture[x-1][y-1], map.altitude[x-1][y-1], 0); 422 | var cTR:int = moistureAndAltitudeToColor(map.moisture[x][y-1], map.altitude[x][y-1], 0); 423 | var cBL:int = moistureAndAltitudeToColor(map.moisture[x-1][y], map.altitude[x-1][y], 0); 424 | var cBR:int = moistureAndAltitudeToColor(map.moisture[x][y], map.altitude[x][y], 0); 425 | 426 | // Figure out which corner is odd, if any 427 | if (cTR == cBL && cBL == cBR && cTL != cTR) { // TL is odd 428 | vertices[x][y].x -= cornerAdjust; 429 | vertices[x][y].y -= cornerAdjust; 430 | } 431 | if (cTL == cTR && cTR == cBL && cBL != cBR) { // BR is odd 432 | vertices[x][y].x += cornerAdjust; 433 | vertices[x][y].y += cornerAdjust; 434 | } 435 | if (cTL == cBL && cBL == cBR && cTL != cTR) { // TR is odd 436 | vertices[x][y].x += cornerAdjust; 437 | vertices[x][y].y -= cornerAdjust; 438 | } 439 | if (cTL == cTR && cTR == cBR && cTL != cBL) { // BL is odd 440 | vertices[x][y].x -= cornerAdjust; 441 | vertices[x][y].y += cornerAdjust; 442 | } 443 | // TODO: this seems like stupid repetitive code TODO: what 444 | // should we do when we have tiles A A B C (A and A are 445 | // adjacent but only two of them)? 446 | } 447 | } 448 | 449 | // Draw grid 450 | // g.lineStyle(1, 0x000000, 0.1); // TODO: set border if tiles not the same 451 | var m:Matrix = new Matrix(); 452 | for (x = bounds.left; x < bounds.right; x++) { 453 | for (y = bounds.top; y < bounds.bottom; y++) { 454 | // TODO: we should have cached the tile ids when we generate the map... 455 | var c:int = moistureAndAltitudeToColor(map.moisture[x][y], 456 | map.altitude[x][y], 0); 457 | g.beginFill(c); 458 | g.moveTo(vertices[x][y].x, vertices[x][y].y); 459 | g.lineTo(vertices[x][y+1].x, vertices[x][y+1].y); 460 | g.lineTo(vertices[x+1][y+1].x, vertices[x+1][y+1].y); 461 | g.lineTo(vertices[x+1][y].x, vertices[x+1][y].y); 462 | g.lineTo(vertices[x][y].x, vertices[x][y].y); 463 | g.endFill(); 464 | } 465 | } 466 | g.lineStyle(); 467 | } 468 | } 469 | } 470 | 471 | import flash.display.*; 472 | import flash.geom.*; 473 | import flash.filters.*; 474 | 475 | class Map { 476 | // Make the map into an island. 0.39 makes almost every map into an 477 | // island; 0.1 makes almost no map into an island. 478 | public static var ISLAND_EFFECT:Number = 0.39; 479 | 480 | public var SIZE:int; 481 | public var SEED:int; 482 | 483 | public var altitude:Vector.>; 484 | public var moisture:Vector.>; 485 | public var rivers:Vector.>; 486 | 487 | function Map(size:int, seed:int) { 488 | SIZE = size; 489 | SEED = seed; 490 | altitude = make2dArray(SIZE, SIZE); 491 | moisture = make2dArray(SIZE, SIZE); 492 | rivers = make2dArray(SIZE, SIZE); 493 | } 494 | 495 | public function generate():void { 496 | // Generate 3-channel perlin noise and copy 2 of the channels out 497 | var b:BitmapData = new BitmapData(SIZE, SIZE); 498 | b.perlinNoise(SIZE, SIZE, 8, SEED, false, false); 499 | 500 | var s:Shape = new Shape(); 501 | 502 | // NOTE: if we remembered the equalization and other parameters, 503 | // we could zoom in at least on the altitude map by using the 504 | // perlinNoise() size and offsets parameters to regenerate 505 | // portions of the map at higher resolution. This could be useful 506 | // when drawing the detail map. 507 | equalizeTerrain(b); 508 | 509 | // Overlay this on an "island" multiplier map 510 | // Based on http://www.ridgenet.net/~jslayton/FunWithWilburVol6/index.html 511 | for (x = 0; x < SIZE; x++) { 512 | for (y= 0; y < SIZE; y++) { 513 | var radiusSquared:Number = (x-SIZE/2)*(x-SIZE/2) + (y-SIZE/2)*(y-SIZE/2); 514 | radiusSquared += Math.pow(Math.max(Math.abs(x-SIZE/2), Math.abs(y-SIZE/2)), 2); 515 | var max_radiusSquared:Number = SIZE*SIZE/4; 516 | radiusSquared /= max_radiusSquared; 517 | radiusSquared += Math.random() * 0.1; 518 | radiusSquared *= ISLAND_EFFECT; 519 | var island_multiplier:Number = Math.exp(-radiusSquared/4)-radiusSquared; 520 | island_multiplier = island_multiplier * island_multiplier * island_multiplier; 521 | // island_multiplier = island_multiplier * island_multiplier * island_multiplier; 522 | 523 | c = b.getPixel(x, y); 524 | var height:int = (c >> 8) & 0xff; 525 | height = int(island_multiplier * height); 526 | if (height < 0) height = 0; 527 | if (height > 255) height = 255; 528 | c = (c & 0xffff00ff) | (height << 8); 529 | b.setPixel(x, y, c); 530 | } 531 | } 532 | 533 | equalizeTerrain(b); 534 | 535 | // Extract information from bitmap 536 | for (var x:int = 0; x < SIZE; x++) { 537 | for (var y:int = 0; y < SIZE; y++) { 538 | var c:int = b.getPixel(x, y); 539 | altitude[x][y] = (c >> 8) & 0xff; 540 | moisture[x][y] = c & 0xff; 541 | } 542 | } 543 | } 544 | 545 | public function equalizeTerrain(bitmap:BitmapData):void { 546 | // Adjust altitude histogram so that it's roughly quadratic and 547 | // water histogram so that it's roughly linear 548 | var histograms:Vector.> = bitmap.histogram(bitmap.rect); 549 | var G:Vector. = histograms[1]; 550 | var B:Vector. = histograms[2]; 551 | var g:int = 0; 552 | var b:int = 0; 553 | var green:Array = new Array(256); 554 | var blue:Array = new Array(256); 555 | var cumsumG:Number = 0.0; 556 | var cumsumB:Number = 0.0; 557 | for (var i:int = 0; i < 256; i++) { 558 | cumsumG += G[i]; 559 | cumsumB += B[i]; 560 | green[i] = (g*g/255) << 8; // int to green color value 561 | blue[i] = (b*b/255); // int to blue color value 562 | while (cumsumG > SIZE*SIZE*Math.sqrt(g/256.0) && g < 255) { 563 | g++; 564 | } 565 | while (cumsumB > SIZE*SIZE*(b/256.0) && b < 255) { 566 | b++; 567 | } 568 | } 569 | bitmap.paletteMap(bitmap, bitmap.rect, new Point(0, 0), null, green, blue, null); 570 | 571 | // Blur everything because the quadratic shift introduces 572 | // discreteness -- ick!! TODO: probably better to apply the 573 | // histogram correction after we convert to the altitude[] 574 | // array, although even there it's already been discretized :( 575 | bitmap.applyFilter(bitmap, bitmap.rect, new Point(0, 0), new BlurFilter()); 576 | 577 | // TODO: if we ever want to run equalizeTerrain after 578 | // spreadMoisture, we need to special-case water=255 (leave it alone) 579 | } 580 | 581 | public function make2dArray(w:int, h:int):Vector.> { 582 | var v:Vector.> = new Vector.>(w); 583 | for (var x:int = 0; x < w; x++) { 584 | v[x] = new Vector.(h); 585 | for (var y:int = 0; y < h; y++) { 586 | v[x][y] = 0; 587 | } 588 | } 589 | return v; 590 | } 591 | 592 | public function blurMoisture():void { 593 | // Note: this isn't scale-independent :( 594 | var radius:int = 1; 595 | var result:Vector.> = make2dArray(SIZE, SIZE); 596 | 597 | for (var x:int = 0; x < SIZE; x++) { 598 | for (var y:int = 0; y < SIZE; y++) { 599 | var numer:int = 0; 600 | var denom:int = 0; 601 | for (var dx:int = -radius; dx <= +radius; dx++) { 602 | for (var dy:int = -radius; dy <= +radius; dy++) { 603 | if (0 <= x+dx && x+dx < SIZE && 0 <= y+dy && y+dy < SIZE) { 604 | numer += moisture[x+dx][y+dy]; 605 | denom += 1; 606 | } 607 | } 608 | } 609 | result[x][y] = numer / denom; 610 | } 611 | } 612 | moisture = result; 613 | } 614 | 615 | public function spreadMoisture():void { 616 | var windX:Number = SIZE/17.0; 617 | var windY:Number = SIZE/23.0; 618 | var evaporation:int = 1; 619 | 620 | var result:Vector.> = make2dArray(SIZE, SIZE); 621 | for (var x:int = 0; x < SIZE; x++) { 622 | for (var y:int = 0; y < SIZE; y++) { 623 | if (altitude[x][y] < mapgen.OCEAN_ALTITUDE) { 624 | result[x][y] += 255; // ocean 625 | } 626 | 627 | result[x][y] += moisture[x][y] - evaporation; 628 | 629 | // Dampen the randomness 630 | var wx:Number = (20.0 + Math.random() + Math.random()) / 21.0; 631 | var wy:Number = (20.0 + Math.random() + Math.random()) / 21.0; 632 | var x2:int = x + int(windX * wx); 633 | var y2:int = y + int(windY * wy); 634 | x2 %= SIZE; y2 %= SIZE; 635 | if (x != x2 && y != y2) { 636 | var transfer:int = moisture[x][y]/3; 637 | var speed:Number = (30.0 + altitude[x][y]) / (30.0 + altitude[x2][y2]); 638 | if (speed > 1.0) speed = 1.0; 639 | /* speed is lower if going uphill */ 640 | transfer = int(transfer * speed); 641 | 642 | result[x][y] -= transfer; 643 | result[x2][y2] += transfer; 644 | } 645 | } 646 | } 647 | 648 | for (x = 0; x < SIZE; x++) { 649 | for (y = 0; y < SIZE; y++) { 650 | if (result[x][y] < 0) result[x][y] = 0; 651 | if (result[x][y] > 255) result[x][y] = 255; 652 | } 653 | } 654 | 655 | moisture = result; 656 | } 657 | 658 | public function carveCanyons():void { 659 | for (var iteration:int = 0; iteration < 10000; iteration++) { 660 | var x:int = int(Math.floor(SIZE*Math.random())); 661 | var y:int = int(Math.floor(SIZE*Math.random())); 662 | 663 | for (var trail:int = 0; trail < 1000; trail++) { 664 | // Just quit at the boundaries 665 | if (x == 0 || x == SIZE-1 || y == 0 || y == SIZE-1) { 666 | break; 667 | } 668 | 669 | // Find the minimum neighbor 670 | var x2:int = x, y2:int = y; 671 | for (var dx:int = -1; dx <= +1; dx++) { 672 | for (var dy:int = -1; dy <= +1; dy++) { 673 | if (altitude[x+dx][y+dy] < altitude[x2][y2]) { 674 | x2 = x+dx; y2 = y+dy; 675 | } 676 | } 677 | } 678 | 679 | // TODO: make the river keep going to the ocean no matter what! 680 | 681 | // Move the particle in that direction, and remove some land 682 | if (x == x2 && y == y2) { 683 | if (altitude[x][y] < 10) break; 684 | // altitude[x][y] = Math.min(255, altitude[x][y] + trail); 685 | } 686 | x = x2; y = y2; 687 | altitude[x][y] = Math.max(0, altitude[x][y] - 1); 688 | rivers[x][y] += 1; 689 | } 690 | } 691 | 692 | for (x = 0; x < SIZE; x++) { 693 | for (y = 0; y < SIZE; y++) { 694 | if (rivers[x][y] > 100) moisture[x][y] = 255; 695 | } 696 | } 697 | } 698 | } 699 | 700 | --------------------------------------------------------------------------------